PlayStation Sound Player on FPGA
初代プレイステーションのサウンドをFPGAで演奏するものを作ってみたので、
忘れないうちにちょっとまとめ的なものを残しておこうかと。
なお現状ではあくまで技術デモのため、HDLコードやFPGAデータを公開する予定はありません。
▼ FPGA使い万事塞翁が馬プレステで遊んでいた日々…あのころの記憶から強くそして優しくBGMが流れてくる。 テレビ持ち込み禁止の寮にポータブル白黒テレビとプレステを持ち込んで遊んでいた。 映像は白黒だけど、せめてサウンドはと部屋の四隅にスピーカーを設置して楽しんだ日々。 いつか、このサウンドを、名曲たちをこの手で甦らせることをおぼろげに想いながら。 プレステの音源チップはスーファミの音源チップの弟(妹?)のようなものなので、 実装に関しては楽できる部分は多い。 スーファミも作ったことだし、じゃあプレステの音源も作れるのでは? というわけで手を出してみた、という流れ。
古人の言によれば、本歌に相対するものは写しではない。
つまり写しはそれ自身のアイデンティティを持っていて、
本歌と同じ世界を共に生きているという。
本歌はそれ以上の成長はないが、写しは独自の成長を続けることができる。
新しい道を行けるのは写しのみなわけだ
▼ プレステサウンドを演奏するには?
サウンド演奏の仕組みを分かりやすく例えると、
サウンドプロセッサが指揮者で、サウンドドライバが演奏者、
波形の音程や発音タイミングなどのデータであるサウンドシーケンスが譜面、
そして波形生成部が楽器という感じかな。
まず、プレステのサウンドは、メインメモリのサウンドドライバをCPUが実行し、 SPU(Sound Processing Unit)にパラメータをセットして、 サウンドメモリの波形をデコードすることで発音している。 PS-X EXEとは、このCPUが実行するメモリデータを、 実行開始するプログラムカウンタ(PC)などの情報と一緒にしたものである。 メモリデータには、サウンドメモリに転送する波形データも含まれる。 以降、PS-X EXEをEXEデータと呼ぶ。 つまり、EXEデータは、メインメモリの2MBのサウンドに関連する部分をぶっこ抜いたものである。
また、サウンドドライバの特徴として、発音チャンネル固定のものと、
DVA(Dynamic Voice Allocation)のものがある。
DVAは空きチャンネルに動的に波形を割り当てて発音する方式。
DVAだと発音チャンネルが固定されないため、初期実装段階ではデバグが面倒になってしまう。
そこで、初期実装段階ではDVAを使用していない“FF7”などのEXEデータをサンプルとして使った。
▼ 開発環境 - こんなツール使ってます -いつもの開発フロー ハードウェア記述言語は国産のSFLを独自にちょっと拡張したSFL+(いつもの)。 環境はWindows7 64bit(メモリ8GB)とCygwin。 sfl2vlコマンドにてSFLをVerilogHDLに変換する。 シミュレーションはVerilatorでVerilogHDLをC++に変換して、 VisualStudioExpress2013でコンパイルし実行する。 もちろんテストベンチはC++で書く。
論理合成・配置配線は、ちょっと古いけどQuartusII WebEdition 12.1sp1を使用。
あとModelSim ASEはインストールしたものの、波形…非同期…うっ頭が、となるので使ってない
今回そんな縛りは必要ないんだけれども、基本的に無償ツールを中心に使用した。
なお、トップモジュールはVHDL(なぜか)で記述し、
PLLのようなデバイス依存のコンポーネントはここに置く。
つまりデバイス依存のものやサブクロックで動作するモジュールについては別途検証しておき、
メインモジュールのシミュレーション時には含めない。
▼ 実装する機能 - プレステの機能のうち何が必要か -サウンド演奏のために必要な機能は以下のようで、当然ながらGTEやGPUやCDコントローラの実装は不要。
機能ブロック図は次のような感じ。なお、SystemCallはR3000Aの機能に含まれる。
実装する各機能ブロック
メモリ関連は次のようなサイズ感になる。
各メモリのサイズ 今回使用した『Terasic DE2-115 FPGAボード』で使用可能なリソースは、
FPGAに実装するのは、 R3000A、割り込みとSystemCall、SPU、DMA、ルートカウンタである。 最初、メインメモリはアクセスが速いSRAMを使用するつもりだったけど、 結局32bit分だと2回リードしなきゃならないので、32bit幅のSDRAMを使用する。 16bit幅のSRAMはサウンドメモリとして使用する。 BIOSとカーネルはFlashメモリに配置し、リセット時にSDRAMへ転送する。 EXEデータはSDカードからSDRAMに配置する。 サウンドはAudioCODECを通して出力する。 リセット時の動作は、
ちなみにルートカウンタやSPUの波形生成部は実機と近いサイクルで動作させるが、
FPGAの回路的には全て50MHzのクロックに完全同期して動作させる。
PLLなどを使っていくつかクロックドメインを分けることも考えられるけど、
わざわざシンクロナイザ(同期化回路)などを実装するのも本末転倒だし、
VerilatorでC++に変換してシミュレーションするのもやりづらいので、
50MHzの完全同期設計で実装した。
ただし、AudioCODEC向けやSDRAM向けのクロックにはPLLで生成したクロックを使用している。
▼ エミュレータ作成 - ソフトウェア上で仕様を確認 -当然ながら仕様を確認しないことにはどうにもならないし、 いきなりHDLで書き下す程度の能力もないので、 資料を見つけてはC++で自作の簡易的なエミュレータを作成した。 まず、プレステのアーキテクチャの全体像として『NYOットやろうぜ』を参考にした。 ここにはSPUに関しても書かれている。 また、レジスタなどの詳細は『Nocash PSX Playstation Specifications』にまとまっている。 CPUはR3000A互換プロセッサである『TX39/H2 コア アーキテクチャ(pdf)』を参考にした。 R3000AのリセットベクタからBIOSを実行するものを作るわけだけど、 BIOSは『PS1実機からのBIOS吸い出し』を参考にしてプレステ実機から取得した。 以前は基板上のROMチップから直接取得する必要があったが、 『PSX BIOS Dumper』という便利なツールのおかげで、 メモリーカード経由でBIOSを抽出できるようになっている。 ここでしっかりとR3000Aをメインに必要な機能をエミュレータとして仕様を実装し確認しておくことで、 ハードウェア記述とそのシミュレーションの時にリファレンスモデルとして照らし合わせて検証することができる。 リファレンスモデルとしては実行速度が速いこともメリットで、 SPUの周波数レジスタにそれっぽい値がセットされるまでの実行時間は2秒ほど。 もちろんR3000Aの命令実行ログも出力できるようにしておく。 なお、ここで作るエミュはあくまでR3000Aと各種レジスタだけで、 サウンド生成部については実装しなかった。 サウンド生成部の作り込みに関してはSFLで記述した方が手っ取り早いし、 エミュでサウンドをちゃんと鳴らすためにはバッファとかタイミングとかソフト寄りのノウハウに頭をひねらせる必要がある。 それに、FPGA実装で必要なメモリアクセスタイミングなどの回路特有のものをエミュで再現するのは無駄にエミュの動作速度を遅くさせてしまうので、 そこまで再現するのは論外。 エミュで確認したいのは、R3000Aがサウンドドライバを実行して、 SPUのレジスタにそれっぽい値がセットされるかどうかを確認するためだから。 それが確認できれば、とっととハードウェア実装に取り掛かる。
おっと、エミュをVivado HLSで高位合成しちゃえっていう話はまた今度な。
▼ CPU(R3000Aもどき)の実装 - サウンドドライバを実行する頭脳 -実行ログのテキスト比較 仕様が確認できたので、これをSFL+で書き下す。この作業が楽しいのだよ。 最近気づいたけど、SFLを書きたいがためにFPGAを使っている感がある。
個人的にCPUのシミュレーションは波形を追うよりも、
エミュのログと比較しながら1命令ずつ実装するのが確実なので、
VerilatorでC++に変換してシミュレーションする。
Verilatorのおかげで、個人でお金をかけずに高速なシミュレーションが可能なのでありますな。
もちろんテストベンチもC++で書けてVerilogHDLで記述に悩む必要もない。
それをVisualStudioExpressでコンパイルし、実行ログを得る。
これらのログをWinMargeなどのテキスト比較ツールで比較することによって動作を確認する。
テキスト比較ツールの良い所は、タイミングがちょっとずれたりしてログが完全に一致していなくても、
あいまい比較をしてくれるので、ログに対してそこまで神経質にならなくてよいところ。
もちろん不一致の部分に関しては明確にハイライトしてくれるので分かりやすい。
ただ、開発初期時は32bitOSだったため、ログファイルのサイズが100MBほどあると比較ができなかった
とりあえずまずは、FPGAでEXEデータのプログラムを、 外部割込みが入るあたりまで実行できるものを実装した。 プロジェクト開始前に開発するものに名前をつけると失敗するというジンクスがあるらしいが、 名前がないといまいち存在の実感がわかないものである (FPGAデータとしてはなおさら)。 そこで「私の船、どうかな」「飛ぶんじゃない?(It may fly.)」という会話にあやかって、 コードネーム“May run”とでもしようかとも考えたけど、 恥ずかしかったので結局“R3000Aもどき”で落ち着いてしまった。
サウンドドライバの実行にはまず、カーネルが必要だ。 BIOSを実行するとその中で、カーネルがメインメモリに展開される。 ちゃんとカーネルが展開されたか確認するためにチェックサムを使っていたけど、 実行前にメモリをゼロフィルしていなかったときはチェックサムが合わず、 混乱したものである。 そして実行前にメモリをゼロフィルして正しいチェックサムを確認できた。 DE2-115ボードには7セグLEDが8個実装されているので、32bitシステムを組むのに便利だ。 それから、毎回カーネル生成をするとEXEデータのロードでプログラムカウンタを監視したりしないといけないので、 カーネルはあらかじめ生成したものをFlashメモリに書き込んでおき、 EXEデータのロード時にFlashメモリからSDRAMのメインメモリにコピーする方法とした。 メモリアクセス命令では、4Byteアクセス、2Byteアクセス、1Byteアクセス命令がある。 ロード命令に関しては4Byte取ってきてCPUの中で欲しい分だけ選択できるが、 ストア命令に関してはCPU外の回路が必要となる。 32bit(4Byte)のうち、メモリに書き込みたいByteだけ書き込むようにする。 SDRAMのByteEnable(Data I/O Mask)ってこういう時のためにあるのね。 メインメモリへの書き込み部は以下のようになった。
instruct psx.WRAM_WRITE par{ any{ psx.Word==4 : par{ sdram.write(0b000000||psx.A<20:2>, psx.Dout, 0b1111); } psx.Word==2 : any{ psx.A<1>==0b0 : par{ sdram.write(0b000000||psx.A<20:2>, 0x0000||psx.Dout<15:0>, 0b0011); } psx.A<1>==0b1 : par{ sdram.write(0b000000||psx.A<20:2>, psx.Dout<15:0>||0x0000, 0b1100); } } psx.Word==1 : any{ psx.A<1:0>==0b00 : par{ sdram.write(0b000000||psx.A<20:2>, 0x000000||psx.Dout<7:0>, 0b0001); } psx.A<1:0>==0b01 : par{ sdram.write(0b000000||psx.A<20:2>, 0x0000||psx.Dout<7:0>||0x00, 0b0010); } psx.A<1:0>==0b10 : par{ sdram.write(0b000000||psx.A<20:2>, 0x00||psx.Dout<7:0>||0x0000, 0b0100); } psx.A<1:0>==0b11 : par{ sdram.write(0b000000||psx.A<20:2>, psx.Dout<7:0>||0x000000, 0b1000); } } } } 面倒なのがSYSCALL命令。 遅延スロットでSystemCall実行しちゃうとリターン時に遅延スロットから先を実行してしまうため、 遅延スロットでない状態を判断してSystemCallを処理してやる必要がある。 また、IRQについては割り込みレジスタを監視するわけだけど、 実行に関しては分岐先が違うだけなので、SYSCALL命令を命令レジスタにぶちこんでやる方法をとった。 その他、メモリアクセスやジャンプ命令などを考慮したパイプライン制御部のコードは以下。 ちょっと分かりにくくなっちまった。
instruct run par{ if(div_wait.do){ } else{ alt{ write_mem_rt : stall_mem2 := 1; read_rt | write_rt : stall_mem := 1; irq_run==2 : irq_run := 3; else : stage_IF(); } alt{ exception : stall_sys := 1; stall_mem | write_mem_rt : ; else : stage_ID(); } alt{ stall_mem | stall_sys : par{ stall_mem := 0; stall_sys := 0; } else : par{ if(write_mem_rt); else stage_EX(); if(stall_mem2) stall_mem2 := 0; else stage_MEM(); stage_WB(); } } } } とりあえずSPU各チャンネルのレジスタを初期化して、 タイマ割り込みが入るところまでの命令を順次実装した。 このあたりの動きはサウンドドライバの作法のようだ。 タイマ割り込みについては、まだルートカウンタを実装しきれてない頃はエミュのログに従って、 タイミング決め打ちで無理やり割り込みレジスタの値をセットしてやる。
実装を進めていると、除算命令のあとにログが一致しない不具合があった。
以下のように、DIV命令の2命令先(MFLO命令)で除算結果をレジスタに取得する挙動が見られた。
キャッシュに関しては、実際に演奏に支障が出る (サウンドドライバの実行が間に合わない)ようであれば実装を検討することとして、 とりあえずは無しでやってみることとした(結果、今回は必要なかった)。 それから演算オーバーフローなどの例外も実装していない。
“VALKYRIE PROFILE”のサウンドを演奏してみたところ、
いくつか追加で命令実装が必要なことが判明した。
最初は割り込み関連の未実装を疑ったが、CPUのログを見る限りだと結局命令の実装が足りなかっただけだった。
必要な命令の機能は、メモリフェッチしたデータをレジスタデータと混ぜてメモリストアするもの。
1命令で2回もメモリアクセスするやっかいなもの |-`).。oO(誰だよこんな命令を作った設計者は)。
2回メモリアクセスするということはR3000Aもどきでは2回ストールすることになるので、
ステージ制御関連の実装が面倒だったが、なんとか実装して曲も演奏できるようになった
で、気づいてみると、R3000Aって典型的なMIPSの5段パイプラインなんだけど、 R3000Aもどきは最後のステージを使ってなくて、4段パイプラインになってしまった (5段パイプラインにするとフォワーディング回路の実装が4段よりも面倒くさい)。 通常レジスタファイルからのリードはIDステージに置くらしいんだけど、 EXEステージに置いてることもあって遅延が大きくなってる。 また各命令の演算もそれぞれで行うように記述したため、 回路規模としても膨れ上がり、動作周波数もようやく50MHzを超える程度。 演算系をALUとしてまとめてしまえばもう少しまともな感じになるが、 初期実装ではとにかく可読性を優先するためとりあえずはこれでいいか、と。 実際、MULT命令(符号付き乗算)とMULTU命令(符号なし乗算)で、 乗算器を2個使ってたらFmaxが48MHzになってしまった。 そこは乗算器を1個にまとめてレジスタで挟み込んだら53MHzになってその場しのぎできたけど。 世間の技術者が許してくれなくてもFPGAが許してくれる。 なんてったってFPGAはFree Programmable Gate Arrayの略だからね!(嘘)
それでようやくR3000AもどきのSFL+コードができた。
べた書き具合がすばらしい。
ちなみに、SPUの周波数レジスタにそれっぽい値がセットされるまでに要するシミュレーション時間は48秒ほどかかる。
エミュだと2秒ほどだから、いかにエミュによる検証が手っ取り早いかが明らかだ。
▼ ルートカウンタの実装 - それはメトロノームのようなもの -演奏速度を決めるのはルートカウンタ、いわゆるタイマであり、 実機では33.8688MHzで動作している。 FPGAボードのクロックは50MHzなので、 このまま使うとルートカウンタ内のタイマレートなど微調整する必要がある。 しかしそれだと実際の値と異なるものを実装することになるのであまりよろしくない。 そこで、DDS(Direct Digital Synthesizer)を使って、 50MHzから約33.8688MHzのサイクルを生成してルートカウンタをドライブする。 なお、DDSの回路的にカウンタのビット幅を増やしてやればより所望のサイクルに近いものが生成でき、 今回は15bitカウンタでほぼ33.8688MHz(誤差0.000000Hz)のサイクルを生成できた。 この信号を192分周すればちょうど176.4kHzになり、SPUの波形デコードタイミングにも使える。 なぜ176.4kHzかというと、サンプリング周波数は44.1kHzだが、仕様では2オクターブ上まで出せるので、 44.1kHzの4倍である176.4kHzが波形デコーダをドライブするサイクルとなる。
ルートカウンタは0,1,2,3の4つあるが、サウンド演奏では0,1は使用されないようだ。
カウンタ2は何らかのタイミングをとっていて、カウンタ3はVBlankのタイミングをとっている。
カウンタが指定されたカウント値になったらIRQが生成され、
CPUは割り込みルーチンに分岐する。
▼ SPUベースの実装 - まず周りから固めていく -
まあ初期実装ということもあって、SNES on FPGAと同じように、
全チャンネルべたに回路を置く形で実装した。
理想的にはパイプライン処理にした方が回路規模的に小さくなるのだけれど。
本来の流れではSNES on FPGAのサウンドDSPをパイプライン化して、
SPUの実装に活かすつもりだったけど、なにせFPGAに余裕があるもので、
そこまでする必然性がなかった。ちゃんと設計しなさいよって感じ。
スーファミの8チャンネル程度ならまだしも、
プレステの24チャンネルはさすがに回路規模増大およびコンパイル時間増大がはなはだしい限りでありますな。
SPUのブロック図
おおざっぱに機能ごとのブロック図を描いて、
それぞれのブロック間のデータやりとりをどのようにするかも検討する。
あとは各種レジスタの実装と、サウンドメモリアクセスのアービトレーション回路の実装。
ベースはそれくらい。
▼ とりあえず音を出してみる - R3000Aもどき、ちゃんと動いてんの? -サウンドドライバが動作すれば、CPUから周波数レジスタへ値が設定される。 とりあえずそれを確認するために(シミュレーションで一応確認はできているものの) 仮で矩形波ジェネレータを24チャンネル分実装して音を出してみる。
うーん前衛的。
メロディなんかは原曲の雰囲気がにじみ出てるけど、ドラムスは適当だね。
まあでも、これで一応サウンドドライバがそれっぽく動いてそうであることは確認できた。
ここまでが長かったんだよ〜、ようやくって感じ。楽しい!
ちなみに、エンベロープ値をリードするサウンドドライバもあったりするけど、
とりあえずこの段階では各チャンネルのノートオンオフフラグをエンベロープ値として返すことで、
お茶を濁している。
▼ DMAの実装 - データ転送のカナメ -SPUでの演奏前に、波形データはメインメモリからサウンドメモリに転送される。 これにはDMAが使用されるが、初期実装ではバグを含んでいたため、 半分しか転送できていなかった。 これを見て、実は波形メモリは512KBもいらない?と勘違いし、 サウンドメモリを内蔵メモリで実装しようとしていた。 結局、あとから波形デコードすべきデータがサウンドメモリに無いということに気づき、 DMAのバグを修正するはめになった。 当然、内蔵メモリで512KBを確保することができず、結局サウンドメモリは、 2MBの外部SRAMを使用することで落ち着いた。
あと、どうやらブロック単位のDMAというものがあるらしく?
合間にCPUが動くらしい。
基本的にサウンドメモリへのDMAによるデータ転送はサウンド演奏前に実行されるようであり、
サウンドの演奏中には影響なさそうだったので今回の実装では無視した。
▼ 波形デコーダの実装 - 音色を生成しよう -
ADPCM波形はBRR(Bit Rate Reduction)で圧縮されており、
波形デコーダではこれをもとのADPCM波形にデコードする。
資料は『SPU ADPCM Samples』を参照した。
サウンドメモリから取得したデータをデコードする 波形データはサウンドメモリから一回のリードで16bit取得し、 これを4bit(1ニブル)ずつデコードする。 つまり4つのニブルがあって、デコードの順番としては4パターン考えられる。 実はこの順番がよく分からなかったので、聴きながらどれが良いか調整した。 最初、“FF7:さらに戦う者たち”のギターが一番それっぽく聞こえる感じのものにしたが、 “FFT”や“FF8”の特定の波形にノイズが乗ってしまう現象に悩まされた。 BRRデコードでは波形のループ位置を指定することができるので、最初はここを疑っていた。 しかしエミュで確認してもどうやらループ指定するところまで波形をデコードしていないことが分かり、 ループ処理については関係なかった(なんだよ)。 結局、ニブル処理の順番が間違っていたらしく、これを修正してより本来の音が出た。 ギターの音はニブルの処理順が違っても、割と違いが分かりづらい波形だったらしい。 コードは以下のように4つのニブルを下位のものから処理する、 いたって順当な形になった。
stage brr_decode { if(src_ok){ any{ dec_rot==0 : par{ decode(src_data<3:0>); } dec_rot==1 : par{ decode(src_data<7:4>); } dec_rot==2 : par{ decode(src_data<11:8>); } dec_rot==3 : par{ decode(src_data<15:12>); src_ok := 0; } } dec_rot++; finish; } if(key_on) finish; } ちなみに波形データをサウンドメモリからリードすることに関して、 24チャンネルべたに実装していることもあって、 各チャンネルでチケット巡回させるラウンドロビン方式でメモリアクセスを行っている。 スマートにするにはリード要求をFIFOかなんかに突っ込んで順番に処理する方が良いんでしょうけど。 SNES on FPGAのBRRデコード部は、 初期にはCycloneIIのそんなに大きくはないFPGAで作っていたということもあって、 乗算器が足りなくシフトと加減算を駆使して実装していた (例えば、乗算係数が5/16なら2bit右シフトしたものと4bit右シフトしたものを加算)。 今回使用したFPGAは乗算器が潤沢にあるので、 単にフィルタパラメータを乗算し加算するだけという非常にシンプルな実装になった。
instruct decode par{ // SRA now_data = brr_sft.con(nibble||0x000, shift).dout; switch(filter){ case 0: par{ f0 = 0; f1 = 0; } case 1: par{ f0 = 60; f1 = 0; } case 2: par{ f0 = 115; f1 = -52; } case 3: par{ f0 = 98; f1 = -55; } case 4: par{ f0 = 122; f1 = -60; } default: par{ f0 = 0; f1 = 0; } } inner_data = (17#now_data) + brr_mul0.con(pre0_data, f0).dout<22:6> + brr_mul1.con(pre1_data, f1).dout<22:6>; any{ inner_data<16:15>==0b10 : clip_data = 0x8000; inner_data<16:15>==0b01 : clip_data = 0x7FFF; else : clip_data = inner_data<15:0>; } pre1_data := pre0_data; pre0_data := clip_data; decode_data := clip_data; } で、以上を実装したものが以下。 音色についてはほぼ原曲に近いものが再現できた。 ただしまだエンベロープが未実装のため、シンバルの尾が長いのがお分かり頂けるだろうか。
また、スーファミのBRRデコードでは、ニブル値を左シフトしていたが、 プレステのBRRデコードでは右シフトするようだ。 ちょっと混乱するが、なにかしら右シフトの方が都合が良いのだろう。 波形生成の段階で、ふり幅の大きな値ほどシフト値が小さいとか、 符号拡張と右シフトの組み合わせが配線的に有利とか。
波形のデコードでしばしば問題になるのが有効bit数をどれくらいにするかということ。
波形自体は符号付き16bitだけれど、加減算や乗算といった内部演算中は17bitで十分のようだ。
32768以上は32767に、また-32769以下は-32768にクリップ処理してやることでノイズは消える。
一応内部bit幅を18bitにしてみたが、17bitと変わらなかったので17bitでよしとする。
▼ エンベロープ(ADSR)の実装 - 波形の発音を整える -
デコードした波形はエンベロープを掛けることで、
音の鳴り出しや鳴り終わりの増減を調節して、波形の印象を変えることができる。
生成した波形にエンベロープを掛けて音の印象操作
いわゆる、Attack(音の始まり),Decay(指定値までの減衰),
Sustain(音の尾っぽ),Release(音の終わり)の4段階からなり、
これをエンベロープ曲線と呼ぶ。
エンベロープ曲線を掛ける エンベロープ曲線は、指定されたパラメータにより値を増減させて生成する。 減増レートをROMにまとめてすっきりできないか試してみたが、 どうにも期待したようなエンベロープが生成できなかった。 結局これも解析資料通りに実装している。
実装に関してはこれもまた可読性が良いように、
Attack→Decay→Sustain→Releaseの順で少しずつ実装。
見た目なんとなくハードウェア様の記述じゃないけどそれがSFLクウォリティ
シンバルの音がちゃんと小さくなるのが聴き取れると思う。 この曲ってシンバルくらいしかエンベロープの動きが分かるものがないんだよね。 うん、音に響きがないからまるで路上ライブだね。 曲を演奏するにあたって、エンベロープの実装が重要なのは、 サウンドドライバがDVAの場合、空きチャンネルを指定して波形再生しているということ。 サウンドドライバは、エンベロープの値を取得して、それがゼロだったら新たな波形を割り当てノートオンする (もちろんちゃんとしたサウンドドライバはもっと複雑な動作をしている)。 つまり、エンベロープがいいかげんだとDVAが期待通りに働かずに、演奏が不安定になる。
ちなみにここまで実装した段階で、“VALKYRIE PROFILE”の曲が演奏できなかった。
そこで、当然ながらシミュレーションで確認しようとする。
ところがVerilatorで全体をC++に変換してみるとコードが1ファイル17万行にもなってしまい、
VisualStudioでコンパイルしようとすると
「ヒープ領域を使い果たしました」などと言われてコンパイルできない。
無償版のVisualStudioだから?
ちゃんと設計していない弊害がこんなところに…。
仕方なく、シミュレーションに関しては、
BRRデコード回路などを一時的に外して変換することで、
CPUの動作確認をするなど対応した(結果、必要な命令が実装されていなかった)。
▼ 波形補間 - ノイズを抑え込め! -荒い波形を滑らかにする
サンプリング周波数が44.1kHzとはいえ、
やはり低い周波数が指定されるとデコード波形には高い周波数成分のノイズが混じる。
低音に関しては波形補間で改善するが、よく使用されるガウシアン補間だと個人的に音がこもってしまう印象がある。
そこで線形補間をしようとするが、高音まで補間をしてしまうとシンバルの音が消えてしまう
(シンバルはホワイトノイズに近い波形のため)。
今回もSNES on FPGAと同様に、周波数に応じて補間間隔を変える応答性の線形補間を使用する。
これにより低音ほど補間が強くかかる仕組みとなっている。
“FF8:The Man with the Machine Gun”などが補間の有り無しが良くわかる。
▼ リバーブの実装 - ここは洞窟かコンサートホールか -プレステ音源で特徴的な機能の一つとしてリバーブがある。 適切なパラメータを設定することで、 音の響き方をさまざまなものに設定することができる。 これによってサウンドの迫力が増して、よりシーンに合った臨場感を演出することができるわけだ。 解析資料を見ると、リバーブの実装には以下が必要のようだ。
バッファメモリに関しては、とりあえず今のところはFPGAの内蔵RAMを使用した。 なぜなら楽だから。CycloneIVの内蔵RAMが480KBくらいあって余ってたのでこれを使う。 リバーブ機能が使用するバッファメモリは、サウンドメモリ領域の512KBのうち、 後半あたりを使用するようなので、256KBでことたりそう。 ただ、そのまま256KBをインプリしてみると、タイミング検証で赤いレポートが…。 Fmaxが44MHzしか出なくって、波形生成部などがまともに動かなくなってしまった。 さすがに内蔵メモリの確保量としては多すぎたか?と反省して確保量を再検討。 “VagrantStory”のバッファスタートアドレスが0x3A910で一番小さかったので、 上位3bit(0b111)は無視して有効アドレスは15bitとなり、 必要な内蔵メモリ量は64KB(16bit幅で32KWord)まで減らせた。 これに加えて、バッファメモリのアドレスバスとデータバスをレジスタで挟み込んでやって、 何とかFmax53MHzをクリアした。 楽しないで、リバーブバッファをサウンドメモリの中に確保するのであれば、 波形フェッチリクエストなどとアービトレーションする必要がある。 実は回路自体は実装してあって、あとは切り替えるだけなのですが。 動作は22050Hzごとで、メモリアクセスは32回行う。 演算はメモリアクセスと並行して行っている。 演算のアルゴリズム(響きとしては“アルガリズマ”だよね)については、 意外と複雑な感じだったので解析資料通りに実装。 サウンドに響きが生まれた。
実際はループバッファとして使用するのでカレントアドレスをインクリメントし、
メモリ領域の最後尾を越えたらスタートアドレスに戻る必要がある。
これを忘れていたため最初、音としては、不意に「コンっ」とか「カンッ」とか聴こえてきていた
実際、リバーブの効き具合をどうするかというのは、
リバーブ出力をメイン出力にどれだけ加算するかによる。
これについては実際に聴いてみて好みで調整した。
この動画はちょっと効きすぎっぽい。
▼ 実装のまとめ - 反省することはありますん -
DE2-115 FPGAボードへのフィッティングレポート。
やはり全チャンネルをパイプライン化せずに実装したため無駄に回路規模が増えた。
これはやはりどうにかせにゃ
未実装の機能は、FM変調とノイズ波形。 それから波形データ終了時の割り込みがあるらしい。 気が向いたら実装する予定。 “FF7”の一部の曲ではFM変調が使用されているようだ。 その昔、プレステを買って、遊んで(いや先にソフトを買ったんだっけ)、 そしてもちろん分解もした。 でもまさか当時の自分が、将来サウンドチップの写しを作ることになろうとは想像できただろうか。 これもSFLとFPGAとの出会いがあってのことだと思う。 そして本プロジェクトは、 構想当初からTwitterと連動して開発を進めたTwitter駆動開発(Twitter Driven Development:TwDD)だったように思う。 Twitterに思いついたアイディアをツイートして検討し、 開発中につまずいたことをツイートし、 動いただの動かないだの進捗をツイートし、 サウンドが演奏できたらこれを成果としてツイートし、 また次のモチベーションにつなげる。 そして本サイトはまさに関連ツイートを時系列にかき集めてまとめたものになっている。 みなさま方からのリプ・リツイート・ふぁぼ等、たくさんたくさん頂いて誠に感謝であります。 ちゃんとFPGAで演奏できてるよ。
さて次は…どうしましょうか。グラフィック?
▼ おまけ:スーファミ音源との違いについてスーファミの音源チップもSONYが設計したものであり、 プレステのSPUとは兄弟(姉妹?)のようなもので、 波形生成に関する部分についてのアーキテクチャは非常に似ている。 SPUがスーファミ音源からどれだけ進化したか表にまとめてみた。
動作周波数の向上により最大同時発音数が3倍になったことと、 サンプリング周波数が一般的なCD-DAに合わせてか44.1kHzになったこと。 そして、BRRデコードの元波形再現性が良くなったこと。 これらにより内蔵音源としての表現能力が大きく向上している。 大きな違いは、サウンドドライバの場所で、 スーファミはサウンドメモリに置かなければならなかったが、 プレステはメインメモリに置くことで、サウンドメモリに余裕ができた。 これは、プレステではメインCPUの性能向上によりサウンドドライバも処理できるようになったことが大きいのではなかろうか。 これに関連して、なぜスーファミではSONY設計の音源を採用したか、 もちろん音源自体の性能の良さを買われたという話は有名だが、他に以下の理由が考えられる。
最終的にはスーファミで使用されるLSIもシュリンクして全て1chipとなったけど、 そんな理由もあったんじゃないかと妄想してしまう。 そしてスーファミは、音源部を自社開発しなかった弊害として、 サウンドメモリへの波形データ転送には、 CPUとSPC700を経由せざるを得ない設計となってしまったのではなかろうか。 この点、プレステではDMAでサウンドメモリにデータ転送できるようになって、 プログラマ的には楽になったのかもしれない。 サウンドデータの転送時間がどれだけゲームに影響しているかは測りかねるところではあるけど。
よく分からないのが、ニブルのシフトがスーファミでは左シフトだったものが、
プレステでは右シフトになったこと。
ゲインが大きいときにシフト値が大きくなるスーファミ方式の方が直観的だと思われるが、
BRRデコード回路的には、左シフト時に必要なシフト値13,14,15の時の処理が不要になるので、
右シフトにしたとも考えられる。
▼ あれやこれや
|