The movie cutscene subtitle + voice pipeline, driven by the ADVERTISE_MOVIE manifest (the authoritative movie -> subtitle -> voice index). Also flushes several sessions of local WIP (async viewer loading, grouped-pool XBG7/hero-ship decode, drawlog tooling). See docs/HANDOFF-movie-voice-subtitles-2026-07-19.md. Subtitles (movie_subtitle.rs): - Full movie->track->text chain; join multi-line captions sharing one timing (fixes S13A dropped "Look at it father" line); overlap-safe active_cues(); Latin-1 accents preserved. Voice (slb.rs): XACT .slb -> XMA1 RIFF; take the FIRST sub-wave bounded by its declared data size (fixes S10-S16 alternate-take garble); list_voice_clips. Manifest (movie_manifest.rs): parse ADVERTISE_MOVIE (0x5B983A08) for the real movie->voice binding (not always VOICE_<movie>; e.g. hokyu -> VOICE_D_* in etc\). Resolve the token's sound.pak path via sounds.tbl. DIRECT bindings only — the demo-id shared-clip fallback for unbound hokyu movies was verified WRONG in-game and reverted (unbound hokyu stay unvoiced; correct join key is an OPEN problem). Viewer: manifest-driven voice (movie player toggle + solo button), standalone "Voice Lines" browser, stacked caption overlay. Tests: 46 formats-lib + 11 viewer-lib + movie_manifest/movie_subtitle/slb disc tests; full workspace green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.0 KiB
Sound bank audio — sound.pak / .slb / XMA1
Reverse-engineered 2026-07-19 (static). dat/sound.pak (+ sound.p00..p04,
~1.08 GB) is the game's entire audio bank: 9519 IPFB entries, each an XACT
.slb sound bank wrapping XMA1 audio.
Names
Entry keys are name_hash(<string>) of filenames stored verbatim in the IDXD
string pools of tables.pak → eng\sounds.tbl (entry 39) and jpn\sounds.tbl
(entry 45). All 9519 recovered. Families:
<lang>\Voice\VOICE_<SPK>_<NNN>.slb— per-line character voice (SPK = ADAN, TCAF, RHIN, ADPL, BIRD, ACRO, ZZZZ…). Onlyeng/jpnvoice exists.<lang>\etc\VOICE_{A,B,C,D}_<NNN>.slb,<lang>\Briefing\BR<n>_<m>.slb.<lang>\Movie\VOICE_<movie>.slb— the continuous voice track for cutscene<movie>.wmv(e.g.eng\Movie\VOICE_RT07A.slb). Played from the video start.BGM_###.slb(32) — music.Static.slb,JNGL_###— SFX/ambience.
Only English and Japanese have voice; subtitles cover more languages, voice does
not. See [crate::slb::movie_voice_name].
Codec
XMA1: RIFF fmt tag 0x0165, a 32-byte XMAWAVEFORMAT, 48000 Hz. The
content is mono — some clips carry it in the left channel only (right is
silence), others are dual-mono (L = R). Downmix to mono by taking the left
channel (it always holds the full signal).
XMAWAVEFORMAT (one stream, 32-byte fmt ): SampleRate @ fmt+16 (u32 LE),
Channels @ fmt+29 (u8), ChannelMask @ fmt+30 (u16). Movie voices report 2
channels / mask 0x2 despite the mono content.
.slb layouts → decodable RIFF
Two on-disc layouts (sound.pak movie voices split ≈ 63 RIFF / 15 headerless):
- RIFF present — a
RIFF/WAVEsits inside the bank; its 32-bytefmtis the real header. The XMA packets are everything after that RIFF'sdatachunk header (slb[data_pos+8..]); the declareddatasize is unreliable (sometimes half the real length), so take to end and clamp to the media length downstream. Some banks have trailing bank data past the real audio — always beyond the video length, so clamping to the video duration drops it. - Headerless — no
RIFFanywhere; a fixed 1392-byte header precedes raw XMA1 packets (48 kHz, 2 channels). Data =slb[1392..].
[crate::slb::to_xma_riff] rebuilds a standalone XMA1 RIFF/WAVE for either
layout, ready for FFmpeg's xma1 decoder.
Decode (FFmpeg)
ffmpeg -i rebuilt.riff -af "pan=mono|c0=c0" -t <media_len> out.wav — the RIFF
is fed to the xma1 decoder, downmixed to the left channel, clamped to length.
Verified: VOICE_S00A → 93.9 s == S00A.wmv 93.87 s; VOICE_ADV → 137.3 s ==
137.7 s; VOICE_RT07A → 49 s ≈ 50 s.
Reading one entry cheaply
sound.pak is ~1 GB. Use [crate::PakArchive::parse_toc] on the small .pak
index, binary-search the name-hash, then read just [offset, offset+size) from
the .pNN segments (the viewer's read_segment_range seeks into the right
segment) instead of loading the whole archive.