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:
gamer147
2026-07-06 22:26:54 -04:00
parent 10f6656c2d
commit ee70beb93d
13 changed files with 206 additions and 24 deletions

View File

@@ -53,4 +53,19 @@ public sealed class GodotAdvHost : IHost
if (_slotBmp.TryGetValue(slot, out var bmp) && bmp != null)
_main.CallDeferred("DrawSlot", slot, bmp, dstX, dstY, width, height);
}
// ---- audio ops (OGG plays natively in Godot) ----
// BGM: addressed by direct name (BGM{id:D3}.OGG), NOT the manifest. Voice: via the per-scene manifest.
public void PlayBgm(long id)
{
var path = _res.BgmPathById(id);
if (path != null) _main.CallDeferred("PlayBgm", path);
}
public void PlayVoice(long id)
{
var asset = _res.Resolve(_scene, id);
var path = asset != null ? ResourceMap.AudioPath(asset) : null;
if (path != null) _main.CallDeferred("PlayVoice", path);
}
}

View File

@@ -11,6 +11,8 @@ public partial class Main : Godot.Control
private readonly Dictionary<int, TextureRect> _slots = new();
private Label _text = null!;
private Label _status = null!;
private AudioStreamPlayer _bgm = null!; // looping background music
private AudioStreamPlayer _voice = null!; // interrupt-on-new voice
private VirtualMachine _vm = null!;
private GodotAdvHost _host = null!;
private volatile bool _done;
@@ -50,6 +52,11 @@ public partial class Main : Godot.Control
catch { /* fall back to the default font */ }
}
_bgm = new AudioStreamPlayer();
_voice = new AudioStreamPlayer();
AddChild(_bgm);
AddChild(_voice);
_selftest = System.Array.IndexOf(OS.GetCmdlineUserArgs(), "--selftest") >= 0;
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
@@ -109,6 +116,25 @@ public partial class Main : Godot.Control
tr.Visible = true;
}
// Load an OGG off disk and play it. BGM loops; voice plays once, cutting off any prior line.
public void PlayBgm(string oggPath)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(System.IO.File.ReadAllBytes(oggPath));
if (stream == null) { GD.Print($"OGG load failed {oggPath}"); return; }
stream.Loop = true;
_bgm.Stream = stream;
_bgm.Play();
}
public void PlayVoice(string oggPath)
{
var stream = AudioStreamOggVorbis.LoadFromBuffer(System.IO.File.ReadAllBytes(oggPath));
if (stream == null) { GD.Print($"OGG load failed {oggPath}"); return; }
stream.Loop = false;
_voice.Stream = stream;
_voice.Play();
}
public void AppendLine(string text) => _text.Text += text + "\n";
public void PageBreak() => _status.Text = "▼ click / Enter";
public void ClearPage() { _text.Text = ""; _status.Text = ""; }