Skip to content

GB サウンドドライバ実装契約(asm 実装者向け)

本書は tools/ref_sim_gb.py(参照シミュレータ)と 1:1 対応する仕様である。 asm ドライバ(issue #7)はここに書かれた順序と条件をそのまま実装すること。 ref_sim_gb.py との書込みイベント列一致(1,500 フレーム / 5 曲 / DRUM_USE_WAVE 両モード) が本 issue の受入基準であり、本書がその一致条件の文章版である。

正典は docs/design/architecture.md §7。本書と正典が食い違う場合は正典が優先し、 その差分は README.md の「未決」に列挙してある。


0. 対応表(本書の節 ↔ ref_sim の実体)

本書ref_sim_gb.py対応する asm シンボル(想定)
§2 書込みプリミティブRefSim.w / RefSim.wcsnd_write_reg / snd_write_if_changed
§3 初期化sound_init / mute_channels / play_songsound_init / snd_mute_channels / sound_play_song
§4 フレーム順序RefSim.ticksound_tick
§5 行処理process_row / trigger / note_offsnd_process_row / snd_trigger / snd_note_off
§6 楽器系列と fxseq_step / env_framesnd_seq_step / snd_env_frame
§7 ピッチcalc_divsnd_calc_div
§8 CH1/CH2update_pulsesnd_update_pulse
§9 CH3update_trisnd_update_tri
§10 CH4update_noisesnd_update_noise
§11 ドラムdrum_pre / drum_post / start_drum / release_ch3snd_drum_pre / snd_drum_post

1. データレイアウト(src/bgm_data.inc

tools/export_bgm_gb.py が生成する。手で編集しない。

1.1 定数

シンボル意味
BGM_SONG_COUNT5曲数
BGM_BANK1曲データの ROMX バンク(architecture.md §2.2)
BGM_PATTERN_LEN641 パターンの行数(ハードコード)
NOTE_DIV_COUNT84周期表のエントリ数
NOTE_DIV_MIDI_LO24周期表の先頭 MIDI 番号
DRUM_KICK / DRUM_SNARE / DRUM_TOM0 / 1 / 2ドラム ID

1.2 固定表(SECTION "BGM Fixed Tables", ROM0

ラベルサイズ内容
note_div168 Bdw × 84。div = round(131072 / f)、MIDI 24-107。リトルエンディアン
vol_mul256 Bvol_mul[cvol*16 + env] = round(cvol*env/15)
noise_nr4316 B`(shift << 4)
snd_wave_data16 Bwave RAM の初期値。-D WAVE_SINE=1 で正弦波に切替
drum_table128 B32 エントリ × 4 B(§11.1)
drum_offset3 Bドラム ID → 先頭エントリ index(0, 12, 21)。ドライバは ×4 する
drum_length3 Bドラム ID → フレーム数(12, 9, 11)

1.3 曲索引表(SECTION "BGM Song Table", ROM0

bgm_song_banks:    db  <曲数ぶんのバンク番号>
bgm_song_addr_lo:  db  LOW(bgmN_hdr) ...
bgm_song_addr_hi:  db  HIGH(bgmN_hdr) ...

1.4 曲データ(SECTION "BGM Songs", ROMX, BANK[1]

曲ヘッダは 10 B(NES 版とレイアウト同一):

offset内容
+0dbspeed88 下位バイト
+1dbspeed88 上位バイト
+2dbオーダー長
+3db音色数
+4dwオーダー表(パターン番号の列)
+6dwパターン表(1 パターン = 5 ワード: p1, p2, tr, nz, dm)
+8dw音色表(1 音色 = 4 ワード: vol, duty, arp, pitch)

系列(vol / duty / arp / pitch)は db len, loop, 値...loop = $FF はループなし。 arp / pitch の値は符号付き 8bit。

1.5 ストリームフォーマット(NES と 100% 同一)

バイト意味行消費
$00空行1
$01-$60ノートオン。直後に `inst<<4vol` の 1 B が続く
$61ノートオフ1
$62 vv音量のみ変更(プレフィクス)0
$63 tt ppエフェクト設定(プレフィクス)。tt: 0=arp 1=slide↑ 2=slide↓ 3=porta 4=vibrato 5=duty強制 15=解除0
$80|n空行 n 行(n = 1-63)n

ノートバイトの意味はチャンネルで変わる:

チャンネルノートバイト
p1 / p2 / trmidi - 23(1 = MIDI 24)。note_div の index は ノートバイト - 1
nzノイズ周期 index + 1(1-16)。noise_nr43 の index は ノートバイト - 1
dmドラム ID + 1(1=KICK, 2=SNARE, 3=TOM)。NES の DPCM サンプル番号を再解釈

2. 書込みプリミティブ

APU レジスタへの書込みは 必ず 次の 2 本のいずれかを経由する。直書き禁止 (architecture.md §7.8、CI が grep で検査)。

asm
; snd_write_reg — in: c = レジスタ下位バイト($10-$26 または $30-$3F)、a = 値
snd_write_reg:
    ld   [$FF00+c], a         ; 実 APU へ(architecture.md §7.8 と同じ綴り)
    ld   h, HIGH(wApuShadow)  ; = $C3
    ld   l, c                 ; $10-$26 -> apu_shadow / $30-$3F -> wave_shadow
    ld   [hl], a
    ret

; snd_write_if_changed — 同じ引数。シャドウと同値なら書かない
snd_write_if_changed:
    ld   h, HIGH(wApuShadow)
    ld   l, c
    cp   [hl]
    ret  z
    jr   snd_write_reg        ; ※ h/l を再計算しても良い

ASSERT LOW(wApuShadow) == $10 / ASSERT LOW(wWaveShadow) == $30 / ASSERT HIGH(wApuShadow) == HIGH(wWaveShadow) をビルド時に置くこと。

レジスタごとの使い分け(これがイベント列の本数を決める):

レジスタ経路理由
NR11 NR21 (duty)snd_write_if_changed状態レジスタ
NR12 NR22 NR42 (音量)snd_write_if_changed状態レジスタ。「値が変わったか」がリトリガ判定そのもの
NR13 NR23 NR33 (周波数下位)snd_write_if_changed状態レジスタ
NR32 (CH3 音量)snd_write_if_changed状態レジスタ。リトリガ不要で即反映
NR43 (ノイズ周期)snd_write_if_changednz 経路)/ 無条件(ドラム経路)ドラムは表どおり書く
NR14 NR24 NR34 NR44常に snd_write_reg(無条件)リトリガは「状態」ではなく「動作」。同値でも書けば再トリガになるので、書くかどうかは §8-§10 の条件で決める

NRx4snd_write_if_changed で書いてはならない。 同値でリトリガが落ちる。 ⚠ ミュートは $00 $08 は DAC が生きたまま音量 0 になり DC オフセットが残る。


3. 初期化

3.1 sound_init(起動時 1 回)

この順序でなければならない。 NR52 bit7 = 0 の間は他の APU レジスタへの書込みが 無視されるため、電源投入が NR50 / NR51 より前でなければならない。

#書込み
1NR52$00(APU 全体 OFF。全レジスタが 0 にクリアされる)
2NR52$80(電源投入)
3NR50$77
4NR51$FF
5-20$FF30-$FF3Fsnd_wave_data の 16 B を昇順に
21NR30$80(CH3 DAC ON)
22NR10$00(CH1 スイープ無効)
23-snd_mute_channels§3.2

wave RAM はここでしか書かない。 演奏中は 1 バイトも書き換えない(受入条件 7)。 書換えには NR30 = 0 → 16 B → 再トリガが要り、クリックノイズと ref sim の複雑化を招く。

3.2 snd_mute_channels

NR12NR22NR32NR42 の順に snd_write_if_changed$00sound_init の直後は全部 $00 なので書込みイベントは 0 本になる。

architecture.md §7.10 の snd_silenceNR52 = 0 → 再初期化)とは別のルーチンsnd_silence はシーン遷移専用で、flow_goto の exit からのみ呼ぶ。

3.3 sound_play_song

  1. snd_playing = 0
  2. ヘッダから speed88 / オーダー長 / 音色数 / 3 ポインタを読む
  3. timer = 0 / ordpos = 0 / row = 0
  4. 全 5 チャンネルの状態を初期化: on=0, hold=0, cvol=0, inst=0, pit=0, sli=0, vibph=0, arptick=0, pos[0..3]=0, fx=$FF(なし), dutyovr=$FF(なし), note=target=1, retrig=0
  5. ドラム状態初期化: drum_active=$FF, drum_frame=0, ch3_drum=0, drum_ch3_trig=0
  6. snd_load_pattern
  7. snd_mute_channels
  8. snd_playing = 1

snd_load_patternストリームポインタと hold だけを初期化する(on は触らない)。


4. フレーム順序(sound_tick

sound_tick はメインループ先頭(vsync 待ちの直後)で毎フレーム 1 回。VBlank ISR ではない (architecture.md §7.1)。この 7 ステップの順序が書込みイベント列の順序を決める。

0. snd_playing == 0 なら即 return
1. 行タイマ
     if timer <= 0:  snd_process_row();  timer += speed88
     timer -= $0100
     if timer <= 0:  row++
                     if row >= 64: row = 0; ordpos = (ordpos+1) % ordlen
                                   snd_load_pattern()
2. snd_drum_pre        (ドラムの寿命切れ判定 + フレーム先頭の CH3 解放)
3. snd_update_pulse ch0(CH1)
4. snd_update_pulse ch1(CH2)
5. snd_update_tri      (CH3。ドラムが借用中なら書込みだけ抑止)
6. snd_update_noise    (CH4。ドラムが占有中なら書込みだけ抑止)
7. snd_drum_post       (表どおりの CH3 / CH4 書込み + フレーム末の CH3 解放)

timer は符号付き 16 bit。timer <= 0 の判定は「上位バイトが負」または 「上位・下位ともに 0」。

ステップ 2 とステップ 7 の分割は必須である。 SNARE の f2 は 「CH3_WRITE = 0 かつ CH3_RELEASE = 1」なのでフレーム先頭で解放して同じフレームの ステップ 5 で tr に書かせる。KICK f11 / TOM f10 は CH3_WRITE = 1 なので フレーム末で解放し、tr は次のフレームから書く。


5. 行処理(snd_process_row

チャンネル 0 → 4(p1, p2, tr, nz, dm)の順に 1 行ぶん読む。

for x in 0..4:
    if hold[x] != 0: hold[x]--; continue
    loop:
        b = *ptr[x]++
        $00      -> break                       (空行)
        >= $80   -> hold[x] = (b & $7F) - 1; break
        $61      -> snd_note_off(x); break
        $62      -> cvol[x] = (*ptr[x]++) & $0F; continue
        $63      -> t = *ptr[x]++;  p = *ptr[x]++
                    t == 15 -> fx[x] = $FF
                    t == 5  -> dutyovr[x] = p & 3
                    else    -> fx[x] = t; fxp[x] = p; arptick[x] = 0; vibph[x] = 0
                    continue
        else     -> iv = *ptr[x]++
                    inst = iv >> 4;  vol = iv & $0F
                    if x < 3 and fx[x] == 3 and on[x]:
                        target[x] = b; inst[x] = inst; cvol[x] = vol   (ポルタメント継続)
                    else:
                        snd_trigger(x, b, inst, vol)
                    break

5.1 snd_trigger

note[x] = target[x] = note
inst[x] = inst;  cvol[x] = vol
pos[x][0..3] = 0;  pit[x] = 0;  sli[x] = 0;  vibph[x] = 0;  arptick[x] = 0
on[x] = 1
if x == 4 (dm):  snd_start_drum(note - 1);  return
retrig[x] = 1
if x < 3:  curp[x] = note_div[note - 1]      (ポルタメントの基準 div)

fx / dutyovr はトリガでリセットしない(NES 版と同一)。

5.2 snd_note_off

on[x] = 0x == 4 なら snd_stop_drum(§11.5)。APU への直接の書込みはしない。 消音は次のステップ 3-6 で各チャンネルが NRx2 = $00 を書いて行う。


6. 楽器系列と fx(snd_env_frame

voldutyarppitch の順に 1 ステップ進める。

snd_seq_step(seq, loop, pos):
    if pos >= len:
        if loop == $FF or loop >= len:  return seq[len-1], pos, held=1
        pos = loop
    return seq[pos], pos+1, held=0
  • vol の戻り値 = envvolduty の戻り値 = envdutyarp の戻り値 = arp補正(符号付き)
  • pitchheld == 0 のときだけ pit[x] += 符号付き値(16 bit 累積)

続けて fx の位相を進める(env_frame の末尾):

fx動作
0 (0xy)arptick = (arptick+1) % 3。1 なら arp補正 += x、2 なら arp補正 += y
1 (1xx)sli -= param(div が減る = ピッチが上がる)
2 (2xx)sli += param(div が増える = ピッチが下がる)
3 (3xx)ここでは何もしない(§7 のポルタメントで処理)
4 (4xy)vibph = (vibph + x*5) & $FF

1xx / 2xx の符号は NES 版から 1 ビットも変えない。 内部表現が div (NES period と同じ向き)だから成立する(architecture.md §7.3)。


7. ピッチ(snd_calc_div

内部状態は常に divX = 2048 - div への変換はレジスタ書込みの瞬間だけ。

nn = clamp(note[x] + arp補正, 1, 84)
if fx[x] == 3:                       # 3xx ポルタメント
    tgt = note_div[target[x] - 1]
    if fxp != 0 and curp != tgt:
        curp += fxp(curp < tgt のとき) / curp -= fxp(curp > tgt のとき)
        目標を跨いだら curp = tgt, note[x] = target[x]
    elif curp == tgt:
        note[x] = target[x]
    d = curp
else:
    d = note_div[nn - 1];  curp[x] = d
d += pit[x] + sli[x]
if fx[x] == 4:                       # 4xy ビブラート
    s = snd_sine16[(vibph >> 4) & 15]
    if s != 0:
        delta = (|s| * (fxp & $0F)) >> 2
        d = d + delta(s > 0) / d - delta(s < 0)
clamp: CH1/CH2 -> 1..2047 / CH3 -> 2..4095

snd_sine16 = 0, 3, 6, 7, 8, 7, 6, 3, 0, -3, -6, -7, -8, -7, -6, -3(NES と同一)。

書込み時の変換:

チャンネル変換値域
CH1 / CH2X = 2048 - divdiv 1-2047 → X 1-2047
CH3X = 2048 - (div >> 1)srl 1 回、floor)div 2-4095 → X 1-2047

CH3 の div 下限は 2 である。 div = 1 だと div >> 1 = 0X = 2048 になり 11 bit に収まらない。 ⚠ CH3 は div を 4095 まで許す。 MIDI 26 のベースが div = 3571 になるので、 assert div <= 2047 を CH3 に適用すると全曲のベースが落ちる。


8. CH1 / CH2(snd_update_pulse

x = 0NR11($11) 基準、x = 1NR21($16) 基準。以下 base = $11 + x*5

if on[x] == 0:
    write_if_changed(base+1, $00)        # NRx2 = $00(DAC オフ)
    return
envvol, envduty, arp = snd_env_frame(x)
div = snd_calc_div(x, arp)
duty = (dutyovr[x] != $FF ? dutyovr[x] : envduty) & 3
vol  = vol_mul[cvol[x]*16 + envvol]
nrx2 = vol << 4
vol_changed = (nrx2 != shadow[base+1])
if vol == 0:
    if vol_changed: write(base+1, $00)
    return                               # ← retrig は保持したまま
X = 2048 - div
write_if_changed(base+0, duty << 6)      # NRx1
if vol_changed:  write(base+1, nrx2)     # NRx2(エンベロープ period = 0)
write_if_changed(base+2, X & $FF)        # NRx3
hi = (X >> 8) & 7
if retrig[x] or vol_changed:
    write(base+3, $80 | hi);  retrig[x] = 0     # NRx4 リトリガ
elif hi != (shadow[base+3] & 7):
    write(base+3, hi)                            # 周波数上位のみ(リトリガしない)

書込み順は必ず NRx1 → NRx2 → NRx3 → NRx4。 リトリガ時に NRx3 が既に新しい値で なければならないため。

R1: ハードウェアエンベロープは常に period = 0(NRx2 の下位 3 bit = 0)。 音量はソフト(vol_mul)で決める。 ⚠ R2: vol_changed が偽なら NRx4 bit7 を書かない。 これが architecture.md §10 リスク 1(毎フレームの duty 位相リセット)の唯一の緩和策であり、 イベント列比較で必ず検出される


9. CH3 = trsnd_update_tri

muted = ドラムが CH3 を借用中(ch3_drum == 1)。

if on[2] == 0:
    if not muted: write_if_changed(NR32, $00)
    return
envvol, envduty, arp = snd_env_frame(2)
vol  = vol_mul[cvol[2]*16 + envvol]
nr32 = 0 (vol==0) / $60 (1-4) / $40 (5-10) / $20 (11-15)
if nr32 == 0:
    if not muted: write_if_changed(NR32, $00)
    return                               # ← calc_div は呼ばない(NES 版と同じ)
div = snd_calc_div(2, arp)
if muted: return                         # 状態だけ進めて書込みは捨てる
X = 2048 - (div >> 1)
write_if_changed(NR32, nr32)
write_if_changed(NR33, X & $FF)
hi = (X >> 8) & 7
if retrig[2]:
    write(NR34, $80 | hi);  retrig[2] = 0
elif hi != (shadow[NR34] & 7):
    write(NR34, hi)

muted でも env_framecalc_div は同じ条件で呼ぶ。 そうしないと DRUM_USE_WAVE = 0= 1tr の内部状態(ポルタメント / ピッチ累積 / ビブラート位相)が 食い違い、両モードのイベント列一致が成立しない。 ⚠ CH3 はリトリガ不要で音量が即反映される。 NR32 の変更でリトリガしてはならない。 リトリガするのは ノートオンドラムからの CH3 復元の 2 箇所だけ。 ⚠ NR30 は演奏中に触らない。 ミュートは NR32 = $00


10. CH4 = nzsnd_update_noise

muted = ドラムが CH4 を占有中(drum_active != $FF)。

if on[3] == 0:
    if not muted: write_if_changed(NR42, $00)
    return
envvol, envduty, arp = snd_env_frame(3)
vol = vol_mul[cvol[3]*16 + envvol]
if vol == 0:
    if not muted: write_if_changed(NR42, $00)
    return
if muted: return
nr42 = vol << 4
vol_changed = (nr42 != shadow[NR42])
if vol_changed: write(NR42, nr42)
idx   = clamp(note[3] - 1 + arp, 0, 15)
width = (dutyovr[3] != $FF ? dutyovr[3] : envduty) & 1
write_if_changed(NR43, noise_nr43[idx] | (width << 3))
if retrig[3] or vol_changed:
    write(NR44, $80);  retrig[3] = 0      # リトリガのみ。length enable は立てない

NR41(長さ)は使わない。 停止は NR42 = $00(DAC オフ)で行う。 ⚠ nz(本ドラムテーブルとは別の CH4 通常トラック)にも R2 を適用する(毎フレームの リトリガは LFSR を 60 Hz でリセットして周期的なブザーになるため)。ドラム(dm)自体は 静的テーブルの CH4_WRITE ビットが立つフレームだけ書くので R2/R3 の対象外 (architecture.md §7.4 R4、issue #7 オーナー判断 U4 で確定)。


11. ドラム(CH4 + CH3 借用)

11.1 表のビット割当(1 フレーム = 4 B)

バイトビット内容
B07-0CH3 X の下位 8 bit(X = 2048 - ch3_divch3_div = round(65536/f)
B12-0CH3 X の上位 3 bit
4-3NR32 コード: 0→$00 / 1→$20(100%) / 2→$40(50%) / 3→$60(25%)
5CH3_WRITE
6CH4_WRITE
7CH3_RELEASE
B27-0NR42$00 は DAC オフ = 停止)
B37-0NR43

エントリ index = drum_offset[id] + frame、バイト offset = index × 4。

ch3_div§7 の div の半分である。 列名を取り違えると 1 オクターブずれる。 ⚠ NR44 = $80 は表に持たない。 CH4_WRITE = 1 のフレームでドライバが定数として書く。

11.2 状態

変数意味
drum_active鳴っているドラム ID($FF = なし)
drum_frame経過フレーム(0 起点)
ch3_drumCH3 をドラムが借用中か
drum_ch3_trig借用開始フレームで CH3 をリトリガするフラグ

tr の退避(tr_held_note / tr_held_vol)は専用変数を持たないtr のチャンネル状態を そのまま走らせ、書込みだけ抑止する方式にしてある(§9)。復元は retrig[2] = 1 を立てるだけ。

11.3 snd_start_drum(id)snd_trigger から)

id = clamp(id, 0, 2)
drum_active = id;  drum_frame = 0
if DRUM_USE_WAVE and drum_table[id][0].CH3_WRITE:
    ch3_drum = 1;  drum_ch3_trig = 1

借用の宣言は snd_process_row の時点(ステップ 1)で行う。 ステップ 5 の tr 更新より 前でなければ、同じフレームで tr とドラムが二重に CH3 を書く。

11.4 snd_drum_pre(ステップ 2)/ snd_drum_post(ステップ 7)

snd_drum_pre:
    if drum_active == $FF: return
    if drum_frame >= drum_length[drum_active]:
        if ch3_drum: snd_release_ch3()
        drum_active = $FF                 # ← ここで CH4 も解放される
        return
    e = entry(drum_active, drum_frame)
    if DRUM_USE_WAVE and e.CH3_RELEASE and not e.CH3_WRITE:
        snd_release_ch3()                 # SNARE f2: フレーム先頭で解放

snd_drum_post:
    if drum_active == $FF: return
    e = entry(drum_active, drum_frame)
    if DRUM_USE_WAVE and e.CH3_WRITE:
        write_if_changed(NR32, e.NR32)
        write_if_changed(NR33, e.X & $FF)
        hi = (e.X >> 8) & 7
        if drum_ch3_trig:  write(NR34, $80 | hi);  drum_ch3_trig = 0
        elif hi != (shadow[NR34] & 7):  write(NR34, hi)
    if e.CH4_WRITE:
        write(NR42, e.NR42)               # 3 本とも無条件
        write(NR43, e.NR43)
        write(NR44, $80)
    if DRUM_USE_WAVE and e.CH3_RELEASE and e.CH3_WRITE:
        snd_release_ch3()                 # KICK f11 / TOM f10: フレーム末で解放
    drum_frame++

snd_release_ch3:
    ch3_drum = 0;  drum_ch3_trig = 0;  retrig[2] = 1

11.5 snd_stop_drumdm$61 ノートオフ)

if drum_active == $FF: return
if ch3_drum: snd_release_ch3()
drum_active = $FF

CH4 への明示的な書込みはしない。同じフレームのステップ 6 で nzwrite_if_changed(NR42, ...) によって取り戻す(nz が off なら $00 を書く)。 ⚠ 収録 5 曲に dm のノートオフは 1 つも無いので、この経路は合成テストでしか通らない。

11.6 優先度

  • CH4: SE(#10、未実装)> dm > nz。ドラムは寿命いっぱい(12 / 9 / 11 フレーム) CH4 を占有する。CH4_WRITE = 0 のフレームでも nz は書けない。
  • CH3: dm ボディ > tr。借用は CH3_RELEASE のフレームまで(KICK f11 / SNARE f2 / TOM f10)。
  • ドラム中に tr へ新しいノートオンが来ても CH3 には書かない(状態だけ更新)。
  • ドラム中に別のドラムが来たら drum_frame = 0 で乗り換える(ch3_drum は保持し、 drum_ch3_trig を立て直す)。

11.7 -D DRUM_USE_WAVE=0

CH3 借用を完全に無効化する。CH3_WRITE / CH3_RELEASE を無視し、ドラムは CH4 だけを鳴らす。 tr(ベース)は 1 フレームも途切れない。ref sim も同じフラグを持ち、両モードで一致検証する。


12. ポーズと消音(architecture.md §7.10)

  • ポーズ: hSoundGate = 1sound_tick をスキップし、NR51 = $00。 解除で NR51 = $FF に復帰し、全チャンネルを再トリガする(retrig[0..3] = 1)。
  • snd_silence(シーン遷移専用): NR52 = $00NR52 = $80NR50 = $77NR51 = $FF。 ⚠ ポーズで snd_silence を呼んではならない。 行位置ごと凍結する要件と両立しない。

13. 検証(受入基準)

#検査手段
1APU 直書きが snd_write_reg の中の 1 件だけgrep -n 'ld \[\$FF[123][0-9A-F]\], a' src/ が 1 件(issue #6 受入条件 1)
2シャドウ境界整列($xx10 / $xx30 / 同一ページ)ASSERT 3 本
3ref_sim_gb.py1,500 フレームのイベント列が完全一致(本数も)tests/test_apu_refsim.py
4DRUM_USE_WAVE両モードで一致同上
5wave RAM の書込みが sound_init の 16 回だけ書込み回数カウンタ
6hQHiwater を PR 本文に貼る共通受入条件 5

イベント列のダンプ形式(e2e.md §5.2):

frame  reg   value
0      FF24  77
96     FF14  87

tools/ref_sim_gb.py <song> -n 1500 [-D DRUM_USE_WAVE=0] が同じ形式で期待値を出す。