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.wc | snd_write_reg / snd_write_if_changed |
| §3 初期化 | sound_init / mute_channels / play_song | sound_init / snd_mute_channels / sound_play_song |
| §4 フレーム順序 | RefSim.tick | sound_tick |
| §5 行処理 | process_row / trigger / note_off | snd_process_row / snd_trigger / snd_note_off |
| §6 楽器系列と fx | seq_step / env_frame | snd_seq_step / snd_env_frame |
| §7 ピッチ | calc_div | snd_calc_div |
| §8 CH1/CH2 | update_pulse | snd_update_pulse |
| §9 CH3 | update_tri | snd_update_tri |
| §10 CH4 | update_noise | snd_update_noise |
| §11 ドラム | drum_pre / drum_post / start_drum / release_ch3 | snd_drum_pre / snd_drum_post |
1. データレイアウト(src/bgm_data.inc)
tools/export_bgm_gb.py が生成する。手で編集しない。
1.1 定数
| シンボル | 値 | 意味 |
|---|---|---|
BGM_SONG_COUNT | 5 | 曲数 |
BGM_BANK | 1 | 曲データの ROMX バンク(architecture.md §2.2) |
BGM_PATTERN_LEN | 64 | 1 パターンの行数(ハードコード) |
NOTE_DIV_COUNT | 84 | 周期表のエントリ数 |
NOTE_DIV_MIDI_LO | 24 | 周期表の先頭 MIDI 番号 |
DRUM_KICK / DRUM_SNARE / DRUM_TOM | 0 / 1 / 2 | ドラム ID |
1.2 固定表(SECTION "BGM Fixed Tables", ROM0)
| ラベル | サイズ | 内容 |
|---|---|---|
note_div | 168 B | dw × 84。div = round(131072 / f)、MIDI 24-107。リトルエンディアン |
vol_mul | 256 B | vol_mul[cvol*16 + env] = round(cvol*env/15) |
noise_nr43 | 16 B | `(shift << 4) |
snd_wave_data | 16 B | wave RAM の初期値。-D WAVE_SINE=1 で正弦波に切替 |
drum_table | 128 B | 32 エントリ × 4 B(§11.1) |
drum_offset | 3 B | ドラム ID → 先頭エントリ index(0, 12, 21)。ドライバは ×4 する |
drum_length | 3 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 | 型 | 内容 |
|---|---|---|
| +0 | db | speed88 下位バイト |
| +1 | db | speed88 上位バイト |
| +2 | db | オーダー長 |
| +3 | db | 音色数 |
| +4 | dw | オーダー表(パターン番号の列) |
| +6 | dw | パターン表(1 パターン = 5 ワード: p1, p2, tr, nz, dm) |
| +8 | dw | 音色表(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<<4 | vol` の 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 / tr | midi - 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 で検査)。
; 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_changed(nz 経路)/ 無条件(ドラム経路) | ドラムは表どおり書く |
NR14 NR24 NR34 NR44 | 常に snd_write_reg(無条件) | リトリガは「状態」ではなく「動作」。同値でも書けば再トリガになるので、書くかどうかは §8-§10 の条件で決める |
⚠ NRx4 を snd_write_if_changed で書いてはならない。 同値でリトリガが落ちる。 ⚠ ミュートは $00。 $08 は DAC が生きたまま音量 0 になり DC オフセットが残る。
3. 初期化
3.1 sound_init(起動時 1 回)
この順序でなければならない。 NR52 bit7 = 0 の間は他の APU レジスタへの書込みが 無視されるため、電源投入が NR50 / NR51 より前でなければならない。
| # | 書込み | 値 |
|---|---|---|
| 1 | NR52 | $00(APU 全体 OFF。全レジスタが 0 にクリアされる) |
| 2 | NR52 | $80(電源投入) |
| 3 | NR50 | $77 |
| 4 | NR51 | $FF |
| 5-20 | $FF30-$FF3F | snd_wave_data の 16 B を昇順に |
| 21 | NR30 | $80(CH3 DAC ON) |
| 22 | NR10 | $00(CH1 スイープ無効) |
| 23- | snd_mute_channels | §3.2 |
⚠ wave RAM はここでしか書かない。 演奏中は 1 バイトも書き換えない(受入条件 7)。 書換えには NR30 = 0 → 16 B → 再トリガが要り、クリックノイズと ref sim の複雑化を招く。
3.2 snd_mute_channels
NR12 → NR22 → NR32 → NR42 の順に snd_write_if_changed で $00。 sound_init の直後は全部 $00 なので書込みイベントは 0 本になる。
⚠ architecture.md §7.10 の snd_silence(NR52 = 0 → 再初期化)とは別のルーチン。 snd_silence はシーン遷移専用で、flow_goto の exit からのみ呼ぶ。
3.3 sound_play_song
snd_playing = 0- ヘッダから
speed88/ オーダー長 / 音色数 / 3 ポインタを読む timer = 0/ordpos = 0/row = 0- 全 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 - ドラム状態初期化:
drum_active=$FF,drum_frame=0,ch3_drum=0,drum_ch3_trig=0 snd_load_patternsnd_mute_channelssnd_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)
break5.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] = 0。x == 4 なら snd_stop_drum(§11.5)。APU への直接の書込みはしない。 消音は次のステップ 3-6 で各チャンネルが NRx2 = $00 を書いて行う。
6. 楽器系列と fx(snd_env_frame)
vol → duty → arp → pitch の順に 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=0volの戻り値 =envvol、dutyの戻り値 =envduty、arpの戻り値 =arp補正(符号付き)pitchはheld == 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)
内部状態は常に div。X = 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..4095snd_sine16 = 0, 3, 6, 7, 8, 7, 6, 3, 0, -3, -6, -7, -8, -7, -6, -3(NES と同一)。
書込み時の変換:
| チャンネル | 変換 | 値域 |
|---|---|---|
| CH1 / CH2 | X = 2048 - div | div 1-2047 → X 1-2047 |
| CH3 | X = 2048 - (div >> 1)(srl 1 回、floor) | div 2-4095 → X 1-2047 |
⚠ CH3 の div 下限は 2 である。 div = 1 だと div >> 1 = 0 で X = 2048 になり 11 bit に収まらない。 ⚠ CH3 は div を 4095 まで許す。 MIDI 26 のベースが div = 3571 になるので、 assert div <= 2047 を CH3 に適用すると全曲のベースが落ちる。
8. CH1 / CH2(snd_update_pulse)
x = 0 → NR11($11) 基準、x = 1 → NR21($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 = tr(snd_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_frame と calc_div は同じ条件で呼ぶ。 そうしないと DRUM_USE_WAVE = 0 と = 1 で tr の内部状態(ポルタメント / ピッチ累積 / ビブラート位相)が 食い違い、両モードのイベント列一致が成立しない。 ⚠ CH3 はリトリガ不要で音量が即反映される。 NR32 の変更でリトリガしてはならない。 リトリガするのは ノートオンと ドラムからの CH3 復元の 2 箇所だけ。 ⚠ NR30 は演奏中に触らない。 ミュートは NR32 = $00。
10. CH4 = nz(snd_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)
| バイト | ビット | 内容 |
|---|---|---|
| B0 | 7-0 | CH3 X の下位 8 bit(X = 2048 - ch3_div、ch3_div = round(65536/f)) |
| B1 | 2-0 | CH3 X の上位 3 bit |
| 4-3 | NR32 コード: 0→$00 / 1→$20(100%) / 2→$40(50%) / 3→$60(25%) | |
| 5 | CH3_WRITE | |
| 6 | CH4_WRITE | |
| 7 | CH3_RELEASE | |
| B2 | 7-0 | NR42($00 は DAC オフ = 停止) |
| B3 | 7-0 | NR43 |
エントリ 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_drum | CH3 をドラムが借用中か |
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] = 111.5 snd_stop_drum(dm の $61 ノートオフ)
if drum_active == $FF: return
if ch3_drum: snd_release_ch3()
drum_active = $FFCH4 への明示的な書込みはしない。同じフレームのステップ 6 で nz が write_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 = 1でsound_tickをスキップし、NR51 = $00。 解除でNR51 = $FFに復帰し、全チャンネルを再トリガする(retrig[0..3] = 1)。 snd_silence(シーン遷移専用):NR52 = $00→NR52 = $80→NR50 = $77→NR51 = $FF。 ⚠ ポーズでsnd_silenceを呼んではならない。 行位置ごと凍結する要件と両立しない。
13. 検証(受入基準)
| # | 検査 | 手段 |
|---|---|---|
| 1 | APU 直書きが snd_write_reg の中の 1 件だけ | grep -n 'ld \[\$FF[123][0-9A-F]\], a' src/ が 1 件(issue #6 受入条件 1) |
| 2 | シャドウ境界整列($xx10 / $xx30 / 同一ページ) | ASSERT 3 本 |
| 3 | ref_sim_gb.py と 1,500 フレームのイベント列が完全一致(本数も) | tests/test_apu_refsim.py |
| 4 | DRUM_USE_WAVE の両モードで一致 | 同上 |
| 5 | wave RAM の書込みが sound_init の 16 回だけ | 書込み回数カウンタ |
| 6 | hQHiwater を PR 本文に貼る | 共通受入条件 5 |
イベント列のダンプ形式(e2e.md §5.2):
frame reg value
0 FF24 77
96 FF14 87tools/ref_sim_gb.py <song> -n 1500 [-D DRUM_USE_WAVE=0] が同じ形式で期待値を出す。