diff --git a/crates/sylpheed-formats/examples/shared_bank_takes.rs b/crates/sylpheed-formats/examples/shared_bank_takes.rs new file mode 100644 index 00000000..a9b4c754 --- /dev/null +++ b/crates/sylpheed-formats/examples/shared_bank_takes.rs @@ -0,0 +1,58 @@ +//! Does a shared resupply voice bank hold ONE line or SEVERAL takes? +//! +//! This decides whether the `samples/movie` lower bound on the sample rate is +//! real. The test is static and does not need audio judgement: the movies that +//! share a bank each have their OWN subtitle table. If those texts are identical +//! the bank plausibly holds one generic line; if they differ, one bank is +//! serving several distinct spoken lines. +use std::collections::BTreeMap; + +use sylpheed_formats::{movie_subtitle, PakArchive}; + +fn main() { + let disc = std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC"); + let lang = PakArchive::open(format!("{disc}/dat/movie/eng.pak")).expect("eng.pak"); + let text = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("text pak"); + let demo_text = movie_subtitle::build_demo_text(&text); + + // bank → the movies bound to it, from the record table (see movie-subtitle-link). + let banks: BTreeMap<&str, Vec<&str>> = BTreeMap::from([ + ("VOICE_D_450", vec!["hokyu_LS_s02A", "hokyu_LS_s03A", "hokyu_LS_s06A"]), + ( + "VOICE_D_451", + vec!["hokyu_LS_s09A", "hokyu_LS_s11A", "hokyu_LS_s15A", "hokyu_LS_s24A", "hokyu_LS_s27A"], + ), + ("VOICE_D_452", vec!["hokyu_DS_s02A", "hokyu_DS_s07A", "hokyu_DS_s08A", "hokyu_DS_s13A"]), + ("VOICE_D_453", vec!["hokyu_LS_s02H", "hokyu_LS_s03H", "hokyu_LS_s06H", "hokyu_LS_s09H"]), + ("VOICE_D_454", vec!["hokyu_DS_s07H", "hokyu_DS_s14H"]), + ]); + + for (bank, movies) in &banks { + println!("\n== {bank}"); + let mut seen: BTreeMap> = BTreeMap::new(); + for m in movies { + let cues = movie_subtitle::track_voice_cues(&lang, m); + let mut line = String::new(); + for (demo, t) in &cues { + let txt = demo_text + .get(demo) + .map(|v| v.join(" ")) + .unwrap_or_else(|| format!("")); + line.push_str(&format!("[{t:.2}] {txt} ")); + } + if line.is_empty() { + line.push_str(""); + } + seen.entry(line.trim().to_string()).or_default().push(m); + } + for (line, ms) in &seen { + println!(" {:?}", ms); + println!(" {line}"); + } + println!( + " -> {} distinct subtitle text(s) across {} movies", + seen.len(), + movies.len() + ); + } +} diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 9e353111..119bb3a1 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -1033,6 +1033,24 @@ premise was wrong.** Candidates: the **7 `.embsec_` sections** (VAs 0x84D0000–0x86AC000, ~129 KB total, executable) or a hashed record in `hidden/MiscBin.pak`. **Finding it gives the actual per-phase clear condition for every stage.** +* ❌ **(2026-08-26) RETRACTED — "the resupply banks are missing audio."** The + error was mine and it stood for three iterations across two write-ups that each + called the result proven: I treated a subtitle cue as a timestamp that must + fall *inside* the voice clip. **A cue is when the line STARTS** — the voice + plays from the cue to at most the movie's end. Under that reading all five + banks fit at plain **48 kHz** (3.31 s in a 5.30 s window, 2.26 s in 4.60 s, …), + and 2–3.5 s is the right length for the lines. Nothing is missing. The + 17 091–20 563 Hz window from the previous entry is void with it. + ✅ **Each shared bank is ONE generic line** — the 3–5 movies sharing a bank have + **identical subtitle text**, 5/5 banks (`examples/shared_bank_takes.rs`). + 🎯 **That also explains the historical in-game rejection** of + `hokyu_DS_s13A → VOICE_D_452`: the line is the generic "Resupply complete. You + are cleared for take-off!", the same for s02A/s07A/s08A/s13A. Someone expecting + a stage-13-specific line would call the generic one wrong while the binding is + right. + ❔ Still open: whether the leading XMA1-mono region is *additional* audio or an + alternate take of the same line — the totals above add it to the RIFF sub-waves + and would double-count if it is a duplicate. * ❌ **(2026-08-25) The `.slb` "multi-subwave" guess is REFUTED, and the voice decoder is discarding up to 87 % of a bank.** The record table gives a **direct** binding `hokyu_DS_s13A -> VOICE_D_452` where the corpus records the diff --git a/docs/re/voice-bank-leading-region.md b/docs/re/voice-bank-leading-region.md index 3db8f5c0..20779825 100644 --- a/docs/re/voice-bank-leading-region.md +++ b/docs/re/voice-bank-leading-region.md @@ -1,9 +1,11 @@ # The resupply voice banks — the decoder discards up to 87 % of them Status: ❌ the recorded "multi-subwave / not cleanly sliced" explanation is -**REFUTED**. ✅ the real defect is measured. 🟡 the in-game verdict that rejected -the `hokyu_DS_s13A` voice binding is therefore **not** evidence against it. ❔ the -binding still needs audio verification, which needs a human. +**REFUTED**. ✅ each shared bank is **one generic line**, which explains the +in-game verdict that rejected the `hokyu_DS_s13A` binding — the line really is +generic. ❌ **my own "audio is missing" conclusion is RETRACTED**: a subtitle cue +is a START time, not a point inside the clip, and under the correct reading every +bank fits at ordinary 48 kHz. Artifact: `crates/sylpheed-formats/examples/voice_bank_shape.rs`. @@ -120,7 +122,39 @@ it.** Two measurements killed it: The refuted attempt is recorded in `slb.rs` beside the code, so the next person does not re-derive the arithmetic and re-make the same change. -## ✅ Audio really is missing — proven by the subtitles, not by impression +## ❌ RETRACTED — "audio is missing". The cue is a START time. + +**This is the load-bearing error of this whole file, and it is mine.** I treated +a subtitle cue as a timestamp that must fall *inside* the voice clip, and +concluded that a 0.07 s clip could not host a cue at 4.70 s. + +A cue is when the line **starts**. The voice plays *from* the cue, so the clip +only has to fit in the window between the cue and the end of the movie. Under +that reading every bank fits comfortably — at plain **48 kHz**: + +| bank | samples | at 48 kHz | cue | window to movie end | fits | +|---|---|---|---|---|---| +| `VOICE_D_450` | 158 967 | 3.31 s | 4.00 s | 5.30 s | ✅ | +| `VOICE_D_451` | 76 084 | 1.59 s | 3.70 s | 5.60 s | ✅ | +| `VOICE_D_452` | 119 562 | 2.49 s | 0.00 s | 8.34 s | ✅ | +| `VOICE_D_453` | 108 608 | 2.26 s | 4.70 s | 4.60 s | ✅ | +| `VOICE_D_454` | 167 828 | 3.50 s | 0.00 s | 9.50 s | ✅ | + +2–3.5 s is also the right length for the lines themselves ("Rhino 3 has landed. +Commencing resupply."). So there is **no evidence any audio is missing**, and the +sample rate is plausibly the ordinary 48 kHz. + +❌ The **17 091 – 20 563 Hz window** from the previous entry is void with it: its +lower bound came from the same misreading. + +What survives from that work: the leading region is XMA1 mono, the decode runs to +the final frame, and the cue values really are seconds. Those were measured. Only +the interpretation of what a cue *means* was wrong — and it was wrong for three +iterations, through two write-ups that each called the result "proven". + +### The original argument, kept + +Below is what I wrote when I believed a cue had to fall inside the clip. The corpus's original wording was that 0.14 s is "far too short for the spoken line". That is a judgement, and judgements about audio cannot be made in this @@ -144,6 +178,30 @@ Three of the five are decisive; the other two have their only cue at 0.0 s, whic tells us nothing in either direction. So **something is genuinely missing from these banks** — independent of anything above, and now measured rather than felt. +## ✅ Each shared bank is ONE generic line + +The banks are bound to 3–5 movie slots each, and the movies sharing a bank have +**identical subtitle text** — 5 banks out of 5 (`examples/shared_bank_takes.rs`): + +| bank | movies | the line | +|---|---|---| +| `VOICE_D_450` | 3 | "Rhino 3 has landed. Commencing resupply." | +| `VOICE_D_451` | 3 (+2 with no cue) | "Rhino Leader has landed. Commencing resupply." | +| `VOICE_D_452` | 4 | "Resupply complete. You are cleared for take-off!" | +| `VOICE_D_453` | 4 | "All right, beginning resupply." | +| `VOICE_D_454` | 2 | "Resupply complete. Switching to take-off sequence." | + +So a bank holds one line, reused across stages — not several takes. + +### 🎯 And this explains the historical in-game rejection + +`hokyu_DS_s13A → VOICE_D_452` was rejected as "the wrong recording". But +`VOICE_D_452`'s line is the **generic** "Resupply complete. You are cleared for +take-off!", identical for `s02A`, `s07A`, `s08A` *and* `s13A`. Someone expecting +a stage-13-specific line would hear the generic one and call it wrong — while the +binding is exactly right. The disc says so, and now the subtitle text says so +independently. + ## ✅ The leading region IS XMA1 — but **mono**, not stereo Retried with the `fmt ` chunk built to `slb::synth_xma1_fmt`'s exact byte layout