diff --git a/crates/sylpheed-formats/examples/cue_unit_check.rs b/crates/sylpheed-formats/examples/cue_unit_check.rs new file mode 100644 index 0000000..1b8c7aa --- /dev/null +++ b/crates/sylpheed-formats/examples/cue_unit_check.rs @@ -0,0 +1,59 @@ +//! Are subtitle cue times really seconds? +//! +//! Every "audio missing" verdict in voice-bank-leading-region.md rests on +//! reading `track_voice_cues`' `f32` as seconds. `parse_timing` does compute +//! `mm*60 + ss` — but only if the token really is `mm:ss.cc`. The movies are on +//! the disc, so their true length is an independent oracle: a cue must land +//! INSIDE its own movie. +use std::process::Command; + +use sylpheed_formats::{movie_subtitle, PakArchive}; + +fn movie_secs(path: &str) -> Option { + let out = Command::new("ffprobe") + .args([ + "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path, + ]) + .output() + .ok()?; + String::from_utf8_lossy(&out.stdout).trim().parse().ok() +} + +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 mut checked = 0; + let mut over = 0; + let mut worst: Vec<(String, f32, f32)> = Vec::new(); + let dir = format!("{disc}/dat/movie"); + let Ok(rd) = std::fs::read_dir(&dir) else { return }; + for e in rd.flatten() { + let p = e.path(); + if p.extension().is_none_or(|x| x != "wmv") { + continue; + } + let base = p.file_stem().unwrap().to_string_lossy().to_string(); + let cues = movie_subtitle::track_voice_cues(&lang, &base); + if cues.is_empty() { + continue; + } + let last = cues.iter().map(|(_, t)| *t).fold(0.0f32, f32::max); + let Some(secs) = movie_secs(&p.to_string_lossy()) else { continue }; + checked += 1; + if last > secs { + over += 1; + worst.push((base, last, secs)); + } + } + worst.sort_by(|a, b| (b.1 - b.2).partial_cmp(&(a.1 - a.2)).unwrap()); + println!("movies with cues checked: {checked}"); + println!("cues landing AFTER the movie ends: {over}"); + for (m, last, secs) in worst.iter().take(8) { + println!(" {m:<28} last cue {last:>8.2}s movie {secs:>8.2}s"); + } + println!( + "\nIf cues were centiseconds the last cue would be ~100x too big and \ + essentially all {checked} would overflow." + ); +} diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index c78af43..9e35311 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -1085,12 +1085,21 @@ premise was wrong.** after counting leading region + RIFF together: 39 742 / 20 563 / 23 108 Hz for the three banks with a usable cue. Two of them agreed at a tidy ~2.1× ratio and the third refuted it. - 🟡 **▶️ Check the OTHER side of the comparison first.** - `movie_subtitle::track_voice_cues` returns `(u32, f32)` and I have been reading - that `f32` as seconds. If it is centiseconds, a frame index, or a per-page - offset, every "audio missing" verdict inherits the error. Cheap to settle - against a movie of independently known length, and it should be settled before - any further audio work. See + ✅ **(2026-08-26) The cue unit IS seconds** — checked against movie duration as + an independent oracle: **66 movies with subtitle tracks, 0 cues land after + their movie ends** (`examples/cue_unit_check.rs`). Centiseconds would have + overflowed essentially all 66. + ❌ **And "the sample rate does not converge" was my own error.** The implied + rates are not competing point estimates — each is a **one-sided bound** + (`samples/cue` is an UPPER bound, `samples/movie` a LOWER one). Intersected, + they give a **non-empty window of 17 091 – 20 563 Hz**: a single rate *is* + consistent with all three banks. + ❔ But that window holds **no standard XMA rate** (22 050 / 24 000 / 32 000 / + 44 100 / 48 000 all fall outside). The lower bound assumes the whole bank plays + within one movie, and each bank is bound to **3–5 movie slots** — so if a bank + holds several takes the lower bound is void, leaving `rate <= 20 563`, which + 22 050 nearly meets. ▶️ Next: establish whether a shared bank is one line or + several takes. 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 4539345..3db8f5c 100644 --- a/docs/re/voice-bank-leading-region.md +++ b/docs/re/voice-bank-leading-region.md @@ -225,18 +225,57 @@ refutes that too: So: the container is identified, the decode is essentially complete, and the duration still does not reconcile. +## ✅ The cue unit is seconds — checked, not assumed + +`parse_timing` computes `mm*60 + ss`, but only if the token really is `mm:ss.cc`. +The movies are on the disc, so their duration is an independent oracle: a cue +must land **inside its own movie**. Over every English movie with a subtitle +track — **66 movies, 0 cues land after the movie ends** +(`examples/cue_unit_check.rs`). Centiseconds would have overflowed essentially +all 66. So the seconds reading stands and the verdicts built on it survive. + +## ❌ "The sample rate does not converge" — that was MY error, twice over + +I reported implied rates of 39 742 / 20 563 / 23 108 Hz and called them +irreconcilable. They are not, because they are not estimates of the same +quantity — each is a **one-sided bound**: + +* the audio must be at least as long as the last cue → `samples / cue` is an + **upper** bound on the rate; +* the audio cannot outlast its movie → `samples / movie` is a **lower** bound. + +| bank | samples | cue | movie | lower (Hz) | upper (Hz) | +|---|---|---|---|---|---| +| `VOICE_D_450` | 158 967 | 4.00 s | 9.30 s | 17 091 | 39 742 | +| `VOICE_D_451` | 76 084 | 3.70 s | 9.30 s | 8 180 | 20 563 | +| `VOICE_D_453` | 108 608 | 4.70 s | 9.30 s | 11 677 | 23 108 | + +Intersecting them gives **17 091 – 20 563 Hz**, a non-empty window. A single +sample rate *is* consistent with all three banks. I had been comparing the +numbers as if they were competing point estimates. + +## ❔ But the window contains no standard rate + +Nothing in 17 091 – 20 563 Hz is a normal XMA sample rate (22 050, 24 000, +32 000, 44 100, 48 000 all sit outside). Two things could move it, and neither +is settled: + +* **The lower bound assumes the whole bank plays inside one movie.** Each of + these banks is bound to **3–5 different movie slots**, so if it holds several + takes rather than one line, its total length has no reason to fit one movie and + the lower bound is void — leaving only `rate ≤ 20 563`, which 22 050 nearly + satisfies. +* The final frame of each stream is lost to the decoder, but that is ~512 + samples and cannot move a bound by 10 %. + +So the honest state is: container identified, decode essentially complete, cue +unit verified, and the rate constrained but **not** determined. + ## 🟡 Where the tension probably is now -The audio side has stopped looking suspect: mono XMA1, decoded to the last -frame. The link I have *not* checked is the other one — `movie_subtitle:: -track_voice_cues` returns `(u32, f32)`, and I have been reading that `f32` as -seconds because the format notes describe `` cue text. If it is -anything else — centiseconds, a frame index, a per-page offset — every "audio -missing" verdict above inherits the error. - -**That is the next thing to check, and it should be checked before any more work -on the audio.** It is cheap: a cue on a movie whose length is known independently -would settle the unit. +Checked and cleared: the cue unit is seconds (above). What is left is whether a +**shared** bank holds one line or several takes — that decides whether the +lower bound above is real, and it is the thing to establish next. ## ❔ The first attempt at this — inconclusive, and for a boring reason