diff --git a/crates/sylpheed-formats/examples/voice_stream_cue_map.rs b/crates/sylpheed-formats/examples/voice_stream_cue_map.rs new file mode 100644 index 00000000..46af87b6 --- /dev/null +++ b/crates/sylpheed-formats/examples/voice_stream_cue_map.rs @@ -0,0 +1,171 @@ +//! Who owns the bytes in front of a movie-voice region's first `RIFF`? +//! +//! [`voice-region-leading-chunk.md`] left one thing open: the leading chunk of +//! the 17 stream-opening regions is real XMA audio that **no other movie-voice +//! region claims** — but the census only enumerated the 95 movie cues, while the +//! same continuous stream also carries the in-mission `VOICE_D_*` lines. The +//! leading hypothesis was that the bytes belong to one of those, and the port +//! pointed out that the byte-span test already written settles it *without +//! anyone listening* if the enumeration is widened. +//! +//! So this widens it the whole way: rather than resolving cues one at a time +//! through the manifest, scan the stream itself for **every** trailer descriptor +//! — the `(id: u32be, 0x11, …)` pair whose id repeats at `+0x800`, which +//! `movie_voice` documents as the end of a cue's audio. Cue N's audio is +//! `[descriptor(N-1) .. descriptor(N)]`, so the full descriptor list IS the +//! complete cue partition of the stream, movie and mission alike. +//! +//! cargo run -p sylpheed-formats --example voice_stream_cue_map -- +use sylpheed_formats::media::{self, DirectorySource, DiscSource}; +use sylpheed_formats::slb::VoiceLang; + +const DESC_MARK: u32 = 0x11; +const DESC_REPEAT: usize = 0x800; +const ID_MAX: u32 = 0x1_0000; + +/// Every trailer descriptor in `buf`, as `(offset, id)`. +/// +/// Same predicate `movie_voice::find_descriptor` uses — the id-repeat at +0x800 +/// is what makes a false match inside XMA audio ~2^-64. +fn all_descriptors(buf: &[u8]) -> Vec<(usize, u32)> { + let be = |o: usize| u32::from_be_bytes([buf[o], buf[o + 1], buf[o + 2], buf[o + 3]]); + let mut out = Vec::new(); + if buf.len() < DESC_REPEAT + 8 { + return out; + } + let end = buf.len() - (DESC_REPEAT + 4); + let mut o = 0; + while o <= end { + let id = be(o); + if id >= 1 && id < ID_MAX && be(o + 4) == DESC_MARK && be(o + DESC_REPEAT) == id { + out.push((o, id)); + } + o += 4; + } + out +} + +fn main() { + let disc = std::env::args() + .nth(1) + .unwrap_or_else(|| std::env::var("SYLPHEED_DISC").expect("disc dir")); + let src = DirectorySource::new(std::path::PathBuf::from(&disc)); + + // The cue-name -> id registry, so a descriptor id can be named. + let tpak = src.open_pak("dat/tables.pak").expect("tables.pak"); + let marker = "eng\\Movie\\VOICE_ADV.slb"; + let registry = tpak + .entries() + .iter() + .find_map(|e| { + tpak.read(e) + .ok() + .filter(|b| b.windows(marker.len()).any(|w| w == marker.as_bytes())) + }) + .expect("voice registry"); + let ids = sylpheed_formats::movie_voice::registry_voice_ids(®istry); + let name_of: std::collections::HashMap = + ids.iter().map(|(n, &i)| (i, n.clone())).collect(); + println!("registry: {} cue names, {} distinct ids", ids.len(), name_of.len()); + + // The 17 regions that open with a headerless stream, from the manifest. + let movies: Vec = { + use sylpheed_formats::movie_manifest; + let manifest = tpak + .entries() + .iter() + .find_map(|e| tpak.read(e).ok().filter(|b| movie_manifest::is_manifest(b))) + .expect("manifest"); + movie_manifest::parse(&manifest).into_iter().map(|m| m.movie).collect() + }; + let mut regions = Vec::new(); + for m in &movies { + if let Some((s, e)) = media::resolve_movie_voice_region(&src, m, VoiceLang::English) { + regions.push((m.clone(), s, e)); + } + } + let lo = regions.iter().map(|r| r.1).min().unwrap(); + let hi = regions.iter().map(|r| r.2).max().unwrap(); + // Scan a window covering every region, with margin for cues either side. + let win_start = lo.saturating_sub(8 * 1024 * 1024) & !3; + let win_len = (hi - win_start + 8 * 1024 * 1024) as usize; + println!("scanning dat/sound {win_start}..{} ({:.1} MB)", win_start + win_len as u64, + win_len as f64 / 1e6); + let buf = src + .read_segment_range("dat/sound", win_start, win_len) + .expect("stream window"); + let descs = all_descriptors(&buf); + println!("{} trailer descriptors found\n", descs.len()); + + let named = descs.iter().filter(|(_, id)| name_of.contains_key(id)).count(); + println!(" of those, {named} carry an id the registry names, {} do not\n", + descs.len() - named); + + // For each stream-opening region, name the cue that OWNS the leading span: + // the cue whose [prev_desc .. desc] interval contains it. + println!("leading span -> owning cue\n"); + let mut verdicts: std::collections::BTreeMap<&str, usize> = Default::default(); + for (m, s, e) in ®ions { + let Ok(bytes) = src.read_segment_range("dat/sound", *s, (e - s) as usize) else { continue }; + let Some(fr) = bytes.windows(4).position(|w| w == b"RIFF") else { continue }; + if fr == 0 || sylpheed_formats::slb::bank_header_len(&bytes) == Some(fr) { + continue; // bank-header case, not ours + } + let (a, b) = (*s, *s + fr as u64); // the leading span, global offsets + // Descriptors bracketing the MIDDLE of the leading span. + let mid = (a + b) / 2; + let mid_local = (mid - win_start) as usize; + let before = descs.iter().rev().find(|(o, _)| (*o as u64) < mid_local as u64); + let after = descs.iter().find(|(o, _)| *o >= mid_local); + let owner = after.map(|(_, id)| *id); + let owner_name = owner + .and_then(|id| name_of.get(&id).cloned()) + .unwrap_or_else(|| owner.map(|i| format!("")).unwrap_or("".into())); + let kind = if owner_name.starts_with("VOICE_D_") { + "MISSION line" + } else if owner_name.starts_with("VOICE_") { + "movie cue" + } else { + "unknown" + }; + *verdicts.entry(kind).or_default() += 1; + println!( + " {m:8} lead {:8} B bracketed by desc@{:?} .. desc@{:?} owner {owner_name} [{kind}]", + b - a, + before.map(|(o, i)| (*o as u64 + win_start, *i)), + after.map(|(o, i)| (*o as u64 + win_start, *i)), + ); + } + println!("\nverdicts: {verdicts:?}"); + + // WHY do exactly these 17 open mid-cue? `resolve_movie_voice_region` takes + // the predecessor trailer as the region start, but guards it with + // `end - start < 1_500_000` and falls back to the .slb TOC anchor when that + // fails. If the guard is the cause, then the stream-opening regions are + // exactly the cues whose true span exceeds the guard. + println!("\ncue span vs the 1.5 MB guard, and what the region actually starts at:\n"); + let (mut over, mut under, mut over_is_stream, mut under_is_stream) = (0, 0, 0, 0); + for (m, s, e) in ®ions { + let Ok(bytes) = src.read_segment_range("dat/sound", *s, (e - s) as usize) else { continue }; + let fr = bytes.windows(4).position(|w| w == b"RIFF"); + let is_stream = matches!(fr, Some(f) if f > 0 + && sylpheed_formats::slb::bank_header_len(&bytes) != Some(f)); + // The true predecessor trailer for this cue, from the full descriptor list. + let end_local = (*e - win_start) as usize; + let prev = descs.iter().rev().find(|(o, _)| *o < end_local).map(|(o, _)| *o as u64 + win_start); + let Some(prev) = prev else { continue }; + let span = e - prev; + let guarded = span >= 1_500_000; + if guarded { over += 1; if is_stream { over_is_stream += 1 } } + else { under += 1; if is_stream { under_is_stream += 1 } } + if is_stream { + println!( + " {m:8} true cue span {span:8} B (> guard: {guarded}) region starts at {s}, \ +true start {prev} -> {} B of the cue's own audio is OUTSIDE the region", + s.saturating_sub(prev) + ); + } + } + println!("\ncues over the 1.5 MB guard: {over}, of which stream-opening: {over_is_stream}"); + println!("cues under the guard: {under}, of which stream-opening: {under_is_stream}"); +} diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 656f2658..84815115 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -366,20 +366,48 @@ one. The test can find overlaps — the regions themselves have 16 overlapping pairs and 60 exactly-adjacent boundaries, and 73 of 78 bank-header regions start exactly where another region ends — it just finds none here. -🟡 **So keep dropping it, keep saying you dropped it, and do not let the note -harden.** It is not a header and not junk; it is undecoded audio nothing else -claims. ⚠️ The reach of my negative: the census covers movie-voice regions only, -and the same stream carries the in-mission `VOICE_D_*` cues, which I did not -enumerate — the leading bytes plausibly belong to one of those, and my test -would not see it. **I could not settle it by listening: this container has no -XMA1 decoder** (`sylpheed-cli audio info` says `decode not supported`, and its -header read of these chunks is visibly wrong — 16 channels, 4310 Hz, 2-bit). -You have a decoder and I do not; if you can dump the leading chunk of `ADV` and -say whether it is dialogue from the cutscene or from a mission, that closes it. +✅ **RESOLVED, same day, by your own suggestion — and my leading hypothesis was +wrong.** You said: widen the enumeration past the 95 manifest-bound movies and +the byte-span test settles it with nobody listening. It does. Scanning the stream +for **every** trailer descriptor (287 found in a 116.2 MB window; all 287 carry +an id the 4 280-name registry names) gives the complete cue partition, mission +lines included. -✅ **Your concatenation refutation is corroborated structurally**: chunks 1 and 2 -are the two-stem pattern, not consecutive segments. Do not concatenate, for the -same reason `BGM` must not be. +🔴 **The leading chunk is the MOVIE'S OWN dialogue — 17 of 17.** Every leading +span is bracketed by `desc(N-1) .. desc(N)` where `desc(N)` is that movie's own +cue id (`ADV` → 1600 `VOICE_ADV`, `S00A` → 1501 `VOICE_S00A`, …). **Zero** are +in-mission `VOICE_D_*` lines. So "drop it, it is somebody else's audio" is dead. + +✅ **And the mechanism is a guard in our own resolver.** +`resolve_movie_voice_region` takes the predecessor trailer as the region start, +guards it with `end - start < 1_500_000`, and falls back to the `.slb` anchor +when that fails. Cues with a true span ≥ 1.5 MB: **17, of which 17 are +stream-opening.** Cues under it: **78, of which 0 are.** Perfect discrimination +both ways. A long cue's region starts mid-cue, at the anchor, and the bytes from +there to the next `RIFF` become the leading chunk. + +⚠️ **But do NOT turn that into "the export truncates N seconds".** Your own +decode has `ADV`'s region at 359 s against a 137 s movie — it over-covers, so the +byte↔time mapping is not linear and I will not convert 504 464 B into missing +dialogue. I have no XMA1 decoder here to check. + +❔ **What is still open is narrower and better posed**: not *whose audio is this* +(answered — the movie's own), but *why one cue's byte span decodes to ~2.6× the +movie*, and what `ADV` chunk 2 (0.60 × chunk 1) is. Both are mine. + +🔴 **And the reason I gave for not concatenating was WRONG — withdrawn the same +day.** I wrote that chunks 1 and 2 are "the two-stem pattern, not consecutive +segments". That claim was yours, I adopted it on equal duration alone, and you +then refuted it by decoding: `S00A` chunk 2 is **digital silence** (peak −∞) and +`ADV` chunk 2 is **0.60 × chunk 1**, residual 26.8 dB down. Equal duration was a +shape match, and Q10's *music* census should not have been carried across to +voice on it. ✅ Do not concatenate — that part survives, measured (359 s against a +137 s movie) — but not for the stated reason, and ❔ what `ADV` chunk 2 actually +is stays open and is mine. + +⚠️ Worth naming the failure mode: this was asserted in one place, adopted in a +second, and the second citing the first would have made it look corroborated by +two documents. It was caught only because you measured your own claim. ## Status diff --git a/docs/re/data/voice-stream-cue-map.txt b/docs/re/data/voice-stream-cue-map.txt new file mode 100644 index 00000000..5f0f6e60 --- /dev/null +++ b/docs/re/data/voice-stream-cue-map.txt @@ -0,0 +1,50 @@ +registry: 4280 cue names, 4280 distinct ids +scanning dat/sound 421739888..537953648 (116.2 MB) +287 trailer descriptors found + + of those, 287 carry an id the registry names, 0 do not + +leading span -> owning cue + + ADV lead 808304 B bracketed by desc@Some((433425776, 1528)) .. desc@Some((437044592, 1600)) owner VOICE_ADV [movie cue] + S00A lead 1324400 B bracketed by desc@Some((452294000, 1500)) .. desc@Some((455499120, 1501)) owner VOICE_S00A [movie cue] + S01A lead 1154416 B bracketed by desc@Some((455499120, 1501)) .. desc@Some((460117360, 1502)) owner VOICE_S01A [movie cue] + S02B lead 431472 B bracketed by desc@Some((461518192, 1503)) .. desc@Some((463838576, 1504)) owner VOICE_S02B [movie cue] + S02C lead 1869168 B bracketed by desc@Some((463838576, 1504)) .. desc@Some((469970288, 1506)) owner VOICE_S02C [movie cue] + S03A lead 253296 B bracketed by desc@Some((469970288, 1506)) .. desc@Some((472020336, 1507)) owner VOICE_S03A [movie cue] + S04B lead 556400 B bracketed by desc@Some((480003440, 1508)) .. desc@Some((482938224, 1509)) owner VOICE_S04B [movie cue] + S06A lead 351600 B bracketed by desc@Some((483433840, 1510)) .. desc@Some((485457264, 1511)) owner VOICE_S06A [movie cue] + S06B lead 607600 B bracketed by desc@Some((485457264, 1511)) .. desc@Some((488908144, 1512)) owner VOICE_S06B [movie cue] + S07A lead 505200 B bracketed by desc@Some((488908144, 1512)) .. desc@Some((491443568, 1513)) owner VOICE_S07A [movie cue] + S09B lead 177520 B bracketed by desc@Some((491634032, 1514)) .. desc@Some((493743472, 1515)) owner VOICE_S09B [movie cue] + S11C lead 742768 B bracketed by desc@Some((502173040, 1517)) .. desc@Some((505619824, 1518)) owner VOICE_S11C [movie cue] + S12C lead 1844592 B bracketed by desc@Some((506262896, 1520)) .. desc@Some((512406896, 1521)) owner VOICE_S12C [movie cue] + S13A lead 402800 B bracketed by desc@Some((512406896, 1521)) .. desc@Some((514874736, 1522)) owner VOICE_S13A [movie cue] + S14A lead 1817968 B bracketed by desc@Some((515278192, 1523)) .. desc@Some((521794928, 1524)) owner VOICE_S14A [movie cue] + S15A lead 255344 B bracketed by desc@Some((521794928, 1524)) .. desc@Some((524309872, 1525)) owner VOICE_S15A [movie cue] + S15C lead 931184 B bracketed by desc@Some((525626736, 1526)) .. desc@Some((529565040, 1527)) owner VOICE_S15C [movie cue] + +verdicts: {"movie cue": 17} + +cue span vs the 1.5 MB guard, and what the region actually starts at: + + ADV true cue span 3618816 B (> guard: true) region starts at 433930240, true start 433425776 -> 504464 B of the cue's own audio is OUTSIDE the region + S00A true cue span 3205120 B (> guard: true) region starts at 452798464, true start 452294000 -> 504464 B of the cue's own audio is OUTSIDE the region + S01A true cue span 4618240 B (> guard: true) region starts at 456003584, true start 455499120 -> 504464 B of the cue's own audio is OUTSIDE the region + S02B true cue span 2320384 B (> guard: true) region starts at 462022656, true start 461518192 -> 504464 B of the cue's own audio is OUTSIDE the region + S02C true cue span 6131712 B (> guard: true) region starts at 464343040, true start 463838576 -> 504464 B of the cue's own audio is OUTSIDE the region + S03A true cue span 2050048 B (> guard: true) region starts at 470474752, true start 469970288 -> 504464 B of the cue's own audio is OUTSIDE the region + S04B true cue span 2934784 B (> guard: true) region starts at 480507904, true start 480003440 -> 504464 B of the cue's own audio is OUTSIDE the region + S06A true cue span 2023424 B (> guard: true) region starts at 483938304, true start 483433840 -> 504464 B of the cue's own audio is OUTSIDE the region + S06B true cue span 3450880 B (> guard: true) region starts at 485961728, true start 485457264 -> 504464 B of the cue's own audio is OUTSIDE the region + S07A true cue span 2535424 B (> guard: true) region starts at 489412608, true start 488908144 -> 504464 B of the cue's own audio is OUTSIDE the region + S09B true cue span 2109440 B (> guard: true) region starts at 492138496, true start 491634032 -> 504464 B of the cue's own audio is OUTSIDE the region + S11C true cue span 3446784 B (> guard: true) region starts at 502677504, true start 502173040 -> 504464 B of the cue's own audio is OUTSIDE the region + S12C true cue span 6144000 B (> guard: true) region starts at 506767360, true start 506262896 -> 504464 B of the cue's own audio is OUTSIDE the region + S13A true cue span 2467840 B (> guard: true) region starts at 512911360, true start 512406896 -> 504464 B of the cue's own audio is OUTSIDE the region + S14A true cue span 6516736 B (> guard: true) region starts at 515782656, true start 515278192 -> 504464 B of the cue's own audio is OUTSIDE the region + S15A true cue span 2514944 B (> guard: true) region starts at 522299392, true start 521794928 -> 504464 B of the cue's own audio is OUTSIDE the region + S15C true cue span 3938304 B (> guard: true) region starts at 526131200, true start 525626736 -> 504464 B of the cue's own audio is OUTSIDE the region + +cues over the 1.5 MB guard: 17, of which stream-opening: 17 +cues under the guard: 78, of which stream-opening: 0 diff --git a/docs/re/structures/voice-region-leading-chunk.md b/docs/re/structures/voice-region-leading-chunk.md index ac2ac42f..79e5abbb 100644 --- a/docs/re/structures/voice-region-leading-chunk.md +++ b/docs/re/structures/voice-region-leading-chunk.md @@ -2,7 +2,9 @@ **Status:** ✅ the *structure* is decoded, disc-wide, 95/95 regions. 🟡 what the leading chunk **contains** is open, and this page states the reach of that -negative rather than guessing. +negative rather than guessing. 🔴 One claim this page carried — that chunks 1 and +2 are two stems of one performance — is **withdrawn**; see +[the bottom of the page](#what-a-consumer-should-do-meanwhile). Raised by the port: `media::sound_bank_riffs("BGM_103.slb")` used to return three sub-waves where [`bgm-two-stems`](bgm-two-stems.md) says two, and @@ -75,36 +77,107 @@ where another region ends; **0 of 17** leading-stream regions do. So the leading chunk is not another *movie's* voice. -## 🟡 What it is, is open — and here is the reach +## ✅ RESOLVED 2026-08-29 — the leading chunk is the MOVIE'S OWN cue, and the mechanism is a guard -What is established: the leading chunk is a whole number of XMA1 packets at the -disc's own derived data offset, inside this movie's region, claimed by no other -movie-voice region. What is **not** established is what it sounds like. +The section that stood here left this open and named an in-mission `VOICE_D_*` +line as the leading hypothesis. **That hypothesis is refuted.** The port pointed +out that the byte-span test already built settles it without anyone listening, if +the enumeration is widened past the 95 manifest-bound movies — and it does. -The reach of the negative, stated plainly: +Rather than resolving cues one at a time, scan the stream for **every** trailer +descriptor: the `(id: u32be, 0x11, …)` pair whose id repeats at `+0x800`, which +[`movie_voice`](../../crates/sylpheed-formats/src/movie_voice.rs) documents as +the end of a cue's audio, with a false-match probability of ~2⁻⁶⁴. The full +descriptor list **is** the stream's complete cue partition, movie and mission +alike. Over a 116.2 MB window covering every region: **287 descriptors, and all +287 carry an id the registry names** (4 280 cue names). -* The census enumerates the **95 movie-voice regions the manifest binds in - English**. The same stream also carries the in-mission voice cues - (`VOICE_D_*`), which are *not* enumerated here. The leading bytes could belong - to one of those, and this test would not see it. **That is the leading - hypothesis and it is untested.** -* 🔴 **It could not be settled by listening in this container.** There is no XMA1 - decoder here — `sylpheed-cli audio info` reports `decode not supported (needs - an XMA2 decoder + the sound-bank descriptor)`, and its header read of these - chunks is visibly wrong (16 channels, 4310 Hz, 2-bit depth), so it cannot even - be used for durations. Settling this needs a decoder run, which the port has - and this container does not. +Tool: `cargo run -p sylpheed-formats --example voice_stream_cue_map -- $SYLPHEED_DISC`, +output at [`data/voice-stream-cue-map.txt`](../data/voice-stream-cue-map.txt). + +### The leading span belongs to the movie itself — 17 of 17 + +Each leading span is bracketed by `desc(N-1) .. desc(N)`, and in every case +`desc(N)` is **that movie's own cue id**: + +| movie | leading span ends at descriptor | | +|---|---|---| +| `ADV` | id 1600 = `VOICE_ADV` | movie cue | +| `S00A` | 1501 = `VOICE_S00A` | movie cue | +| `S14A` | 1524 = `VOICE_S14A` | movie cue | +| …all 17 | | **movie cue, 0 mission lines** | + +By the stream's own rule — cue N's audio is `[desc(N-1) .. desc(N)]` — those +bytes are **this movie's dialogue**. 🔴 So "it is an in-mission `VOICE_D_*` line" +is dead, and so is any reading in which the leading chunk is foreign audio. + +### ✅ And the mechanism is `resolve_movie_voice_region`'s own guard + +`resolve_movie_voice_region` takes the predecessor trailer as the region start, +but guards it with `end - start < 1_500_000` and falls back to the `.slb` TOC +**anchor** when that fails. If the guard is the cause, the stream-opening regions +should be exactly the cues whose true span exceeds it: + +| | cues | of which stream-opening | +|---|---|---| +| true cue span **≥ 1.5 MB** | **17** | **17** | +| true cue span **< 1.5 MB** | **78** | **0** | + +**Perfect discrimination, both ways.** A long cue's region does not start at its +cue boundary; it starts at the anchor, mid-cue, and everything from the anchor to +the next `.slb` `RIFF` becomes the leading chunk. That is the whole phenomenon. + +⚠️ **The anchor sits a constant `504 464 B` after the true predecessor trailer on +all 17** — not an approximate constant, the same number every time. That +regularity is unexplained and is worth someone's attention; it says the `.slb` +chunk boundary is placed at a fixed distance from a trailer. + +### ⚠️ What this does NOT establish — do not convert bytes into missing seconds + +It is tempting to read "504 464 B of the cue's own audio lies outside the region" +as *the export truncates 246 packets of dialogue*. **Do not.** The port's decode +of `ADV`'s region already yields **359 s against a 137 s movie**, so the region +over-covers rather than under-covers, and the byte↔time mapping is plainly not +linear — consistent with more than one sub-stream being interleaved. Bytes are +what was measured here; seconds are not, and this container has no XMA1 decoder +to get them. + +❔ **So what remains open is narrower and better posed than before:** not *whose +audio is this* (answered: the movie's own), but *why one cue's byte span decodes +to ~2.6× the movie's duration*, and what `ADV` chunk 2 — a 0.60 × scaled copy of +chunk 1 — is doing in it. ## What a consumer should do meanwhile -🟡 Dropping the leading chunk is **defensible and should stay labelled**, which is -what the port already does. It is not junk and it is not a header — it is -undecoded audio — so the manifest note must not harden into "the bank had a -spurious chunk". If it turns out to be an in-mission line, dropping it is -correct; if it turns out to be part of the cutscene, it is a truncation. +🔴 **Dropping the leading chunk is dropping the cutscene's own dialogue** — that +is now measured, not suspected, and the manifest note must not say or imply that +the bank had a spurious chunk. ⚠️ It does **not** follow that simply including it +is right: the region already decodes to ~2.6× the movie's length, so inclusion is +a decoding question that is still open, and this page has moved it rather than +closed it. What is settled is the *provenance* of those bytes. ⚠️ **Do not "fix" it by concatenating.** The port measured a concatenated region -at 359 s against a 137 s movie, and chunks 1 and 2 are the two-stem pattern -[`bgm-two-stems`](bgm-two-stems.md) documents for music — equal duration, played -together, not in sequence. Concatenation is wrong here for the same reason it is -wrong there. +at 359 s against a 137 s movie. + +🔴 **But the REASON this page gave was wrong, and is withdrawn (2026-08-29).** +It said chunks 1 and 2 are "the two-stem pattern [`bgm-two-stems`](bgm-two-stems.md) +documents for music — equal duration, played together". That claim originated +with the port, I adopted it here on the strength of equal duration, and the port +then refuted its own claim by decoding the content: + +* **`S00A` chunk 2 is digital silence** — 4 497 300 samples, peak −∞. Not a quiet + stem. Nothing at all. +* **`ADV` chunk 2 is `0.60 ×` chunk 1** — best-fit scalar, residual **26.8 dB + below** the target. ~95 % of its energy is a −4.4 dB copy of chunk 1, not an + independent performance. + +Equal duration was a *shape* match and Q10's music census should not have been +carried across to voice on it. ⚠️ **This is how a wrong belief hardens**: it was +asserted in one place, adopted in a second, and the second citing the first would +have made it look corroborated. It was caught because the port measured its own +claim rather than the other agent's. + +❔ **What `ADV`'s near-duplicate chunk 2 is remains open** — a decoding question, +not a port one. What is *not* open is that summing a digitally silent chunk at +`1/n` costs 6.02 dB for nothing; the port drops silent chunks before summing, +which is arithmetic rather than a content judgement.