Research 08 / Audio pipeline

不只把聲音放大,而是保留原曲內部的關係。

PC 原版的 OPL2 音樂不能原封不動交給 GBA 播放。本專案將事件、音色與 聲道關係轉成 Maxmod 可負擔的 tracker/PCM 表示,完整保留九個 OPL2 source channel,並以離線量測修正聲道響度、鼓聲瞬態、有限提示曲與場景切換。

兩條來源路徑,一個 runtime soundbank

音樂仍由 ROMFS 的 music.mus 與原始 song ID 決定流程; build 端使用 41 首 TYM 表示保存 LDS/OPL2 pattern、patch、note、 channel volume 與 carrier total-level,再轉成 Maxmod 的 IT modules; 41 首中實際使用第九聲道的 26 首不再刪除最低能量聲部。 音效則直接由 tyrian.sndvoices.snd 拆出原始 8-bit PCM。最後由 mmutil 組成同一份 soundbank.bin

41music modules
334calibrated OPL sources
29 + 9SFX + voice samples
Runtime 不以「第幾個 GBA 檔案」重寫遊戲規格。關卡、Game Menu、 Demo、JukeBox、Boss 與提示音仍傳入原版的 song/sound ID; GBA adapter 只負責找到相對應的 Maxmod module 或 sample。

為什麼不能逐曲正規化

早期轉換沿用為 SNES S-DSP/BRR 建立的 channel gain,還會把每首歌 最大的 voice gain 再正規化。結果是同一音色跨歌曲沒有共同響度基準, 而一首歌最強的聲道會連帶改變其他聲道比例。稀疏鼓聲尤其容易為了追上 整曲 RMS 而被放大成尖銳瞬態。

新流程以 channel-calibration.json 的完整 OPL2 stem RMS 作固定 reference,對 Maxmod 真正使用的 signed 8-bit sample、 IT volume-column 量化與 runtime module volume 896/1024 重新量測。原始參考與 GBA 內建喇叭都是 mono,因此以 L+R fold-down 比對,但 IT 內的 stereo pan 仍完整保留。

  • 保留 TYM event velocity 已包含的 carrier loudness;
  • 全 catalog 只使用一次固定 +3 dB presentation gain;
  • 不做 per-track 或 per-sample master normalization;
  • 8-bit 量化後重新量測,clipping 或缺少 target 會讓 build 失敗。
14.062 dBlegacy mean absolute error
0.160 dBcalibrated mean absolute error
0clipped samples

鼓聲要看 peak,不只看 RMS

短 one-shot 的平均能量很低,若只追 RMS,kick、snare、hi-hat 或 crash 的單一下 peak 可能被推得過高。校準器因此另以原版 peak 的 1.60× 作軟上限;22 個稀疏 percussion sources 寧可略低於 RMS target,也不製造刺耳爆點。

程序鼓聲以 GBA mixer 原生 15,768 Hz 產生,並依類型使用不同 sample length,不再共用同一段 46 ms noise。 每個 one-shot 使用 1.5 ms smoothstep attack 與 5 ms release,並只移除 極低頻 DC;沒有對全部曲目強套 120 Hz high-pass 或 8 kHz shelf, 避免把不同 Tyrian 音色 EQ 成同一種聲音。

原版 SFX 與語音不經手工替代

tyrian.snd 內 29 個一般音效與 voices.snd 內 9 段語音以原始 archive offset 拆出,轉為 11,025 Hz mono 8-bit WAV。 與 OpenTyrian 的 JE_loadSndFile() 相同,每段 voice 都移除 尾端 100-byte 損壞資料,不只處理 Level End。遊戲中的爆炸、打擊、 Cube、Boss 與提示語音仍依 source sound number 呼叫。

極限全武器壓力測試可以選擇把密集 SFX 導向 GBA 原生 DMG square channel,以隔離 sample-mixer voice pressure;這是診斷 profile, 一般遊戲仍使用完整 Maxmod SFX。

循環曲與只播一次的提示曲

Maxmod 的 MM_PLAY_ONCE 只控制 order list 結束後的行為; 原始 IT 內若仍有 Bxx 跳轉,歌曲仍會無限循環。因此 End of Level(09)、Game Over(10)與 Secret Level(30)各保留兩種 module: 一般版供 JukeBox 循環,_once 版只移除 Bxx,讓遊戲流程自然 播到結尾停止。

兩種檔案的 PCM payload 完全相同,Maxmod 也會全域去重相同 sample。 三份有限 order/pattern 只讓 soundbank 增加 4,028 bytes,換來正確的 勝利、死亡與秘密關卡播放語意。

切歌先跨過零點

直接在 Direct Sound FIFO 還含非零波形時 mmStop(), 容易在選單進關卡的一瞬間產生爆音。現行 VBlank 狀態機只淡出背景 module,保留 UI 確認音效:

  1. 18 VBlanks 淡出。選單歌曲平順降到零。
  2. 1 個完整靜音 VBlank。讓 Maxmod 送出靜音 buffer。
  3. 停止並載入場景。切換 module 與關卡資料。
  4. 30 VBlanks 淡入。新關卡歌曲從零開始。

暫停則比照來源邏輯,只把 module volume 降為一半;死亡淡出、勝利與 Game Over 的 natural stop 都由 telemetry 計數,避免日後又退回無限循環。

把聽感變成可重建的證據

Build 會輸出逐曲、逐 source 的 gain、RMS error、peak ceiling、 clipping 與 track mapping 報告;回歸另驗證 41 首/334 個 OPL sources、 三首 finite cues、18 個 mixer slots、場景淡出/靜音/淡入步數、自然 停止次數與所有 source SFX ID。人耳 A/B 仍不可取代,但每次修改不必 再從零猜測「哪個聲道變大」。

離線量測不是宣稱已取得實機 LUFS 真值。它提供固定、可重現的比較基準; 最後仍要在 mGBA 與真實 GBA 輸出上檢查音樂、音效及語音的相對感受。