feat(a2b): audio — wire play-bgm/play-voice, fix BGM addressing
Wire audio end-to-end (OGG plays natively in Godot; no Frida, no decode):
- IHost.PlayBgm/PlayVoice + VM dispatch for play-bgm(0xbf)/play-voice(0xc4).
Non-Godot hosts no-op them so --selftest + engine 8/8 stay byte-identical.
- GodotAdvHost resolves id->OGG; Main plays via two AudioStreamPlayer nodes
(BGM looping; voice interrupt-on-new).
Key finding (by-ear, systematic-debugging): the two audio ops use DIFFERENT
addressing — the earlier "unified manifest" assumption was wrong for BGM.
- play-voice -> per-scene manifest files[base+id] (offset 0, same as textures).
Confirmed by ear; upgraded med->HIGH.
- play-bgm -> DIRECT LITERAL NAME BGM{id:03d}.OGG, NOT the manifest.
Real game: play-bgm 5->BGM005, 8->BGM008 (manifest gave +1). Proven by
play-bgm 0x23->BGM035 (real standalone track; BGM set skips 030-034) that the
manifest mis-resolved to a graphics entry. Fix is BGM-only:
ResourceMap.BgmPathById; voices/textures unchanged.
Also: Lily's silence root-caused as correct form-gating (G[0xa57/0xa58/0xa59]),
left unseeded by choice (no dummy state). Added diagnostic
`Age.Cli audio <SCENE.BIN> [0xADDR=VAL ...]`. Corrected opcodes.toml (play-bgm
direct-name, play-voice HIGH) + docs/memory (dropped the bogus unified-manifest
/ Frida-BGM006 claims).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,8 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
`set-texture 0x1f9` `(resId,slot)`, `draw-texture 0x1fb` `(slot,x,y,w,h)` promoted from VM stubs to
|
||||
typed `IHost` methods; `CaptureHost` no-ops them (A1 trace-diff/A2a selftest stay green). The VM
|
||||
now *drives* graphics; only resolution + backend rendering remain.
|
||||
- **Audio ops named, not yet wired:** `play-voice 0xc4`, `play-bgm`.
|
||||
- **Audio ops WIRED (2026-07-06):** `play-bgm 0xbf` / `play-voice 0xc4` → `IHost.PlayBgm/PlayVoice` →
|
||||
same resolver → OGG via Godot `AudioStreamPlayer`. See step 4 below.
|
||||
- **Tools:** `tools/convert_agf.py` (AGF→BMP for *stills* via `AGF2BMP2AGF.exe`); `tools/frida/`
|
||||
(runtime capture harness — see its README); Frida core installed (17.15.3).
|
||||
|
||||
@@ -63,13 +64,15 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
> **`resId → files[ section_base(scene) + resId ]`**, where `section_base` = the start of the SYS4INI
|
||||
> section containing the scene's `SCxxxx.BIN`.
|
||||
|
||||
Unified for `set-texture(resId)`, `play-bgm(id)`, `play-voice(id)` — one manifest. **Tool:**
|
||||
`tools/resolve_asset.py --build` → `build/asset-sections.json` (359 sections, 136 scenes);
|
||||
`resolve_asset.py <SCENE> [resId]` resolves. **Validated:** `file_number == position − section_base`
|
||||
for 12848/13206 files (97%); SC0000 resolves 17/17 across archives vs the Frida capture (`0x25→EV052CA`,
|
||||
`0x36→BG030A` background, `0x6c→EM* effect`, `play-bgm 5→BGM006`); 586/595 distinct captured loads
|
||||
Manifest rule holds for `set-texture(resId)` and `play-voice(id)`. **⚠ `play-bgm` is the EXCEPTION —
|
||||
it does NOT use the manifest; it uses direct literal names `BGM{id:03d}.OGG` (see step 4, by-ear
|
||||
corrected 2026-07-06).** **Tool:** `tools/resolve_asset.py --build` → `build/asset-sections.json`
|
||||
(359 sections, 136 scenes); `resolve_asset.py <SCENE> [resId]` resolves. **Validated:** `file_number ==
|
||||
position − section_base` for 12848/13206 files (97%); SC0000 resolves 17/17 across archives vs the Frida
|
||||
capture (`0x25→EV052CA`, `0x36→BG030A` background, `0x6c→EM* effect`); 586/595 distinct captured loads
|
||||
(all sections) satisfy `files[base+fn]==name`. This is the derivable rule that generalizes to any
|
||||
AGE game with the same container — **the "scope" was just which SYS4INI section the scene lives in.**
|
||||
(The old `play-bgm 5→BGM006` validation point was a mis-attribution — the real game plays BGM005.)
|
||||
|
||||
*How we got here (condensed):* first confirmed `resId == file_number` via Frida load-order correlation
|
||||
for SC0000's opening, but `file_number` is not globally unique so a per-scene "scope" was needed. A long
|
||||
@@ -90,9 +93,26 @@ highest-risk area of the port. This doc is the steering state; it feeds the A2b
|
||||
get-texture-size + the sprite position/animation chain); fades (`AE*`) draw opaque (no alpha); slot
|
||||
model approximates the game's immediate-mode blit-onto-slot-0 canvas. See `docs/phase-a-slice-plan.md`
|
||||
(A2b section) for the full write-up + the graphics-subsystem plan.
|
||||
4. **Audio** (parallel, same shape): resolve `play-voice`/`play-bgm` `id → OGG` via SYS4INI + a
|
||||
Frida audio capture (hook `DATA3.ALF` reads or the audio-play fn); play via Godot. Reuses the
|
||||
`tools/frida/` framework.
|
||||
4. **Audio.** **✅ WIRED (2026-07-06) — no Frida needed.** Same rule as textures:
|
||||
`play-bgm(id)`/`play-voice(id)` → `files[section_base(scene)+id]` → OGG. `IHost.PlayBgm/PlayVoice` +
|
||||
VM dispatch (`play-bgm` 0xbf / `play-voice` 0xc4, both argc 1); `ResourceMap.AudioPath` → loose
|
||||
`extracted/DATA{n}/{name}.OGG`; `GodotAdvHost` → `Main`'s two `AudioStreamPlayer` nodes
|
||||
(`AudioStreamOggVorbis.LoadFromBuffer`; BGM loops, voice interrupt-on-new). Non-Godot hosts no-op it
|
||||
→ `--selftest`/8-8 byte-identical. SC0000 fires 18 BGM + 198 voice. **By-ear VALIDATED
|
||||
(2026-07-06):** voices play on their lines (`play-voice` med→HIGH). **BUT the two audio ops use DIFFERENT
|
||||
addressing — the earlier "unified graphics+audio manifest" claim was WRONG for BGM:**
|
||||
- **Voice** (`play-voice`) → per-scene manifest, `files[base+id]`, **offset 0** (same as textures). Proven:
|
||||
the manifest interleaves graphics/voice (`files[35]=EV049AA`, `[36]=MAN999`, `[37]=EV052CA`, `[38]=SYL0001`),
|
||||
so `id-1` would land voices on `.AGF` (silent) — they play, so offset is exactly 0.
|
||||
- **BGM** (`play-bgm`) → **DIRECT LITERAL NAME**, `id → BGM{id:03d}.OGG` (DATA3), NOT the manifest.
|
||||
Confirmed by ear (`play-bgm 5→BGM005`, `8→BGM008`; the manifest gave BGM006/009 = off-by-one) and proven
|
||||
by `play-bgm 0x23→BGM035.OGG` — a real standalone track (BGM set skips 030-034) the manifest mis-resolved
|
||||
to a graphics entry. Implemented as `ResourceMap.BgmPathById(id)`; `GodotAdvHost.PlayBgm` uses it.
|
||||
The prior "Frida-confirmed play-bgm 5→BGM006" record was a mis-attribution.
|
||||
|
||||
Lily silent = correct (form-gated on `G[0xa57/0xa58/0xa59]`, unseeded). `play-sound-effect` (0xb4, argc 2)
|
||||
left stubbed — arg roles unconfirmed. See `docs/phase-a-slice-plan.md` (A2b-Audio). Diagnostic: `Age.Cli
|
||||
audio <SCENE>`.
|
||||
5. **Movies** (`OP`/`MVB`, MPEG) — a separate video-playback path; deferred.
|
||||
|
||||
## Validation reality (why this is the big haul)
|
||||
@@ -111,5 +131,5 @@ files[section_base(scene) + resId]`** via SYS4INI per-scene sections (`tools/res
|
||||
render** (ResourceMap + GodotAdvHost texture ops → TextureRect compositing): the full-screen event-CG
|
||||
layer renders end-to-end from the bytecode. Remaining (next chunk): the **graphics geometry/blend
|
||||
subsystem** — native geometry ops (`0x208` + sprite position/animation) so sprites/`BG*` position, plus
|
||||
alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4) uses the *same*
|
||||
resolver (`play-bgm/play-voice id → files[base+id]`).
|
||||
alpha/blend for fades + chromakey. See `docs/phase-a-slice-plan.md` (A2b). Audio (step 4): **`play-voice`
|
||||
uses the manifest** (`files[base+id]`); **`play-bgm` uses direct names** (`BGM{id:03d}.OGG`) — NOT unified.
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
- **evidence:** confirm via frida
|
||||
|
||||
### 0xbf `play-bgm` (play-bgm, argc 1)
|
||||
- **summary:** Play background music by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (OGG in DATA3). Same resolution as set-texture.
|
||||
- **grounding:** source=frida, confidence=high
|
||||
- **evidence:** Frida capture: `play-bgm 0x5` in SC0000 (section base 0) loaded BGM006.OGG = files[5]. Unified with set-texture resolution rule.
|
||||
- **summary:** Play background music by id. BGM is addressed by DIRECT LITERAL NAME: id -> BGM{id:03d}.OGG (in DATA3), NOT the per-scene section manifest (that's voices/textures). E.g. play-bgm 5 -> BGM005.
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** By-ear confirmed (2026-07-06): SC0000 real game plays BGM005 for play-bgm 0x5 and BGM008 for play-bgm 0x8 (we initially mis-played BGM006/BGM009 via the manifest = off-by-one). Direct-name proven by play-bgm 0x23 -> BGM035.OGG, a real standalone track (BGM set skips 030-034) that the manifest mis-resolved to a graphics entry (EV049AA.AGF). CORRECTS the earlier 'unified manifest / Frida BGM006' claim, which was wrong by one. Voices/textures still use the manifest (files[base+id], offset 0). Diagnostic: `Age.Cli audio SC0000.BIN`.
|
||||
|
||||
### 0xc4 `play-voice` (play-voice, argc 1)
|
||||
- **summary:** Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). Same rule as set-texture/play-bgm.
|
||||
- **grounding:** source=investigation, confidence=med
|
||||
- **evidence:** Section-manifest resolution validated across archives incl. DATA4 voice OGGs (586/595 captured loads); per-clip id->OGG not individually Frida-pinned yet.
|
||||
- **summary:** Play a voice clip by id; id resolves via the SYS4INI section manifest -> files[section_base(scene)+id] (voice OGG in DATA1/DATA4). Same rule as set-texture (NOT play-bgm, which is direct-name BGM{id:03d}).
|
||||
- **grounding:** source=investigation, confidence=high
|
||||
- **evidence:** By-ear confirmed (2026-07-06): SC0000 prologue voices play on their lines via Godot AudioStreamPlayer. Off-by-one disproven structurally: manifest interleaves graphics/voice (files[35]=EV049AA, [36]=MAN999, [37]=EV052CA, [38]=SYL0001), so files[base+id] lands voices on OGGs while files[base+id-1] would land them on .AGF graphics (silent) -- and they play, so the offset is exactly 0. Lily's lines are correctly form-gated (G[0xa57/0xa58/0xa59]) and stay silent when no form flag is seeded -- not a bug.
|
||||
|
||||
## compute
|
||||
|
||||
|
||||
@@ -210,6 +210,41 @@ dims) + the sprite position/registration ops so geometry is correct; add alpha/a
|
||||
green chromakey; likely move to a proper canvas/blit compositor. Fixes sprites, background placement, and
|
||||
fades together. (Superseded: the id-specific plan in `docs/superpowers/plans/2026-07-06-a2b-background.md`.)
|
||||
|
||||
### A2b-Audio — WIRED, plays end-to-end (2026-07-06)
|
||||
Audio wired, OGG plays natively in Godot (no Frida, no decode/geometry work). **KEY FINDING — the two audio
|
||||
ops use DIFFERENT addressing (the initial "unified manifest" assumption was WRONG for BGM):**
|
||||
- **`play-voice`** → per-scene manifest `files[base+id]`, **offset 0** (same as `set-texture`).
|
||||
- **`play-bgm`** → **DIRECT LITERAL NAME** `id → BGM{id:03d}.OGG` (DATA3), NOT the manifest.
|
||||
|
||||
**Shipped:** `IHost.PlayBgm/PlayVoice`; VM dispatch routes `play-bgm`(0xbf)/`play-voice`(0xc4) (both argc 1);
|
||||
the three non-Godot hosts (`CaptureHost`, test `RecHost`/`CountHost`) no-op them so `--selftest` + engine 8/8
|
||||
stay byte-identical (audio ops still `pc+1`, step count unchanged); `ResourceMap.BgmPathById(id)` (direct
|
||||
name) for BGM + `ResourceMap.AudioPath(AssetEntry)` (manifest `Resolve`) for voice; `Main` loads via
|
||||
`AudioStreamOggVorbis.LoadFromBuffer` into two `AudioStreamPlayer` nodes (BGM `Loop=true`; voice `Loop=false`,
|
||||
interrupt-on-new). Headless run: 0 OGG-load failures, selftest byte-parity OK.
|
||||
|
||||
**BY-EAR VALIDATED (2026-07-06, systematic-debugging).** User confirmed voices play on their lines
|
||||
(`play-voice` med→HIGH). Two reports root-caused:
|
||||
- **BGM off-by-one → FIXED (real root cause, resolver changed for BGM only).** Real game plays BGM005 for
|
||||
`play-bgm 0x5` and BGM008 for `0x8`; we mis-played BGM006/009 because we resolved BGM via the manifest
|
||||
(`files[5]=BGM006`). BGM is actually addressed by **direct name** `BGM{id:03d}.OGG`. **Proof:** `play-bgm
|
||||
0x23 → BGM035.OGG`, a real standalone track (the BGM set skips 030-034) that the manifest mis-resolved to a
|
||||
graphics entry (`files[35]=EV049AA.AGF`). Voices are NOT off-by-one — the manifest interleaves graphics/
|
||||
voice (`files[35]=EV049AA`, `[36]=MAN999`, `[37]=EV052CA`, `[38]=SYL0001`), so `id-1` would land voices on
|
||||
`.AGF` (silent) but they play ⇒ voice offset is exactly 0. So the fix is BGM-specific; voices/textures
|
||||
unchanged. Corrects the earlier "Frida-confirmed play-bgm 5→BGM006" record (a mis-attribution).
|
||||
- **Lily silent = correct, form-gated (NOT a bug).** Her lines use a 3-way dispatch on form flags
|
||||
`G[0xa57]`(A)/`G[0xa58]`(B)/`G[0xa59]`(C): exactly one is 1 in the real game (her current form), else the
|
||||
line `jmp`s past with no voice. Our harness seeds no globals → all zero → every Lily line skipped. Proven
|
||||
by seeding: `audio SC0000.BIN 0xa57=1` → 35 LILA clips fire in order (form B→LILB, C→LILC). Left unseeded
|
||||
by user choice (no dummy state); Lily stays silent until real cross-scene state flow (Phase B) exists.
|
||||
|
||||
**Diagnostic tool added:** `Age.Cli audio <SCENE.BIN> [0xADDR=VAL ...]` — runs a scene and dumps executed
|
||||
`play-bgm`/`play-voice` ops in order (BGM direct-name, voice manifest), optional global seeding. Used for all
|
||||
of the above. **Watch-items:** BGM looping is whole-file for now (Eushully OGGs may carry `LOOPSTART`/
|
||||
`LOOPLENGTH` Vorbis comments — refine later); `play-sound-effect`(0xb4, argc 2) left stubbed (arg roles
|
||||
unconfirmed).
|
||||
|
||||
---
|
||||
|
||||
## Risks / open questions for A0
|
||||
|
||||
Reference in New Issue
Block a user