diff --git a/crates/sylpheed-formats/examples/voice_len_vs_subs.rs b/crates/sylpheed-formats/examples/voice_len_vs_subs.rs new file mode 100644 index 0000000..44e19ab --- /dev/null +++ b/crates/sylpheed-formats/examples/voice_len_vs_subs.rs @@ -0,0 +1,78 @@ +//! Is audio actually missing from the resupply banks? Ask the subtitles. +//! +//! The `.slb` decode of `VOICE_D_453` yields 0.14 s, which "looks too short" — +//! but that judgement was an impression. The subtitle track for each movie +//! carries cue times, so it says independently how long the spoken line runs. +//! If the last cue lands near the decoded length, nothing is missing; if it lands +//! far past it, audio really is being lost. +use sylpheed_formats::{movie_subtitle, slb, PakArchive}; + +/// XMA1 at 48 kHz stereo, 16-bit → bytes per second of PCM. +const PCM_BYTES_PER_SEC: f32 = 48000.0 * 2.0 * 2.0; + +/// Decode one sub-wave with FFmpeg and return its length in seconds. Measured, +/// not estimated from a compression ratio — the ratio guess was the first version +/// of this and it is not good enough to hang a conclusion on. +fn decoded_secs(riff: &[u8]) -> f32 { + use std::io::Write; + use std::process::{Command, Stdio}; + let Ok(mut c) = Command::new("ffmpeg") + .args(["-v", "error", "-i", "pipe:0", "-f", "s16le", "pipe:1"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + else { + return f32::NAN; + }; + let buf = riff.to_vec(); + let mut stdin = c.stdin.take().unwrap(); + std::thread::spawn(move || { + let _ = stdin.write_all(&buf); + }); + let out = c.wait_with_output().expect("ffmpeg"); + out.stdout.len() as f32 / PCM_BYTES_PER_SEC +} + +fn main() { + let disc = std::env::var("SYLPHEED_DISC").expect("set SYLPHEED_DISC"); + let snd = PakArchive::open(format!("{disc}/dat/sound.pak")).expect("sound.pak"); + let lang = PakArchive::open(format!("{disc}/dat/movie/eng.pak")).expect("eng.pak"); + + // movie → the bank the record table binds it to. + let bound = [ + ("hokyu_LS_s02A", 450), + ("hokyu_LS_s09A", 451), + ("hokyu_DS_s13A", 452), + ("hokyu_LS_s02H", 453), + ("hokyu_DS_s07H", 454), + ]; + println!( + "{:<18} {:>6} {:>10} {:>12} {:>12}", + "movie", "bank", "last cue s", "decoded s", "verdict" + ); + for (movie, n) in bound { + let cues = movie_subtitle::track_voice_cues(&lang, movie); + let last = cues.iter().map(|(_, t)| *t).fold(0.0f32, f32::max); + let path = format!("eng\\etc\\VOICE_D_{n}.slb"); + let Some(entry) = snd.find_by_name(&path) else { continue }; + let bytes = snd.read(entry).expect("read"); + // Sum the sub-waves' payloads as the decoder currently sees them. + let riffs = slb::to_xma_riffs(&bytes); + let secs: f32 = riffs.iter().map(|r| decoded_secs(r)).sum(); + // One-directional: a subtitle that appears at t seconds cannot sit inside + // a clip shorter than t. A cue at 0.0 tells us nothing either way. + let verdict = if last == 0.0 { + "no cue signal" + } else if last > secs { + "AUDIO MISSING" + } else { + "consistent" + }; + println!( + "{movie:<18} {:>6} {last:>10.2} {secs:>12.2} {verdict:>12} cues={}", + format!("D_{n}"), + cues.len() + ); + } +} diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index d6adc7e..07e9763 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -1057,9 +1057,18 @@ premise was wrong.** Byte coverage was the wrong success metric. The rule also matches **1524 of 8021** RIFF-bearing `sound.pak` entries, including `RT*` banks that work today, so it risked a wide regression to not-fix five banks. - ▶️ Next cheap probe: vary the synthesised `fmt` (channels / streams / sample - rate) instead of assuming the container. Then a human has to listen; audio - judgement cannot be done here. See + ✅ **(same day) Audio really IS missing — proven by the subtitle cue times**, + not by the "sounds too short" impression the docs recorded. A subtitle that + appears at *t* seconds cannot sit inside a clip shorter than *t*, and three of + five banks fail that: `D_450` cue 4.00 s vs 1.41 s decoded, `D_451` 3.70 vs + 1.81, `D_453` **4.70 vs 0.07**. The other two have their only cue at 0.0 s and + give no signal. Artifact `examples/voice_len_vs_subs.rs`, FFmpeg-measured. + ❔ **The fmt-variation probe was INCONCLUSIVE** — 36 combinations over + `VOICE_D_453`'s leading region all produced 0 PCM bytes, *including* ones + equivalent to the crate's working `synth_xma1_fmt`, so the probe tested my + hand-built `fmt` chunk rather than the hypothesis. Not evidence the region is + non-XMA. ▶️ Retry building the chunk with the crate's own helper and varying + its parameters. See [`voice-bank-leading-region.md`](voice-bank-leading-region.md). * ❌ **(2026-08-25) My own boot-nav diagnosis, MEASURED AND WITHDRAWN.** I said the run died because `skip_intro.sh` gates the title test at `rmse <= 1500` diff --git a/docs/re/voice-bank-leading-region.md b/docs/re/voice-bank-leading-region.md index 7b76a2c..66efe2f 100644 --- a/docs/re/voice-bank-leading-region.md +++ b/docs/re/voice-bank-leading-region.md @@ -120,6 +120,43 @@ 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 + +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 +container. The subtitle tracks settle it instead: each carries **cue start +times**, and a subtitle that appears at *t* seconds cannot sit inside a clip +shorter than *t*. + +Decoded with FFmpeg (measured, not estimated from a compression ratio — the +first version of this used an 8:1 guess and that is not good enough to hang a +conclusion on), artifact `examples/voice_len_vs_subs.rs`: + +| movie | bank | last cue | decoded audio | verdict | +|---|---|---|---|---| +| `hokyu_LS_s02A` | `D_450` | 4.00 s | 1.41 s | **audio missing** | +| `hokyu_LS_s09A` | `D_451` | 3.70 s | 1.81 s | **audio missing** | +| `hokyu_LS_s02H` | `D_453` | 4.70 s | **0.07 s** | **audio missing** | +| `hokyu_DS_s13A` | `D_452` | 0.00 s | 1.21 s | no signal | +| `hokyu_DS_s07H` | `D_454` | 0.00 s | 0.21 s | no signal | + +Three of the five are decisive; the other two have their only cue at 0.0 s, which +tells us nothing in either direction. So **something is genuinely missing from +these banks** — independent of anything above, and now measured rather than felt. + +## ❔ Varying the XMA format — inconclusive, and for a boring reason + +The recorded next step was to vary the synthesised `fmt` (channels, streams, +sample rate) rather than assume the container. I tried 36 combinations over +`VOICE_D_453`'s 22-packet leading region and **every one produced 0 PCM bytes** — +including combinations that should be equivalent to the crate's own +`synth_xma1_fmt(2, 2, 48000)`, which does at least parse (it yields 1792 bytes). + +That means the probe tested **my hand-built `fmt` chunk**, not the hypothesis. It +is not evidence that the region is non-XMA. The next attempt should build the +chunk with the crate's own helper and vary its parameters, rather than +hand-rolling the WAVEFORMATEX. + ## What this does not settle * ❔ **What the leading region holds.** Its *size* is now exact (1392 + n·2048)