X68SoundとNuked-OPMの比較調査

本記事は、portable_mdxに含まれるYM2151(OPM)エミュレーション実装(src/x68sound/、以下「X68Sound」)と、シリコン解析ベースのサイクル精度エミュレータNuked-OPMを照合し、両者の挙動の差異・一致点を調査した結果のまとめである。

用語についての注意: Nuked-OPMはシリコンダイの解析に基づいて高い精度を目指したソフトウェアエミュレータであり、物理的なYM2151チップそのものを測定した実測データではない。したがって本記事の「一致」「相違」という判定は、2つのソフトウェア実装を照合した結果であって、YM2151の個体の挙動を確かめたものではない。

まとめで使う主な用語を、先にここで定義しておく(詳しくは「検証方法」以降の各節)。

まとめ

確認できた相違点

確認できた一致点

完全一致・高精度一致の項目(数値上の差は見つかっていない)

大枠は一致するが、細部に差が残る項目(複数の条件で検証し、一致する範囲と差がある範囲を確認済み)

X68Soundの内部実装はNuked-OPMと大きく異なる一方、エンベロープ・ピッチ・MUL・アルゴリズム結線・ミキシング・LFOなど音の核となる部分は、上の一致点の範囲で高い再現度を示した。ただしエンベロープとピッチには、キースケールやDT1がDT2・PMと組み合わさる条件で、相違点に挙げた差が残る。相違点は次の8種類に分けられる。

波形の差は、1段の発音では無視できる大きさでも、変調段を重ねるほど拡大する。どのオペレータがキャリアかという判定は8種すべてで一致するが、3〜4段の直列変調を深くかけると出力波形はほぼ別物になる。オペレータごとの出力を比べると、差は遅延の違う経路の直後の段で急に大きくなり、TL=30〜40ではその大きさをほぼ遅延の違いだけで再現できる。さらに深い変調では、演算方式(線形乗算と対数-指数変換)の違いの寄与のほうが大きい。結線が一致していても、変調器の出力を何サンプル目のものとして受け取るかと、変調の演算方式が違うため、深い変調では出力波形が一致しない。

実際の楽曲再生における聴感上の影響

上記の差分は、数値上の乖離の大きさと、実際の楽曲でその条件が使われる頻度によって、聴感への影響度が大きく変わる。本記事で確認した数値をもとに、おおまかに次のように分類できる。

聴感に影響しうる項目

特定の条件でのみ聴感に影響しうる項目

通常の楽曲再生では聴き取りにくい項目

ただし、これは本記事で検証した条件・数値から推測した傾向にすぎず、実際の聴こえ方は楽曲の編成・音色設定・再生環境によって変わりうる。

前提: 2つの実装アプローチの違い

このため、「内部の値が完全に同じかどうか」を単純比較できない部分(音を混ぜ合わせる結線のロジック、ピッチ補正の計算方法など)がある。

検証方法

一方、「同じ設定を与えたときに最終的にどんな音量・周波数になるか」は、両方を実際に動かせば数値で照合できる。そこですべての項目について、片方のソースコードを読んで「こう動くはずだ」と推測するだけでなく、実装ごとに次の方法で内部の数値を取得し、両者の観測条件を揃えたうえで照合した。

X68Sound側の検証

src/x68sound/を実際にコンパイルし、公開APIのレジスタ書き込み関数(X68Sound_OpmReg/OpmPoke)で駆動して検証した。src/x68sound/の複製には、計算内容を変えない読み出し専用のアクセサ(エンベロープ値・位相増分・LFO値・AM適用量・ノイズ更新回数などをそのまま返すだけのgetter)と、音声出力用の内部ループ(pcmset62())から1ティック分の処理(timer処理・コマンド実行・エンベロープ更新・LFO更新・オペレータ出力計算)を切り出したヘルパーを追加した。このほか、計算には使わない計数・記録用の変数を3か所に置いた(ノイズの引き直し回数、出力段のクリップ直前の値の最大値、各オペレータが直近に出力した値)。複製は参考文献に挙げたコミット60c43e7のsrc/x68sound/から作っており、このコミットとの差分はこれらと、LFOの値を返す2つの関数へのconst修飾の追加だけである。その後のコミット0c754b8は、コマンドバッファの大きさを実行時に変えるAPIをsrc/x68sound/に追加しているが、既定の大きさ(添字マスク65535)は変わらないため、既定設定での挙動は複製と同じである。Nuked-OPM側はopm_t構造体のpublicフィールドとして内部状態が最初から公開されているため、X68Sound側だけ薄いラッパーを挟むことで、両実装を対称な条件(実ライブラリを実際に駆動し、その内部状態を直接読む)で比較している。

観測点の非対称と、共通観測点の取り方

音量に関わる項目(TL減衰カーブ・AM深度・複数チャンネルのミキシング・アルゴリズム結線・PAN)では、どの段の値を読むかが結果を大きく左右する。X68Soundの音声出力(GetPcm())は、OPMコアの出力そのものではなく、X68000というハードウェア全体をモデル化した出力段を通過した後の値だからである。具体的にはpcmset62()の中で、

という処理が行われる。最初の合算より後の段はいずれもX68000のハードウェア側の特性であってOPMコアの挙動ではない。一方Nuked-OPM側のdac_outputは、チップのDAC(仮数10bit+指数3bitの浮動小数点表現)を通した直後の値で、上記のようなフィルタやリサンプルを含まない。

この非対称な比較を行うと、フィルタによる包絡線の鈍りやスケールダウン後の量子化が、あたかもOPMコアの実装差であるかのように見えてしまう。そこで本記事では、OPMコアの合算直後(上の箇条書きの1段目)を共通の観測点とした。X68Sound側はPANマスク加算を行うアクセサを追加し、Nuked-OPM側はミキサのクランプ前の線形合計(chip.mix2[])を読んでいる。両者は絶対スケールが500倍あまり違う(X68Sound側の内部表現が9bit広い)ため、数値は基準比・理想比・相対低下率といったスケールに依存しない形で比較している。

この比は固定ではない。波形をサンプル単位で位置合わせし、最小二乗法で利得を求めると、1チャンネルではTL=0・20で513.3、TL=40で514.5、TL=64で524.6となり、低音量ほど大きくなる。これはNuked-OPMのオペレータ出力が14bit幅で量子化され、低レベルほど理論値より小さく出るためで、TL減衰カーブの節で見る差と同じものである。チャンネル数を増やすと8チャンネルで518.1〜529.6まで動き、波形の残差も信号の0.3%から1.6%へ増える。ただしこちらは実装差ではない。各チャンネルのKEY ONを順に書き込むとNuked-OPM側だけ書き込みごとに2.5サンプル進むため、チャンネル間の位相関係が両実装で揃わないことによる。いずれにしても比は3%ほど動くため、絶対値の換算には使えない。

飽和・クリップについては、この共通観測点での線形加算と、その後段の表現(Nuked-OPMのミキサ段の飽和とDAC、X68Soundの出力段クリップ)を分けて測定した。

レジスタ書き込みの扱いの非対称

両実装は「レジスタ書き込みが実際に反映されるまでの時間」の作り方が構造的に違う。X68Soundはコマンドキューに積み、CmndRateベースの間引きでExecuteCmnd()が順次捌く。Nuked-OPMは実チップのバス同様、アドレス書き込み後とデータ書き込み後にそれぞれクロックを進める必要がある(本記事の検証では各40クロック=計2.5サンプルとした)。

Nuked-OPMへの書き込みはチップ時間を2.5サンプル進めるが、X68Soundへの書き込みは進めない。この非対称は2通りの形で測定結果に影響する。

Nuked-OPM側の検証

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()自体は特定の周波数を持たないサイクルベースの実装である。そこでOPM_Clock()1回が入力クロック何周期に当たるかを、X68Soundの値を使わずにNuked-OPM単独で求めた。データシートのタイマー周期の式(タイマーAは64×(1024−CLKA)入力クロック、タイマーBは1024×(256−CLKB)入力クロック)に対し、Nuked-OPMのタイマーがオーバーフローする間隔をOPM_Clock()の呼び出し回数で数えると、CLKA 3点・CLKB 3点のすべてでちょうど半分の回数だった。つまりOPM_Clock()1回は入力クロック2周期に当たる。また内部のスロットカウンタ(cycles)はOPM_Clock()32回で1巡し、32スロットのパイプラインを一回りする。以上から、1音声サンプルはOPM_Clock()32回=入力クロック64周期であり、4MHzでは62500Hzになる。以降の全ての時間・周期の比較は、この「32クロック=1サンプル、62500Hzサンプリング」という換算に基づいている。

検証範囲についての注意

以下の一致確認は、特定のパラメータの組み合わせでのみ行ったものであり、全パラメータ空間を網羅的に確認したわけではない。具体的には:

他のレート・音階・波形の組み合わせで異なる挙動が出る可能性は排除できていない。特に「一致」と結論した項目についても、この限定された範囲内での一致であることに留意が必要である。

確認できたNuked-OPMとの相違点

CSMモード

YM2151にはCSM(Composite Sine Mode)という機能があり、有効にするとタイマーAがオーバーフローするたびに全チャンネルへ自動でKEY ON/OFFのパルスを送る。これはレジスタ$14の最上位ビット(bit7)で切り替える。Nuked-OPMはこのビットを読み取って実装しているが、X68Sound側のレジスタ$14処理はタイマーの開始/停止・割り込み許可・フラグリセットに使う下位ビットのみを扱っており、bit7(CSM)には対応していない。

これを実測で確認するため、KEY ONレジスタ($08)を一度も書かず、音色設定とタイマーA(CLKA=1000)の動作、そしてCSMビットだけを与えて4000サンプル分の出力を観測した。

設定 X68Sound Nuked-OPM
CSM=0 peak 0(非0サンプル 0/4000) peak 0(非0サンプル 0/4000)
CSM=1 peak 0(非0サンプル 0/4000) peak 2920(非0サンプル 3998/4000)

CSM=0ではどちらも完全に無音であり、CSM=1にするとNuked-OPMだけがKEY ONなしで発音を始めた。X68Soundは同じ書き込みに反応しない。

条件を広げても結果は変わらない。全8チャンネルに同じ音色を置き、CLKAを1023・1016・1000・960・768・0(タイマーAの周期1〜1024サンプル)に振って8000サンプルずつ観測すると、Nuked-OPMはどの設定でも全8チャンネルが発音し、X68Soundはどのチャンネルも発音しなかった。X68Soundはbit7を立ててもタイマーAのオーバーフローフラグは通常どおり立ち、ch0をKEY ONした状態での出力もCSM=0のときと1サンプルも違わなかった。X68Soundはbit7を読まずに無視しており、副作用もない。CSM専用の効果音・特殊奏法を使う楽曲でのみ影響が出る、限定的な相違点である。

ノイズ生成器の疑似乱数アルゴリズム

Nuked-OPMのノイズ発生回路は、内部に16bitのシフトレジスタ(LFSR)を持ち、決まった規則でビットをシフト・反転させることで疑似乱数を生成する。この同じ1つの回路が、ノイズ音そのものと、LFOのノイズ波形の両方に使われている。X68Soundは、種の値に定数を掛けて足す乱数生成(線形合同法)を使っており、しかもノイズ音とLFOノイズ波形とで別々に呼び出している。このため、実際に鳴る波形のビット列自体はNuked-OPMとは別物になる。

一方、統計的な性質は一致している。出力ビットを±1として20万サンプル分を比べると、平均(偏り)はNFRQ=31でX68Sound -0.002・Nuked-OPM +0.001、NFRQ=16で-0.006・+0.009と、どちらも0に近い。自己相関(ずれ1〜4サンプル)はNFRQ=31で両実装とも±0.004以内、NFRQ=16では両実装とも0.875・0.750・0.625・0.500前後となり、1回の値が8サンプル保持されることによる同じ減衰を示した。ノイズを有効にしたオペレータの出力レベルも、同じオペレータの正弦波の実効値に対する比で比べると、TL=0・16・32のいずれでも両実装の差は0.15%以内だった。

ノイズ周波数(レジスタ$0F)を書き換えたときの挙動

Nuked-OPMのノイズの周期を数えるカウンタは常に動き続けていて、周波数レジスタを書き換えても「次に何カウントで折り返すか」という目標値が変わるだけで、カウンタ自体はリセットされない。X68Soundは周波数レジスタが書き換わるたびにカウンタを0から再開する。

これをNFRQ=15(周期8.5サンプル)で実測した。内部の更新(X68Soundは乱数の引き直し、Nuked-OPMはnoise_timer_ofの立ち上がり)の直後に揃え、そこから e サンプル後に同じ値を書き直して、次の更新までの時間を測る。カウンタがリセットされるなら e によらず約1周期待つことになり、リセットされないなら「1周期 − e」で次が来るはずである。書き込みの起点は、X68Soundはコマンドをキューに積んだ時点、Nuked-OPMはアドレス書き込みの時点とした。

書き直しまでの経過 e(サンプル) 0 1 2 3 4 5 6 7
X68Sound(サンプル) 9 10 9 9 10 9 9 1
Nuked-OPM(サンプル) 8.50 7.50 6.50 5.50 4.50 3.50 2.50 1.50

Nuked-OPMは正確に「8.5 − e」で次の更新が来ており、カウンタは継続している。X68Soundはe=0〜6で常に1周期強(コマンドキューの消化待ちの分だけ8.5より長い)待っており、カウンタがリセットされている。e=7だけ1サンプルで来るのは、書き込みがキューから捌かれる前に本来の更新時刻が来たためである。X68Soundのみリセットすることが、書き込み位相によらず確認できた。ノイズ周波数を曲中で頻繁に変更する楽曲では、ノイズの波形タイミングがNuked-OPMとずれる可能性がある。

残る論点は、「周波数レジスタの値ごとに、ノイズがどれだけの間隔で変化するか」という対応関係そのものである。この測定にあたっては観測量の選び方に注意が必要になる。両実装は乱数を進める仕組みが構造的に違うため、乱数の歩進回数同士は直接比較できない。

比較できる量は2つある。1つ目は更新の時刻そのもので、X68Soundの引き直しの時刻と、Nuked-OPMのnoise_timer_ofが立ち上がる時刻(NFRQ=31では常に1なので、歩進するクロック)を4000サンプル分記録して間隔を比べた。これは決定論的な量なので、統計的な誤差を含まない。

NFRQ 0 1 8 15 16 24 28 29 30 31
理論値(サンプル) 16 15.5 12 8.5 8 4 2 1.5 1 0.5
X68Sound 平均(取りうる間隔) 16.000 (16) 15.498 (15, 16) 12.000 (12) 8.499 (8, 9) 8.000 (8) 4.000 (4) 2.000 (2) 1.500 (1, 2) 1.000 (1) 1.000 (1)
Nuked-OPM 平均(取りうる間隔) 16.000 (16) 15.500 (15.5) 12.000 (12) 8.500 (8.5) 8.000 (8) 4.000 (4) 2.000 (2) 1.500 (1.5) 1.000 (1) 0.031 (1/32)

NFRQ=0〜30では平均間隔が両実装で一致する。X68Soundは1サンプル単位でしか引き直せないため、半端な周期(15.5、8.5、1.5)を2つの整数間隔の交互で実現しており、Nuked-OPMは半サンプル単位で正確に刻む。NFRQ=31だけは内部の速さが違う(X68Sound 1サンプル、Nuked-OPM 1クロック)が、後述のとおり出力は両実装とも1サンプルに1回で頭打ちになる。

2つ目は、実際に出力へ出るノイズ値が変化する間隔である。X68SoundはOp::NoiseValue、Nuked-OPMはOPM_NoiseChannel()が1サンプルに1回(cycles==12)ラッチするnc_sign_lockが、それぞれ出力に使われる値にあたる。LFSRが内部でどれだけ速く回っても、出力へ出るのはこのレートに制限される。

NFRQ 0 8 16 24 28 29 30 31
引き直し間隔の理論値(サンプル) 16.0 12.0 8.0 4.0 2.0 1.5 1.0 0.5
X68Sound 引き直し間隔(内部) 16.000 11.998 8.000 4.000 2.000 1.500 1.000 1.000
X68Sound 出力値の変化間隔 32.154 24.067 16.013 7.946 4.021 3.004 2.003 2.003
Nuked-OPM 出力値の変化間隔 31.949 24.570 16.051 7.994 3.947 3.033 1.980 2.005

出力値の変化間隔は、NFRQ=0〜31の全32段階で両実装に有意な差は見られなかった。ただし20000サンプルでの比較では統計的な揺らぎが大きく、段階によっては2%程度の差が出る。そこで上表の8段階は20万サンプルに延ばして95%信頼区間を求めた。

NFRQ 0 8 16 24 28 29 30 31
X68Sound 32.08±0.56 24.06±0.37 16.01±0.20 7.97±0.07 3.98±0.03 2.99±0.02 1.993±0.009 1.993±0.009
Nuked-OPM 31.66±0.54 23.69±0.35 15.91±0.20 7.99±0.07 4.00±0.03 3.00±0.02 2.000±0.009 1.997±0.009
差÷合成信頼区間 0.55 0.73 0.36 0.18 0.45 0.47 0.60 0.35

差を2つの信頼区間の合成で割った値は8段階すべてで1未満であり、95%信頼区間の範囲で一致している。変化間隔が引き直し間隔のおよそ2倍になるのは、乱数を引き直しても約半分の確率で同じ値が出るためで、これも両実装に共通する性質である。

NFRQ=30と31で両実装とも約2サンプルで頭打ちになる点も一致している。頭打ちになる理由は実装ごとに違う。X68Soundはif (NoiseCycle < NoiseStep) NoiseCycle = NoiseStep;というクランプによって、引き直しそのものが1サンプルに1回へ制限される。Nuked-OPMはNFRQ=31でnoise_timer_ofの判定が noise_timer == (noise_freq ^ 31) すなわち noise_timer == 0 となって常に成立し、LFSRが毎クロック(1サンプルに32回、NFRQ=30の倍)歩進するようになるものの、出力へのラッチが1サンプルに1回しかない。どちらも「1サンプルに1回、新しい乱数ビットが出力へ出る」という同じ状態に収束する。

周期の対応関係自体はNuked-OPMと一致している。差異は、レジスタを書き換えた瞬間にカウンタが巻き戻るかどうかと、半端な周期を1サンプル単位の交互で近似するかどうか(平均は一致)の2点にとどまる。

波形生成の演算方式(線形乗算と対数-指数変換)

上記「前提」のとおり、両実装は波形の計算手順が異なり、対数領域での丸め・量子化による微妙な歪みまではX68Soundで再現していない。この差が出力波形にどれだけ現れるかを、OPMコア観測点の16384サンプルの波形で測った。両系列を時間方向に位置合わせし、最小二乗法で求めた利得を掛けたうえで、残差の実効値を信号の実効値で割った値(以下「波形の残差」)を指標にする。あわせて、第2〜第6倍音の強さを基本波に対するdBで比べた(KC=$40・MUL=1・FB=0、全オペレータ同じTL)。

CON 0 1 2 3 4 5 6 7
波形の残差(TL=20) 15.9% 12.2% 15.1% 11.8% 1.4% 1.9% 2.4% 1.3%
波形の残差(TL=0) 99.1% 99.6% 91.6% 91.3% 6.7% 6.8% 3.2% 1.3%

CON=7(4オペレータとも独立した正弦波)では残差が1.3%で、1オペレータだけを鳴らした場合は0.3%である。この差はオペレータの計算ではなく、Nuked-OPMのミキサが出力の1サンプルにまとめるスロットの範囲による。4オペレータの出力を同じサンプル番号で足し合わせて比べると残差は0.3%に戻る。1スロットだけを鳴らして、各スロットの出力がどの出力サンプルに加算されるかを直接測ると、Lチャンネルでは、M1(全チャンネル)とM2(ch0〜6)がC1・C2より1サンプル後の計算結果として同じ出力サンプルに入り、キャリアどうしが1サンプルずれて加算される。ch7のM2だけはC1・C2と同じサンプルの計算結果が入るため、ch7ではCON=7の残差が1.1%になる。Rチャンネルは区切りの位置が違い、ch7のC2を除く全スロットが同じサンプルの計算結果としてまとまる(ch7のC2だけが1サンプル前の計算結果)。本記事の波形比較はLチャンネルで行っている。

変調をかけると差はさらに広がる。2段の構成(CON=4〜6)ではTL=0でも残差が7%未満で、倍音の強さの差も0.5dB程度に収まる。3〜4段の直列変調を含むCON=0〜3では、TL=20ですでに12〜16%あり、第6倍音のように強さが17〜18dB違う倍音も出る(CON=2・3)。変調を最も深くしたTL=0では91〜100%になる。残差が100%に近いということは、利得を最適に合わせても両者の波形がほぼ無相関であることを意味する。TL=0のCON=0〜3では第2〜第5倍音の強さも最大14dB程度違い、音色として別の波形になっている。変調指数が大きいと変調波のわずかな違いがキャリアの位相を大きく動かし、段を重ねるごとにその違いが拡大する。差の元が演算方式(線形乗算と対数-指数変換)か、変調波が次の段へ渡るまでの遅延かは、次の「変調経路の遅延」の節でオペレータごとの出力を比べて切り分ける。

FB(オペレータ1のセルフフィードバック)でも同じ傾向が出る。CON=7でオペレータ1だけをTL=0で鳴らし、FBを0〜7まで変えて比べた。

FB 0 1 2 3 4 5 6 7
波形の残差 0.3% 0.3% 0.3% 0.4% 1.1% 57.6% 92.0% 99.8%

FB=0〜4では残差が1.1%以下で、両実装の波形は一致する。倍音の強さの差も、FB=1〜4では0.7dB以内である(FB=0は倍音そのものが基本波の-80dB以下で、比較する意味がない)。FB=5では倍音の強さが0.1dB以内で一致するにもかかわらず残差が57.6%ある。FB=6・7では倍音の強さも最大3dB程度違う。

FB=5以上では、残差の値そのものも測る区間によって変わる。16384サンプルを2048サンプルずつ8区間に分け、全体で求めたラグと利得のまま区間ごとの残差を見ると、FB=3は全区間で0.4%、FB=4は0.8〜1.3%と安定しているのに対し、FB=5は37〜65%と区間によって大きく変わる。最小二乗で求めた利得(両実装のスケール比)も、FB=3・4では513前後と正弦波のときと同じ値なのに対し、FB=5で418.6、FB=6・7では199.7・32.3まで崩れる。利得が崩れるのは、波形の相関が落ちて最小二乗の当てはめが0に近づくためである。

FBの構造は両実装で同じである(X68Soundは(o + Inp_last) >> (9-FB)、Nuked-OPMは(m1[0] + m1[1]) >> 1 >> (9-FB)で、どちらも直前2サンプルの出力の和を同じ量だけ縮めて自分の位相に足す)。したがって差は演算方式の違いから来るが、それが「演算方式の違いが系統的に別の波形を作る」ことによるのか、「どんな微小な差でも拡大する条件だから」なのかは、別の問題である。

これを切り分けるため、両実装の演算をハーネス内でそのまま再現した。X68Soundは正弦表と振幅表の乗算、Nuked-OPMは対数正弦表と指数表(opm.cから抜き出したもの)による計算で、どちらも同じ位相とエンベロープの記録で駆動する。自分の記録で走らせた再現は、FB 0〜7のすべてで実測と1サンプルも違わなかった。そのうえで、それぞれの方式の中で、M1の位相を1回だけ正弦表の1段(1/1024周期)ずらし、元の波形からどれだけ離れるかを見た(ずらす位置を8通りに振り、ずらした後の4096サンプルで評価)。

FB 0〜3 4 5 6 7
実測の残差 0.3〜0.4% 1.2% 60.1% 92.5% 99.8%
同じ記録での演算方式だけの違い 0.3〜0.4% 1.1% 57.4% 92.0% 99.8%
1回ずらしたときの残差(X68Sound方式) 0.02%以下 0.05%以下 0.15%以下 0〜30.1% 0〜88.4%
1回ずらしたときの残差(Nuked-OPM方式) 0.03%以下 0.07%以下 0.25%以下 0〜40.6% 0〜99.9%

(この表の実測の残差は、FBの状態を両実装とも0から始めるためにKEY ON前から記録し直した値である。KEY ON後200サンプルから記録した上の表とは測定区間が違うため、FB=5で57.6%と60.1%のように値がわずかに違う)

FB=5までは、どちらの方式でも、位相を1回ずらしても波形は元に戻る。FB=5の57%の差は、微小な差が拡大した結果ではなく、2つの演算方式がそれぞれ別の安定した波形に収束した結果である(倍音の強さが0.1dB以内で一致するのに残差が大きいのは、倍音どうしの位相関係が違うためと考えられるが、確認していない)。FB=6・7では、ずらす位置によっては元に戻らず、X68Sound方式で最大30%・88%、Nuked-OPM方式で最大41%・99.9%まで離れる。この2つは同じ演算方式の中でも微小な差が拡大する条件であり、実装間の差を特定の演算の違いに帰着させることはできない。

変調経路の遅延

3〜4段の直列変調で波形の残差が大きくなる原因を切り分けるため、両実装からオペレータごとの出力を取り出した。X68Soundはオペレータの出力計算が書き込んだ値を記録用の変数に写し、Nuked-OPMはパイプラインの出力段(OPM_OperatorPhase14)がop_out[5]に置く値をスロットごとに読む。キャリアの出力の和がOPMコアの出力と一致することを確かめてから、次の2つを測った。

1つ目は、変調器の出力をキャリアが何サンプル後に使うかである。キャリアを先に鳴らしておき、記録の途中で変調器だけを追加でKEY ONして、変調器の出力が0でなくなったサンプルと、キャリアの出力が対照実行(変調器をKEY ONしない)と食い違い始めたサンプルとの差を取った。

CON 0 0 0 1 1 1 2 2 2 3 3 3 5 5 5
経路 M1→C1 C1→M2 M2→C2 M1→M2 C1→M2 M2→C2 M1→C2 C1→M2 M2→C2 M1→C1 C1→C2 M2→C2 M1→C1 M1→M2 M1→C2
X68Sound 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Nuked-OPM 0 1 0 1 1 0 0 1 0 0 1 0 0 1 0

(CON=4のM1→C1・M2→C2、CON=6のM1→C1は両実装とも0)

X68Soundは1サンプルの中でM1・C1・M2・C2の順に計算し、どの経路でも同じサンプルの変調器出力を使う。Nuked-OPMは1つの演算回路を時分割で使い、OPM_Clock()1回につき1スロットずつ、M1(ch0〜7)・M2・C1・C2の順に処理する。あるスロットが変調入力を取り込んでから、その出力が次の段へ渡せる形で出てくるまでには、約16スロット分の時間がかかる。この値は推定で、OPM_OperatorPhase15()が直前の出力op_out[5]をそのまま変調入力に使うのが、処理順で16スロット離れたM1→C1とM2→C2だけであることから求めた。このため、変調先より16スロット以上前に処理される変調元(M1→C1、M2→C2、M1→C2)は今のサンプルの出力を使えるが、8スロット前のM1→M2(CON=1・5)とC1→C2(CON=3)、処理順が後のC1→M2(CON=0〜2)は、1サンプル前の値を使う。

2つ目は、差がどの段で大きくなるかである。オペレータごとに見ると、差は遅延の違う経路の直後で急に大きくなる。TL=20のCON=1では、M1・C1の残差が0.3%のまま、1サンプル遅れる経路の先にあるM2で4.2%、C2で12.2%になる。CON=3では、C1→C2だけが遅れるため、C1が1.4%、M2が0.3%のまま、C2で11.8%になる。

遅延の違いが残差のどれだけを占め、演算方式(線形乗算と対数-指数変換)の違いがどれだけを占めるかは、両実装の演算をハーネス内でそのまま再現して測った。X68Sound方式はALPHATBLとSINTBLの乗算で、変調入力を20bitの位相に足してから上位10bitで正弦表を引く。Nuked-OPM方式は、1/1024周期単位の位相に変調器出力の和の半分を足し、対数正弦表と減衰量の和を指数表で戻して14bitの符号付き出力にする(表はopm.cから抜き出して使う。Nuked-OPMは2つの変調入力を別々の変数へビットごとのORで入れてから足すが、全条件で再現が実測と一致したことから、FB以外の結線では2つの入力が同じ変数に重ならず、和と同じ結果になると分かる)。どちらもNuked-OPMの実測の20bit位相とエンベロープ値で駆動し、遅延の配置は別に与える。自分の方式・自分の遅延で自分の記録から再現した出力は、両実装ともTL 5段階×CON 8種のすべてで実測と1サンプルも違わなかった。そのうえで、演算方式と遅延の配置を1つずつ入れ替えた。

TL CON 実測の残差 遅延だけ入れ替え(X68Sound方式 / Nuked-OPM方式) 演算方式だけ入れ替え(X68遅延 / Nuked遅延)
40 0 / 1 / 2 / 3 1.4 / 1.8 / 2.0 / 2.0% 1.5〜2.2% / 1.5〜2.2% 0.5〜1.0% / 0.5〜1.0%
30 0 / 1 / 2 / 3 5.3 / 5.5 / 12.1 / 10.4% 4.6 / 4.7 / 12.8 / 9.7% 1.2 / 1.5 / 1.8 / 1.8%
20 0 / 1 / 2 / 3 16.0 / 12.2 / 15.1 / 11.9% 9.7 / 8.8 / 14.2 / 10.9% 12.7 / 7.7 / 4.1 / 4.1%
10 0 / 1 / 2 / 3 93.9 / 42.9 / 36.3 / 29.5% 23.8 / 22.8 / 30.8 / 22.6% 93.2 / 37.1 / 18.1 / 18.1%
0 0 / 1 / 2 / 3 99.1 / 99.6 / 91.6 / 91.3% 50.4 / 49.3 / 64.1 / 49.7% 98.7 / 99.6 / 88.1 / 88.1%

(TL=30〜0の「遅延だけ入れ替え」「演算方式だけ入れ替え」は、もう一方の条件をX68Sound側に固定した値。もう一方をNuked-OPM側に固定しても差は2.1ポイント以内。実測の残差は、この比較のためにKEY ON前から記録し直した値)

TL=30〜40では、演算方式だけを入れ替えた差は2%未満で、実測の差は主に遅延の違いで生じる。ただしTL=40では、演算方式の差0.5〜1.0%も、実測の差1.4〜2.0%と比べて小さくない。TL=20では両方が同じ程度に寄与し、CON=0では演算方式の違い(12.7%)が遅延の違い(9.7%)を上回る。TL=10以下では、演算方式の違いだけで実測の差に近い大きさになり、遅延の違いよりも大きい。深い変調では、演算方式の違いのほうが大きな要因である(残差は足し合わせられる量ではないため、どちらが大きいかの比較にとどめる)。演算方式のうちどの段(位相への変調量の足し方、正弦の計算、出力のビット幅)が差を生んでいるかまでは切り分けていない。

どちらの方式が理想的な計算に近いかは、各実装の位相の実測値で駆動する倍精度の理想モデル(正弦波と変調の加算だけを倍精度で行い、振幅はTLから理論値で決める。遅延の配置は各実装に合わせる)との差で見られる。

TL CON X68Soundとモデルの差 Nuked-OPMとモデルの差
40 0 / 1 / 2 / 3 0.3 / 0.2 / 0.4 / 0.4% 0.5 / 0.5 / 0.9 / 0.9%
30 0 / 1 / 2 / 3 0.7 / 0.7 / 1.0 / 1.0% 1.6 / 1.9 / 2.1 / 2.1%
20 0 / 1 / 2 / 3 3.0 / 1.0 / 0.9 / 0.9% 13.1 / 8.0 / 4.2 / 4.2%
10 0 / 1 / 2 / 3 59.5 / 16.4 / 7.3 / 7.3% 97.4 / 45.4 / 21.8 / 21.5%
0 0 / 1 / 2 / 3 82.1 / 15.3 / 8.0 / 8.0% 99.0 / 99.6 / 89.6 / 90.9%

X68Soundはモデルとの差が小さく、TL=0でもCON=0(82.1%)を除いて8.0〜15.3%にとどまる。X68Soundは変調入力を20bitの位相に足してから切り捨てるため、倍精度の計算に近い。Nuked-OPMはTL=20ですでに4〜13%、TL=0では90〜100%離れる。CON=1〜3の深い変調で両実装の波形が一致しないのは、主にNuked-OPMの演算が理想的な計算から離れるためである。Nuked-OPMはシリコン解析に基づく実装なので、実チップも同じように理想計算から離れている可能性があるが、実チップでは確認していない。

なお、CON=4〜7は遅延の違う経路を含まない(CON=5のM1→M2を除く)。CON=5で1サンプル遅れるM2は、Nuked-OPMのミキサで次のサンプルの計算結果として出力へ入るため、遅延の影響の一部が打ち消される。TL=0で比べると、同じサンプル番号のオペレータ出力を足した和では残差が13.0%あるのに対し、OPMコア観測点(Lチャンネルのミキサ出力)ではch0〜6で6.8%にとどまる。ch7だけはM2がC1・C2と同じサンプルの計算結果として入るため(前掲の直接測定による)打ち消しが起きず、ミキサ出力でも13.0%のままである。

ステータスレジスタの書き込みビジー(BUSY)ビット

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と完全に一致していた。

周期の測定には注意が必要である。ステータスレジスタのオーバーフローフラグを読み、クリア書き込みをしてから次を待つ、という方法では、「レジスタ書き込みの扱いの非対称」の節で述べたとおり、書き込みの分だけNuked-OPM側の周期が短く見える。そこで、測定ループ中にレジスタ書き込みを挟まず、カウンタを直接観測する方法をとった。X68SoundはTimerAcounter/TimerBcounterの巻き戻り、Nuked-OPMはtimer_a_of/timer_b_ofのオーバーフロー信号を数え、起動直後の4周期を捨てたうえで連続する周期の間隔を取っている。

タイマーAの定常周期(Ta=64×(1024-CLKA)/4MHz、62500Hz換算で (1024-CLKA)/1000 サンプル)

CLKA 0 256 512 768 896 960 1000 1016 1020
理論値 1024 768 512 256 128 64 24 8 4
X68Sound 1024 768 512 256 128 64 24 8 4
Nuked-OPM 1024 768 512 256 128 64 24 8 4

タイマーBの定常周期(Tb=1024×(256-CLKB)/4MHz、62500Hz換算で 16×(256-CLKB) サンプル)

CLKB 0 64 128 192 224 240 250 252 254 255
理論値 4096 3072 2048 1024 512 256 96 64 32 16
X68Sound 4096 3072 2048 1024 512 256 96 64 32 16
Nuked-OPM 4096 3072 2048 1024 512 256 96 64 32 16

定常周期は、タイマーA・Bともに全設定で両実装が理論値と完全に一致する(各設定で連続4周期を測り、すべて同値)。

差が出るのは起動後の1周期目だけである。タイマーを起動する前に何サンプル待ってから書き込むかを変えて1周期目を測ると、次のようになる(CLKB=250、理論値96サンプル)。

事前待ち(サンプル) 0 1 2 … 10 11 12 13 …
X68Sound 96 96 96 96 96 96 96 96 96
Nuked-OPM 91 90 89 … 81 80 95 94 …

Nuked-OPMの1周期目は80〜95の範囲を周期16で循環し、X68Soundは常に96で一定である。原因はOPM_DoTimerB()の構造にある。タイマーBの16サンプル分周段(timer_b_sub)はICリセットでしか初期化されず、LOADビット書き込みではリセットされない。つまり分周段はフリーランで動き続けており、書き込みがその16サンプル周期のどの位相に当たるかによって、最初のカウントアップまでの待ち時間が16通りに変わる。表の1周期目の長さは、Nuked-OPMへのデータ書き込み後のバス待ち(40クロック=1.25サンプル)が終わった時点から数えているため、理論値との差は1〜16サンプルになる。X68SoundはTimerBcounterをLOAD時に0から開始するため、常に理論値どおりになる。

Nuked-OPMのこの挙動がシリコン解析の結果を正しく再現しているなら、実チップでもタイマーBの1周期目は起動タイミング次第で短くなる可能性がある。ただし実チップでは確認していない。2周期目以降は両実装とも完全に一致するため、周期的な割り込みとして使う限り影響はない。

複数チャンネル同時発音時の加算と飽和

複数チャンネルを同時に鳴らしたときの合算・飽和(音割れ)の挙動を、実際にコンパイルしたX68SoundライブラリとNuked-OPMの両方で、1〜8チャンネルの出力を比較する形で検証した。

この項目は観測点の選び方が結果を決定的に左右する。X68Soundの音声出力には、前述のとおりOPMコアの外側に1/1024(直流での値)のスケールダウンとIIR・FIRのローパス、リサンプルが挟まっている。これをNuked-OPMのDAC出力と直接比べると、出力段のスケール設計の違いが「ミキサの飽和アルゴリズムの違い」に見えてしまう。そこで以下では、

を分けて測定する。「理想比」は実測値÷(1チャンネルの値×チャンネル数)で、線形加算どおりなら100%のまま推移するはずの値。絶対スケールは両実装で500倍あまり違い、しかも条件によって3%ほど動くため、理想比のみを比較する。

OPMコアの加算は両実装とも線形

チャンネル数 1 2 3 4 5 6 7 8
X68Sound 理想比(TL=64) 100.0% 100.0% 99.9% 99.9% 99.8% 99.7% 99.6% 99.4%
Nuked-OPM 理想比(TL=64) 100.0% 100.0% 100.0% 100.0% 100.0% 100.0% 100.0% 99.6%
X68Sound 理想比(TL=0) 100.0% 100.0% 99.9% 99.9% 99.8% 99.7% 99.6% 99.4%
Nuked-OPM 理想比(TL=0) 100.0% 100.0% 99.9% 99.7% 99.5% 99.3% 98.9% 98.6%

TL=0(最大音量)でもTL=64(控えめ)でも、両実装とも8チャンネルまで理想比99.4〜100.0%を保つ。

ただしこのピークの理想比は、観測窓の中で各チャンネルがどれだけ揃ったかに左右される量であり、加算が線形かどうかを直接示してはいない。100%からのわずかな下振れは、同一音階・同位相で鳴らしたためにピークがサンプル点と一致しないことによる端数である。実際、チャンネルごとにKFをずらすと両実装とも全チャンネルで100.0%になるが、これは観測窓の中で各チャンネルが再び整列する瞬間があり、その値がピークとして記録されるためで、位相が散った状態を測れているわけではない。

そこで線形性そのものは、位相や統計に依存しない方法で確かめた。各チャンネルを単独で鳴らした波形を1サンプルずつ記録し、それらを足し合わせたものと、全チャンネルを同時に鳴らした波形とをサンプル単位で突き合わせる。ミキサが線形なら両者は完全に一致するはずである。レジスタ書き込み列とKEY ON列は全ての測定で同一にし(鳴らすチャンネルはTLだけで選ぶ)、書き込み回数の違いによる位相のずれが混入しないようにした。

条件 2チャンネル 4チャンネル 8チャンネル
TL=64 同一音階 0 / 0 0 / 0 0 / 0
TL=0 同一音階 0 / 0 0 / 0 0 / 0
TL=0 KFでデチューン 0 / 0 0 / 0 0 / 0
TL=0 時差KEY ON(chkをk×37サンプル後) 0 / 0 0 / 0 0 / 0

(各セルは「単独発音の総和」と「同時発音」の最大差。X68Sound / Nuked-OPMの順)

12条件すべてで、2000サンプル中1サンプルの違いもなかった。OPMコアのミキサは両実装とも厳密に線形であり、両者間にも差はない。Nuked-OPMのchip.mix2[]は18bit幅の累算器で、8チャンネル最大音量の合計(実測64416)を飽和なしに保持できる。X68Sound側も単純な整数加算で、この段ではクリップしない。

差が出るのは、その後段の出力表現

チャンネル数(TL=0) 1 2 3 4 5 6 7 8
Nuked-OPM DAC出力 8176 16320 24512 32640 32768 32768 32768 32768
Nuked-OPM 理想比 100.0% 99.8% 99.9% 99.8% 80.2% 66.8% 57.3% 50.1%
Nuked-OPM クランプ発生 - - - - あり あり あり あり
X68Sound 出力段後 4218 8433 12641 16843 21030 25208 29372 32746
X68Sound 理想比 100.0% 100.0% 99.9% 99.8% 99.7% 99.6% 99.5% 97.0%

Nuked-OPMの飽和は、DACではなくミキサ段で起きる。ミキサは18bitの合計mix2の上位ビット(bit15〜17)を見て、±32768の範囲を超えていれば出力をその上下限に張り付かせる(OPM_Mixer())。1〜8チャンネル×TL 9点の72条件でmix2と飽和フラグを照合すると、飽和が立った|mix2|の最小値は32780、立たなかった最大値は32764で、境界はこの間にあった。コード上の判定((mix2>>15)&7が1〜3なら上限、4〜6なら下限)から、境界は±32768である。1チャンネル最大音量が8176なので、約4.0チャンネル分=12.1dBの余裕しかない。5チャンネル目で飽和が始まり、理想比は99.8%から80.2%へ急落して8チャンネルでは50.1%まで落ちる。

その後段のDAC(仮数10bit+指数3bit)は、飽和の判定には関わらず、振幅に応じた粗さで量子化する。dac_outputと2サンプル前のmix2との差は、|mix2|が512未満では0、512〜1023で最大1、以降は振幅が倍になるごとに最大誤差も倍になり、16384〜32767では最大63になった。最大誤差を振幅で割った相対誤差は、各帯域の上端で約1/512(約-54dB)、下端で約1/256(約-48dB)であり、振幅によらずこの範囲に収まる。X68Soundの出力段の下位10bit切り捨て(&0xFFFFFC00)は、Nuked-OPMの単位に換算すると約2の固定刻みで、低振幅ではNuked-OPMより粗く、高振幅では細かい。

X68Soundはこのミキサ段の飽和を再現していない。OPMコアの合計は飽和させずに出力段へ渡し、1/1024にスケールダウンしてから62500Hzのまま±32767でクリップし、その後でFIRローパスとリサンプルを通す。クリップ直前(FIRの手前)の値のピークは1チャンネル最大音量で4223なので、約7.8チャンネル分=17.8dBの余裕がある。8チャンネルではクリップ直前のピークが33566となって実際にクリップが起き、FIR後のピークは32746、理想比97.0%になる(上の表の「出力段後」はFIR後の値)。

両者の余裕の差は5.7dB(約1.9倍)で、最大音量で何チャンネルまで歪まずに鳴らせるかの違いにそのまま対応する。Nuked-OPMの飽和はチップ内部のミキサの挙動(シリコン解析の結果)であり、X68Soundの出力段のクリップはX68000側の出力段のモデルである。つまりこの差は、X68SoundがYM2151内部の飽和を再現していないことによる。X68Soundの出力段では、クリップの手前でADPCM・PCM8の出力もOPMと同じ値に加算される。OPMの1チャンネル分がクリップの上限に対して小さく取られているのはこの加算の余地のためとも考えられるが、コード上に設計意図の記述はない。いずれにしても結果として、チップ内部の上限に当たるはずの振幅でもX68Soundでは歪まない。

なお、この違いが聴感に現れるのは出力振幅が上限に近い条件に限られる。TL=64程度の通常の音量域では、上の表のとおり両実装ともクリップ・クランプが発生せず、8チャンネルまで純粋な線形加算である。

キースケールによる音階依存の速度変化

エンベロープのアタック/ディケイ1/ディケイ2/リリースは、キースケール(KS)により音階(KC)が高いほど速くなる。X68SoundはKc>>(5-Ks)、Nuked-OPMはpg_kcode[slot]>>(sl_ks[slot]^3)でこの補正を適用する。

検証では、KC=$00〜$7F全域・KS=0・1・3で、KEY ON書き込み直後から最大音量到達までのサンプル数(アタック時間)を比較した(他パラメータは前述のエンベロープ検証と同一)。前述のレジスタ書き込みレイテンシの1回の測定値(X68Sound 2サンプル・Nuked-OPM 1サンプル、差+1)を差し引いたものを「補正差」として併記する。レイテンシの差は書き込み位相によって+1〜+3の幅で動くため、補正差にも±1〜2サンプル程度の誤差が含まれる。

KS=1・AR=20(KCは同値区間があるため代表値のみ抜粋)

KC $00 $10 $20 $30 $40 $50 $60 $70 $7E $7F
X68Sound 868 703 589 505 439 352 295 253 253 220
Nuked-OPM 874 694 586 496 433 349 289 250 250 250
補正差 -7 +8 +2 +8 +5 +2 +5 +2 +2 -31

KS=0

KC $00 $20 $40 $60 $7E $7F
X68Sound 868 703 589 505 505 439
Nuked-OPM 874 694 586 496 496 496
補正差 -7 +8 +2 +8 +8 -58

$00〜$7E(最大値$7Fを除く全域)では、キースケール設定にかかわらず、KCに対する段階的なアタック時間短縮パターンが一致した。補正後の残差は-7〜+8サンプル(アタック時間の0.3〜1.7%)に収まっている。この残差は、エンベロープ用クロックの位相(KEY ONが更新タイミングのどこに当たるか)による端数とも、KCに依存する系統的な差とも考えられる。どちらかを確かめるため、KS=1で代表10点のKCについて、KEY ON前の待ち時間を0〜46サンプルの24通りに振って測り直した(こちらは補正前の生の差)。

KC $00 $10 $20 $30 $40 $50 $60 $70 $7E $7F
差の範囲 -6〜+6 -9〜+15 -3〜+9 -3〜+9 0〜+6 -3〜+9 0〜+6 0〜+6 0〜+6 -33〜-27
差の平均 0.0 +4.0 +5.5 +4.8 +2.0 +2.8 +3.5 +3.0 +3.0 -30.0

$00〜$7Eでは、差は待ち時間に応じて-9〜+15の範囲で動き、平均は0〜+5.5に収まる。差は位相によって揺れており、KCに依存する系統的な差は見られない。1回の測定での残差-7〜+8は、位相による端数と判断できる。

ただし$7F(理論上最高音)だけは例外である。待ち時間をどう振っても差は-27〜-33サンプルに留まり、位相による揺れとは区別できる。1回の測定でも、残差が一桁から一気に-31/-58サンプルへ跳ね上がる。X68Soundは$7Eまでの傾向より速くアタックが完了する(KS=1で253→220、KS=0で505→439)のに対し、Nuked-OPMは$7Eと$7Fで変化しない(それぞれ250→250、496→496)。

KS=3でもKC=$00〜$7F全域を確認したところ、KCが上がるほど加速する傾向自体は一致し、補正差もKC=$00の-7を除いて+2〜+5に収まっていた(KC=$00の値はKS=0・1と同じで、補正前の生の差は-6)。ただし$60付近から両実装ともレート上限に張り付き(X68Soundは4サンプル、Nuked-OPMは1サンプルで頭打ち)、$7Eと$7Fが同値になるため、$7F特有の余分な加速はこの条件では観測できなかった。これは矛盾ではなく、KS=3ではKC=$60時点で既に実効レートがテーブル上限に達しているため、$7Fの1段階分の余分なブーストが上限で吸収されるためである。

この挙動はX68SoundのOp::SetKC()の実装に起因する。音階レジスタ値(0〜127)をKc = (n&127); ++Kc;で内部値へ変換するため、$7F時のみKc=128となり7bit範囲を1超える。この値がKc>>(5-Ks)に使われることで、$7Fだけ1段階余分にレートが上がる。影響はキースケール対象の4ステージ(アタック/ディケイ1/ディケイ2/リリース)すべてに共通し、最大音階付近でのみ現れる限定的な相違点である。

キースケールとDT1が参照するキーコード

YM2151のキーコードは、キースケール(エンベロープの速度補正)とDT1(細かいデチューン)の両方の入力になる。問題は、DT2(粗いデチューン)やLFOのPMでピッチを動かしたとき、どちらの段のキーコードを参照するかである。

Nuked-OPMは、OPM_PhaseCalcFNumBlock()の中でKC・KFにPMとDT2を加えたキーコードを求め、その上位5bitをpg_kcodeに保存する。DT1の補正量(OPM_PhaseCalcIncrement())とキースケール(OPM_EnvelopePhase2()のpg_kcode >> (KS^3))はどちらもこのpg_kcodeを使う。X68Soundは、DT1の補正量(Op::CulcDt1Pitch())もキースケール(Op::CulcArStep()など)も、レジスタに書かれたKCから作ったKcだけを使い、DT2とPMはピッチテーブルの参照位置にしか加えない。

キースケール×DT2

KC=$40・AR=10で、DT2を0〜3に変えたときのアタック時間(KEY ONから最大音量まで)と、リリース時間(RR=5、KEY OFFから減衰量900まで)を、DT2=0に対する比で比べた。

段 KS X68Sound DT2=1 / 2 / 3 Nuked-OPM DT2=1 / 2 / 3
アタック 0 1.000 / 1.000 / 1.000 1.000 / 1.000 / 1.000
アタック 1 1.000 / 1.000 / 1.000 1.000 / 1.000 / 1.000
アタック 2 1.000 / 1.000 / 1.000 0.799 / 0.799 / 0.799
アタック 3 1.000 / 1.000 / 1.000 0.663 / 0.663 / 0.567
リリース 2 1.000 / 1.000 / 1.000 0.857 / 0.857 / 0.857
リリース 3 1.000 / 1.000 / 1.000 0.750 / 0.750 / 0.600

X68SoundはDT2によらず所要時間が変わらないのに対し、Nuked-OPMはKS=2・3で所要時間が短くなる。KS=3・DT2=3では、アタックが0.567倍、リリースが0.600倍になった。KC=$40のときpg_kcodeはDT2=0で16、DT2=1・2で18、DT2=3で19となり、KSの右シフト量が小さいほどこの差が実効レートに残る。KS=0・1で差が出ないのは、この条件ではシフト後の値が変わらないためで、KCがシフト後の値の境界付近にあればKS=0・1でも差が出うる。KS=3・AR=10でKC=$00・$20・$40・$54を比べても、DT2=1〜3のNuked-OPMのアタック時間はDT2=0の0.57〜0.72倍で、X68Soundは1.000倍のままだった。

DT1×DT2

ピッチの全数照合(一致項目の「ピッチの全数照合」の節)で見つかった不一致のうち、音域上端を除くほぼすべてがこの組み合わせである。DT1の補正量(DT1=dの位相増分からDT1=0の位相増分を引いた値)は、KF=0・MUL=1で次のようになる。

KC DT2 X68Sound DT1=1 / 2 / 3 Nuked-OPM DT1=1 / 2 / 3
$20 0 1 / 2 / 4 1 / 2 / 4
$20 3 1 / 2 / 4 1 / 3 / 5
$40 0 2 / 5 / 8 2 / 5 / 8
$40 3 2 / 5 / 8 3 / 7 / 10
$60 0 5 / 11 / 16 5 / 11 / 16
$60 3 5 / 11 / 16 7 / 14 / 20

DT2=0では完全に一致し、DT2≠0ではNuked-OPMの補正量だけがDT2で上がったキーコードに応じて大きくなる。DT1(≠0、4)とDT2(≠0)を同時に使う組み合わせでは、MUL=1・音域上端を除く134664通りのうち99936通りが不一致になるが、差は位相増分で最大6(KC=$5A・KF=32・DT1=3・DT2=3)、セントでは最大2.7セント(KC=$07・DT1=6・DT2=1)である。

DT1×PM

LFO波形0・LFRQ=224・PMS=7・PMD=127でPMをかけ、DT1=3・7の位相増分からDT1=0の位相増分を引いた値を8192サンプル分記録した。X68SoundのDT1補正量はPM中も一定(KC=$40・DT1=3で常に8)だったのに対し、Nuked-OPMではpg_kcodeがPMに合わせて13〜18の範囲で動き、DT1補正量も6〜9の範囲で変化した。KC=$4E・$6Aでも同様である(Nuked-OPMでそれぞれ8〜13、16〜22、X68Soundは10、19で一定)。

キースケール×PM

PMでpg_kcodeが動く以上、Nuked-OPMではキースケールの実効レートもPMの影響を受けるはずである。KC=$40・KS=3・AR=10に、LFO波形0・LFRQ=224(1周期4096サンプル)・PMS=7・PMD=127のPMをかけ、KEY ONまでの待ち時間を1周期にわたって8通りに振ってアタック時間を測った。

KEY ON時のLFO位相(周期) PMなし 0 1/8 2/8 3/8 4/8 5/8 6/8 7/8
X68Sound(サンプル) 1738 1738 1730 1746 1738 1730 1746 1738 1730
Nuked-OPM(サンプル) 1748 1556 1380 1576 1904 2244 2020 1868 1716
Nuked-OPM(PMなしに対する比) 1.000 0.890 0.789 0.902 1.089 1.284 1.156 1.069 0.982

Nuked-OPMのアタック時間は、KEY ON時のLFO位相によって0.79〜1.28倍に変わる。PMでpg_kcodeが上がった状態でエンベロープが進む区間ほど、キースケールの補正で実効レートが上がって速くなる(LFO位相とPMの向きの対応までは確認していない)。X68Soundの揺れ(1730〜1746)は8サンプル刻みの16サンプル幅で、エンベロープ用クロックの位相による端数の範囲にとどまる。PMSを下げた条件、KS=1・2、リリースは測っていない。

音域上端のピッチ

ピッチの全数照合では、DT1=DT2=0でもKC=$7Fの64通り(KF全段階)が不一致だった。X68Soundの位相増分は$7Eから上に進み続け、KFによっては87936に達するが、Nuked-OPMは82976で頭打ちになり、最大で約1半音(100.6セント)の差になる。DT2を加えると頭打ちになるKCが下がり、DT2=1ではKC=$77以上、DT2=2では$75以上、DT2=3では$72以上で同じ差が出る(DT2=3・KC=$7Fでは約1050セント)。

MUL=13〜15でも不一致が増える(MUL=15では320通り)。Nuked-OPMは位相増分を20bitに丸める(inc &= 0xfffff)ため、上端のKCで大きなMULを掛けると値が折り返し、X68Soundは折り返さない。KC×KF×MULを走査した不一致1427通りのうち403通りは、X68Soundの値の下位20bitがNuked-OPMの値と一致する、この折り返しだけによる差だった。

アタックの開始時刻

一致項目の「AR/D1R/D2R/RRのレート値ごとの速度カーブ」の節で示すとおり、両実装のエンベロープは同じ速度・同じ形で進み、違うのは進み始める時刻だけである。その時刻の差が書き込み位相による揺れなのか、系統的な遅れなのかを切り分けるため、KEY ON(リリースはKEY OFF)の前の待ち時間を振り、エンベロープ値が500に達するまでの時間の差(X68Sound − Nuked-OPM)を測った(KC=$40)。

待ち時間を振る幅には注意が要る。Nuked-OPMは、実効レートの下位2bitに応じて一定の周期の中で更新を間引く(OPM_EnvelopePhase2()のeg_shiftによる分岐)。このため下位2bitが0でないと更新が不等間隔になり、今回の条件では「u、u、2u」(uは基本の更新間隔)の繰り返しで、パターン全体の周期は4uになる。KC=$40・KS=0では、試したARすべてでこの条件に当たる。待ち時間を1u分しか振らないと、Nuked-OPMの4uのパターンのうち一部の位相だけで平均することになり、差の平均がどの位相を拾ったかに左右される。

そこで待ち時間を8uにわたって32通りに振った。あわせて、新しく初期化した状態(cold)と、同じ音色で1音鳴らしてリリースさせた後(warm)の2通りで測った。warmを加えたのは、X68SoundのKEY ON処理(Op::KeyON())がエンベロープの端数の累積値(Xr_step)をリセットせず、前の音から引き継ぐためである。実際の再生では、ほとんどの音がwarmの状態で鳴る。

条件 KS 更新間隔u cold 差の範囲 cold 平均(u単位) warm 差の範囲 warm 平均(u単位)
AR=4 0 1536 -762〜+2310 +774(+0.50) -2282〜+790 -746(-0.49)
AR=8 0 384 -186〜+582 +198(+0.52) -554〜+214 -266(-0.69)
AR=12 0 96 -42〜+150 +54(+0.56) -122〜+70 -50(-0.52)
AR=16 0 24 -6〜+42 +12(+0.50) -38〜+10 -14(-0.58)
AR=20 0 6 -3〜+9 +4.5(+0.75) -5〜+1 -2(-0.33)
AR=4 1 1536 +6(一定) +6(0.00) +22(一定) +22(+0.01)
AR=8 1 384 +6(一定) +6(+0.02) -362〜+22 -74(-0.19)
AR=12 1 96 +6(一定) +6(+0.06) -74〜+22 -2(-0.02)
RR=4 0 384 +4(一定) +4(+0.01) +21(一定) +21(+0.05)
RR=7 0 48 +4(一定) +4(+0.08) -25〜+23 -1(-0.02)
RR=10 0 6 -2〜+4 -0.5(-0.08) -3〜+3 0(0.00)
RR=13 0 6 -2〜+1 -0.5(-0.08) 0(一定) 0(0.00)

(書き込みレイテンシの差(AR=31でcold +2〜+3、warm +1〜+2サンプル)は差し引いていない)

更新間隔は両実装で一致しており、レートそのものは同じである。実効レートの下位2bitが0の条件(KS=1のAR、およびリリース)では、cold(前の音なし)の差は待ち時間によらずほぼ一定(+4〜+6サンプル。RR=10・13のみ-2〜+4)になる。warm(前の音あり)では同じ条件でも揺れる場合があり、AR=8・KS=1で-362〜+22、AR=12・KS=1で-74〜+22、RR=7で-25〜+23になった。X68Soundが前の音から引き継ぐXr_stepによって、開始時刻が更新間隔の範囲で動くためである。下位2bitが0でない条件(KS=0のAR)では、差が約2uの幅で揺れる。その平均はcoldで+0.5u(X68Soundが遅い)、warmで-0.5u(X68Soundが早い)と符号が反転した。つまり、X68Soundが一方向に遅れるわけではない。アタックの開始時刻が、X68Soundでは前の音から引き継いだXr_stepによって、Nuked-OPMではエンベロープ用クロックの位相によって決まり、両者の決まり方が違う。その結果、開始時刻の差は±1u程度の範囲で音ごとに変わる。

遅いアタックでは、uが絶対時間として大きい(AR=4・KS=0でu=1536サンプル=約25ミリ秒)。ただしNuked-OPM自身もKEY ONの位相によって開始時刻が同じ程度揺れるため、この差は揺れ方の違いであって、一定のずれではない。

LFO波形1(矩形波)のAM深度

LFO波形0(ノコギリ波)のAM深度は、LFRQ=224以下ではNuked-OPMと完全に一致する(一致項目の「LFO AM(振幅変調)深度の数値比較(波形0)」の節)。一方、波形1(矩形波)では、LFOの速さによらず小さな相違が残る。

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・16・32・64・96・127の6パターンをLFRQ=224・255の2点で測ると、オン区間の値はNuked-OPMが「255を基準にした式」(255*Amd)>>7と、X68Soundが(256*Amd)>>7と、12条件すべてで一致した(AMD=127でNuked-OPM 253・X68Sound 254、AMD=1で1・2)。X68Soundが常に1大きい。差分は最大でも1(255スケール中、約0.4%)で聴感上の影響はほぼ無視できるが、数値上のずれとしては確認できた。LFRQ=128〜255の7点でも、AMD=127での値域はX68Soundが0〜254、Nuked-OPMが0〜253で、LFOの速さによらずこの1の差が残る。

同じ非対称性はPM側にもあり、波形1のみ振幅絶対値が128(他波形は127)になる(PmTblValue = 128; / -128;)。X68SoundのPM式(PmTblValue*Pmd*Pmsmul)>>(7+5)<<Pmsshlで見積もると、PMS=1・PMD=127時の差は最大3程度で、AM側の直接的な1ずれより影響は小さい。なお、後述のPMS非線形処理と重なるため、単独の実測分離は難しい。

LFO波形2(三角波)の周期内の更新パターン

波形2(三角波)は周期・振幅(AM/PMの最大値・最小値)は一致するが、1周期内の更新間隔と変化量(量子化粒度)は一致しない。

LFRQ=255・AMD=127の条件で定常状態(起動直後の過渡期を除いた区間)を比較すると、X68Soundは16サンプルに1回、5〜8ずつ値が変化するのに対し、Nuked-OPMは8サンプルに1回、2〜4ずつ値が変化する。

X68Sound  : (1,+8) (17,+8) (33,+6) (49,+8) (65,-5) (81,-8) (97,-8) (113,-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は三角波時のみ内部歩進クロック自体を高頻度化する。このため瞬間値がずれる。下の表のとおり、ラグと利得を合わせた残差はLFRQ=255で最大6.2(255スケール中約2.4%)になる。すなわちLFRQ=255の三角波は「周期・振幅は一致するが、瞬間値は完全一致しない」。

ただしこの更新パターンの差は、速いLFOに限られる。AMD=127でLFRQを128〜255の7点に振り、2周期分をラグと利得を合わせて照合した。

LFRQ 128 176 208 224 240 248 255
1周期の更新回数 X68Sound / Nuked-OPM 255 / 255 255 / 255 255 / 255 255 / 255 128 / 255 85 / 170 66 / 132
残差の最大 2.0 2.0 2.0 2.0 4.1 6.3 6.2

LFRQ=224以下では更新回数が一致し、残差は2以下になる。LFRQ=240以上ではX68Soundの更新回数がNuked-OPMの約半分に落ち、残差が4〜6に広がる。これはPMや波形0のAMで見る「速いLFOでの更新の粗さ」と同じ現象である。

レジスタ$01(LFO RESET)の反映タイミング

X68Soundはレジスタ$01のbit1(0x02)でLFOを即時にリセット/再開する単純な2値処理(Lfo::LfoReset()/Lfo::LfoStart())だが、Nuked-OPMは同レジスタを8ビットのテストモードビット(mode_test[0..7])として扱う。そこで内部実装の一致ではなく、「リセット中はLFO出力が固定されるか」「解除後に再開するか」という観測可能な挙動を比較した。最終的な挙動(固定・再開)自体は両実装で一致する。

反映までの時間は、観測量の選び方で結果が変わる。「固定値253に到達するまで」を数えると、LFRQ=255のノコギリ波が1周期の中で自然に253を通過した時点も数えてしまう。リセットしない対照実行との値の食い違いを見る方法も、LFO値が約4サンプルに1回しか更新されないため、その分解能がそのまま誤差になる。そこで、レジスタが適用された時刻とLFO出力が実際に凍結する時刻を、クロック単位で別々に測った。

レジスタの適用そのものは待ち時間に依存しない。データ書き込みからmode_test[1]が立つまでのクロック数を、バス待ちを8〜128クロックの範囲で振って測ると、いずれも2クロックで一定だった。バス待ちは検証側が選ぶ値(データ書き込みに必要な最小ウェイトを安全側で上回る設定)だが、これが遅延の主因ではない。

遅れるのはLFO出力が凍結するまでである。バス待ちを40クロックに固定し、書き込み前の経過サンプル数だけを0〜15で振ると、次のようになる。

書き込み前の経過サンプル 0 1 2 3 4 5 6 7 8
Nuked-OPM 凍結まで(クロック) 344 312 280 248 216 184 152 120 344
Nuked-OPM 凍結まで(サンプル) 10.75 9.75 8.75 7.75 6.75 5.75 4.75 3.75 10.75
X68Sound 凍結まで(サンプル) 1 2 1 1 2 1 1 2 1

Nuked-OPMの凍結遅延は3.75〜10.75サンプルの範囲を8サンプル周期で循環する。X68Soundは書き込み位相によらず1〜2サンプルで凍結する。つまり両者の差は固定値ではなく、書き込みがLFO内部サイクルのどの位相に当たるかで約2〜10サンプルのあいだを動く。

この遅延はLFOの速さによらない。遅いLFOでは値が数百サンプルに1回しか変わらず、値が最後に変化した時刻では凍結を検出できないため、十分待った後の固定値を先に求め、その値に達して以後変わらなくなった時刻を測った(書き込み前にLFO値が固定値から離れるまで進めてから、書き込み前の経過サンプルを0〜15に振る)。波形0〜2×LFRQ 128・160・200・240・255の15条件で、Nuked-OPMは波形0・2で3.75〜10.75サンプル、波形1で4.75〜11.75サンプル、X68Soundはすべて1〜2サンプルだった。Nuked-OPMのAM値のラッチはlfo_bit_counterの一巡(8サンプル)ごとに起こり、LFRQはこの周期に関わらないためである。固定値は波形0・2では両実装とも253、波形1ではX68Sound 254・Nuked-OPM 253で、前節の1の差がリセット中にも残る。

これはNuked-OPMの構造から説明できる。位相カウンタ(lfo_counter1〜3)はICリセットでのみ初期化され、$01 bit1(mode_test[1])は出力側シフトレジスタの帰還信号w3へ作用する。そのため、ビットが立っても効果が現れるのはシフトレジスタが次に一巡したときであり、書き込み位置から一巡完了までの残り時間がそのまま遅延になる。X68Soundのような「次のティックで即リセット」にはならない。

LFO PM(ピッチ変調)の深度と曲線形状

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実測のOp::DeltaT(どちらも「1サンプルあたり位相がどれだけ進むか」を表す実際の内部値)を、相互相関で遅延補正した上で照合した。

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.8 2.8 2.8 3.3 9.2 20.8
位置合わせ後の最大のずれ(セント換算) 0 9(約3.0セント) 7(約2.3セント) 9(約3.0セント) 8(約2.7セント) 10(約3.3セント) 26(約8.6セント) 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回で、X68Soundの更新はNuked-OPMの約半分の細かさしかない。残差の拡大がこの更新回数の差によるものかどうかは、本節の最後でLFRQを振って確かめる。

乖離量が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=96と127でほぼ同じ(63と65)である。

なお上の残差は、時間方向のラグだけを合わせて算出したものである。ラグに加えて最適な利得(振幅比)も合わせて残差を取り直すと、次のようになる。

PMS 1 2 3 4 5 6 7
X68Sound 振れ幅 28 72 144 288 588 2374 4878
Nuked-OPM 振れ幅 30 66 138 288 584 2370 4860
最適利得 0.775 0.959 1.022 1.006 1.005 1.007 1.012
利得も合わせた残差(最大) 5.8 6.8 10.0 8.3 10.1 22.0 41.1
振れ幅に対する割合 19.5% 10.3% 7.3% 2.9% 1.7% 0.93% 0.85%

(2周期分で照合。振れ幅に対する割合の分母はNuked-OPMの振れ幅)

PMS=4以上では最適利得が1.00〜1.01とほぼ1であり、振れ幅そのものは0.4%以内で一致している。したがって残差は深さの違いではなく、曲線形状の違いを表す。PMS=7・PMD=127での残差は、利得も合わせると41(約14セント)、ラグのみ合わせた場合で65(約21セント)となる。

PMS=1・2で最適利得が0.78・0.96と1から離れるのは、振れ幅自体が28〜72と小さく、整数丸めの影響が相対的に大きいためで、絶対量としてはいずれも数単位(1セント未満)に収まる。

ここまでの数値はすべてLFRQ=255(LFO周波数約59Hz)での値であり、ビブラートとしては極端に速い。曲線形状の差が更新回数の違いによるものなら、LFOを遅くしたときに差は縮むはずである。そこでPMS=7・PMD=127のまま、LFRQを128〜255の7点に振って、ラグと利得を合わせた残差を2周期分で求めた。

LFRQ 128 176 208 224 240 248 255
LFO周期(サンプル) 262144 32768 8192 4096 2048 約1366 約1057
最適利得 1.0042 1.0042 1.0042 1.0042 1.0024 1.0024 1.0115
残差の最大(セント) 6.0(2.0) 6.0(2.0) 6.0(2.0) 6.0(2.0) 17.9(5.9) 17.3(5.7) 41.1(13.7)
1周期の更新回数 X68Sound / Nuked-OPM 253 / 253 253 / 253 253 / 253 253 / 253 127 / 253 84 / 169 66 / 131

LFRQ=224以下(LFO周波数約15Hz以下)では、1周期あたりの更新回数が両実装とも253回で一致し、残差は6(約2セント)まで下がる。これがPM適用の計算式そのものの差にあたる。LFRQ=240以上では、X68SoundのLFO出力の更新回数がNuked-OPMの約半分に落ち、その粗さの分だけ残差が5.9〜13.7セントへ広がる。つまりLFRQ=255で見た曲線形状の差の大部分は、X68Soundの更新の粗さによるものである。通常のビブラート(数Hz)で問題になるのは約2セントのほうである。

1サンプルあたりの処理負荷(CPU時間)

これまでの検証はいずれも「出力される値が一致するかどうか」という機能面の比較だった。前述の実装アプローチの違いを踏まえれば、処理負荷(CPU時間)にも違いが出るはずである。そこで、前述のX68Sound側検証用ヘルパー(pcmset62()から切り出した内部ティック関数)とNuked-OPMのOPM_Clock()を、それぞれ「1サンプル分の処理」の単位(32クロック=1サンプル)として400万サンプル分(62500Hzで約64秒相当)連続して進め、実行時間を計測した(g++ -O2、Apple Silicon/arm64、ウォームアップ10万サンプル分を計測前に除外)。「無音状態(リセット直後、KEY ONなし)」と「8チャンネル・全オペレータ発音状態(CON=7・TL=0・AR=31でKEY ON)」の2条件で計測した。

条件 X68Sound(ナノ秒/サンプル) Nuked-OPM(ナノ秒/サンプル) 比率(Nuked-OPM÷X68Sound)
無音状態 59.8(59.7〜60.0) 1276.6(1276.5〜1279.5) 21.3倍
8チャンネル全オペレータ発音 60.1(59.8〜60.7) 1385.1(1381.6〜1388.5) 23.0倍

(各条件3回計測の中央値。括弧内は最小〜最大)

この表は1回の実行(3回計測)の値である。同じバイナリを別の機会に実行し直すと、X68Soundは60.9ナノ秒(無音・8チャンネルとも)、Nuked-OPMは1350.3・1451.5ナノ秒となり、比率は22.2倍・23.8倍だった。1回の実行の中の最小〜最大の幅(1.5%以内)より、実行ごとの差(Nuked-OPMで最大6%程度)のほうが大きい。比率は約21〜24倍の範囲で見るのが妥当である。

ただしこのInternalTick()はpcmset62()から音声出力段を除いたものであり、X68Soundが実再生で実際に払うコスト(下位10bit切り捨て・ハイパスフィルタ・固定ゲイン・クリップ・64タップFIRローパス・62500→44100リサンプル)が含まれていない。実再生に近い値としてGetPcm()経由でも計測し、出力サンプル(44100Hz)あたりの値をOPM内部サンプル(62500Hz)あたりへ換算した。

条件 X68Sound 出力段込み コアのみとの比 対Nuked-OPM
無音状態 81.1ナノ秒(80.9〜81.9) 1.4倍 15.7倍
8チャンネル全オペレータ発音 81.1ナノ秒(80.7〜81.2) 1.3倍 17.1倍

出力段はOPMコアの約4割増しのコストであり、これを含めた実再生相当の比率は約16〜17倍になる(別の実行では83.8・83.9ナノ秒で16.1倍・17.3倍)。「約21〜24倍」はOPMコア同士を比べた数字であって、X68Soundを使う側が実際に得る差ではない点に注意が必要である。

ここまでの数値は「1サンプルの処理に要したCPU時間」であり、「負荷」(実時間予算に対する消費割合)とは異なる。62500Hzでの1サンプルあたりの実時間予算は16,000ナノ秒(1/62500秒)であり、これを基準に割合へ換算すると次のようになる。

条件 X68Sound コアのみ X68Sound 出力段込み Nuked-OPM
無音状態 0.37% 0.51% 7.98%
8チャンネル全オペレータ発音 0.38% 0.51% 8.66%

Nuked-OPMも単一系統であれば実時間予算の8〜9%程度に収まる(別の実行では8.44%・9.07%)。ただし1系統分の割合であり、複数系統を同時にエミュレーションする場合や、他の処理と実行時間を奪い合う環境では、16倍という比率がそのまま消費時間の差として現れる。

いずれの実装も「無音状態」と「8チャンネル全オペレータ発音状態」との間で処理時間に大きな差は見られなかった(X68Soundは0.5%程度、Nuked-OPMは8.5%程度の増加。別の実行ではそれぞれ0%・7.5%)。これは、X68SoundのOutput系関数もNuked-OPMのOPM_Clock()パイプラインも、発音中かどうかに関わらず全32オペレータ(あるいは全32スロット)を毎サンプル一律に処理する固定コスト構造になっているためであり、発音数によって処理負荷が変動する実装ではないことが確認できた。Nuked-OPM側に残る8.5%程度の増加は、発音時にエンベロープ・位相計算の分岐がより多く成立することによるものとみられる。

処理負荷の差の主因は、両実装の内部構造の違いにあると考えられる。Nuked-OPMのOPM_Clock()は、1回の呼び出しにつきOPM_PhaseGenerate・OPM_EnvelopePhase1〜6・OPM_DoLFO1・OPM_Noise・OPM_DoRegWriteなど51個のサブルーチンを毎回呼び出す(このほかOPP互換モードでのみOPP_TLRampを呼ぶ)。これらはシリコンダイ解析で判明したチップ内部パイプラインの各ステージ(位相生成・エンベロープ計算・LFO・ノイズ・レジスタ反映など)に対応しており、1回のOPM_Clock()呼び出しは1/32サンプル分のチップクロックにしか相当しないため、1サンプルを生成するにはこれを32回、つまり延べ1632回のサブルーチン呼び出しを行う。一方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の方が重い」という傾向は環境に依らず成り立つと考えられる。この差は出力される音そのものには影響しない。

確認の結果、Nuked-OPMと一致していた項目

リセット直後の無音状態

レジスタに何も書き込まない状態で音声出力を取得した場合、X68Sound・Nuked-OPMともに出力は完全に無音(全サンプル0)であった。両実装とも、リセット直後は明示的な音色設定・KEY ONが行われるまで音が出ない、という基本的な前提は一致している。

DT1ピッチテーブル(音程の細かいデチューン)

DT1は8段階(0〜7、内4段は逆方向)で音程を細かくずらすパラメータ。両実装を実際に駆動し、X68SoundのOp::DeltaTとNuked-OPMのchip.pg_inc[](どちらも「1サンプルあたり位相がどれだけ進むか」を表す内部値)を読んで比較した(MUL=1・KF=0・DT2=0)。同じ音階・逓倍数設定でDT1を0〜7まで変えたところ、X68SoundとNuked-OPMで完全に一致した。

音階(KC) DT1=0 1 2 3 4 5 6 7
$20 1299 1300 1301 1303 1299 1298 1297 1295
$40 5196 5198 5201 5204 5196 5194 5191 5188
$60 20784 20789 20795 20800 20784 20779 20773 20768

DT1によるピッチ補正量は、DT2=0である限りNuked-OPMと数値レベルで一致する。DT2と同時に使ったときの差は、相違点の「キースケールとDT1が参照するキーコード」の節を参照。

DT2テーブル(音程の大きなデチューン)

DT2は4段階(0〜3)で音程を大きくずらすパラメータで、X68Soundは{0, 384, 500, 608}という4つの数値を持つ変換テーブルを使い、これを音程テーブルの参照位置(インデックス)に直接足し込んでいる。Nuked-OPMはこの変換を固定テーブルではなくビット演算で計算しているため、DT1のケースと同じ方法(1サンプルあたりの音程の進み幅を両方で計算して照合する)で検証した。結果は次のとおり、こちらも完全に一致した。

音階(KC) DT2=0 1 2 3
$20 1299 1837 2039 2249
$40 5196 7348 8156 8996
$60 20784 29392 32624 35984

(MUL=1・KF=0・DT1=0)

DT2によるピッチ補正量も、DT1と同様にNuked-OPMと一致している(音域上端を除く。相違点の「音域上端のピッチ」を参照)。

ピッチの全数照合(KC×KF×DT1×DT2×MUL)

DT1・DT2・KF・MULの各節の表は、少数の代表点での比較である。ピッチはレジスタ値から決定論的に決まるので、組み合わせを全数照合できる。KC 128段階×KF 64段階×DT1 8段階×DT2 4段階×MUL 16段階の全4,194,304通りについて、両実装の位相増分を読んで照合した(Nuked-OPMは書き込みから2サンプル後に読み、3サンプル後に読んでも値が変わらないことを間引きで1024回確認した)。

不一致は1,864,957通り(44.5%)あった。これを次の順に分類すると、どれにも当てはまらない不一致は0通りだった。

つまり、DT1=0(または4)かDT2=0で、音域上端でも20bitの折り返しでもなければ、全組み合わせで数値完全一致である。KC下位4bitの値が3・7・11・15の、音名として定義されていないキーコードも同じ扱いで一致する。

エンベロープジェネレータのカーブ(アタック/ディケイ/サスティン/リリース)

Nuked-OPMを実際にレジスタ書き込みで駆動し、内部のエンベロープ値(値が大きいほど音が小さい。Nuked-OPMの内部レジスタは10bitで0〜1023の範囲を取る)を直接記録するテストと、X68Sound実ライブラリに追加した読み出し専用アクセサ経由で同じくエンベロープ値を直接記録するテストを用意し、同一パラメータ(アタックレート(AR)20・ディケイ1レート(D1R)12・ディケイ2レート(D2R)6・リリースレート(RR)7・キースケール(KS)1・音階KC(Key Code)=$40・微調整KF(Key Fraction)=0・サスティンレベル(D1L)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付近になるあたり)、ディケイからサスティンへの切り替わりタイミング、リリースが完了するタイミングまで、進み始める時刻のずれ(「AR/D1R/D2R/RRのレート値ごとの速度カーブ」の節で詳述する。1回の測定の中では一定だが、書き込み位相や前の音の状態によって変わる)を除けば、数値・カーブの形ともほぼ完全に一致した。事前計算テーブル方式とビットレベルのパイプライン方式という全く違う実装から、同じ音量変化のカーブが得られている。

瞬時アタック(アタックレート最大時)の挙動

アタックレートを最大値(31)にすると、キースケールや音階の値によらず、内部の「実効レート」がテーブルの上限に達し、X68Soundのコードはこの条件のとき「1回の更新で最大音量に到達する」という特殊な値を使うようになっている。実際にNuked-OPMをアタックレート最大で駆動したところ、KEY ON直後の最初の更新で内部エンベロープ値が無音状態から即座に0(最大音量)へジャンプすることを確認した。両者とも「アタックレート最大時は事実上一瞬で音量が最大になる」という挙動が一致している。

正弦波・エンベロープの振幅変換テーブルの解像度

Nuked-OPMが持つ2つの256エントリのテーブル(対称性を利用して実質1024段階相当になる)と、X68Soundが持つ1024エントリの正弦波テーブル・振幅変換テーブルは、解像度としては同等であった。差があるのは解像度ではなく、前述の「線形領域で乗算する」方式と「対数で足してから指数変換する」方式という、計算手順の違いのみである。

アルゴリズム結線(4つのオペレータの繋ぎ方、レジスタ$20-$27で指定)

YM2151は1音につき4つのオペレータ(発音ユニット)を持ち、そのうちどれとどれを「変調する側」「音として出す側(キャリア)」にするかを8パターンのアルゴリズムから選べる。これを検証するため、コンパイル済みのX68Soundライブラリと、Nuked-OPMの両方に対して、8つのアルゴリズムそれぞれで「4オペレータとも同じ音量にした状態」と「1オペレータずつ音量を最小にした状態」を作り、出力される音の大きさ(実効値)を比較するテストを行った。あるオペレータを消したときに出力が大きく下がれば、それは「音として出す側(キャリア)」だったと判定できる。観測点はOPMコア(出力段を通さない線形合計)で、2048サンプル分の実効値を取っている。

レジスタ上の並び順(物理的な1〜4番目)と、アルゴリズム図で説明される論理的な並び順(M1,C1,M2,C2)は変換が必要なため、その対応関係を踏まえて解釈すると、どのオペレータを消すと音が小さくなるか(=どれがキャリアか)のパターンが両実装で一致した。

CON op0無音化 op1無音化 op2無音化 op3無音化
0 96.0% / 95.2% 94.4% / 93.7% 89.0% / 88.5% 0.0% / 0.0%
1 96.6% / 96.0% 94.9% / 94.5% 96.6% / 96.0% 0.0% / 0.0%
2 101.6% / 100.9% 94.2% / 93.8% 98.3% / 97.3% 0.0% / 0.0%
3 98.3% / 97.1% 101.6% / 100.6% 94.2% / 93.6% 0.0% / 0.0%
4 45.9% / 45.8% 45.9% / 45.8% 50.0% / 50.0% 50.0% / 50.0%
5 106.0% / 105.8% 66.7% / 66.7% 66.7% / 66.7% 66.7% / 66.7%
6 185.8% / 185.8% 53.6% / 53.6% 123.8% / 123.9% 53.6% / 53.7%
7 75.0% / 75.0% 75.0% / 75.0% 75.0% / 75.0% 75.0% / 75.0%

(各セルは「X68Sound / Nuked-OPM」の出力低下率。全オペレータ有効時を100%とする)

CON=4〜7では両実装の値の差が0.2ポイント以内に収まる(CON=5のop0で106.0%/105.8%、CON=4のop0・op1で45.9%/45.8%など。CON=7は完全に一致)。ただしこれは2048サンプル・TL=20での値で、窓を16384サンプルにすると最大0.62ポイント(CON=6)、TL=0では最大0.77ポイント(CON=6)まで広がる。例えば「4オペレータが全て独立」のCON=7では、どちらの実装でもどのオペレータを1つ消しても出力がちょうど75.0%に低下する(4本中3本が残る計算と一致)。

CON=0〜3(3〜4段の直列変調を含む構成)では残差が出る。これが測定由来か実装由来かを確かめるため、実効値の窓長(2048/16384サンプル)と変調の深さ(TL=20/TL=0)を変えて測り直した。

条件 CON=0 CON=1 CON=2 CON=3
TL=20・窓2048 +0.53〜+0.85 +0.42〜+0.57 +0.44〜+0.97 +0.62〜+1.20
TL=20・窓16384 +0.37〜+1.25 +0.26〜+0.78 +0.19〜+1.11 +0.37〜+1.36
TL=0・窓2048 -0.06〜+0.70 -4.53〜-3.89 -3.52〜-3.04 -3.66〜-3.23
TL=0・窓16384 -0.96〜-0.49 -4.02〜-3.61 -1.99〜-1.60 -2.24〜-1.56

(値は「X68Soundの出力低下率 − Nuked-OPMの出力低下率」のポイント差の範囲)

TL=20では窓長を8倍にしても残差の大きさはほとんど変わらず(範囲の上限の差は0.4ポイント以内)、測定窓の取り方による誤差ではない。TL=0では窓長によって値が動き、CON=2・3では窓2048で約3.5ポイント、窓16384で約2ポイント、CON=0では符号まで変わる。深い変調では出力の波形そのものが両実装で違う(相違点の「波形生成の演算方式」の節で見た残差91〜100%)ため、実効値の比も窓の中の波形の違い方に左右される。窓長によらず、TL=0では差の符号がTL=20と逆になり、CON=1〜3ではX68Soundの低下率がNuked-OPMより1.6〜4.5ポイント小さい。TL=0で差が広がるのは、変調波形の違いが変調段を通るたびに増幅されるためで、その違いはTL=20では変調経路の遅延と演算方式の両方から、TL=0では主に演算方式から来る(相違点の「変調経路の遅延」の節)。また差が最大になるのは、4段すべてが直列のCON=0ではなくCON=1(窓2048で-4.53、窓16384で-4.02ポイント)であり、直列段数だけでは差の大きさは決まらない。

いずれの条件でも、どのオペレータを消すと出力が大きく下がるか(=どれがキャリアか)の判定は8種すべてで一致しており、アルゴリズムの結線そのものに相違はない。ただし出力低下率は実効値の比なので、波形そのものの違いは示さない(波形の比較は相違点の「波形生成の演算方式(線形乗算と対数-指数変換)」の節)。

PAN(L/R出力の有効/無効ビット、レジスタ$20-$27のbit6-7)

CON/FBと同じレジスタのbit6・bit7は、それぞれチャンネルの音を左右どちらの出力へ混ぜるかを指定する。両実装をOPMコア観測点(X68Sound: PANマスク加算後のL/R、Nuked-OPM: chip.mix2[0]/mix2[1])で駆動し、bit6だけを立てたとき・bit7だけを立てたときの左右チャンネルの出力音量(実効値)を測定した。

設定 X68Sound L X68Sound R Nuked-OPM L Nuked-OPM R
bit6のみ 523012.3 0.0 1016.8 0.0
bit7のみ 0.0 523012.3 0.0 1016.5

bit6を立てると左チャンネルのみ、bit7を立てると右チャンネルのみに音が出る。無効側は両実装とも厳密に0だった。左右が入れ替わっているようなこともなかった。有効側のL/R同士の値も、それぞれの実装内で完全に対称である(X68Soundは523012.3で完全一致、Nuked-OPMは1016.8対1016.5でサンプリング位相による端数のみ)。

チャンネルとアルゴリズムを広げても同じ結果になる。全8チャンネル×CON 8種×PAN 4通り(L、R、LR、なし)の256条件で、無効側は両実装とも厳密に0、有効側は0でなく、LR両方を有効にしたときのLとRはX68Soundでは完全に等しく、Nuked-OPMでは1%以内で等しかった。

LFO周波数の周期

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

LFRQ=240以下の16点では、1周期を測るだけでX68SoundとNuked-OPMの値が完全一致した(62500Hzサンプリング換算)。

一方LFRQ=248以上では、上表のように8サンプルの差が出る点がある。これは実装間の差ではなく、1周期の長さが両実装とも周期ごとに揺れていることによる。周期が1000サンプル台まで短くなると、LFOカウンタの歩進がサンプル境界と一致しなくなり、連続する周期の長さが8サンプル刻みで交互に変わる。1周期だけを測って突き合わせると、この揺らぎがそのまま差に見えてしまう。

そこで起動直後を捨てたうえで100周期の平均を取り直した。

LFRQ 224 240 248 252 254 255
X68Sound(100周期平均) 4096.00 2048.00 1365.28 1170.24 1092.32 1056.96
Nuked-OPM(100周期平均) 4096.00 2048.00 1365.28 1170.24 1092.24 1057.04
差 0 0 0 0 +0.08 -0.08
X68Sound 1周期の揺らぎ 4096 2048 1360〜1376 1168〜1184 1088〜1104 1056〜1072
Nuked-OPM 1周期の揺らぎ 4096 2048 1360〜1368 1168〜1176 1088〜1096 1056〜1064

平均で見ると全20点で一致し、差は最大0.08サンプル(0.006%)に収まる。LFO周波数の周期は波形0の全設定でNuked-OPMと一致している。

ただし瞬間的な周期の揺らぎ幅には違いがあり、X68Soundが16サンプル幅(例: 1360〜1376)で揺れるのに対しNuked-OPMは8サンプル幅(1360〜1368)に収まる。平均が同じなので長期的なLFO位相はずれないが、1周期単位で見るとX68Soundの方がばらつきが大きい。

波形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側でも同じ関係が成り立つことを実測で確認した。

LFO波形1〜3(矩形・三角・ノイズ)の種類

残る3種類(波形1=矩形波、2=三角波、3=ノイズ)についても、レジスタで指定する波形番号が両実装で同じ形を指しているかを確認した。X68Sound実ライブラリに追加した読み出し専用アクセサ経由のテストと、Nuked-OPMを実際に駆動して出力値を記録したテストで、それぞれの波形番号ごとの出力値の並びを比較したところ、

という形の分類が、レジスタ番号(0〜3)とも完全に一致した。ノイズ波(波形3)の実際の値の並び自体は、前述の「ノイズ生成器の方式が異なる」という差異により両実装で一致しないが、「波形番号3がノイズ波を指す」という割り当て自体は両実装で一致している。

値の並びが一致しなくても、統計的な性質は比べられる。波形3をAMD=127・PMS=7・PMD=127で6万サンプル分記録すると、次のようになった。

LFRQ 観測量 X68Sound 平均 / 標準偏差 / 範囲 / 更新間隔 Nuked-OPM 平均 / 標準偏差 / 範囲 / 更新間隔
200 AM 129.2 / 74.1 / 0〜253 / 64.1 125.5 / 73.6 / 0〜253 / 64.3
200 PM(位相増分) 5439 / 1382 / 3310〜8224 / 64.0 5395 / 1432 / 3284〜8188 / 36.4
240 AM 125.2 / 73.9 / 0〜253 / 16.1 126.9 / 73.0 / 0〜253 / 8.7
255 AM 125.2 / 73.9 / 0〜253 / 16.1 127.0 / 72.9 / 0〜253 / 14.5

AMの平均・標準偏差・範囲は両実装で一致しており、一様分布(平均126.5、標準偏差約73)に近い。更新間隔はLFRQ=200のAMでは一致するが、LFRQ=240ではX68Soundが約2倍長く、ほかの波形で見た「速いLFOでの更新の粗さ」と同じ傾向が出る。PMの更新間隔はLFRQ=200でもNuked-OPMのほうが短い(36.4対64.0サンプル)。PM側の更新間隔の差の原因は調べていない。

LFO AM(振幅変調)深度の数値比較(波形0)

前述の周期の一致はあくまで「同じ間隔で繰り返すか」の確認であり、実際に音量へ加算される深度の値そのものが一致するかは別問題である。これを検証するため、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.38 0.50 0.49 0.73 0.94

残る差1〜2は、LFRQ=255という速いLFOで測ったことによる。同じ比較をLFRQ=224(1周期4096サンプル)で行うと、AMD全6点で最大値(1、31、63、127、191、253)も位置合わせ後の差(すべて0)も完全に一致した。LFRQ=255ではX68SoundのLFO出力が1周期に66回しか更新されず(Nuked-OPMは132回)、波形テーブルの途中の値を読み飛ばすため、AMD=127でも最大値が250にとどまる(Nuked-OPMは253)。回路の実装方式が全く異なるにもかかわらず、AMDの適用結果そのものは両実装で一致している。

なおAMD=127でLFRQを振った比較では、LFRQ=248だけ位置合わせ後の最大差が252〜253と、ほぼ振れ幅いっぱいになった。これはノコギリ波の立ち下がり(最大値から0への急変)が、両実装でずれた位置に来るためと考えられる(差が大きいサンプルの位置までは確認していない)。「LFO周波数の周期」の節で見るとおり、この付近のLFRQでは1周期の長さが周期ごとに揺れ、その揺れ方が両実装で違う(X68Sound 16サンプル幅、Nuked-OPM 8サンプル幅)。立ち下がりの位置が1サンプルでもずれると、その点で差は振れ幅いっぱいになる。LFRQ=248でも最大値は両実装で252・253とほぼ等しく、深度の違いではない。

KF(音階微調整、レジスタ$30-$37)によるピッチ増分

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と一致している。

TL(トータルレベル、レジスタ$60-$7F)の減衰カーブ

TLはオペレータの出力音量を127段階で減衰させるパラメータ。音階KC=$40・CON=7(全独立、対象オペレータ以外はTL=127で無音化)・AR=31(瞬時アタック)・D1R=0の条件で、TL=0〜127のうち19点を確認した。

観測点はOPMコア(出力段を通さない線形合計)で、2048サンプル分の実効値を取っている。この項目を音声出力のピーク振幅で測ると、X68Sound側の出力段にある下位10bit切り捨てとFIRローパスが低レベル側ほど相対的に大きく影響するため、出力段由来の量子化誤差をOPMコアの実装差と取り違えることになる。

絶対スケールが500倍あまり違うため、TL=0を各実装ごとの基準100%とした相対値で比較する。理論値はTLを8段階進めるごとに1/2(16段階で1/4)。

TL 0 8 16 24 32 40 48
理論比 100.0000% 50.0000% 25.0000% 12.5000% 6.2500% 3.1250% 1.5625%
X68Sound(基準比) 100.0000% 50.0000% 25.0000% 12.5000% 6.2500% 3.1250% 1.5625%
Nuked-OPM(基準比) 100.0000% 49.9999% 24.9995% 12.4962% 6.2443% 3.1177% 1.5544%
TL 56 64 72 80 88 96 104〜127
理論比 0.7812% 0.3906% 0.1953% 0.0977% 0.0488% 0.0244% 0.0122%以下
X68Sound(基準比) 0.7812% 0.3906% 0.1953% 0.0977% 0% 0% 0%
Nuked-OPM(基準比) 0.7729% 0.3822% 0.1866% 0.0886% 0.0392% 0.0142% 0%

TL=0〜80の11点では、両実装とも理論比に対して基準比0.01ポイント以内で一致する。X68Soundは事前計算テーブルを使うため小数点以下4桁まで理論値と完全一致し、Nuked-OPMはオペレータ出力が14bit幅に量子化されている分、理論値からわずかに(最大0.009ポイント)下振れする。OPMコアで見る限り、低レベル側の量子化誤差はX68Sound側ではなくNuked-OPM側に現れる。

ただし「0.01ポイント以内」は絶対値での話であり、TLが大きい領域では基準比そのものが小さいため、相対的な差は無視できない大きさになる。TL=64では0.3906%対0.3822%で相対2.2%、TL=80では0.0977%対0.0886%で相対9.3%の差がある。とはいえこの領域の出力は1チャンネル最大音量の0.1%未満(-60dB以下)であり、音量バランスとして意味を持つTL=0〜48の範囲では相対差も約0.5%以内(TL=48で0.52%)に収まっている。

唯一の差は無音に達する位置で、X68SoundはTL=88で完全に0になるのに対し、Nuked-OPMはTL=96まで最小値1が残り、TL=104でようやく0になる。ただしこの領域はどちらも1チャンネル最大音量の0.05%未満(-66dB以下)であり、実質的には両者とも無音である。

AME(振幅変調ON/OFF)とAMS(LFO振幅感度)によるAM深度のゲーティング・スケーリング

AME(レジスタ$A0-$BFのbit7)はLFOによる振幅変調を有効/無効にするビット、AMS(レジスタ$38-$3Fの上位2bit)はその感度(深さ)を4段階で調整するパラメータ。

どちらも音量の時間変化として現れるため、音声出力の実効値の包絡線の変動幅で測りたくなるが、この観測量は二重の意味で量的比較に向かない。第一にX68Sound側は出力段のハイパス・FIRローパス・リサンプルが包絡線を鈍らせる。第二に、そもそもエンベロープの減衰量は対数領域で加算されてから指数変換されるため、内部の加算量と出力の線形振幅は比例しない。この方法ではON/OFFの確認はできても、AMSの段階比を逆算することは原理的にできない。

そこで実際にエンベロープへ加算された減衰量を直接比較する。X68SoundはOp::Output()がALPHATBLの参照位置へ足し込む値(lfolevel & Ame)、Nuked-OPMはOPM_EnvelopePhase2がeg_levelへ足し込むchip.eg_amである。どちらも1024段階のエンベロープ領域という同じ単位なので、スケール変換なしに直接照合できる。

AMSの適用方法も両実装で構造的に一致している。X68SoundはLfo::CulcAmValueで((AmTblValue*Amd)>>7) << Ams[ch](Ams[ch]はAMS=0で31、AMS=1/2/3で0/1/2)、Nuked-OPMはOPM_EnvelopePhase2でAMS=0のとき0、1/2/3のときlfo_am_lockを0/1/2ビット左シフトする。いずれも左シフトによる等比スケーリングである。

LFO波形0・LFRQ=255・AMD=127の条件での実測値は次のとおり。

条件 X68Sound 実効深度[min,max] Nuked-OPM 実効深度[min,max] 深度幅の差
AME=0 0, 0 0, 0 0
AMS=0 0, 0 0, 0 0
AMS=1 0, 250 0, 253 -3
AMS=2 0, 500 0, 506 -6
AMS=3 0, 1000 0, 1012 -12

AME=0では両実装とも実効深度が厳密に0であり、ゲーティングは完全に一致する。AMS=0(X68Soundでは感度シフト量31相当、Nuked-OPMでは明示的に0を代入)も同様に厳密な0で一致した。

AMS=1→2→3のスケーリングも、両実装とも厳密に1:2:4倍である。

AMS 1 2 3
X68Sound 深度幅 250 500(2.00倍) 1000(4.00倍)
Nuked-OPM 深度幅 253 506(2.00倍) 1012(4.00倍)

残る差はAMS=1時点での基準値そのもの(250対253)である。AMDを振ると次のようになる(LFRQ=255)。

AMD 1 16 32 64 96 127
X68Sound 深度幅 1 31 63 126 189 250
Nuked-OPM 深度幅 1 31 63 127 191 253
差 0 0 0 -1 -2 -3

AMD=1〜32では完全一致し、AMDが大きくなるにつれてX68Soundがわずかに小さくなる(最大でAMD=127時の3、1024段階中約0.3%)。これはX68Soundの(AmTblValue*Amd)>>7という線形式とNuked-OPMのビットシリアル乗算回路の丸めの違いと解釈したくなる。しかしLFRQ=224で測るとAMD全6点で最大値が完全に一致するため(「LFO AM(振幅変調)深度の数値比較(波形0)」の節を参照)、丸めの違いではない。LFRQ=255でX68SoundのLFO出力の更新が粗く、ノコギリ波の頂点付近の値を読み飛ばすことによる差である。

すなわち、AME/AMSについてはゲーティング・等比スケーリングともに両実装で厳密に一致しており、AMDの適用結果も一致している。LFRQ=255で残る最大3の差は、LFO更新の粗さによるものである。なお同じ条件を音声出力の実効値の包絡線で測ると、変動幅の基準比はX68Sound 60.3%・Nuked-OPM 72.6%となり12ポイントもの差が出るが、これは上記のとおり出力段のフィルタと対数-指数変換を経たことによる測定上の見かけであって、実装間の差ではない。

AR/D1R/D2R/RRのレート値ごとの速度カーブ

エンベロープの4つの速度パラメータ(アタックレートAR、ディケイ1レートD1R、ディケイ2レートD2R、リリースレートRR)について、既存のエンベロープカーブ検証(1組の設定のみ)を拡張し、それぞれのレート値を0〜31(RRのみ0〜15)の範囲で広く振って、内部エンベロープ値が目標値へ到達するまでのサンプル数を比較した。値の変化がきわめて遅くなるレート値では、目標値に到達するまで数十万〜数百万サンプル相当のティックを進める必要があるため、測定には十分大きな上限(300万サンプル)を設けた。

AR(D1R=12・D2R=6・RR=7・D1L=8・KC=$40・KS=0固定、KEY ON直後から最大音量(XrEl=0)到達までのサンプル数)

AR 1 4 8 12 16 20 24 28 31
X68Sound 445423 149743 37423 9343 2347 586 154 37 1
Nuked-OPM 445419 148971 37227 9291 2307 585 150 36 0

D1R(AR=31瞬時アタック・D2R=0・RR=7・D1L=15(閾値62)・KC=$40・KS=0固定、瞬時アタック完了後にXrElが500へ到達するまでのサンプル数)

D1R 1 4 8 12 16 20 24 28 31
X68Sound 測定不能※ 1023726 255918 63966 16002 3999 1002 252 189
Nuked-OPM 測定不能※ 1022955 255723 63915 15963 3999 1002 252 189

※D1R=1は300万サンプルの上限内では到達しなかった。X68Soundの内部レート表・更新間隔(エンベロープクロックは3ティックに1回しか進まない)から理論値を計算すると、目標値到達には約307万サンプル(62500Hzで約49秒)を要する計算になり、上限にわずかに届かなかっただけで、実装上の不具合ではない。

D2R(AR=31・D1R=31・RR=7・D1L=1(閾値2)・KC=$40・KS=0固定、サスティン相到達後にXrElが500へ到達するまでのサンプル数)

D2R 1 4 8 12 16 20 24 28 31
X68Sound 2872299 957675 239403 59835 14967 3750 945 243 186
Nuked-OPM 2872296 956904 239208 59784 14976 3750 945 243 186

RR(AR=31・D1R=0・D2R=6・D1L=8・KC=$40・KS=0固定、瞬時アタック後にKEY OFFし、XrElが900へ到達するまでのサンプル数)

RR 1 4 7 10 13 15
X68Sound 2763245 345389 43157 5396 674 338
Nuked-OPM 2763241 345385 43201 5398 676 340

D1R・D2Rはレート値20以上でサンプル数が完全に一致する。RRはどのレート値でも差が4サンプル以内(RR=7のみ-44)である。残る差はARで大きく見え、AR=4では772サンプル(0.52%)に達する。

ただしこれはレートの相違ではない。終点(目標値到達)だけを比べると「速度が違う」ようにしか見えないため、カーブ上の複数点で到達サンプル数を測って切り分けた。

目標エンベロープ値 900 700 500 300 100 10 0
AR=4 X68Sound 3826 11506 22258 39154 71410 128242 149746
AR=4 Nuked-OPM 3052 10732 21484 38380 70636 127468 148972
差 +774 +774 +774 +774 +774 +774 +774
AR=8 差 +198 +198 +198 +198 +198 +198 +198
AR=20 差 +3 +3 +3 +3 +3 +3 +3

同じ切り分けをD1R・D2R・RRについても行った(それぞれ4つのレート値、カーブ上10点。ARは6つのレート値、12点)。

次の表と上の複数点の表は、「KEY ON(D1R・D2R・RRは瞬時アタックの完了)からのサンプル数」を両実装で同じ起点から数えている。前掲のレート値ごとの表は、X68Soundだけコマンドキューが空になるまで進めてから数え始めているため、AR・RRでは同じ条件でも差が1〜2サンプル小さく出る(AR=4で772と774、AR=16で40と42、RR=4で4と5)。

ステージ レート値と一定オフセット
AR 4:+774 / 8:+198 / 12:+54 / 16:+42 / 20:+3 / 24:+3〜+6
D1R 8:+195 / 12:+51 / 16:+39 / 20:+0
D2R 8:+195 / 12:+51 / 16:-9〜+15 / 20:+0〜+6
RR 4:+5 / 7:-43 / 10:-1 / 13:-1

差はカーブ上のどの点で測っても一定である。例外はD2R=16・D2R=20・AR=24で、目標値によって2つの値(D2R=16では-9と+15、D2R=20では+0と+6、AR=24では+3と+6)を行き来する。これは目標値を跨ぐサンプルが1更新ぶん前後するためで、レートの差ではない。すなわち両実装のエンベロープは4ステージとも同じ速度・同じ形で進んでおり、違うのは進み始める時刻だけである。

ただし上の表のオフセットは、それぞれ1回の測定での値である。レートが遅いほど更新間隔が長い(AR=4で1536サンプル、AR=8で384サンプル)ため、KEY ONが更新周期のどこに当たるかで、オフセットは最大で1更新間隔ぶん変わりうる。RRの「7:-43」がほかの値と符号も大きさも揃わないのも、1回の測定がたまたま更新周期の端に当たったためと考えられる。そこでKEY ON/KEY OFF前の待ち時間を8更新間隔にわたって振り、前の音を鳴らさない状態と鳴らした後の状態の両方で測り直した。オフセットは±1更新間隔程度の範囲で動き、その平均の符号も前の音の有無で反転した。一定のずれではなく、開始時刻の決まり方の違いによる揺れである(詳細は相違点の「アタックの開始時刻」の節)。

D1L(サスティンレベル、レジスタ$E0-$FFの上位4bit)の全範囲

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と一致することを確認できた。

MUL(逓倍数、レジスタ$40-$5Fの下位4bit)によるピッチ増分スケーリング

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) 2598 5196 10392 15588 20784 25980 31176 36372 41568
Nuked-OPM(pg_inc) 2598 5196 10392 15588 20784 25980 31176 36372 41568
MUL 9 10 11 12 13 14 15
X68Sound(DeltaT) 46764 51960 57156 62352 67548 72744 77940
Nuked-OPM(pg_inc) 46764 51960 57156 62352 67548 72744 77940

MUL=0〜15の全16段階で、両実装の値が完全に一致した。MUL=1のときの5196は、KF・DT1の検証での基準値と同じである。

KEY ONの挙動(位相リセット、再KEY ON、リリース中のKEY ON、スロットマスク)

KEY ON(レジスタ$08)には、エンベロープを始める以外にもいくつかの決まりがある。これらを4つの観点で比べた(KC=$40・MUL=1)。

CT1/CT2出力(レジスタ$1Bのbit6・bit7)

レジスタ$1Bの上位2bitは、YM2151の汎用出力ピンCT1・CT2を制御する。Nuked-OPMでは、bit6がio_ct1、bit7がio_ct2にそのまま反映された($00・$40・$80・$C0の4通りで確認)。X68Soundはbit7だけを読み、ADPCMの基本クロックの選択(AdpcmBaseClock、0なら8MHz・1なら4MHz)に使う。bit6は読んでいない。X68000でCT1・CT2がそれぞれ何に配線されているかは本記事では確認しておらず、X68Soundのこの割り当てが実機どおりかは判定していない。OPMの発音そのものには影響しない。

参考文献

YM2151のレジスタ構成やアルゴリズムの一般的な知識は主に上記2つのソースコードに基づき、本記事の数値検証も基本的にはその実行結果の照合による。ただし「ステータスレジスタの書き込みビジー(BUSY)ビット」の節では待機クロック数について、「Nuked-OPM側の検証」の節では時間軸の校正のためのタイマー周期の式について、YM2151データシートの仕様値を参照している。

[トップページに戻る]