NSF Player for Gameboy Color ver.2


img
GB 音源の研究の成果を反映させるべく改良を加えてみました。
といっても大きく変わったところは無く、内部的な変更が主で
仕切り直しの意味が大きいです。

速度的な面はかなり煮詰まってきて、音質の向上や、まともに聴ける物についての
再現性をどれだけ上げられるかが課題になってきました。
しかし他のプレイヤーで聴ける NSF の単なる答え合わせというだけでは
面白くないので、お遊びで機能を一つ加えてみました。





■ NSF Player for Gameboy Color ver.2

・GBNSF ver.2 本体 gbnsfv2_20090713.zip (2009.07.13版) (new)

 .NET Framework 3.5 以上が必要です。Windows Update で入手できます。

 使い方は前のバージョンと同じです。Drag & Dropでいけます。
 手順は以下の通り。

変換が終わると、拡張子が「.gbc」のファイルが出力されます。
GB エミュレータまたは GB実機上で実行できます。

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


 メモリインターフェースを変えたので、VRC6/7 namco106等拡張音源を使う NSF は暴走すると思います。
 一部の NSF で、拡張音源を使っていないのに ROM 領域に書き込みをするものがあり、これも暴走します。


前のバージョンからの主な変更点を書いておきます。
 ・ソースコードを整理して CPU 関係をまとめました。nsf80.z80の方は幾分すっきりしました。
 ・メモリーインターフェースを書き直しました。詳しくは下の方にまとめてあります。
  全体として数%高速化したかどうかという所です。

 ・エンベロープのパラメータを見直しました。NESとGBの減衰時間が近似するようにしました。
 ・スイープ機能をソフトウェアエミュレーションで実装しました。
   ch.1/2で違いが無いなど完全ではありませんが、ほぼ違和感なく聞こえるのではないかと。
 ・曲名の他に作曲者情報など表示するようにしてみました。
 ・ステレオ化機能を追加してみました。曲選択時に Bボタンを押しているとch.1/2がステレオ化します。

 12.03こっそり修正。バンク無し版NSFの速度が若干向上(気のせいレベル)

20090526版での変更

 ・CPUエミュレータを若干高速化。21の命令でそれぞれ28クロック高速になりました。
 ・APUシミュレータを8割方書き直し。再現性を上げたつもりですが、正直微妙…。確実に重くはなっています。
 ・CPU→APU と交互に実行する方式をやめ、音源レジスタに書き込んだ時点で変更を反映するようにしました。
  フレームシーケンサ(音長をカウントする音源の機構)は従来通り vblank タイミングで動いています。
 ・その他いろいろ勘違いしていた部分、無駄のあった部分を修正。NEZPLUGを参考にしました。

20090604版での変更

 ・ノイズチャンネルの音量を保持したレジスタをバンク切り替えの巻き添えで上書きしていたバグを修正。
 ・NESのハードウェアエンベロープループが正しく機能していなかった件を修正。
  ビブラートっぽい効果が正常になりました。が、GBには無い機能なのでソフト的に実現しています。
  曲によってはプチノイズが大量に乗ります。

20090713版での変更

 ・CPU B**系分岐命令8個,bit命令,LSR命令5個を高速化。256バイト境界時に無駄に待避していたレジスタを解放。
 ・APU 三角波チャンネルの線形カウンタの挙動を修正。線形カウンタに0がセットされたときは、bit7(長さカウンタ有効無効)が
  0,1どちらであっても直ちに消音となる。これで音長をコントロールしているドライバがありました。音長関係はこれでほぼ解決??
 ・前回の更新でプチノイズが発生していたエンベロープ処理を修正。
  ただしエンベロープ処理自体に不具合があります。キーオンが二重三重になってしまうというもの。スーパーマリオのゲームオーバー等で顕著。
  完全なソフトエンベロープにしてしまえば解決するのですが、GBではプチノイズが大量発生してしまうので難しいです。
  88版では直してあります。


■ What You Hear Is What You Get ??

前回 NSF Player を一応完成させて「もうしばらく NES も GB もいいや」と思っていたのですが
時間が経つといろいろアイデアが浮かんでくるもので、どうすれば何clock 縮まるかとか
そんなことを To Do に書き連ねていたら、ついに Version.2 に着手することになってしまいました。

結論から言うと、モノになったのは半分もないのですが、どこがダメだったのか書き残しておくと
次に繋がるかもしれないので、ここにそのあれこれを長々と記してみます。

・メモリインターフェースの改良

前のバージョンを作る際の候補の一つでした。
アドレス変換が遅くなりそうだったので見合わせていたのですが、今回はこれを採用。
img

NSF には Load Address というのが設定されていて、これは NSF 本体(プログラムとデータ)を
NES の、どのアドレスから配置すべきか、ということを示しています。
バンクを使わない NSFの場合、NSF ファイルのヘッダを除いた 1バイト目は、この Load Address に
あるものとされます。以下 NSFファイルの終端まで連続して配置されます。
バンクを使う場合は、NSF ファイルのヘッダを除く 1バイト目からバンク#0とするのですが、
こちらも同様に Load Address と $0FFF で AND を取った値だけずらして開始することになります。

例えば、Load Address=$8123 だった場合、$8000-$8122 までは $00 で埋めておいて良いわけです。
バンクを使う場合も $123 バイト分、バンク#0の頭に空白を入れておきます。以下、全バンク $123バイトずれます。
(バンクを使わない NSF はバンクが順に #0,#1,#2,#3,#4,#5,#6,#7 固定の NSF と見ることもできます。)

以前のバージョンは、このずらす作業を GB側で行っており、RAM に転送したときにちょうど NES の
アドレス空間に合致するようにずらす値を計算していたのですが、今回はコンバータで Player と NSF を
くっつける際にあらかじめずらしておくようにしました。

これにより、NES のアドレスと GB のアドレスの下位 12bit(AND $0FFF)が一致するようになり、
「アドレスが一致するようにずらして RAM に転送した NSF」にアクセスすることなく
直接 ROM に格納した NSF にアクセスすれば良いことになりました。
NESの4バンクがGBの1バンク(16KB)に格納できるため、バンク計算もシフト演算で楽に出来ます。
また、ストア系命令で $5FF8-$5FFF に書き込まれたかどうかのチェックも省略できます。

しかしデメリットもあって・・・
1)この方式でアクセスできるのは NES の ROM だけなので、アクセスする場所によって場合分けが必要。
2)RAM 転送方式では、バンク情報に基づいた内容が連続して RAM 上に再現されるため、アクセスの際に
  バンクを計算し直す必要は無かったが(NES の $8xxx なら必ず GB の RAM バンク #8)、この方式では
  1回のアクセス毎にバンク情報を参照して計算し直さなければならない。
3)この方式では命令がバンク境界をまたぐとき $xFFF -> $x000 の際に次のバンクがどこか
  必ずチェックが必要になる。

このあたりは NES_BankMemoryAdr のコードに試行錯誤が刻まれています。
上記2)3)ですが、バンクを使わない NSF の場合は、明らかに次のバンクが連続したアドレスであることが
分かっているので(#0〜#7固定バンクと見ることができる)、簡略化して高速化を図っています。

ときどき、もの凄く遅くてとても聴けたものではない NSF などがありますが(主にボーカル再現ものとか)
ピッチベンドなどを多用してメモリアクセスが頻繁にあると、このボトルネックの BankMemoryAdr にひっかかって
酷いことになります。

img

こちらは RAM のアクセスに関してです。
以前のバージョンでは NES に実装されていない RAM または I/O についても 32KB 分の領域を取って
アクセスを簡略化していたのですが、どう考えても無駄なので手を加えてみました。
ROMと同じく BankMemoryAdr で RAM 側に場合分けされた時に上の図のようにアクセスします。
ROM->RAM 転送をやめたことと併せて省メモリになりました。32KB版を新たに用意する必要もありません。

副産物として RAM メモリをバンク切り替えする必要がなくなったので、CPUエミュレータで切り替えていた
部分のコード(主にゼロページやスタックメモリアクセス)を削除することが出来ました。
ただし、やはりこちらもアドレス計算が若干増えたのでトータルでの負荷は相変わらずです。
さらに副産物として、範囲外メモリアクセスを全く想定していないので、FDS等拡張音源で妙なところに
アクセスすると、メモリが破壊されて暴走する可能性があります。

・CPUエミュレーション

GB は常時 NES の PC があるバンクとアドレスを保持しているのですが、NES のメモリアクセスの際に
GB のバンクを切り替える必要が出てきます。逆に言うと、この時以外はバンクが切り替わらないので
毎命令実行時にバンクを元に戻していた部分を、必要に応じて行うことで高速化しました。
CLC 命令などメモリアクセスを伴わない命令はバンク切り替え->バンク戻しの必要が無いわけです。
20clock 減ですが、微妙に効果があったように思います。

フラグのテーブル化(失敗)

ADC 命令など、命令自体は単純でもフラグの変化が多いものはコードサイズも大きく速度も遅いです。
(ADC命令で、フラグ変化の計算に最長84clock,最短でも72clockかかる)
これをテーブル引きすれば速くなるのではないかと思ったのですが、まずテーブルのサイズ。
a と b を足して cf を加えるとすると、全部で 17bit * 1byte=128KB のテーブルが必要になります。
(a と b が逆でも ADC の結果は同じだがフラグ変化は異なるため、半分にはならない)
これをテーブル参照するコードを書いてみたところ、124clockも掛かってしまいました。
バンク計算・切り替えやバンク戻しが必要になるため、単純に計算した方が早いという結果に終わり失敗。

Vblank の解放(失敗)

タイトルが横スクロールするといいかも、と思ってまずは vblank を割り込み禁止状態で占有している
CPU を busy フラグを見て駆動する方式に変えたところ、とある NSF がもっさり遅くなってしまい、
かなりギリギリのタイミングであることが判明。
うまくいけば Hblank を見てスクロールレジスタを弄る程度の余裕はあるだろうという甘い考えも頓挫。
同様に GUI っぽいインターフェースも作れたらと思っていたのですが今のところ妄想です。

高速化の余地(未定)

ストア系命令は ROM にアクセスしないだろう、という観点から ROM/RAM の場合分けをスキップできるかも。
これで 20clock 減。

NES の 1バンク = GB の 1バンク、ということにすると、アクセスは何割か高速化できるはずです。
4で割ったり4で割った余りを求めるコードをそっくり取り払えるので。
コンバータでの対応が必要ですが、NSF 自体の 4倍の容量が必要になるでしょう。でもまだ余裕?

・APU シミュレーション

エンベロープパラメータ修正

NES のエンベロープは初期ボリュームが 15(最大)固定で、x/240 秒(x=1〜16)ごとに 1 ずつ減っていく仕様です。
以前のバージョンでは、この減衰時間(1〜16)を GB の仕様にあわせた(1〜7)に変換していたのですが、あまりにも
いいかげんだったので、より近くなるように改良してみました。

GB のエンベロープ仕様では、初期ボリュームが 0〜15 で自由、x/64秒(x=1〜7)ごとに 1 ずつ減らせます。
一番短いエンベロープでは、NESが 1/240(秒) * 15(音量)で、0.0625秒で終わってしまうのに対し、
GB では 1/64 * 15 でおよそ0.234秒かかります。聴感上けっこう違いが出てくるので、GB の方は初期音量を
若干下げてエンベロープを始めることで、全体として減衰時間が近くなるように調整しました。
ノイズドラム等でエッヂの効き方が変わっていると思います。

スイープ機能追加

以前のバージョンでは GB のハードウェア機能を流用して ch.1 のみスイープを実装していたのですが
いかんせん仕様の違いで違和感がありすぎたので、ソフトウェアで再現してみることにしました。
NES のスイープ機能はフレームシーケンサと連動していて、1/120秒に1回、音程周波数を更新します。
これを 現状 1/60秒毎に回している GB で一度に2回分処理を行うことで、更新周期を同期させます。
(1/120 秒に 1 回 = 1/60 秒で 2 回)
重い処理なのでどうかと思ったのですが、割と良い感じに鳴っています。

本当は ch.1/2 で周波数計算に若干違いがあるらしいのですが、それは実装してません。
オーバー・アンダーフロー時のステータス挙動などにまだ不明な点があります。

ステレオ化機能

以前のバージョンでは、NR51のパン機能を使ってチャンネルの On/Off を行っていたのですが、
これを各チャンネルのマスターコントロールに切り替えました。
ということで左右にチャンネルを振ることが可能になったので、お遊びでつけてみました。
そういえばこんな機能、実際に拡張コネクタに刺す機械などでありましたね。

本当はユーザーインターフェースを作って、チャンネルミュートや各チャンネルを LR に割り付けるなど
設定できるようにしたかったのですが、現状ではここまでということで。

長さカウンタ・線形カウンタ(不完全)

ch.1/2/4 の長さカウンタについては、大体理解できていると思います。
残る ch.3 が難問で、改善されていません。
ある程度原因は推測できているのですが、
・長さ(線形)カウンタの有効/無効はフレームシーケンサが決定する。
・CPU が音源レジスタに書き込んでも、フレームシーケンサが次のクロックで動くまでは確定しない。
という点だと思います。
これを NES エミュレータで検証するため、テストプログラムを書いてみました。

NES 音源テストプログラム nes_test1.zip

長さ有効・線形カウンタ=$7F で発音します。A ボタンで長さ有効・線形カウンタ=$01 にします。
B ボタンは長さ無効を書き込みます。(いずれも $4008 のみ操作)

リセットして音が鳴っている間にボタンを押すと、ボタン操作が反映されないのが分かります。
しかし、ボタンを押したままリセット、あるいは十分速いタイミングでボタンを押すと反映されます。
フレームシーケンサは240Hzで動いているので、CPUがそれより速いペースで音源レジスタに書き込むと
前に書き込んだ値が取り消しになると考えられます。

ところが、本プレイヤーでは、60Hzで CPU->APU->CPU->APU と交互に処理しているので、
フレームシーケンサの動作より先に書き込んだかどうかというのは分からないのです。

うまい解決策は無いものでしょうか。
あるいは筆者の理解が全然間違っていたりするのかも。

クリックノイズ対策

ソフトエンベロープを多用する NSF はかなり音が荒れます。特に実機で。
GB 音源テストのページにもありますが、音量レジスタの扱いが難しいので
綺麗に再現できないのはある程度仕方ないのかとも思います。
一応、前の音量と同じ場合は音量レジスタに書き込まない等の対策は取ってありますが、
あまりやりすぎると処理が重くなるので、妥協しています。
本当は全部取り払ってエミュレータ専用にしたいくらいなのですが。

未実装機能

$4017:フレームIRQというのが何なのかよくわかりません。必要?
このレジスタを同期を取るためにアクセスしている NSF を見るのですが・・・。

DPCM:機能はおおよそ理解しました。Timer割り込みで再生するコードの初期版を
一応書いてみたのですが、それ以前に Vblank 割り込みが解放できなかったのでボツに。
実装したところでノイズに音割れでまともには聴こえないでしょう。

・その他

漢字フォントを入れたかった。4Bankあれば 8x8dot フォントが第一・二水準まで入るので
良さそうだったのですが、他の実装項目が優先ということで。

とりあえず、激烈に遅い NSF を「頑張ってるな」程度に聴けるようにするのが目標です。
良いアイデアお持ちの方はご連絡いただけると幸いです。
NES->GBのリコンパイラとかは無しで(笑)。



▲ TOP