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>
This commit is contained in:
62
docs/re/structures/sound-slb.md
Normal file
62
docs/re/structures/sound-slb.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# 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…). 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.
|
||||
Reference in New Issue
Block a user