音源チップからビープ音まで

メガドライブのYM2612を、JavaScriptでレジスターごとに操作します。

このページは、音源チップの基礎とC++のビープ音チュートリアルに対応するブラウザー版です。1つのチャンネルと、音を出す1つのオペレーターを使います。以下のコード全体をPlaygroundで編集・実行できます。

1. 音源チップは何をする?

YM2612は、プログラムが書き込んだ設定から音を作ります。6つのFMチャンネルがあり、それぞれ4つのオペレーターを持ちます。オペレーターは正弦波をもとにした信号を作り、エンベロープで音量を時間変化させます。音声出力になったり、別のオペレーターを変調したりします。

最初のビープ音には、音を出すオペレーター1つで十分です。変調とフィードバックを使わず、一定の音を鳴らします。

JavaScript → レジスター設定 → YM2612エミュレーター → 音声 → スピーカー

実機ではCPUがチップのデータバスへバイトを送り、クロックがチップを動かします。ここではPlaygroundがエミュレーターを実行し、生成したサンプルをブラウザーの音声出力へ送ります。コードでは設定とタイミングを指定します。

2. レジスターは小さな設定の入れ物

レジスターにはアドレスがあり、1バイトの値を持ちます。アドレスで変更する項目を選び、値で設定を指定します。JavaScriptの0xという接頭辞は16進数を表します。0x07は7、0x7fは127です。

write(0xb0, 0x07); // CH1:アルゴリズム7、フィードバック0

このwrite(address, value)はPlaygroundのヘルパーです。C++のチュートリアルで使う「レジスターを選ぶ」「値を送る」という2回のバス書き込みをまとめています。

// C++ / チップのバス操作:
chip.write(0, 0xb0); // bank 0のレジスターを選びます。
chip.write(1, 0x07); // 値を送ります。

// JavaScript / Playground:
write(0xb0, 0x07);

多くのチャンネル用レジスターは、CH1〜CH3のbank 0と、CH4〜CH6のbank 1に分かれます。Playgroundでは、このbankの引数をportと呼びます。明示するとwrite(0, 0xb0, 0x07)です。引数2つの形式ではbank 0を使います。

C++側のバスオフセットは別です。0と1はbank 0の選択・書き込み、2と3はbank 1の選択・書き込みに使います。このビープ音ではすべてbank 0へ書き込みます。

3. 単純な音色を選ぶ

アルゴリズム7は、4つのオペレーターを並列に音声出力へつなぎます。OP1〜OP3をほぼ無音にして、OP4だけを鳴らします。

ALG 7: OP1 + OP2 + OP3 + OP4→ 出力
OP1〜OP3はほぼ無音 · OP4を鳴らす · FBなし

4つのハードウェアスロットのオフセットは0, 4, 8, 12です。レジスター群の基準アドレスにオフセットを足すと、スロットを選べます。最後の12(0x0c)がOP4です。たとえば、そのTLは0x4cです。

これはハードウェアのアドレスオフセットで、PlaygroundのOP1–OP4定数とは異なります。中央の2スロットは論理的な順序が異なりますが、この例では同じ設定を書き込みます。

CH1のレジスター群設定このビープ音での設定
0x30 + offsetDT / MULTI:デチューンと周波数倍率0x01:デチューンなし、1倍
0x40 + offsetTL:減衰量0x7fでほぼ無音。0x00はOP4用です。大きい値ほど小さな音になります。
0x50 + offsetAR:立ち上がりの速さ0x1f:最速の立ち上がり、レートスケーリングなし
0x80 + offsetSL / RR:減衰段階の切り替えレベルと離鍵後の減衰速度0x0f:SL 0、最速のリリース
0xb0アルゴリズム / フィードバック0x07:アルゴリズム7、フィードバックなし
0xb4左右の出力 / LFO感度0xc0:左右出力を有効にし、感度は0

最初にfm.reset()を呼びます。D1RとD2Rは0になるので、キーオフまで音が一定に続きます。その後、リリースで素早く減衰します。

4. 音程を設定してからキーオンする

チップは音程を、音域を表すBLOCKと11ビットの数値FNUMで指定します。C++の例ではBLOCK 4、FNUM 0x269を使います。2つのレジスターに分けて書きます。

write(0xa4, 0x22); // 上位:(BLOCK 4 << 3) | FNUMの上位ビット2
write(0xa0, 0x69); // 下位:FNUMの残り8ビット

上位を先に書き、次に下位を書きます。下位の書き込みで音程設定が確定します。この値はチュートリアルの音程を再現するもので、Hzの数値ではありません。

最後に、レジスター0x28でキーオンとキーオフを操作します。0xf0では、上位4ビットが4つのオペレーターをオンにし、下位ビットがCH1を選びます。キーオンはエンベロープを開始する操作で、音色や音程の指定ではありません。

write(0x28, 0xf0); // CH1:キーオン
await sleep(3);    // 3秒間鳴らします。
write(0x28, 0x00); // CH1:キーオフ

5. ビープ音を鳴らす

PlaygroundでRunを押してください。ex03_beep.cppのレジスター設定をJavaScriptで書き、時間を置いてキーオフする完全な例です。Playgroundにはfm、write、sleepが用意されています。

Playground全体で開く →

1つ変えて聴き比べる

6. レジスターのバイトから演奏用の操作へ

レジスターでの設定が分かったら、Playgroundのメソッドを使って、同じチャンネル設定をより直接的に書けます。

fm.setAlgo(CH1, 7, 0);          // write(0xb0, 0x07)
fm.setPan(CH1, true, true);    // write(0xb4, 0xc0)
fm.setFrequency(CH1, 4, 0x269); // 0xa4と0xa0へ書き込みます
fm.keyOn(CH1);                // write(0x28, 0xf0)
await sleep(3);
fm.keyOff(CH1);               // write(0x28, 0x00)

このコードはチャンネル設定と再生の部分を置き換えます。上のオペレーター設定も必要です。「音色設定 → 音程設定 → キーオン → 待つ → キーオフ」という流れは同じです。

次は、オペレーターとFMの音作り、または1つの音から調律と音程を調べるページへ進めます。