本記事は、portable_mdxに含まれるYM2151(OPM)エミュレーション実装(src/x68sound/、以下「X68Sound」)と、シリコン解析ベースのサイクル精度エミュレータNuked-OPMを照合し、両者の挙動の差異・一致点を調査した結果のまとめである。
用語についての注意: Nuked-OPMはシリコンダイの解析に基づいて高い精度を目指したソフトウェアエミュレータだが、あくまでソフトウェアであり、物理的なYM2151チップそのものを測定した実測データではない。本記事の「一致」「相違」という判定は、X68SoundとNuked-OPMという2つのソフトウェア実装を照合した結果であり、YM2151の物理的な個体を測定して得たものではない。
確認できた相違点
確認できた一致点
完全一致・高精度一致の項目(数値上の差は見つかっていない)
大枠は一致するが、細部に差が残る項目(複数の条件で検証し、一致する範囲と差がある範囲を確認済み)
X68Soundの内部実装はNuked-OPMと大きく異なる一方、エンベロープ・ピッチ・アルゴリズム結線・LFOなど音の核となる部分の再現度は高い。確認できた主な差分は、CSM、ノイズ生成器(疑似乱数方式と周波数書き換え時のカウンタリセット)、波形生成方式(掛け算/対数変換)、BUSYビット、同時発音時の飽和特性、キースケール最大音階での加速、タイマーBの周期のわずかな差、MULのわずかな比例差、最速レート付近のエンベロープ速度差、TL減衰カーブの量子化域(TL=64以降)でのズレ、AME/AMSによるAM深度水準の差、LFO波形1のAM深度差、LFO波形2の周期内量子化パターン差、LFO周波数(波形0の一部条件)のわずかな差、レジスタ$01(LFO RESET)反映遅延差、LFO PM深度の曲線形状差(PMS依存)、そして処理負荷(Nuked-OPMが約20倍重い)である。
上記の差分は、数値上の乖離の大きさと、実際の楽曲でその条件が使われる頻度によって、聴感への影響度が大きく変わる。本記事で確認した数値をもとに、おおまかに次のように分類できる。
聴感に影響しうる項目
特定の条件でのみ聴感に影響しうる項目
通常の楽曲再生では聴き取りにくい項目
ただし、これは本記事で検証した条件・数値から推測した傾向にすぎない。実際の聴こえ方は、楽曲の編成・音色設定・再生環境によって変わりうる点に留意されたい。
すべての項目について、片方のソースコードを読んで「こう動くはずだ」と推測するだけでなく、実際に両方を動かして得られた数値・挙動を照合する、という方針で検証した。具体的には次の2種類の方法を使い分けた。
src/x68sound/を実際にコンパイルし、公開APIのレジスタ書き込み関数(X68Sound_OpmReg/OpmPoke)で駆動して検証した。観測点は項目によって次の2通りを使い分けた。
src/x68sound/の複製に計算内容を変えない2種類の薄いラッパーを追加し、同じくレジスタ書き込みで駆動しながら内部状態を直接記録した。1つは読み出し専用のgetter(エンベロープ値・位相増分・LFO値などをそのまま返すだけ)。もう1つは音声出力用の内部ループ(pcmset62())から1ティック分の処理(timer処理・コマンド実行・エンベロープ更新・LFO更新・オペレータ出力計算)を切り出したヘルパーで、音声出力段を経由せず直接呼び出して測定を高速化した。Nuked-OPM側はopm_t構造体のpublicフィールドとして内部状態が最初から公開されているため、X68Sound側だけ薄いラッパーを挟むことで、両実装を対称な条件(実ライブラリを実際に駆動し、その内部状態を直接読む)で比較した。Nuked-OPMはOPM_Write()でレジスタに書き込み、OPM_Clock()を繰り返し呼ぶことでチップの状態を1クロックずつ進められる。PCM出力を経由せず、内部変数(エンベロープ値、位相増分、LFO値など)を直接読み出せるため、これを使って比較の基準となる数値を得た。
本記事の時間・周波数に関する比較はすべて、YM2151の入力クロックを4MHzと仮定している。これはX68000に搭載されたYM2151の動作クロックであり、X68Sound自身が持つ定数OpmRate = 62500(コメントに「入力クロック÷64」とある通り、62500×64=4,000,000Hz)からもこの前提が読み取れる。Nuked-OPMのOPM_Clock()自体は特定の周波数を持たないサイクルベースの実装だが、既知の遅いレート(アタックレートを1段階ずつ進める設定)で更新間隔を実測し、X68Sound側の理論値(3サンプルに1回更新)と照合したところ、「32クロック(OPM_Clock()32回)=1音声サンプル」という対応が確認でき、これは4MHz入力クロックを仮定した場合の値(62500Hz)と一致する。以降の全ての時間・周期の比較は、この「32クロック=1サンプル、62500Hzサンプリング」という換算に基づいている。
以下の一致確認は、特定のパラメータの組み合わせでのみ行ったものであり、全パラメータ空間を網羅的に確認したわけではない。具体的には:
OPM_LFOApplyPMS)を経た実際のピッチ位相増分(pg_inc)と、X68Soundの計算式による予測値を1サンプル単位で照合して確認した(詳細は後述)。他のレート・音階・波形の組み合わせで異なる挙動が出る可能性は排除できていない。特に「一致」と結論した項目についても、この限定された範囲内での一致であることに留意が必要である。
ALPHATBLを参照して線形の振幅値に戻している。この考え方はNuked-OPMの「対数領域で足し算してから指数変換する」方式と本質的に同じである(真の対数演算ではなく、計算済みテーブル参照で近似している点が異なる)。このため、「内部の値が完全に同じかどうか」を単純比較できない部分(音を混ぜ合わせる結線のロジック、ピッチ補正の計算方法など)がある一方、「同じ設定を与えたときに最終的にどんな音量・周波数になるか」は、両方を実際に動かして数値で照合できる。以下の検証は基本的に後者の方法(実際に動かして出力を比較する)で行った。
YM2151にはCSM(Composite Sine Mode)という機能があり、有効にするとタイマーAがオーバーフローするたびに全チャンネルへ自動でKEY ON/OFFのパルスを送る。これはレジスタ$14の最上位ビット(bit7)で切り替える。Nuked-OPMはこのビットを読み取って実装しているが、X68Sound側のレジスタ$14処理はタイマーの開始/停止・割り込み許可・フラグリセットに使う下位ビットのみを扱っており、bit7(CSM)には対応していない。CSM専用の効果音・特殊奏法を使う楽曲でのみ影響が出る、限定的な相違点である。
Nuked-OPMのノイズ発生回路は、内部に16bitのシフトレジスタ(LFSR)を持ち、決まった規則でビットをシフト・反転させることで疑似乱数を生成する。この同じ1つの回路が、ノイズ音そのものと、LFOのノイズ波形の両方に使われている。X68Soundは、種の値に定数を掛けて足す乱数生成(線形合同法)を使っており、しかもノイズ音とLFOノイズ波形とで別々に呼び出している。統計的なノイズ特性は近いが、実際に鳴る波形のビット列自体はNuked-OPMとは別物になる。
Nuked-OPMのノイズの周期を数えるカウンタは常に動き続けていて、周波数レジスタを書き換えても「次に何カウントで折り返すか」という目標値が変わるだけで、カウンタ自体はリセットされない。X68Soundは周波数レジスタが書き換わるたびにカウンタを0から再開するため、ノイズ周波数を曲中で頻繁に変更する楽曲では、ノイズの波形タイミングがNuked-OPMとズレる可能性がある。
なお、「周波数レジスタの値ごとに、ノイズが何サンプルおきに更新されるか」という対応表そのものは、Nuked-OPMを実際に駆動して測定した値と完全に一致することを確認した(レジスタ値0で16サンプルおき、15で8.5サンプルおき、24で約4サンプルおき、30で1サンプルおき、など)。差異が生じるのは、あくまで「レジスタを書き換えた瞬間にカウンタが巻き戻るかどうか」という一点のみで、周期の計算式自体はNuked-OPMと一致している。
上記「前提」の通り。最終的な数値は近い値になるが、対数領域での丸め・量子化による微妙な歪みまでは再現していない。ビット単位で完全に同じ波形を狙うなら対応が必要だが、通常の音楽鑑賞であれば聴感上の差は問題になりにくい。
Nuked-OPMはレジスタ書き込み後、内部処理が終わるまでステータスレジスタ最上位ビット(BUSYビット)が1になる。ソフト側はこのビットを見て、前の書き込み完了まで待つ。実測では、アドレス書き込みのみではBUSYは立たず、データ書き込み直後にのみBUSYが立ち、32クロック(OPM_Clock()32回。「32クロック=1サンプル=62500Hz」換算で16マイクロ秒)後にクリアされた。
この実測値はYM2151データシートの仕様値とも整合する。データシートではウェイトがアドレス書き込み後12クロック、データ書き込み後68クロック(3.58MHz基準でそれぞれ約3.4マイクロ秒以上、約19マイクロ秒以上)とされており、4MHz換算では約3マイクロ秒と約17マイクロ秒になるため、実測したBUSY時間(約16マイクロ秒)ともおおむね一致する。なお本記事の検証ハーネスは、アドレス・データともにOPM_Clock()40回(約20マイクロ秒相当)待機としており、最小待機時間を安全側で上回る設定である。
X68Soundのステータスレジスタ読み出し関数は、タイマーA/Bの割り込みフラグしか操作しておらず、BUSYビットを立てる処理が存在しない。そのため、この関数を呼んでも常に「ビジーではない」という値しか返ってこない。
これは実動作に影響しうる相違点である。MXDRV側の「X68000 BIOS(IOCS)同様にBUSYが下がるまで待つ」処理は、X68Sound側でBUSYが立たないため実質的に待たない。X68Soundの実際の書き込み間隔は、BUSY判定とは別のコマンドキュー処理(デフォルト24マイクロ秒/回)に依存する。Nuked-OPM実測の必要ウェイト(データ書き込み後16マイクロ秒)に対し、デフォルトは約1.5倍であり、安全側である(設定関数で変更可能)。
タイマーA/Bの開始/停止、割り込み許可、フラグリセットに対応するビットの割り当ては、Nuked-OPMと完全に一致していた。
さらに、実際にタイマーを起動してからステータスレジスタのオーバーフローフラグが立つまでのサンプル数を実測して比較した。タイマーAはCLKA=1000(Ta=64×(1024-1000)/4MHz=384μs、62500Hzサンプリング換算で理論値24サンプル)、タイマーBはCLKB=250(Tb=1024×(256-250)/4MHz=1536μs、理論値96サンプル)という条件で確認した。
| タイマー | 設定 | 理論値 | X68Sound | Nuked-OPM |
|---|---|---|---|---|
| タイマーA | CLKA=1000 | 24サンプル | 23サンプル | 23サンプル |
| タイマーB | CLKB=250 | 96サンプル | 95サンプル | 90サンプル |
タイマーAは両実装とも理論値より1サンプル早く、かつ両実装間で完全に一致した。一方タイマーBは、両実装とも理論値より数サンプル早いものの、X68Sound(95)とNuked-OPM(90)の間に5サンプル(理論値比約5%)の差が残った。カウンタの起動タイミング(LOADビット書き込みからカウント開始までの端数処理)に起因する丸め誤差の可能性はあるが、この検証はCLKB=250という1条件のみで行っており、タイマーAのように両実装間の完全一致を確認できたとは言えない。
複数チャンネルを同時に鳴らしたときの合算・飽和(音割れ)の挙動を、実際にコンパイルしたX68SoundライブラリとNuked-OPMの両方で、同じ音階・同じタイミングでKEY ONした1〜8チャンネルの出力を比較する形で検証した。
以下の「peak値」は、PCM出力サンプル1つあたりの振幅(絶対値)の最大値で、16bit相当のスケール(理論上の最大値は32768)を基準にした数値。X68Sound側は出力バッファがそのままshort(16bit符号付き整数)型のため、値をそのまま使っている。Nuked-OPM側はchip->dac_output(10bit仮数部と3bit指数部というチップのDAC(デジタル-アナログ変換器)表現から計算した整数値で、理論上の最大振幅がちょうど32768になる)を使っており、両者とも同じ16bit相当のスケールに収まるため、直接数値を照合して比較できる。
音量を控えめ(TL=64、Total Levelで減衰を深めた設定)にして飽和が起きにくい条件で比較すると、1チャンネル時を基準にしたチャンネル数ごとの出力の伸び方は次のようになった。
| チャンネル数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| X68Sound(peak値) | 17 | 33 | 49 | 64 | 79 | 93 | 107 | 121 |
| Nuked-OPM(peak値) | 31 | 62 | 89 | 114 | 133 | 148 | 156 | 159 |
| X68Sound(理想比) | 100.0% | 97.1% | 96.1% | 94.1% | 92.9% | 91.2% | 89.9% | 89.0% |
| Nuked-OPM(理想比) | 100.0% | 100.0% | 95.7% | 91.9% | 85.8% | 79.6% | 71.9% | 64.1% |
(理想比=実測値÷(1チャンネルの値×チャンネル数)。線形加算どおりなら100%のまま推移するはずの値)
X68Soundは8チャンネルまで理想比89.0〜100.0%のなだらかな低下にとどまるのに対し、Nuked-OPMは3チャンネル目から理想比が明確に下がり始め、8チャンネルでは64.1%まで落ち込む。飽和の影響がX68Soundより早い段階から、かつ急激に現れていることが理想比の推移からも確認できる。
さらに音量を最大(TL=0)にした条件では、この差がより顕著になった。
| チャンネル数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
| X68Sound(peak値) | 4047 | 8083 | 12048 | 15989 | 19786 | 23546 | 27086 | 30576 |
| Nuked-OPM(peak値) | 8176 | 16032 | 23296 | 29568 | 32768 | 32768 | 32768 | 32768 |
| X68Sound(理想比) | 100.0% | 99.9% | 99.2% | 98.8% | 97.8% | 97.0% | 95.6% | 94.4% |
| Nuked-OPM(理想比) | 100.0% | 98.0% | 95.0% | 90.4% | 80.2% | 66.8% | 57.3% | 50.1% |
Nuked-OPMは5チャンネル以上を同時に最大音量で鳴らすと出力が上限値(32768)に達して完全に飽和するのに対し、X68Soundは8チャンネル全てを最大音量で鳴らしても上限には達しなかった。理想比で見るとX68Soundは8チャンネルでも94.4%と緩やかな低下にとどまるのに対し、Nuked-OPMは飽和が始まる5チャンネル目から理想比が急落し、8チャンネルでは50.1%まで落ち込む。X68Soundは各チャンネルの出力を線形領域で単純に加算してから最終段で緩やかなクリップ処理を行う実装であるのに対し、Nuked-OPMは内部DAC(対数領域からの変換、飽和特性を含む)をそのまま再現しているため、より少ないチャンネル数・低い音量から飽和が始まる。密集した多重発音を伴う楽曲では、この飽和特性の違いが聴感上の音圧感・歪み方の違いとして現れうる。
エンベロープのアタック/ディケイ1/ディケイ2/リリースは、キースケール(KS)により音階(KC)が高いほど速くなる。X68SoundはKc>>(5-Ks)、Nuked-OPMはpg_kcode[slot]>>(sl_ks[slot]^3)でこの補正を適用する。
検証では、KC=$00〜$7F全域・キースケール0/1/3で、KEY ON直後から最大音量到達までのサンプル数(アタック時間)を比較した(他パラメータは前述のエンベロープ検証と同一)。
| KC | $00 | $10 | $20 | $30 | $40 | $50 | $60 | $70 | $7E | $7F |
|---|---|---|---|---|---|---|---|---|---|---|
| X68Sound(アタック時間・サンプル) | 879 | 714 | 600 | 516 | 450 | 363 | 306 | 264 | 264 | 231 |
| Nuked-OPM(アタック時間・サンプル) | 869 | 689 | 581 | 497 | 434 | 350 | 290 | 248 | 248 | 248 |
(キースケール1・アタックレート20。KCは同値区間があるため代表値のみ抜粋)
$00〜$7E(最大値$7Fを除く全域)では、キースケール設定にかかわらず、KCに対する段階的なアタック時間短縮パターンが一致した。差は、既知のレジスタ反映遅延由来のオフセットのみである。
ただし$7F(理論上最高音)だけは例外で、X68Soundは$7Eまでの傾向より速くアタックが完了する(キースケール1で264→231、キースケール0で516→450)。Nuked-OPMは$7Eと$7Fで変化しない(キースケール1で248→248、キースケール0で497→497)。
キースケール3でもKC=$00〜$7F全域を確認したところ、KCが上がるほど加速する傾向自体は一致していた。ただし$60付近から両実装ともレート上限に張り付き(X68Soundは15サンプル、Nuked-OPMは0サンプルで頭打ち)、$7Eと$7Fが同値になるため、$7F特有の余分な加速はこの条件では観測できなかった。これは矛盾ではなく、キースケール3ではKC=$60時点で既に実効レートがテーブル上限に達しているため、$7Fの1段階分の余分なブーストが上限で吸収されるためである。
なお、このアタック時間の絶対値は、KEY ONがエンベロープ用クロックのどの位相で反映されるか(レジスタ書き込みコマンドをキューから何クロック分の余裕を見て捌くか)に敏感で、テストハーネスの設定を変えるだけで両実装とも十〜数十サンプル単位で変動することを確認した。上表はある1つの設定での測定値であり、絶対値そのものよりも、KCに対する段階的な短縮パターンの一致と$7Fでの挙動の違いという相対的な傾向に着目されたい。
この挙動はX68SoundのOp::SetKC()の実装に起因する。音階レジスタ値(0〜127)をKc = (n&127); ++Kc;で内部値へ変換するため、$7F時のみKc=128となり7bit範囲を1超える。この値がKc>>(5-Ks)に使われることで、$7Fだけ1段階余分にレートが上がる。影響はキースケール対象の4ステージ(アタック/ディケイ1/ディケイ2/リリース)すべてに共通し、最大音階付近でのみ現れる限定的な相違点である。
LFO波形0(ノコギリ波)のAM深度は、位置合わせ後にNuked-OPMと±2以内で一致する。一方、同じ比較を波形1(矩形波)に適用すると小さな相違が残る。
X68SoundのAM変換テーブルは、波形0・2では0〜255の範囲に収まっているのに対し、波形1(矩形波)だけは、矩形波が2値のうち大きい方を取る区間(オン区間)の値が256に設定されている(x68sound_lfo.cppのcase 1:ブロックでAmTblValue = 256;)。この256は他の波形の最大値255より1大きい。深度計算式(AmTblValue*Amd)>>7にそのまま代入されるため、AMD=127(最大深度)のとき、波形1のオン区間の深度は(256*127)>>7=254になる。
一方、Nuked-OPMを実際に矩形波(波形1)・AMD=127で駆動してlfo_am_lockを実測すると、オン区間の値は253だった。これは波形0・2の最大深度と同じ値であり、X68Soundの254より1小さい。AMD=1, 32, 64, 96, 127の5パターンでも、Nuked-OPMの実測値は「255を基準にした式」(255*Amd)>>7と一致し、X68Soundの計算値(256基準)より常に1小さい側にずれていた。差分は最大でも1(255スケール中、約0.4%)で聴感上の影響はほぼ無視できるが、数値上のズレとしては確認できた。
同じ非対称性はPM側にもあり、波形1のみ振幅絶対値が128(他波形は127)になる(PmTblValue = 128; / -128;)。X68SoundのPM式(PmTblValue*Pmd*Pmsmul)>>(7+5)<<Pmsshlで見積もると、PMS=1・PMD=127時の差は最大3程度で、AM側の直接的な1ズレより影響は小さい。なお、後述のPMS非線形処理と重なるため、単独の実測分離は難しい。
波形2(三角波)は周期・振幅(AM/PMの最大値・最小値)は一致するが、1周期内の更新間隔と変化量(量子化粒度)は一致しない。
LFRQ=255・AMD=127の条件で定常状態(起動直後の過渡期を除いた区間)を比較すると、X68Soundは約16サンプルに1回、6〜8ずつ値が変化するのに対し、Nuked-OPMは約8サンプルに1回、2〜4ずつ値が変化する。
X68Sound : (9,+8) (25,+6) (41,+8) (57,-5) (73,-8) (89,-8) (105,-8) ...
Nuked-OPM : (2,+4) (10,+2) (18,+4) (26,+4) (34,+4) (42,+4) (50,+3) (58,-4) (66,-4) ...
(カッコ内は「サンプル番号, 前回値からの変化量」)
平均変化率(約-0.5/サンプル)はほぼ一致し、これが周期一致の理由である。差は実装方式にあり、X68Soundは波形0ロジックの歩進を2倍化して三角波を生成するのに対し、Nuked-OPMは三角波時のみ内部歩進クロック自体を高頻度化する。このため瞬間値は最大4(255スケール中約1.6%)ずれうる。すなわち三角波は「周期・振幅は一致するが、瞬間値は完全一致しない」。
X68Soundはレジスタ$01のbit1(0x02)でLFOを即時にリセット/再開する単純な2値処理(Lfo::LfoReset()/Lfo::LfoStart())だが、Nuked-OPMは同レジスタを8ビットのテストモードビット(mode_test[0..7])として扱う。そこで内部実装の一致ではなく、「リセット中はLFO出力が固定されるか」「解除後に再開するか」という観測可能な挙動を比較した。
波形0・LFRQ=255・AMD=127で周期途中に$01=0x02を書き込むと、X68Soundは次サンプルで固定値253へ切り替わる一方、Nuked-OPMは253到達まで約4サンプル遅れた。続けて$01=0x00で再開すると、X68Soundは次サンプルで249へ進むが、Nuked-OPMは約7サンプル253のまま据え置かれた。つまり最終挙動(固定・再開)自体は一致するが、反映タイミングはNuked-OPMの方が数サンプル遅い。
コントロールとして$01を書き込まずに観察すると、値はなだらかに減少し続け、253側への急な変化は起きなかった。このため上記の変化はレジスタ書き込み起因と判断できる。遅延量(約4〜7サンプル)は、書き込み待ち時間(データ書き込み後68クロック、4MHz換算で約17マイクロ秒=1サンプル未満)だけでは説明しにくい。Nuked-OPMでは位相カウンタ(lfo_counter1〜3)はICリセットでのみ初期化され、$01 bit1(mode_test[1])は出力側シフトレジスタの帰還信号w3へ作用するため、X68Soundのような「次サンプルで即リセット」にはならず、反映まで数クロック遅れることが主因と考えられる。
X68SoundはPM深度を(PmTblValue*Pmd*Pmsmul[pms])>>(7+5)<<Pmsshl[pms]で線形計算し、結果をSTEPTBL参照位置へ加算する。一方Nuked-OPMはOPM_LFOApplyPMS()で波形上位ビットと下位4ビットを使う非線形ビット演算を行う。
比較は、KC=$40・DT1=DT2=0・LFO波形0・LFRQ=255でPMS=0〜7を走査し、Nuked-OPM実測のchip.pg_inc[]とX68Sound予測値を、相互相関で遅延補正した上で照合した。
| PMS | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| 振れ幅(山と谷の差)の一致度 | - | 93.3% | 109.1% | 104.3% | 100.0% | 100.7% | 100.2% | 100.4% |
| 位置合わせ後の平均のズレ | 0 | 3.0 | 2.9 | 2.9 | 2.8 | 3.3 | 9.2 | 20.8 |
| 位置合わせ後の最大のズレ(セント換算) | 0 | 8(約2.7セント) | 7(約2.3セント) | 9(約3.0セント) | 8(約2.7セント) | 10(約3.3セント) | 25(約8.2セント) | 65(約20.8セント) |
(「ズレ」の単位はpg_inc/DeltaT、KC=$40・DT1=DT2=0・MUL=1・PMD=127での基準ピッチ約5200に対する差分。X68Sound側のOp::DeltaTとNuked-OPM側のchip.pg_inc[]は基準ピッチの計算式自体がわずかに異なるため、比較前に両系列をそれぞれ自分自身の平均値でセンタリングし、PM変調による形状の違いだけを取り出している)
PMS=1〜5では振れ幅はほぼ一致し(93〜110%)、位置合わせ後の残差も最大10(3セント程度)に収まる。PMS=6以上になると残差が拡大し、PMS=7・PMD=127で平均21、最大65(約20.8セント)となる。振れ幅自体はPMS=4以降もほぼ100%で一致しており、乖離は深さではなく曲線形状に起因する。
主因は曲線形状の差である。1LFO周期中にpg_inc/DeltaTが値を更新する回数を数えると、同条件(LFRQ=255・PMS=7)でX68Soundが66回に対しNuked-OPMは131回とほぼ2倍細かく、X68Soundの線形近似では中間形状を再現しきれない。最大デチューン幅(山谷到達値)はほぼ一致するため深さの聴感は近いが、なめらかさは高PMSほどNuked-OPMの方が高い。
乖離量がPMD(深度パラメータ)に対してどう変化するかも、PMS=7固定でPMDを32・64・96・127の4パターンに振って確認した。
| PMD | 32 | 64 | 96 | 127 |
|---|---|---|---|---|
| 振れ幅の一致度 | 100.5% | 99.9% | 100.3% | 100.4% |
| 位置合わせ後の平均のズレ | 9.9 | 11.7 | 15.7 | 20.8 |
| 位置合わせ後の最大のズレ(セント換算) | 15(約5.0セント) | 29(約9.6セント) | 63(約20.5セント) | 65(約20.8セント) |
振れ幅の一致度はPMDによらずほぼ100%を保つ一方、位置合わせ後のズレ(平均・最大とも)はPMDが大きいほど拡大しており、乖離量はPMDにほぼ比例して増加すると言える。
これまでの検証はいずれも「出力される値が一致するかどうか」という機能面の比較だった。前述の実装アプローチの違いを踏まえれば、処理負荷(CPU時間)にも違いが出るはずである。そこで比較ハーネスを使い、前述のX68Sound側検証用ヘルパー(pcmset62()から切り出した内部ティック関数)とNuked-OPMのOPM_Clock()を、それぞれ「1サンプル分の処理」の単位(32クロック=1サンプル)として400万サンプル分(62500Hzで約64秒相当)連続して進め、実行時間を計測した(g++ -O2、Apple Silicon/arm64、ウォームアップ10万サンプル分を計測前に除外)。「無音状態(リセット直後、KEY ONなし)」と「8ch全チャンネル・全オペレータ発音状態(CON=7・TL=0・AR=31でKEY ON)」の2条件で計測した。
| 条件 | X68Sound(ナノ秒/サンプル) | Nuked-OPM(ナノ秒/サンプル) | 比率(Nuked-OPM÷X68Sound) |
|---|---|---|---|
| 無音状態 | 約59 | 約1249〜1277 | 約21〜22倍 |
| 8ch全オペレータ発音 | 約59〜60 | 約1249〜1260 | 約21倍 |
(3回計測した実測値の範囲)
ここまでの数値は「1サンプルの処理に要したCPU時間」であり、「負荷」(実時間予算に対する消費割合)とは異なる。62500Hzでの1サンプルあたりの実時間予算は16,000ナノ秒(1/62500秒)であり、これを基準に割合へ換算すると次のようになる。
| 条件 | X68Sound | Nuked-OPM |
|---|---|---|
| 無音状態 | 約0.37% | 約7.8〜8.0% |
| 8ch全オペレータ発音 | 約0.37〜0.38% | 約7.8〜7.9% |
Nuked-OPMも単一系統であれば実時間予算の8%程度に収まる。ただし1系統分の割合であり、複数系統を同時にエミュレーションする場合や、他の処理と実行時間を奪い合う環境では、20倍という比率の差がそのまま効いてくる。
いずれの実装も「無音状態」と「8ch全オペレータ発音状態」との間で処理時間に有意な差は見られなかった(差はいずれも5%程度以内)。これは、X68SoundのOutput系関数もNuked-OPMのOPM_Clock()パイプラインも、発音中かどうかに関わらず全32オペレータ(あるいは全32スロット)を毎サンプル一律に処理する固定コスト構造になっているためであり、発音数によって処理負荷が変動する実装ではないことも確認できた。
処理負荷の差の主因は、両実装の内部構造の違いにあると考えられる。Nuked-OPMのOPM_Clock()は、1回の呼び出しにつきOPM_PhaseGenerate・OPM_EnvelopePhase1〜6・OPM_DoLFO1・OPM_Noise・OPM_DoRegWriteなど50個のサブルーチンを毎回呼び出す。これらはシリコンダイ解析で判明したチップ内部パイプラインの各ステージ(位相生成・エンベロープ計算・LFO・ノイズ・レジスタ反映など)に対応しており、1回のOPM_Clock()呼び出しは1/32サンプル分のチップクロックにしか相当しないため、1サンプルを生成するにはこれを32回、つまり延べ1600個のサブルーチン呼び出しを行う。一方X68Soundは、あらかじめ計算済みのサイン波・エンベロープテーブルを線形領域で参照する軽量な計算を32オペレータ分行っている。1オペレータあたり常に行うのはテーブル参照1回と乗算1回(SINTBL参照とAlphaとの乗算)のみで、Op::Outputのif (LfoPitch != lfopitch) if (LfoLevel != lfolevelame)により、LFOのピッチが変化した時だけSTEPTBL参照と乗算がもう1回、LFOのレベルが変化した時だけALPHATBL参照がもう1回、それぞれ追加される。
この比率の絶対値は測定環境に依存しうるが(検証範囲についての注意を参照)、OPM_Clock()内部の呼び出し数という実装そのものに起因する差である以上、「Nuked-OPMの方が重い」という傾向は環境に依らず成り立つと考えられる。この差は出力される音そのものには影響しない。
レジスタに一切書き込みを行わない状態で音声出力を取得した場合、X68Sound・Nuked-OPMともに出力は完全に無音(全サンプル0)であった。両実装とも、リセット直後は明示的な音色設定・KEY ONが行われるまで音が出ない、という基本的な前提は一致している。
DT1は8段階(0〜7、内4段は逆方向)で音程を細かくずらすパラメータ。X68Soundの内部計算式(音程テーブルの値にDT1補正量を足してから、オペレータの逓倍数を掛ける)を手計算し、Nuked-OPMが実際に計算する「1サンプルあたり音程がどれだけ進むか」という値と比較した。同じ音階・逓倍数設定でDT1を0〜7まで変えたところ、X68SoundとNuked-OPMで完全に一致した。
| 音階(KC) | DT1=0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| $20 | 2598 | 2600 | 2602 | 2606 | 2598 | 2596 | 2594 | 2590 |
| $40 | 10392 | 10396 | 10402 | 10408 | 10392 | 10388 | 10382 | 10376 |
| $60 | 41568 | 41578 | 41590 | 41600 | 41568 | 41558 | 41546 | 41536 |
DT1によるピッチ補正量は、少なくとも試した範囲ではNuked-OPMと数値レベルで一致する。
DT2は4段階(0〜3)で音程を大きくずらすパラメータで、X68Soundは{0, 384, 500, 608}という4つの数値を持つ変換テーブルを使い、これを音程テーブルの参照位置(インデックス)に直接足し込んでいる。Nuked-OPMはこの変換を固定テーブルではなくビット演算で計算しているため、DT1のケースと同じ方法(1サンプルあたりの音程の進み幅を両方で計算して照合する)で検証した。結果は次の通り、こちらも完全に一致した。
| 音階(KC) | DT2=0 | 1 | 2 | 3 |
|---|---|---|---|---|
| $20 | 2598 | 3674 | 4078 | 4498 |
| $40 | 10392 | 14696 | 16312 | 17992 |
| $60 | 41568 | 58784 | 65248 | 71968 |
DT2によるピッチ補正量も、DT1と同様にNuked-OPMと一致している。
Nuked-OPMを実際にレジスタ書き込みで駆動し、内部のエンベロープ値(値が大きいほど音が小さい。Nuked-OPMの内部レジスタは10bitで0〜1023の範囲を取る)を直接記録するテストと、X68Sound実ライブラリに追加した読み出し専用アクセサ経由で同じくエンベロープ値を直接記録するテストを用意し、同一パラメータ(アタックレート20・ディケイ1レート12・ディケイ2レート6・リリースレート7・キースケール1・音階KC(Key Code)=$40・微調整KF(Key Fraction)=0・サスティンレベル8)でKEY ON→KEY OFF→RELEASEの生のエンベロープ値を照合した。
X68Sound : 959, 899, 842, 789, 739, 692, 648, 607, 569, 533, 499, 467, 437, 409, 383, 359, 336, 314, 294, 275, ...
Nuked-OPM : 959, 899, 842, 789, 739, 692, 648, 607, 569, 533, 499, 467, 437, 409, 383, 359, 336, 314, 294, 275, ...
KEY ON直前の無音状態の値(X68Soundは1024、Nuked-OPMは1023。10bitレジスタの範囲0〜1023に対しX68Sound内部表現は1大きい)を除き、KEY ON後に最初に届く値から並べると、上記の通り両実装で完全に一致する減衰列が得られた。
減衰の最後(値が0付近になるあたり)、ディケイからサスティンへの切り替わりタイミング、リリースが完了するタイミングまで、レジスタ書き込みの遅延による数十サンプル程度の一定オフセットを除けば、数値・カーブの形ともほぼ完全に一致した。事前計算テーブル方式とビットレベルのパイプライン方式という全く違う実装であるにもかかわらず、出力される音量変化のカーブは実質同一であった。
アタックレートを最大値(31)にすると、キースケールや音階の値によらず、内部の「実効レート」がテーブルの上限に達し、X68Soundのコードはこの条件のとき「1回の更新で最大音量に到達する」という特殊な値を使うようになっている。実際にNuked-OPMをアタックレート最大で駆動したところ、KEY ON直後の最初の更新で内部エンベロープ値が無音状態から即座に0(最大音量)へジャンプすることを確認した。両者とも「アタックレート最大時は事実上一瞬で音量が最大になる」という挙動が一致している。
Nuked-OPMが持つ2つの256エントリのテーブル(対称性を利用して実質1024段階相当になる)と、X68Soundが持つ1024エントリのサイン波テーブル・振幅変換テーブルは、解像度としては同等であった。差があるのは解像度ではなく、前述の「線形領域で乗算する」方式と「対数で足してから指数変換する」方式という、計算手順の違いのみである。
YM2151は1音につき4つのオペレータ(発音ユニット)を持ち、そのうちどれとどれを「変調する側」「音として出す側(キャリア)」にするかを8パターンのアルゴリズムから選べる。これを検証するため、コンパイル済みのX68Soundライブラリと、Nuked-OPMの両方に対して、8つのアルゴリズムそれぞれで「4オペレータとも同じ音量にした状態」と「1オペレータずつ音量を最小にした状態」を作り、出力される音の大きさ(実効値)を比較するテストを行った。あるオペレータを消したときに出力が大きく下がれば、それは「音として出す側(キャリア)」だったと判定できる。
レジスタ上の並び順(物理的な1〜4番目)と、アルゴリズム図で説明される論理的な並び順(M1,C1,M2,C2)は変換が必要なため、その対応関係を踏まえて解釈すると、どのオペレータを消すと音が小さくなるか(=どれがキャリアか)のパターンが両実装で完全に一致した。例えば「4オペレータが全て独立」のアルゴリズムでは、どちらの実装でもどのオペレータを1つ消しても出力が75%に低下し(4本中3本が残る計算と一致)、「キャリア3本・変調用1本」の構成のアルゴリズムでも両実装で一致した。
CON/FBと同じレジスタのbit6・bit7は、それぞれチャンネルの音を左右どちらの出力へ混ぜるかを指定する。コンパイルしたX68Soundライブラリを使い、bit6だけを立てたとき・bit7だけを立てたときの左右チャンネルの出力音量(実効値)を測定したところ、bit6を立てると左チャンネルのみ、bit7を立てると右チャンネルのみに音が出た。Nuked-OPMのソースコードでも同様にbit6が左、bit7が右の出力有効フラグとして扱われており、割り当ては一致している。左右が入れ替わっているようなことはなかった。
X68SoundのLFO波形テーブルは512個のデータを持つが、実は前半256個と後半256個が同じ内容になっており、実際に繰り返される波形の周期は256個分。これを踏まえてLFO周波数の設定値ごとの周期を計算し、Nuked-OPMを同じ設定で実際に駆動して波形が繰り返される間隔を実測した。測定方法は、波形0はX68Sound実ライブラリに追加した読み出し専用アクセサを使い実際にレジスタ書き込みで駆動して測定、Nuked-OPM側はOPM_Clock()を回して出力の折り返しタイミングを検出する方法(のこぎり波・矩形波は急激な立ち下がりを、滑らかな三角波は符号のゼロクロス点を検出)で、それぞれ独立に周期をサンプル数で測定するというものである。
LFRQ設定値を0〜255の範囲で20点、波形0(ノコギリ波)について測定した結果は次の通り。
| LFRQ | 0 | 16 | 32 | 48 | 64 | 80 | 96 | 112 | 128 | 144 | 160 | 176 | 192 | 208 | 224 | 240 | 248 | 252 | 254 | 255 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| X68Sound(サンプル) | 67108864 | 33554432 | 16777216 | 8388608 | 4194304 | 2097152 | 1048576 | 524288 | 262144 | 131072 | 65536 | 32768 | 16384 | 8192 | 4096 | 2048 | 1360 | 1168 | 1088 | 1056 |
| Nuked-OPM(サンプル) | 67108864 | 33554432 | 16777216 | 8388608 | 4194304 | 2097152 | 1048576 | 524288 | 262144 | 131072 | 65536 | 32768 | 16384 | 8192 | 4096 | 2048 | 1368 | 1176 | 1088 | 1056 |
20点中18点でX68SoundとNuked-OPMの値が完全一致した(62500Hzサンプリング換算)。LFRQ=248・252の2点のみ、それぞれ8サンプルの差が残ったが、これは周期自体が1000サンプル台と短くなり、周期検出(振幅値の折り返しエッジ検出)の分解能に対して相対的な誤差が大きくなったことによるものとみられ、他の18点も含めた全体の傾向としては一致していると言える。波形1(矩形波)・波形2(三角波)についても、LFRQ={32, 64, 96, 128, 160, 192, 224, 255}の8点で同様に測定した結果、いずれもX68Sound・Nuked-OPMともに上表の波形0と全く同じ周期になり(例えばLFRQ=128ならどの波形でも262144サンプル)、かつ両実装間でも8点すべてで完全に一致した。これは偶然ではなく、X68Sound側の実装を読むと、波形1は波形0と全く同じテーブル歩進式(idxadd = LfoSmallCounter>>4)を使っており、波形2だけテーブル歩進を2倍(idxadd+idxadd)にする代わりにテーブル自体を2倍の長さ(重複なしの512エントリ)参照するため、3波形とも同じLFRQに対して理論上同じ周期になる、という構造になっている。Nuked-OPM側でも同じ関係が成り立つことを実測で確認した。
残る3種類(波形1=矩形波、2=三角波、3=ノイズ)についても、レジスタで指定する波形番号が両実装で同じ形を指しているかを確認した。X68Sound実ライブラリに追加した読み出し専用アクセサ経由のテストと、Nuked-OPMを実際に駆動して出力値を記録したテストで、それぞれの波形番号ごとの出力値の並びを比較したところ、
という形の分類が、レジスタ番号(0〜3)とも完全に一致した。ノイズ波(波形3)の実際の値の並び自体は、前述の「ノイズ生成器の方式が異なる」という差異により両実装で一致しないが、「波形番号3がノイズ波を指す」という割り当て自体は両実装で一致している。
前述の周期の一致はあくまで「同じ間隔で繰り返すか」の確認であり、実際に音量へ加算される深度の値そのものが一致するかは別問題である。これを検証するため、LFO波形0(ノコギリ波)・AMS=1(シフト量0、深度がそのまま反映される設定)の条件で、AMDレジスタ値を6パターン(1, 16, 32, 64, 96, 127)変えながら、1LFO周期分(LFRQ=255設定で1056サンプル)の深度の値をサンプル単位で全数記録し、照合した。
X68Soundは(AmTblValue*Amd)>>7という単純な線形式で深度を計算しているのに対し、Nuked-OPMはビットシリアルな乗算回路(OPM_DoLFOMult)でAMD値と生の波形値を1ビットずつ掛け合わせる、という全く異なる回路構成になっている。両者はレジスタ書き込み後の内部遅延の分だけ出力のタイミングがずれるため、起動直後の過渡期を飛ばした後の1LFO周期分を記録し、両系列を自分自身の平均値でセンタリングしてから相互相関で位置合わせして差分を取った。
結果は次の通り、位置合わせ後の最大差はAMD値が大きいほどわずかに広がるものの、いずれも0〜255スケール中で2以下に収まった。
| AMD | 1 | 16 | 32 | 64 | 96 | 127 |
|---|---|---|---|---|---|---|
| 位置合わせ後の最大差 | 0 | 1 | 1 | 1 | 2 | 2 |
| 位置合わせ後の平均差 | 0.00 | 0.74 | 0.48 | 0.47 | 0.78 | 0.94 |
回路の実装方式が全く異なるにもかかわらず、実際に出力される深度の数値はほぼ完全に一致していると言える。
KF(Key Fraction)は音階(KC)の半音の間を64分割してピッチを微調整するパラメータ。DT1/DT2の検証と同じ方法(1サンプルあたりの位相増分を両実装で計算・実測して比較)で、音階KC=$40・DT1=DT2=0・MUL=1固定のままKFを0〜63の範囲で9点確認した。
| KF | 0 | 8 | 16 | 24 | 32 | 40 | 48 | 56 | 63 |
|---|---|---|---|---|---|---|---|---|---|
| X68Sound(DeltaT) | 5196 | 5232 | 5272 | 5308 | 5348 | 5384 | 5424 | 5464 | 5496 |
| Nuked-OPM(pg_inc) | 5196 | 5232 | 5272 | 5308 | 5348 | 5384 | 5424 | 5464 | 5496 |
9点すべてで数値が完全に一致した。KFによるピッチ補正量は、DT1/DT2と同様にNuked-OPMと一致している。
MUL(Multiple)はオペレータの発振周波数を音階のn倍(0のときは0.5倍)にするパラメータ。音階KC=$40・DT1=KF=0固定で、MUL=0〜15の全16段階を確認した。
| MUL | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| X68Sound(DeltaT) | 2608 | 5216 | 10432 | 15648 | 20864 | 26080 | 31296 | 36512 | 41728 |
| Nuked-OPM(pg_inc) | 2598 | 5196 | 10392 | 15588 | 20784 | 25980 | 31176 | 36372 | 41568 |
| MUL | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|
| X68Sound(DeltaT) | 46944 | 52160 | 57376 | 62592 | 67808 | 73024 | 78240 |
| Nuked-OPM(pg_inc) | 46764 | 51960 | 57156 | 62352 | 67548 | 72744 | 77940 |
MULが大きくなるにつれて両実装とも比例的に増加する形は一致するが、X68Soundの値がNuked-OPMより常にわずかに(MUL=1で20、MUL=15で300、比率にして一貫して約0.38%)大きい。この比率はMULの値によらず一定であり、丸め誤差というよりも、両実装のピッチテーブルの基準値そのものにごくわずかな倍率差があることを示唆している。差は約0.38%(セント換算で約6.6セント)で、通常の音楽鑑賞では判別が難しい程度である。
TLはオペレータの出力音量を127段階で減衰させるパラメータ。複数チャンネル同時発音時の検証と同じ方法(音声出力サンプルのピーク振幅を比較)で、音階KC=$40・CON=7(全独立、対象オペレータ以外はTL=127で無音化)・AR=31(瞬時アタック)・D1R=0の条件で、TL=0〜127のうち12点を確認した。
| TL | 0 | 16 | 32 | 48 | 64 | 80 | 96 | 112 | 120 | 124 | 126 | 127 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| X68Sound(peak振幅) | 4217 | 1055 | 264 | 68 | 18 | 6 | 0 | 0 | 0 | 0 | 0 | 0 |
| Nuked-OPM(peak振幅) | 8160 | 2040 | 510 | 127 | 31 | 7 | 1 | 0 | 0 | 0 | 0 | 0 |
X68SoundとNuked-OPMは前述の通り出力の絶対スケールが異なる(約52%、複数チャンネル同時発音の検証で確認した出力スケール差と同じ比率)ため、TL=0を各実装ごとの基準100%とした相対値も併記する。
| TL | 0 | 16 | 32 | 48 | 64 | 80 | 96 |
|---|---|---|---|---|---|---|---|
| X68Sound(基準比) | 100% | 25.02% | 6.26% | 1.61% | 0.43% | 0.14% | 0% |
| Nuked-OPM(基準比) | 100% | 25.00% | 6.25% | 1.56% | 0.38% | 0.086% | 0.012% |
基準比で見ると、TL=0〜48では両実装の値がほぼ完全に一致する(TL=16で25.0%、TL=32で6.25%と、TLを16段階進めるごとに正確に4分の1へ減衰する関係が両実装とも高精度で成り立っている)。TL=64以降は値が小さくなり、量子化誤差の影響で基準比のズレが相対的に目立つようになるが、TL=96以降はどちらもほぼ無音(0または1)に達しており、実質的な減衰しきい値の位置も一致している。
AME(レジスタ$A0-$BFのbit7)はLFOによる振幅変調を有効/無効にするビット、AMS(レジスタ$38-$3Fの上位2bit)はその感度(深さ)を4段階で調整するパラメータ。どちらも音量の時間変化として現れるため、実際の音声出力のRMS包絡線(キャリア周期をほぼ1周期分カバーする150サンプル幅の窓でRMSを取り、その最大値と最小値の差)で比較した(CON=7で対象オペレータ以外を無音化し、AR=31・D1R=0で音量そのものの変化を止め、LFOだけを変動要因にした条件)。
X68SoundとNuked-OPMは前述の通り出力の絶対スケールが異なる(1チャンネル分にどれだけ余裕を残すかという設計の違いによる、約2倍程度の差)ため、絶対値だけでは深さの比較がしにくい。そこで、AME=0(無変調)時の定常RMSレベルを各実装ごとの基準100%とした相対値も併記する。
| AME=0 | AME=1 | |
|---|---|---|
| X68Sound RMS実測範囲(min-max) | 2813-2964 | 388-2130 |
| Nuked-OPM RMS実測範囲(min-max) | 5784-5807 | 497-4706 |
| X68Sound RMS変動幅 | 151 | 1742 |
| Nuked-OPM RMS変動幅 | 23 | 4209 |
| X68Sound RMS変動幅(基準比) | 0.0%(基準) | 60.3% |
| Nuked-OPM RMS変動幅(基準比) | 0.0%(基準) | 72.6% |
(基準比の基準値は、AME=0時の実測範囲の中央値。X68Sound側は(2813+2964)/2≈2889、Nuked-OPM側は(5784+5807)/2≈5796)
AME=0(無効)では両実装ともRMSの変動幅がほぼ0に近い値(キャリア波形の位相差による測定ノイズの範囲)に収まり、AME=1(有効)ではどちらも最大音量の半分以上という大きな変動幅を示した。ON/OFFの効果自体は両実装で明確に一致するが、基準比で見た深さの絶対水準はX68Sound 60.3%・Nuked-OPM 72.6%であり、12ポイント(相対比で約2割)の差が残る。一致しているのはON/OFFの切り替わりのみで、深さの絶対水準までは一致していない。
同様の条件でAMSを0〜3まで振ったところ(AME=1固定)は次の通り。
| AMS | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| X68Sound RMS実測範囲(min-max) | 2813-2964 | 388-2130 | 85-1635 | 10-1500 |
| Nuked-OPM RMS実測範囲(min-max) | 5784-5807 | 497-4706 | 43-4254 | 0-3593 |
| X68Sound RMS変動幅 | 151 | 1742 | 1550 | 1490 |
| Nuked-OPM RMS変動幅 | 23 | 4209 | 4211 | 3593 |
| X68Sound RMS変動幅(基準比) | 0.0%(基準) | 60.3% | 53.7% | 51.6% |
| Nuked-OPM RMS変動幅(基準比) | 0.0%(基準) | 72.6% | 72.7% | 62.0% |
(AMS=0の実測範囲はAME=0と完全に一致しており、基準比の基準値もAME=0の表と同じものを使っている)
AMS=0(X68Soundの実装では感度シフト量31相当となり、AM深度が内部的に完全にゼロになる設定)は両実装ともAME=0のときとほぼ同じ、ノイズレベルの変動幅に留まり、「AMS=0のときAM変調が事実上かからない」という点は一致した。一方、AMS=1〜3の間では、内部的な深度パラメータは2倍・4倍と等比的に増えるはずだが、基準比で見ても実際の出力RMS変動幅はそのようには増加しなかった(X68Sound: 60.3%→53.7%→51.6%、Nuked-OPM: 72.6%→72.7%→62.0%)。X68SoundはAMS=1が最大でそこから単調に減少するのに対し、Nuked-OPMはAMS=1からAMS=2でほぼ横ばい(72.6%→72.7%、差0.1ポイントで測定誤差の範囲内)のあとAMS=3で減少しており、厳密な最大値の位置は一致していない。これはエンベロープの減衰量が対数的(指数変換後)に音量へ反映される仕組み上、内部の加算量が等比的に増えても出力の線形振幅に与える影響は徐々に小さくなる(音量が下がるほど同じ加算量あたりの線形振幅変化が縮む)ためと考えられ、矛盾ではない。ただし、この非線形性のために、音声出力のRMS変動幅だけからAMS=1/2/3の内部スケール比(本来は1倍・2倍・4倍のはず)を厳密に逆算することはできず、ON/OFFの確認以上の量的な検証には限界がある。
エンベロープの4つの速度パラメータ(アタックレートAR、ディケイ1レートD1R、ディケイ2レートD2R、リリースレートRR)について、既存のエンベロープカーブ検証(1組の設定のみ)を拡張し、それぞれのレート値を0〜31(RRのみ0〜15)の範囲で広く振って、内部エンベロープ値が目標値へ到達するまでのサンプル数を比較した。値の変化がきわめて遅くなるレート値では、目標値に到達するまで数十万〜数百万サンプル相当のティックを進める必要があるため、測定には十分大きな上限(300万サンプル)を設けた。
AR(D1R=12・D2R=6・RR=7・SL=8・KC=$40・KS=0固定、KEY ON直後から最大音量(XrEl=0)到達までのサンプル数)
| AR | 1 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 31 |
|---|---|---|---|---|---|---|---|---|---|
| X68Sound | 445437 | 149757 | 37437 | 9357 | 2361 | 600 | 168 | 51 | 15 |
| Nuked-OPM | 445427 | 148979 | 37235 | 9299 | 2315 | 581 | 152 | 38 | 0 |
D1R(AR=31瞬時アタック・D2R=0・RR=7・SL=15(D1L閾値62)・KC=$40・KS=0固定、瞬時アタック完了後にXrElが500へ到達するまでのサンプル数)
| D1R | 1 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 31 |
|---|---|---|---|---|---|---|---|---|---|
| X68Sound | 測定不能※ | 1023741 | 255933 | 63981 | 16017 | 4014 | 1017 | 267 | 204 |
| Nuked-OPM | 測定不能※ | 1022943 | 255711 | 63903 | 15951 | 3981 | 978 | 228 | 168 |
※D1R=1は300万サンプルの上限内では到達しなかった。X68Soundの内部レート表・更新間隔(エンベロープクロックは3ティックに1回しか進まない)から理論値を計算すると、目標値到達には約307万サンプル(62500Hzで約49秒)を要する計算になり、上限にわずかに届かなかっただけで、実装上の不具合ではない。
D2R(AR=31・D1R=31・RR=7・SL=1(D1L閾値2)・KC=$40・KS=0固定、サスティン相到達後にXrElが500へ到達するまでのサンプル数)
| D2R | 1 | 4 | 8 | 12 | 16 | 20 | 24 | 28 | 31 |
|---|---|---|---|---|---|---|---|---|---|
| X68Sound | 2872317 | 957693 | 239421 | 59853 | 14985 | 3768 | 963 | 261 | 214 |
| Nuked-OPM | 2872107 | 956715 | 239019 | 59595 | 14787 | 3549 | 747 | 45 | 0 |
RR(AR=31・D1R=0・D2R=6・SL=8・KC=$40・KS=0固定、瞬時アタック後にKEY OFFし、XrElが900へ到達するまでのサンプル数)
| RR | 1 | 4 | 7 | 10 | 13 | 15 |
|---|---|---|---|---|---|---|
| X68Sound | 2763261 | 345405 | 43221 | 5430 | 708 | 372 |
| Nuked-OPM | 2763228 | 345372 | 43188 | 5397 | 672 | 336 |
いずれのパラメータも、遅い〜中程度のレート値では両実装の一致度が高い(多くの点で99%以上)。一致度が下がるのはレート値が最大に近く変化が速い領域に限られ、特にD2R=28・31では大きな差が生じている(X68Soundは261・214サンプルなのに対し、Nuked-OPMは45・0サンプル)。この領域は前述の「キースケール$7F」「アタックレート最大時」の検証と同様、レジスタ反映の位相・エンベロープクロックの端数処理に測定結果が敏感になる境界条件であり、AR=31(瞬時アタック)でX68Sound=15・Nuked-OPM=0という既知のオフセットと同種の現象と考えられる。速度カーブの大局的な形(レートが大きいほど指数的に速くなる)はAR/D1R/D2R/RRいずれも一致している。
D1L(サスティンレベル、しばしばSLとも表記される)は、ディケイ1(D1R)からディケイ2(D2R、サスティン)へ切り替わる音量のしきい値を16段階で指定するパラメータ。X68Soundの内部テーブルD1LTBLは、D1L=0〜14ではD1L*2、D1L=15だけ特別に(15+16)*2=62という値を持つ。AR=31・D1R=10・D2R=0(サスティン相に入ったら音量を保持させる)・RR=7・KC=$40・KS=0固定で、D1L全16段階について、ディケイが収束した後のエンベロープ値を比較した。
| D1L | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 理論値 | 0 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 62 |
| X68Sound到達値(理論値×16) | 0 | 32 | 64 | 96 | 128 | 160 | 192 | 224 | 256 | 288 | 320 | 352 | 384 | 416 | 448 | 992 |
| Nuked-OPM到達値 | 0 | 32 | 64 | 96 | 128 | 160 | 192 | 224 | 256 | 288 | 320 | 352 | 384 | 416 | 448 | 992 |
全16段階で、両実装の到達値が理論値と完全に一致した。D1L=15だけが不連続に大きい値(62、他の段階の延長線上なら30になるところ)を取るという、X68Soundのテーブルにある特別扱いも含めて、Nuked-OPMと一致することを確認できた。
23ea53bb442b3f761ded3cd8a27399dd46db34fc。src/x68sound/ 以下のソースコード — 本記事の比較対象。対象コミットはd058ca0031544883ed22831b9d783d9eec5a6f88。YM2151のレジスタ構成やアルゴリズムの一般的な知識は主に上記2つのソースコードに基づき、本記事の数値検証も基本的にはその実行結果の照合による。ただし「書き込みビジー」の節のみ、待機クロック数についてYM2151データシートの仕様値を参照し、実測BUSY時間との照合に用いている。