dev APUファミコンのサウンドユニットを実装することで、 なつかしのサウンドを再現することができました。 モジュールごとに分割すれば見通しよく実装できます。 _
|
CPUのクロックはとにかくフレームレートの60Hzに合わせればよかったため、 クロック数が重要であり、周波数については正確ではありません。 APUはサウンドジェネレータであるため、ベースクロックが均一でないと、 サウンドも不安定になります。 そこでAPUへはできるだけ実機の約1.789MHzに近くて均一な周期を入力する必要があります。 CQ出版評価ボードの33MHzを18で分割すると1.833MHz、 19で分割すると1.737MHzとなるので、 18分割を何回、19分割を何回と繰り返すことで1.789MHzに近い周波数が得られるようです。 その比率は18分割を56.2%、19分割を43.8%となり、18分割の回数が多くなります。 18分割と19分割を交互に行いながら、 12.4%分の18分割をできるだけ短い範囲にちりばめることで、実機に近い周波数を生成できます。 分数-N合成よりもよりばらつきを抑え、単純な回路で構成可能なのがDDS。 ポイントは適切なビット幅と加算値を使用すること。 こちらで解説。 | ! |
今回使用した周波数合成は分数-N合成と呼ばれるらしい。
他にもダイレクトディジタル周波数合成(DDS)があるらしい。 |
(DMCは置いといて)各チャンネルの出力は4bitなので、 これを加算すると6bitになります。 出力ピンを減らすために、1ビットDACを使用します。 ベースクロックとして評価ボードの33MHzを使用しました。 とりあえずPWMを実装してみたところ、サウンドは聞こえますが、やはりバリが強く感じられます。 これはPWMを使用すると、量子化誤差によるノイズ成分が各周波数帯域に均等にのってしまうためです。 また可聴領域外の波形を拾ってしまい、サウンドの再現性は低くなります。 このためデルタシグマ型DACを実装しました。 これはシフトレジスタと加算器のみで構成できるため実装も容易です。 全体の量子化誤差量は変わりませんが、 ノイズ成分を人間の可聴領域よりも高い周波数帯へ押しやってしまうため、 PWMより音のバリは低減されます。 | ! | ハナッから実装してればいいものを… |
NESサウンド用ファイルフォーマットNSFの演奏が可能です。
バージョンアップ! 1MBまでのNSFファイルに対応。 複数曲対応。SDカードからの読み込み。 各種拡張音源実装(N106、MMC5、FDS、VRC6、SN5B、VRC7)。
| ! | 仕事は単純なんだけど、思いのほか感動。 |
![]() ディレイユニットにより、音源の定位をずらしてステレオにしました。 正面を0度として、+60度、+30度、-30度、-60度の位置にそれぞれ 矩形波1、三角波、ノイズ、矩形波2というように配置。 定位をずらすには、ずらしたい方向のサウンドを遅らせればいいので、 256サンプル遅れで60度、128サンプル遅れで30度と、だいたいですが。 サンプリングはAPU内の1.79MHzで行っています。 | ! | ほら、GBってステレオっしょ。 ステレオ感ではGBの勝ち。 |
これはNESに限ったことではないですが、
出力周波数の低域ではそうでなくとも、
高域になるにつれてなんだか音痴になるそうです。
所望(Hz) 理想設定 実際設定 出力(Hz) 誤差(Hz) 55 1015.9 1016 55.00 0.00 110 507.4 507 110.10 0.10 * 220 253.2 253 220.20 0.20 * 440 126.1 126 440.40 0.40 * 880 62.5 63 873.91 6.09 * 1760 30.7 31 1747.82 12.18 * 3520 14.8 15 3495.65 24.35 7040 6.9 7 6991.30 48.70 14080 2.9 3 13982.60 97.40 * よく使用される 欲しい出力周波数に対する理想設定を四捨五入し、 実際にタイマへ設定する値を算出します。 希望した出力周波数とその設定値のときの実際の出力周波数の誤差をとると、 高音域になるほど誤差が多くなっています。 これは高域になるにつれて理想設定と実際の設定値の差の割合が大きくなるためです。 三角波のテストをエミュ形式(.nes)にしておいときます。 Aボタンを押す度に周波数が2バイ2バイ。 [apu_tri.nes] | ! |
聞く所によると、「チューニングの許容範囲は0.5Hz以内」(ジャズ研部長談)らしいので、
数値だけを見れば聴く人が聴けば違和感を感じそうです。
しかしNESの中で完結していれば(他の楽器と聴き比べなければ)、 音階は相対的なものなのでそう気になることはないと思います。 「これがNESサウンドなんだよ」と言ってしまえばそれまで。 |
三角波のコントロールレジスタ$4008のビット7については、 1を書き込んだ場合、長さカウンタが停止されず、 またラインカウンタの値も反映されないため音が出続けてしまう。 (W:Write)
W $4015 $04 三角波チャンネル有効
W $4008 $F8 リニアカウンタOFF
W $400A $D5 周波数下位$D5
W $400B $08 周波数上位$0、長さカウンタ $01(2.1秒)
♪ドーーー…と鳴り続ける。
そこで、ラインカウンタを有効にするため、$4008のビット7に0をセットし、 ラインカウンタ値を有効にする。 これで長さカウンタが動作している間にリニアカウンタも動作し、 0.5秒間発音する。
W $4015 $04 三角波チャンネル有効
W $4008 $78 リニアカウンタON、リニアカウンタ120(0.5秒),
W $400A $D5 周波数下位$D5
W $400B $08 周波数上位$0、長さカウンタ $01(2.1秒)
♪ド、と0.5秒間鳴る。
これはマリオのコードを参照したものだが、 音長を長さカウンタでなくリニアカウンタでコントロールしているからと思われる。 リニアカウンタは240Hzで動作するため、 フレームタイミングと合わせやすいからこのような方法が使われるのでしょうか。
ドラゴンクエストでサウンドが鳴らない不具合が発覚。 ログを調査したところどうやらフレームIRQを使用してサウンドを鳴らしている模様。
フレームIRQとは、$4017へ上位2ビット0の値をwriteするとフレームシーケンサモード0:240Hz、 IRQ有効となり、60HzでIRQがCPUに対して生成される。 フレームIRQはカートリッジからのIRQ同様に、 イネーブルされるとCPUへIRQ割り込みが入るが、 NMI割り込みと違ってCPUのIフラグがセットされているとすぐに割り込み処理は実行されない。
ここでポイントなのは、IRQはイネーブルホールドであり、 ワンクロックのトリガではないということ。 CLI命令やRTI命令実行後にIフラグがクリアされ、 そのときまでIRQがイネーブルであればIRQ割り込みが処理される。
はじめ、IRQ信号をワンクロックのトリガで実装していたところ、 Iフラグがセットされていた場合にIRQが無視され(当然だが)てしまい、 ドラクエのサウンドが不安定になっていた。 IRQをイネーブルホールドすることでCLI命令の後にIRQ割り込みが実行され、 正常にサウンドが鳴った。
ちなみにこのイネーブルホールドされるフレームIRQは、 $4015へのreadによってディセーブルされる。
FF3のバトル曲ではドラムパートが特徴的なキック音を出している。 これについて以前どこかで、容量の都合でちょっと変わったDPCMの使い方をしている、と聞いたので改めて調べてみた。 [ファイナルファンタジー用語辞典Wiki の 音楽/【バトル1】(FF3)]によると、 「PCM音の再生、停止時にどうしても発生してしまうクリックノイズをわざと発生させる」ことでバスドラム音を実現しているとのこと。
具体的に実際のログを見てみると、
NMI ・・・ LDA 0xFF STA 0x4011 Write 0x4011 0xFF (ビット7は無視されるので実質0x7F) ・・・ Write 0x4015 0x0F (ビット4が0なのでDPCMの通常動作は無効) ・・・ LDA 0x00 STA 0x4011 Write 0x4011 0x00 ・・・というように、DPCMのデルタカウントレジスタ(0x4011)に直接値を0xFF→0x00と書き込むことで、 そのまま0→Max→0のパルスが出力される。これがクリックノイズの正体。 といってもパルス幅が短時間だと本当に単なるクリックノイズなので、 上の・・・の所では他の処理が実行されていて、ある程度のパルス幅が確保されている。
つまりどういう事かと言うと、APUのチャンネル制御レジスタ(0x4015)のビット4でDPCMを無効にしても0x4011によって出力は変化するので、 デルタカウンタから出力のパスはマスクせずに直結させる実装が正解。
なお、FamiTrackerでこれに近いことをやる場合はNabecさんの [他チャンネルへの影響を抑えたDPCMノイズの使用方法について]が詳しい。
資料を参考にして5日ほどで矩形波×2、三角波、ノイズを実装。 一番楽なのは三角波でした。
各チャンネルで共通に使用されるタイマユニット、
エンベロープジェネレータ、長さカウンタはインスタンスとして利用できるようにまとめました。
長さカウンタテーブルは、各チャンネルの周波数レジスタ2への書き込み時にのみ使用されるため、
一つだけ実装して使いまわしです。
論理合成結果
Total equivalent gate count for design: 8,037あんまり意味があるデータとは思えない。
各チャンネルの出力は、実機では非線形の加算回路で加算されて出力されるようです。 FPGA内部でテーブル用意して真似できそうですが、とりあえず単なる加算して、PWMで出力。 出力には簡単なローパスフィルタをかましてヘッドフォンジャックへ。
この時点では、DMCのサンプルが手に入ってないためまだ実装していません。
フレームシーケンサの実装でマリオのサウンドがより生々しくなりました。 エンベロープジェネレータをそのまま240Hzでたたいてしまうと、 マリオのジャンプ時にピコノイズが入ってしまうようです。 実質192Hzでたたいていることになりますが、これでいいんだろうか?
沙羅曼蛇はパワーオンリセットを使ってるらしく、 ボードの電源ONからFPGAへの書き込みで音が鳴った。 別に書き込みタイミングが違うとかそういう問題じゃないんですね。
アルマジロでは三角波の無音化のために周期1の波形を指定しているようです。
これは56KHzで出力されるため人間の耳には聞こえないはずが、「キーン」という音が…。
いや俺の耳が良いわけじゃなくて。
どうやらPWMでサンプリングしてたため、可聴領域外の周波数を拾っていたらしい。
デルタシグマ型のDACを実装して無事カットできました。
スパルタンXの「アター」がDMCを使っているようなので、 実装しました。これに伴いAPUからのメモリアクセスが必要になります。
[ APUからのメモリアクセス > DMA > CPUからのメモリアクセス ]
という優先順位にして、自分より優先度が高いアクセスが発生している間は スリープしておくという状況。
合成結果
Total equivalent gate count for design: 10,495サウンドチャンネルを全て実装し、自前の演算器を使用してこのゲート数。 多いんだか少ないんだか。
サウンドをmidiで出力するっての一度やってみたかったんだが、 NESサウンドを愛してる人たちから怒られそうだ。
それよりNSFファイル再生機能がほしいなあ、ということでVer.3で追加。
ただ音を鳴らしてるだけじゃ芸がないので、ステレオにしてみました。 パッドの振動に加えて、より臨場感あふれるマリオがお楽しみいただけます。 ついでに、ある意味これがAPUの耐久テストとなるNSFサウンドの再生をやってみた。 サンプルとして、恐れ多くも例のコンペで優勝された方のオリジナル曲を使用させていただきました。 NES実機でのサウンド比較でも使用されています。聴いたらスゲッス。
ということで付加ルーチンを修正して聴いてみたところ、 高い再現性で演奏することができました。これはイイ!スゲエ! mp3にレコしてアップしたいっす。他のnsfも概ね良好で、 こうなったらN106とかほしいなあ。 | ! |
その気でいれば。
*耐久テスト まるでAPUが火を噴きそうなnsfだ! エッ? NESでビブラートって可能なん? この音圧はなんなんだ…。 |