FM音源ドライビング! task of VM2413

VRC7の実物を拝む勇気は無い。
以下、SFLからVHDLで記述されたFM音源をドライブする方法について記す。
VRC7を搭載したFCカートリッジとして唯一「ラグランジュポイント」が挙げられる。
しかしメイキングNSF業界ではVRC7を使用している曲がみられ、
VRC7を接続できれば 6502+YM2413 というある意味LowBitRevolution的でありながら滑らかなサウンドが期待できる。
VRC7の音源は、YAMAHAのFM音源ICであるYM2413(OPLL)と互換らしい。
YM2413はMSXのFM音源カートリッジFM-PACなどに搭載されたもので、
1chipMSXでも使われている。
ともすればソースコードがオープンになっている可能性が高く、
発掘したものが以下。
[VM2413(GitHub)]
[旧サイト]
Digital Sound AntiquesのBrezza氏。こちらがVM2413のオリジナルか。
[1chipMSX改]
HRA!氏。解析資料や改造したHDLコードを公開されている。
VRC7とはプリセット音色が一部異なるという噂もあるが、
てっとりばやく流用させていただきます(あとで指摘されるわけだが)。
ありがとうございます。
目次
・VM2413の構造
・マッパーとの接続
・VRC7への書き込みデータ
・書き込みタイミング
・シミュレーションで演奏波形を得る
・サウンド出力
・真のVRC7へ
_ ▼ VM2413の構造
VM2413のVHDLコードを入手できたので、
おもむろに QuartusII 7.2 Web Editionでコンパイルして回路規模などチェックする。
Brezza氏が公開しているコードの方が回路規模が小さく、
リリリトライしやすそうなのでまずこちらを使用してみる。
HRA!氏が公開しているものはBrezza氏のものよりタイムスタンプが新しく、コードが追加・修正されているようだ。
モジュール構成はほとんど同じなので、後から差し替えることも手間はかからない。

単純な性格だけど、本当のところ何を考えているのかよく分からない。そんな構造。
それぞれのピンの機能については名前のとおりだし、
基本的な情報は「YM2413アプリケーションマニュアル」から得ることができる。
(それでもvm2413のサウンドが耳に届くまでには、
いろんなYM2413を知っている人から見れば幼稚なアプローチだったのは否定できない。)
_ ▼ マッパーとの接続
とりあえずvm2413をNSFマッパーに接続して、なんとなく6502からアクセスできるようにする(この試みは失敗する)。
6502から見たメモリマップとしてvrcvii.txtによると、$9010へのwriteでレジスタアドレスを指定し、
$9030へのwriteで内部レジスタへデータがセットされるようだ。

なんとなく接続
・MapperNSF.sflp
レジスタアドレスとレジスタデータのwrite。Aとprg_Dinはそれぞれnesからのアドレスとデータ。
input A<15>, prg_Din<8>;
instrin write(A, prg_Din);
vm2413_interface vrc7;
instruct write par{
// VRC7 $9010, $9030
if(map_rom & (A<14:0>==0b001000000010000)){ // $9010
vrc7.write(0b0, prg_Din);
}
if(map_rom & (A<14:0>==0b001000000110000)){ // $9030
vrc7.write(0b1, prg_Din);
}
}
・vm2413_interface.vhd
レジスタポインタ動作はOpll.vhdの中で実装されているので、
その入力としてA_bufとD_bufをwrite時に保持する。
process(p_reset, m_clock) begin
if(p_reset='0') then
A_buf <= '0';
D_buf <= X"00";
elsif(m_clock'event and m_clock='1') then
if(write='1') then
A_buf <= A;
D_buf <= D;
end if;
end if;
end process;
またWE_nはwrite時にイネーブルとし、
Opll.vhdでクロックイネーブル時にデータが取り込まれたタイミングでディセーブルする。
process(p_reset, m_clock) begin
if(p_reset='0') then
WE_n <= '1';
elsif(m_clock'event and m_clock='1') then
if(write='1') then
WE_n <= '0';
elsif(run='1') then
WE_n <= '1';
end if;
end if;
end process;
vm2413との接続部分で、トップモジュールはOpll.vhd。
実際のYM2413ではXINに約3.58MHzのクロックが入力されるようだが、
ここではボードの50MHzを使用し、XENAに50MHzを14分周した信号を入力する(約3.57MHzごとにrunが1になる)。
VM2413の内部を確認してもこれで約3.57MHzで動作してくれるようだ。
XINにPLLから生成した3.58MHzを入力しクロックドメインを分ける方法もあるけど、
今回は完全同期で使用する。
OC : opll port map(
XIN => m_clock, --: in std_logic; 50MHz
XOUT => open, --: out std_logic;
XENA => run, --: in std_logic; 約3.57MHz
D => D_buf, --: in std_logic_vector(7 downto 0);
A => A_buf, --: in std_logic;
CS_n => WE_n, --: in std_logic;
WE_n => WE_n, --: in std_logic;
IC_n => IC_n, --: in std_logic; リセット
MO => MO, --: out std_logic_vector(9 downto 0); メロディ
RO => RO --: out std_logic_vector(9 downto 0); リズム
);
vm2413のサウンドとしてメロディとリズムが出力される。
NSFではリズムを利用しているものが見当たらなかったので、MOをそのままDACに接続した。
適当な接続ののち、SFL+はsflp、sfl2vlにてVerilogHDLに変換する。
QuartusIIはVerilogHDLもVHDLも読み込んでくれるので一緒くたにつっこんでコンパイル&コンフィグし、
VRC7を使用するNSFを演奏させてみた。
……が、なんかノイズが鳴ってるようにしか聞こえなかったわけで。
やっぱダメか。
で、原因を考えると、それぞれの信号の機能としては上記のとおりで問題ないが、
正常に演奏されなかったのはvm2413へのwriteタイミングが悪いということらしい。
6502がwriteしたタイミングでそのままvm2413へwriteしても取りこぼしがあるようだ。
「昨日のメール?ごめん今日のしか見てないけど」
なんて場合はまたメール出せばいいけど(出せねー)、できれば確実に届けたいこの想い。
またメロディ出力については、複数のチャンネル出力がどのように出力されるのか?
ゼロレベルは?振幅は?というように、波形の形式が分からないままでそのままDACに接続しても正常に鳴ってるのか判断できない。
言葉が通じないだけでなく文化も知らないのでは、お得意のボディジェスチャも通じない。
つまり次のことを明らかにすべき必要がある。
・vm2413へのwriteタイミング
・MOの出力タイミングと波形形式
これらは入手した資料を探っても明確にはならなかったので(見つけられなかった、とも言う)、
シミュレーションで検証し推察する。
資料は文字記録の断片でしかなく、事象の本質はコードのみから得られる。
_ ▼ VRC7への書き込みデータ
vm2413をシミュレーションするためにはサンプルとして入力データが必要ですね。
354.nsfをPCのNSFプレイヤで再生してみると、
分かりやすい音が演奏されてるようなのでこれをサンプルNSFとして使用する。
自作のエミュでVRC7へのwriteログを取ってみた。
frame 0
frame 1
frame 2
frame 3
ch0 A$30 D$0F inst 0 vol 15
org A$00 D$22
org A$01 D$61
org A$02 D$1B
org A$03 D$05
org A$04 D$C0
org A$05 D$A1
org A$06 D$F8
org A$07 D$E8
ch0 A$30 D$03 inst 0 vol 3
ch0 A$20 D$00 PitchH $000 sus 0 key 0 b 0
ch1 A$31 D$0F inst 0 vol 15
ch1 A$31 D$08 inst 0 vol 8
ch1 A$21 D$00 PitchH $000 sus 0 key 0 b 0
ch2 A$22 D$00 PitchH $000 sus 0 key 0 b 0
ch3 A$23 D$00 PitchH $000 sus 0 key 0 b 0
ch4 A$24 D$00 PitchH $000 sus 0 key 0 b 0
ch5 A$25 D$00 PitchH $000 sus 0 key 0 b 0
ch6 A$26 D$00 PitchH $000 sus 0 key 0 b 0
ch7 A$27 D$00 PitchH $000 sus 0 key 0 b 0
frame 4
...
frame 55
ch0 A$30 D$03 inst 0 vol 3
ch0 A$20 D$09 PitchH $100 sus 0 key 0 b 4
ch0 A$10 D$11 PitchL $111
ch0 A$20 D$19 PitchH $111 sus 0 key 1 b 4
ch1 A$21 D$00 PitchH $000 sus 0 key 0 b 0
frame 56
...
二度書きや未使用チャンネルを除くと、
org A$00 D$22 ┐
org A$01 D$61 │
org A$02 D$1B │
org A$03 D$05 │カスタム波形パラメータ
org A$04 D$C0 │
org A$05 D$A1 │
org A$06 D$F8 │
org A$07 D$E8 ┘
ch0 A$30 D$03 inst 0 vol 3 ← カスタム波形(0)を選択
ch0 A$20 D$09 PitchH $100 sus 0 key 0 b 4 ┐再生周波数の設定
ch0 A$10 D$11 PitchL $111 ┘
ch0 A$20 D$19 PitchH $111 sus 0 key 1 b 4 ← キーON
サンプルデータとしてこんな感じで設定できれば、チャンネル0からカスタム波形が再生されるはずだ。
_ ▼ 書き込みタイミング
タイミングを見るのには波形表示が分かりやすいので、
ModelSim-Altera Web Edition 6.1gを使用する。
vm2413.vhdにsubtype宣言してあるのでまずこれをコンパイルする。
そして他のVM2413のvhdをModelSimでコンパイルすると、
「vm2413.vhdでfunctionの入力信号名が一致していません!」ごめんなさい、
「integerの'range、'high、'lowはサポートしていません!」しろよ、
「case文でwhen others項がありません!」ごめんなさい、
「functionのリターンタイプで(8 downto 0)が(17 downto 9)と合っていません!」エラーにするなよ
などと合いの手を入れながら修正。
vm2413のシミュレーションにModelSimは使用されてないようだ。
また今回使用するModelSimは単一言語のみのサポートなので、
SFLをsfl2vhでVHDLに変換して使用する。
変換したところ状態変数の値が重複するというバグがあったらしく、
ツール開発者に報告したところすぐに修正していただいた。
ツールを使用して気になった点は何でも開発者に報告すべきで、
運がよければ対応していただける。
ありがとうございました。
clkは50MHzで、vm2413.runが14clkサイクル、
6502からのwriteは最短で28×(LDA(2)+STA(4))=168clkサイクルが想定される。
ただしwriteエラーの観測性とシミュレーション速度を上げるため(この場合は短時間シミュレーションなので効果は薄いが)、
vm2413.runを3clkサイクル、
6502からのwriteを6×(LDA(2)+STA(4))=36clkサイクルとした。
テストベンチはvm2413へのwriteプロシージャと、
-- LDA(2) + STA(4) = 6 cpu_run × 2
procedure write(
wadrs : in std_logic_vector(7 downto 0);
wdata : in std_logic_vector(7 downto 0);
signal we : out std_logic;
signal sel : out std_logic;
signal data : out std_logic_vector(7 downto 0)
) is
begin
-- アドレスwrite
for I in 0 to 5 loop
wait until cpu_run='1';
end loop;
we <= '1';
sel <= '0';
data <= wadrs;
wait until clk'event and clk='1';
we <= '0';
-- データwrite
for I in 0 to 5 loop
wait until cpu_run='1';
end loop;
we <= '1';
sel <= '1';
data <= wdata;
wait until clk'event and clk='1';
we <= '0';
end;
先ほどのwriteサンプルデータ。
process begin
we <= '0';
sel <= '0';
data <= (others=>'Z');
wait for CYCLE*5;
write(X"00", X"22", we, sel, data);
write(X"01", X"61", we, sel, data);
write(X"02", X"1B", we, sel, data);
write(X"03", X"05", we, sel, data);
write(X"04", X"C0", we, sel, data);
write(X"05", X"A1", we, sel, data);
write(X"06", X"F8", we, sel, data);
write(X"07", X"E8", we, sel, data);
write(X"30", X"03", we, sel, data);
write(X"20", X"09", we, sel, data);
write(X"10", X"11", we, sel, data);
write(X"20", X"19", we, sel, data);
wait;
end process;
シミュレーション波形で確認すべきは、
書き込みデータがvm2413の内部レジスタに正しくセットされるかどうかということ。
内部レジスタなどの管理はController.vhdで行われている。
見たところwriteの処理回数が少なく、6502からのwriteが取りこぼされている。
このためControllerの内部レジスタ(regs_wdata)の値が期待したものになっていない。

伝わらない、この想い。
Controller.vhdのコメントによれば、メロディ+リズムの各チャンネルはスロットごとに切り替わりながら動作する。
各スロットは4ステージで18スロットあり、9チャンネル分の処理で72サイクル必要とのこと。
注目すべきは内部レジスタの更新タイミングで、
各スロットの3ステージ目で1つのチャンネル分のみ更新される(上図黄色い縦ライン)。
つまり特定のチャンネルのレジスタを更新するためには、
データをwriteしたのち再writeまで72サイクルのウエイトをはさむ必要がある。
ただしレジスタアドレスはwriteによって即座に更新されるためウエイトは不要のようだ。
YM2413しかりvm2413では内部レジスタにRAMを使用しているため、このような構成になっているのか?
素直にアドレスデコーダと分散レジスタで実装されていればウエイトは必要ないが、
それでは回路規模が大きくなるのか?などと考えていると……。
「 YM2413アプリケーションマニュアル
・アドレスライトモード
(略)アドレスを書いた後に、(略)12クロックのウェイトが必要です。
・データライトモード
(略)別のアドレスに書き込む前には、84クロックのウェイトが必要です。 」
……ああ、書いてあるね!スルーしてたよマジゴメン。
まあvm2413を使用するうえではアドレスwriteには12サイクルほどのウエイトは必要なく、
データwriteについては84-12=72サイクルのウエイトが必要なことは間違いないようだ。
vm2413へのwriteは72サイクルのウエイトが必要だが、
6502はそんなこと知ったこっちゃなしに最短で12サイクルごとにwriteしてくる。
そんな速度差の違うオブジェクト間のデータ通信バッファとしてFIFOを使用する。
ここで問題になるのはFIFOの深さをどれくらいに見積もればいいかということ。
vm2413のwrite平均処理時間 Ts=72/2=36 [cycle/write]。
vm2413の動作サイクル 59524 [cycle/frame] から、1フレームで処理できるwrite回数は 59524/Ts=1653 [write/frame] となる。
6502のwrite速度はvm2413のwrite処理速度の最大3倍なので(下のうまくいった波形でも見て取れる)、1フレームあたり可能なwrite回数は 551 [write/frame] となる。
……かといって551回というのは6502がwriteしかしない場合なのでそんなわけはなくって。
実際のところ手持ちのNSFで確認すると、frameあたり40回から多くて160回のwriteが発生している。
FIFOに使用するFPGAの内部ブロックRAMは16bit×256depth単位ということもあり、
FIFOの深さとしては256で十分と考えられる。
・vrc7_fifo.sflp
ということでMapperNSF.sflpとvm2413_interface.vhdの間にFIFOを置いて、
6502からのwriteを受け入れる。
input A, D<8>;
instrin write(A, D);
mem fifo_cells[256]<9>;
reg_wr wadrs<8>;
instruct write par{
fifo_cells[wadrs] := A || D;
wadrs++;
}
vm2413へのwriteは3.58MHzごとに起動し、データwrite時のみ72サイクルのウエイト。
fifo_cellsの出力はその場でレジスタに保持すれば、内部ブロックRAMがDPRAMとして割り当てられる。
reg_wr radrs<8>, wdata<9>;
vm2413_interface vm2413inf;
reg_wr count<7>;
instruct run par{
vm2413inf.run();
generate opll_write.do();
}
stage opll_write {
state_name read_st, write_st, wait_st;
first_state read_st;
finish;
state read_st par{
if(radrs!=wadrs){
wdata := fifo_cells[radrs];
goto write_st;
}
}
state write_st par{
radrs++;
vm2413inf.write(wdata<8>, wdata<7:0>);
if(wdata<8>) count := 0b1000111; // 72-1
goto wait_st;
}
state wait_st par{
if(/|count) count--;
else goto read_st;
}
}
で、GO。
vrc7_fifoにダーッと書き込んで、vm2413にてろてろと流し込む。
これで内部レジスタに期待通りに値がセットされた。

全てを受け止めてくれ!
_ ▼ シミュレーションで演奏波形を得る
期待通りにサウンド波形が出力されているか確認する。
演奏データとして、TemporalMixerからメロディ(MO)とリズム(RO)がそれぞれ出力される。
ほとんどのNSFはリズムを使用していないようなので、メロディに注目する。
その出力タイミングを見ると、チャンネルごとに出力スロットが決まっていて、
かつ2ステージ目から3ステージ分有効な値が出ている。
ここで、チャンネルミュート(mmute)時の値は0x200らしい。

無音は0x200
問題は、vm2413からステージの現在値が出ていないため、
どのタイミングの出力を有効とすればいいか判断できない、ということ。
とりあえずサンプル1チャンネルの4ステージのうち1ステージのみ有効として(sound_en)、
sound出力をファイルに書き出してみる。
・vrc7_tb.vhd
VerilogHDLならテストベンチから下位モジュールの信号を観測できるけど、
VHDLはModelSimのSignalSpy機能が必要になる。
SignalSpyのドキュメントはModelSimインストールディレクトリ/modelsim_ae/docs/pdf/oem_man.pdfを参照。
use std.textio.all;
use work.txt_util.all;
library modelsim_lib;
use modelsim_lib.util.all;
signal stage : integer range 0 to 3;
signal slot : integer range 0 to 17;
-- 内部信号の引き出し
process begin
init_signal_spy("/vrc7/vm2413inf/oc/slot", "slot");
init_signal_spy("/vrc7/vm2413inf/oc/stage", "stage");
wait;
end process;
sound_en <= '1' when slot=0 and stage=3 else '0';
process(sound_en)
variable line_out : line;
file out_file : text open write_mode is "out_sound.txt";
begin
if(sound_en'event and sound_en='1') then
write(line_out, hstr(sound(11 downto 0)));
writeline(out_file, line_out);
end if;
end process;
出力されたsoundデータは GNUPLOT win32版で波形表示。
ModelSimでアナログ波形表示ってできたっけ?
(できるみたいですね)
まずsound出力は10ビットなので、最上位ビットを符号とみてみたサウンド波形。
出力は-512〜511の範囲となるが、トんでる波形なのでこれは不採用。
次にsound出力を符号なしとみた波形。
出力は0〜1024の範囲でニュートラルは512となり、耳に優しそうな滑らかな曲線を描いている。
よってsound出力の形式は符号なし10ビット、無音512のようだ。
ちなみに下波形はHRA!氏が公開しているvm2413に置き換えてみたときの出力。
内部処理のビット幅が調節され演算精度が上がっている(その分回路規模は増えている)ため、
わずかに滑らかな波形となっているようだ。
エンベロープの立ち上がりが早いのも修正されたものだろうか?
実チップのYM2413より音質が向上しすぎると、別の音色になってしまうことも危惧されるようだ。
複数チャンネルにサンプルをセットしたときの出力については、
他のチャンネルも同様に4ステージのうち3ステージで有効な値が出力されている。
これをどうにかしてDACにつなげてやればチャンネル合成したサウンドが鳴る!

勝手なタイミングで出力しやがる
_ ▼ サウンド出力
以上のアプローチから、
・vm2413へのwriteタイミング
アドレスwriteはウエイト不要。
データwriteは72サイクルのウエイトが必要。
・メロディ(MO)の出力タイミングと波形形式
各チャンネルスロットで、4サイクルのうち3サイクルが有効値。
符号なし10ビット(0〜1023)、無音値512。
ということが解明された。
vm2413のwriteについてはすでに実装済みだが、
サウンド出力についてはDACへの接続方法を検討する必要がある。
9チャンネル分のMOを加算してDACに渡せばよいが、
各チャンネルの「4サイクルのうち3サイクルが有効値」ということを考慮すると。
案1.vm2413からsound_enを出力するよう修正する。
案2.4サイクル分加算し、3で割る。
案3.4サイクルごとにDACに出力する。ただし1/4の確率で無音となる。
案1については、3サイクル目にsound_enを1とするように修正すればいいが、
先人が作成したコードはできればそのまま使用したい。
案3については、1/4の確率で無音にならないように、
ウィンドウシフトするような仕組みがあればいいが面倒だし。
よって、案2が一番楽のようだ。
つまり、変化量(MO-0x200)を4サイクル分加算し、3で割ったものを9チャンネル分加算する、
これをそのタイミングでのVM2413の出力としてDACに渡せばいい。
3で割る、という点については、精度が必要であれば割り算器が必要だ。
ただしそのたどり着く先は結局人間系の耳なので、4で割ったとしても問題ない。
これなら2ビット接続をずらすだけなので実にエコロジーだ。
余裕があるのならさらに4ビットシフトしたものを加算したっていい。

解析結果を反映した接続
・vrc7_fifo.sflp
結局sound出力は13ビットになった。
vm2413からの出力はもう面倒なので72サイクル分加算して4で割る。
0x200を引くことで無音からの変化を得るところは単に最上位ビットを反転するだけでいい。
output sound<13>;
reg_wr sound_reg<13>;
reg_wr chcount<7>;
sel vmsound_signed<10>;
reg_wr sound_total<15>;
// -0x200 vm2413inf.soundを0〜1023から-512〜511に変換
vmsound_signed = (^vm2413inf.sound<9>) || vm2413inf.sound<8:0>;
if(run){
if(chcount==0b1000111){
sound_reg := sound_total<14:2>;
sound_total := 0b000000000000000;
chcount := 0b0000000;
}
else{
// <10s>×9ch×3st=<15s>
sound_total += (15#vmsound_signed);
chcount++;
}
}
sound = sound_reg;
これでFM音源の滑らかなサウンドが出力されるはずだ。
が、ほんのほんのわずかにエコーがかかったような雑音がする。
_ ▼ サウンド出力改
わずかな雑音が聴こえたのは、72の加算をウィンドウとしてみたとき、
vm2413のウィンドウとのずれが原因と考えられる。
そこで先ほどの案3のように、
チャンネルごと特定タイミングで値を加算する方法が必要になる。
これは次のコードのように、前サイクルで無音だった時に加算することで実現する。
reg_wr vmsound_signed_old<10>;
reg_wr sound_total<13>;
if(run){
vmsound_signed_old := vmsound_signed;
if(chcount==0b1000111){
sound_reg := sound_total;
sound_total := 0b0000000000000;
chcount := 0b0000000;
}
else{
if(vmsound_signed_old==0b0000000000){
// nsfでは8chのみ使用 <10s>×8ch=<13s>
sound_total += (13#vmsound_signed);
}
chcount++;
}
}
これでようやくメロディ出力を聴く事ができた。
やっぱりサウンドモジュールの一番のデバッガは耳ですね。
VRC7のサウンドが得られたので、これと他の音源をミックスしDACに出力する。
ちなみにここで使用したオンボードのDAC入力は符号付であり、
他の音源は符号なし、VRC7は符号付きだ。
このためそれぞれの音源のミックス(加算)には注意が必要となる。
またそれぞれの音の大きさにも配慮が必要だ。
これもまた耳で聴いてみてミックスバランスを調節するしかない。
_ ▼ 真のVRC7へ 2012/10/03
実装したNSF PlayerをChipTune界隈の方々に使っていただいたところ、
「プリセットの音色がVRC7じゃない」との指摘をいただきました。
vm2413をそのまま接続してるだけなので、
当然ながらプリセットの音色はYM2413のものです。
そこで、VRC7のプリセット音色をvm2413のものと置き換えればVRC7になるのではないかと考えました。
VRC7の音色としてVirtuaNESのvrc7tone.hのパラメータを、
vm2413のVoiceRom.vhdのフォーマットに変換し置き換えました。
これで音色はVRC7のものとなり、真のVRC7を堪能できるようになりました。
_ ▼ コード
参考までに、今回作成したマッパーとVM2413を接続するコードを置いておきます。
・vrc7_fifo.sflp
・vm2413_interface.vhd
・vrc7_tb.vhd
<メモ>
今回、特に要望も無かったのでvm2413の利用記録をメモしといた。
解析や実装はパラレルで進むため音が鳴るまではものの数時間で済むが、
波形とったり矛盾しないコードを載せたり、シーケンシャルにドキュメント化するってのは大変だ。
Copyright(C) pgate1 All Rights Reserved.
|