ファミコンのノイズチャンネルの再現方法

2021-03-07

ファミコンエミュレータで音を鳴らす際にノイズチャンネルはどうやって鳴らせばいいのか分からなくて、ひとまず適当に createPeriodicWave() をランダムで埋めてホワイトノイズみたいなものを鳴らしていただけだった。 しかしOptcarrotのソースを読んでるうちに少し分かってきた。

ノイズチャンネルの動作

サウンドチップであるAPUの解説はNesdev wikiのAPU Noiseに詳しい:

  • 15ビットのレジスタがある(初期値は1)
  • ビット0が0の場合、指定の音量を出力
  • クロックが進むとき:
    • ビット0とビット1のXORを取る
    • 1ビット右シフト
    • 最上位ビット(14)はXORの結果とする

このように動かしたときビット0が疑似乱数のように0と1を繰り返し、それによってノイズを発生させることができる。 周期は32767になる。

モードによってはビット1とじゃなくビット6とXORを取る。 その場合周期は93または31となるとのこと(その時のレジスタの値により)。

クロックの進め方

同じNesdev wikiのAPUページの用語集に注意点として書いてある:

A timer is used in each of the five channels to control the sound frequency. It contains a divider which is clocked by the CPU clock. The triangle channel’s timer is clocked on every CPU cycle, but the pulse, noise, and DMC timers are clocked only on every second CPU cycle and thus produce only even periods.

5チャンネルの各チャンネルには、音の周波数を制御するためのタイマーが搭載されています。 これにはCPUクロックでクロックされる分周器が含まれています。 トライアングルチャンネルのタイマーはCPUサイクルごとにクロックされますが、パルスタイマー、ノイズタイマー、DMCタイマーは2CPUサイクルごとにクロックされるため、偶数周期しか生成されません。

なのでノイズチャンネルはCPUの周波数(≒1.79MHz)の半分で動作する。

A divider outputs a clock periodically. It contains a period reload value, P, and a counter, that starts at P. When the divider is clocked, if the counter is currently 0, it is reloaded with P and generates an output clock, otherwise the counter is decremented. In other words, the divider’s period is P + 1.

分周器は周期的にクロックを出力します。 分周器には周期リロード値Pと、Pから始まるカウンタが含まれています。 分周器がクロックされると、現在カウンタが0であればPでリロードされて出力クロックが発生し、そうでなければカウンタがデクリメントされます。 つまり、分周器の周期はP+1です。

P値は$400eの下位4ビットの値を元にテーブル参照: [4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068]

WebAudioで波形を鳴らす(AudioWorklet)

WebAudioで任意の波形を鳴らすにはScriptProcessorNodeでできる…と思ってたんだけど すでにdeprecatedにされていた:

@deprecated — As of the August 29 2014 Web Audio API spec publication, this feature has been marked as deprecated, and was replaced by AudioWorklet (see AudioWorkletNode).

メッセージに従ってAudioWorkletNodeを見てみることにした。 Examplesが取っ掛かりとしてわかりやすい。

  • 別スクリプトをworkletとして起動する
  • registerProcessorで登録したクラスのprocess関数が呼び出されるので、Float32Array型のoutputを埋めてやると再生される
  • メイン側とやり取りするにはportが使える
    • parametersは波形の各時刻に関する情報の受け渡し用途
  • サンプリングレートはsampleRateに格納されている
  • 動作を停止させるにはprocessfalseを返してやる

悲しいことにAudioWorkletSafariで未実装なので、 使えない場合にはScriptProcessorNodeにフォールバックしてやる必要がある (またはpolyfillを使う)。

サンプルごとの処理

AudioWorklet側のサンプリングレートが例えば44.1kHzの場合、1秒分の波形は44100個の値に対応する。 サンプル1つごとに時間的には1/44100秒進むことになるので、それに対応する時間分APU側のタイマーを進めてクロック処理してやればよい。

できれば整数で扱いたいので、あらかじめサンプリングレートとAPUの周波数の最小公倍数から適切な係数を掛けて固定小数点として扱うとよい: Optcarrotのコードを参照

(Optcarrotの数値を見ていくとどうやらCPUの周波数/2じゃなくそのままの値に、また1/(P+1)じゃなく1/Pになってるぽいんだが、聞き分けられるほどの違いを感じなかった、 なにが正解なんだ…。)

成果物