diff --git a/crates/sylpheed-formats/examples/adv_voice_dump.rs b/crates/sylpheed-formats/examples/adv_voice_dump.rs new file mode 100644 index 00000000..fd68936b --- /dev/null +++ b/crates/sylpheed-formats/examples/adv_voice_dump.rs @@ -0,0 +1,41 @@ +//! Dump `ADV`'s voice chunks as RIFF/XMA, so each can be decoded and identified. +//! +//! `intro-audio-decomposed.md` measured that the intro's output is the movie's own +//! WMA Pro 5.1 track at 0.600 **plus** three streams occupying a front pair, a +//! centre (with a silent partner) and a rear pair. What it could **not** say is +//! *which* stream sits where — the assignment there is by position, not content. +//! The port needs that to weight a positional downmix. +//! +//! This writes the chunks out so they can be decoded (ffmpeg has `xma2`) and +//! correlated against the per-channel residuals. +//! +//! cargo run -p sylpheed-formats --example adv_voice_dump -- OUTDIR [MOVIE] + +use sylpheed_formats::media::{self, DirectorySource, DiscSource}; +use sylpheed_formats::slb::{self, VoiceLang}; + +fn main() { + let out = std::env::args().nth(1).expect("OUTDIR"); + let movie = std::env::args().nth(2).unwrap_or_else(|| "ADV".into()); + let disc = std::env::var("SYLPHEED_DISC").expect("SYLPHEED_DISC"); + let src = DirectorySource::new(std::path::PathBuf::from(&disc)); + std::fs::create_dir_all(&out).expect("outdir"); + + let (start, end) = media::resolve_movie_voice_region(&src, &movie, VoiceLang::English) + .expect("voice region"); + let bytes = src + .read_segment_range("dat/sound", start, (end - start) as usize) + .expect("region"); + println!("{movie}: region {start}..{end} = {} B", end - start); + + let riffs = slb::to_xma_riffs(&bytes); + println!("{} RIFF chunk(s)", riffs.len()); + for (i, r) in riffs.iter().enumerate() { + let p = format!("{out}/{movie}_{i}.xma"); + std::fs::write(&p, r).expect("write"); + // the probe reports `byte_size` = RIFF total - 60; print both so the + // dump can be tied to a specific XMA context by its own number + println!(" chunk {i}: {} B byte_size-equivalent {} -> {p}", + r.len(), r.len() as i64 - 60); + } +} diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index da553a86..7526394b 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -2565,9 +2565,31 @@ not apply to this capture** — the measured map is the identity. So the census' silent LFE. Measure channel order per capture; a 6×6 matrix that comes out a clean permutation is its own control. -⚠️ Reach: one boot, one movie. **Which** of the three XMA contexts is front, centre -or rear is *not* determined — the assignment above is by position. And whether 0.600 -is a fixed mix constant or a volume setting is unknown. +✅ **UPDATE — the assignment is now determined, so your weights are unblocked:** + +| stream | `byte_size` | → | downmix weight you cited | +|---|---|---|---| +| ctx0 | 1 294 336 | **FL, FR** | 0.4142 | +| ctx1 | 1 118 208 | **FC** (LFE silent) | 0.2929 | +| ctx2 | 1 171 456 | **BL, BR** | 0.2929 | + +Settled by **level**, under the same 0.600 gain the bed uses: each stream lands +within **0.5 dB** of exactly one residual pair and misses the others by 4–6 dB. +Ratio test (immune to chunk 0 being a clipped tail): chunk0 − chunk2 = +5.88 dB +against FL − BL = +6.18 dB, agreeing to 0.30 dB; swapped it would be wrong by 11.76. +Structural confirmation: ctx1 is the only stream with a digitally silent channel and +LFE is the only channel with an empty residual. ✅ **One mixer gain, not two** — the +same 0.600 scales bed and voice. + +🔴 **Two instruments failed first and both looked convincing** — worth knowing +before you try to reproduce it. Envelope correlation returns **0.86–0.95 for every +stream against every channel**, because all six channels share the dialogue's +timing; that is no resolving power, not a result. Sample-level correlation returns +≈ 0, because the chunks do not start with the movie. + +⚠️ Reach: levels, not waveforms — three numbers agreeing to 0.5 dB plus a 1:1 +structural match. One boot, one movie. Whether 0.600 is a fixed mix constant or a +volume setting is still unknown. [`intro-audio-decomposed.md`](../re/structures/intro-audio-decomposed.md) · [numbers](../re/data/intro-audio-decomposition.txt) diff --git a/docs/re/data/adv-stream-assignment.txt b/docs/re/data/adv-stream-assignment.txt new file mode 100644 index 00000000..949c59cd --- /dev/null +++ b/docs/re/data/adv-stream-assignment.txt @@ -0,0 +1,46 @@ +# Which ADV voice stream sits where -- the assignment, and how it was settled. +# +# 2026-08-30. Chunks dumped by examples/adv_voice_dump.rs from the resolved +# movie voice region (dat/sound 433930240..437044592), decoded with ffmpeg's +# xma decoder to f32le 48 kHz stereo. +# +# chunk bytes byte_size probe ctx dur L rms R rms L/R r R silent +# 0 806972 806912 ctx0 TAIL 84.55s -24.79 -24.81 +0.932 53.1% +# 1 1118268 1118208 ctx1 137.32s -20.33 -inf +0.000 100.0% +# 2 1171516 1171456 ctx2 137.32s -30.67 -30.68 +0.962 53.6% +# +# ctx0's full byte_size is 1294336; the region resolver starts at the +# predecessor cue's trailer, so chunk 0 is its clipped tail (62 %). +# +# 🔴 WHAT DID NOT WORK: envelope correlation cannot discriminate. +# Every residual channel shares the dialogue's activity timing, so a +# per-pair lag search returns 0.86-0.95 for EVERY chunk against EVERY +# channel. Recorded because it looks like a strong result and is not. +# +# 🔴 Sample-level correlation also fails: the chunks do not start with the +# movie and the XMA decode's framing offset is unknown, so r ~ 0. +# +# ✅ WHAT SETTLES IT: level, under the SAME 0.600 gain the bed uses. +# +# chunk level x0.600 nearest residuals (|error| dB) +# 0L -24.79 -29.23 FL 0.05 FR 0.06 FC 3.96 +# 0R -24.81 -29.25 FL 0.03 FR 0.04 FC 3.98 +# 1L -20.33 -24.77 FC 0.50 FL 4.51 FR 4.52 +# 2L -30.67 -35.11 BL 0.35 BR 0.38 FR 5.82 +# 2R -30.68 -35.12 BL 0.34 BR 0.37 FR 5.83 +# +# Ratio test, immune to any worry about chunk 0 being clipped: +# chunk0L - chunk2L = +5.88 dB ; FL - BL = +6.18 dB -> agree to 0.30 dB +# swapped, the ratio would be wrong by 11.76 dB +# +# Structural confirmation: chunk 1 is the ONLY chunk with a digitally silent +# channel (R, 100 %), and LFE is the ONLY output channel with an empty +# residual (-115.73 dBFS). One-to-one. +# +# Internal L/R correlation also tracks: +# chunk0 +0.932 <-> FL/FR residual +0.918 +# chunk2 +0.962 <-> BL/BR residual +0.929 +# +# ==> ctx0 (1294336) -> FL, FR +# ==> ctx1 (1118208) -> FC, LFE silent +# ==> ctx2 (1171456) -> BL, BR diff --git a/docs/re/structures/intro-audio-decomposed.md b/docs/re/structures/intro-audio-decomposed.md index bea0a690..c4d5e195 100644 --- a/docs/re/structures/intro-audio-decomposed.md +++ b/docs/re/structures/intro-audio-decomposed.md @@ -93,9 +93,63 @@ clean permutation is its own control.** ## Reach ⚠️ **One boot, one movie.** `ADV` only. -⚠️ **The stream→channel assignment is by position, not by content.** Which of the -three XMA contexts is the front, centre or rear stream is *not* determined here — -that needs the streams decoded and correlated individually, which was not done. +✅ **The assignment is now determined** — see the section below. It was open when +this page was first written. ⚠️ `--gpu=null`, so no video cross-check. ✅ The 0.600 gain is measured on this run; whether it is a fixed mix constant or a volume setting is not established. + + +## ✅ Which stream is which (2026-08-30, later) + +The three chunks were dumped from the resolved voice region +(`examples/adv_voice_dump.rs`) and decoded: +[`../data/adv-stream-assignment.txt`](../data/adv-stream-assignment.txt). + +| chunk | `byte_size` | probe ctx | L rms | R rms | R silent | +|---|---|---|---|---|---| +| 0 | 806 912 | **ctx0, clipped tail** (full 1 294 336) | −24.79 | −24.81 | 53.1 % | +| 1 | 1 118 208 | ctx1 | −20.33 | **−inf** | **100 %** | +| 2 | 1 171 456 | ctx2 | −30.67 | −30.68 | 53.6 % | + +### 🔴 Two instruments failed first, and both look convincing + +* **Envelope correlation cannot discriminate.** A per-pair lag search returns + **0.86–0.95 for every chunk against every channel**, because all six residual + channels share the dialogue's activity timing. A number that high reads as a + result; it is the instrument having no resolving power. Recorded so nobody + reports it as one. +* **Sample-level correlation returns ≈ 0.** The chunks do not start with the movie + and the XMA decode's framing offset is unknown. + +### ✅ Level settles it, under the same 0.600 gain + +| chunk | level | × 0.600 | nearest residuals (error, dB) | +|---|---|---|---| +| 0L | −24.79 | −29.23 | **FL 0.05** · FR 0.06 · FC 3.96 | +| 1L | −20.33 | −24.77 | **FC 0.50** · FL 4.51 | +| 2L | −30.67 | −35.11 | **BL 0.35** · BR 0.38 · FR 5.82 | + +Each stream lands within **0.5 dB** of exactly one residual pair and misses the +others by ~4–6 dB. **The same 0.600 that scales the movie bed also scales the +voice** — which is itself worth having: it is one mixer gain, not two. + +✅ **Ratio test, immune to chunk 0 being clipped:** chunk0 − chunk2 = **+5.88 dB** +against FL − BL = **+6.18 dB**, agreeing to **0.30 dB**; swapped, the ratio would be +wrong by **11.76 dB**. + +✅ **Structural confirmation.** Chunk 1 is the *only* chunk with a digitally silent +channel, and LFE is the *only* output channel with an empty residual (−115.73 dBFS). +One to one. And the internal L/R correlations track: chunk 0 **+0.932** against the +FL/FR residual's **+0.918**, chunk 2 **+0.962** against BL/BR's **+0.929**. + +| stream | → | +|---|---| +| ctx0 · 1 294 336 | **FL, FR** | +| ctx1 · 1 118 208 | **FC** (LFE silent) | +| ctx2 · 1 171 456 | **BL, BR** | + +⚠️ **Reach.** Levels, not waveforms — this is an argument from three numbers +agreeing to 0.5 dB and a 1:1 structural match, not from a matched waveform. One +movie, one boot. And chunk 0 is a clipped tail, which is why the ratio test is +quoted alongside the absolute match.