Files
Syplheed-Reborn/docs/re/structures/sound-slb.md
MechaCat02 95de29f290 feat(formats,viewer): movie subtitles, voice decode, and the movie manifest
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>
2026-07-19 20:15:41 +02:00

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.pakeng\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…). Only eng/jpn voice 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/WAVE sits inside the bank; its 32-byte fmt is the real header. The XMA packets are everything after that RIFF's data chunk header (slb[data_pos+8..]); the declared data size 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 RIFF anywhere; 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.