NSF Player for Gameboy Color ver.3


img
GB 上での NES(RP2A03)エミュレータの成果として NSF プレイヤーを作りました。

NES の演奏プログラムを CPU レベルでエミュレートし、出音は GB のハードウェアを流用して鳴らします。

NES と GB の音源は似ているところもあるけれどやはり別物なので PC での NSF プレイヤーのようなこなれた鳴り方はしません。

聴き慣れた曲も違うハードで再生すると、ドライバの軽重や編曲の巧妙さを知る機会になることでしょう。





■ NSF Player for Gameboy Color ver.3

・GBNSF ver.3 本体 gbnsfv3_20251019.zip (2025.10.19版)

添付の RGBFIX は RGBDS contributors による Open Source Software です。
Released under the MIT license

NSF が何であるかについての説明は省略。分かる方向けです。

変換方法は以下の通り。
変換したい NSF を sample.nsf とすると、makensf.bat に放り込むことで GBNSF.GBC が出来上がります。



GBC エミュレータまたは GBC 実機上で実行できます。
モノクロ GB では実行できません。

または、以下のようにドラッグ&ドロップの代わりに、コマンドラインから変換することも出来ます。



同梱の nsfremap.exe は python 製で、例えば NSF のロードアドレスが $8123 だった場合、
NSF データ本体の前に $123bytes の $00 を挿入してロードアドレスを $8000 に修正して tmp.nsf として出力します。
実行の高速化の一環としてファイルを加工するものです。

操作方法は、十字キー左右で曲番号を送り、A ボタンで演奏開始、B ボタンで演奏停止です。

かつて公開していた V2 以前のバージョンから変更を加えています。
・CPU エミュレーションのバグ修正、高速化。トータルではあまり速くなっていない。
・APU エミュレーションの精度向上。フレームシーケンサを 240Hz で動作するようにした。
・エンベロープ、スイープを GB のハード機能を使用せず、ソフト的なエミュレーションで再現するようにした。

■ ありすぎる制限

まず、一聴して「音がガビガビ」という感想を持つと思います。
これは GB 音源の宿命で、音量レジスタを短時間に細かく操作するとポップノイズが発生するという仕様によるものです。
NES の場合はこの仕様とは無縁なのでエンベロープなどを多用する曲が作れるのですが GB だと厳し目。

DPCM が鳴りません。
これは単に音源チャンネルが足りないだけです。
NES の ch1.2(矩形波) は GB の ch1.2(矩形波)で、
NES の ch.3(三角波) は GB の ch.3(波形メモリ)で、
NES の ch.4(ノイズ) は GB の ch.4(ノイズ) で出力しています。
NES と GB のそれぞれの仕様も違うので変換を工夫しています。
ノイズはまだまだ似ていません。

一部の NSF が鳴りません。
拡張音源を使っているものは鳴りません。音源が無いという理由のほかにメモリモデルが特殊で対応が難しいためです。
バンク切り替えや特殊レジスタへの書き込みなどに ROM 領域への書き込みが必要なのですが、
読み込みとは別動作が必要なものは例外処理を追加しなければならず難しいのです。

本体のみのノーマル音源な NSF も鳴らない(暴走する)ものがあるかもしれません。
これは $6000-$7FFF の領域(本来は FDS 用の RAM)を存在している前提で流用する NSF があるからです。
一応、元の NSF の仕様でも「初期化してから演奏開始せよ」≒存在が前提にはなっているのですが…。

演奏が遅いのはエミュレーションの負荷が高さが原因です。
NES における NMI は GB の vblank 毎に実行しています。
フレームシーケンサは 240Hz で動作して本来 NES の CPU とは独立して動きますが、GB ではタイマー割込みで駆動します。
なので vblank とは排他動作になるので実質 240Hz では駆動出来ていないことになります。
影響はエンベロープやスイープ、音長に現れます。

テストプログラムの作成や実行で nesdev.wiki や messen エミュレータには相当お世話になりました。多謝。


■ 大雑把な解説

8bit CPU(GB) で 8bit CPU(NES) をエミュレートしています。
インタープリター方式で、フラグ変化などは GB のフラグを流用したり新たに計算したりしています。
テーブルを使うのも一つの手なのですが、参照の手間を考えるとそこまで有利でも無い気がします。

NSF のファイルは 4KB 単位のメモリブロックになっており、GB ではこれを 16KB 単位 $4000-$7FFF のバンクメモリに順次格納しています。
NSF 仕様ではこのメモリブロックは $8000-$FFFF の 32KB メモリ範囲に 4KB 単位で自由に並べ替えて配置できます(演奏中も変更可)。
なのでエミュレートする際は、例えば NES のアドレス $8000 がどのバンクのどのアドレスを指すのかを逐一調べる必要があり、
GB の場合は該当アドレスが格納されているバンクに適切に切り替えた後、アドレスを変換して読み書きする処理をしています。



こんなに極端な配置ではない…。

NES のメモリアクセスの内、RAM は $0000-$07FF の 2KB しか(基本的には)ないのですが、
GB 側で 2KB だけを用意しておけばよいのかというとそんなことはなく、読み書きできるメモリマップド I/O レジスタも用意しなければいけません。
具体的には $2000-$2007 の PPU、$4000-$401F の APU、$5FF8-$5FFF のバンク切り替えレジスタです。
ROM/RAM 含め、64KB メモリ空間のアクセスの内、どのアドレスにアクセスしたらどこにリダイレクトさせるかは
いちいち条件分岐していては遅すぎるのでテーブル化して参照しています。



NSF に PPU は必要ないので省略していますが、NES エミュレータの方では $B000-$BFFF に割り当てています。
$6000-$7FFF の FDS RAM は無理をすれば割り当てられるのですが、連続した 8KB を確保するための犠牲が大きいので断念。

APU(音源レジスタ)のように書き込んだら何か特定の動作をするようなものは
一旦書き込んだ後、その値を解釈して音源シミュレータにて近似した動作になるよう変換した上で GB 音源を操作しています。

過去のバージョンではフレームシーケンサは vblank 同期(つまり 60Hz)で一度に 4 回分の動作をすることで 240Hz 相当にしていました。
今バージョンでは 240Hz に近いタイマー割込みで駆動し、エンベロープもそれに同期した音量変化をするようにしています。



▲ TOP