diff --git a/crates/sylpheed-formats/examples/voice_stream_cue_map.rs b/crates/sylpheed-formats/examples/voice_stream_cue_map.rs index 46af87b6..b78a37f5 100644 --- a/crates/sylpheed-formats/examples/voice_stream_cue_map.rs +++ b/crates/sylpheed-formats/examples/voice_stream_cue_map.rs @@ -168,4 +168,38 @@ true start {prev} -> {} B of the cue's own audio is OUTSIDE the region", } 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}"); + + // How many streams is ONE cue stored as? The port measured that a region's + // leading chunk is the TAIL of its first full-length chunk, i.e. the cue is + // re-presented. Structurally that predicts a fixed number of stream starts + // inside a cue's TRUE span [desc(N-1) .. desc(N)] -- which is measurable + // from the bytes alone, with no decoder. + println!("\nstream starts inside each cue's TRUE span (desc(N-1)..desc(N)):\n"); + let mut hist: std::collections::BTreeMap = Default::default(); + let mut hist_long: std::collections::BTreeMap = Default::default(); + for w in descs.windows(2) { + let (a, b) = (w[0].0, w[1].0); + if b <= a || b - a < 4096 { + continue; + } + let span = &buf[a..b]; + // A stream start is a RIFF; plus the run before the first one, when it + // is not a bank header, is itself a stream. + let riffs = span + .windows(4) + .enumerate() + .filter(|(_, w)| *w == b"RIFF") + .count(); + let lead_is_stream = match span.windows(4).position(|w| w == b"RIFF") { + Some(f) if f > 0 => sylpheed_formats::slb::bank_header_len(span) != Some(f), + _ => false, + }; + let streams = riffs + usize::from(lead_is_stream); + *hist.entry(streams).or_default() += 1; + if b - a >= 1_500_000 { + *hist_long.entry(streams).or_default() += 1; + } + } + println!(" all inter-descriptor spans: {hist:?}"); + println!(" spans >= 1.5 MB (the long cues): {hist_long:?}"); } diff --git a/docs/port/HANDOFF.md b/docs/port/HANDOFF.md index 84815115..98100dae 100644 --- a/docs/port/HANDOFF.md +++ b/docs/port/HANDOFF.md @@ -391,9 +391,44 @@ decode has `ADV`'s region at 359 s against a 137 s movie — it over-covers, so 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. +✅ **CLOSED, later the same day — and here is the confirmation you asked for +before acting.** + +**Yes: drop the leading chunk. It is a DUPLICATE, not a truncation.** Your +exporter's current behaviour is right, and now for a stated reason rather than a +hedge — make the manifest note cite +[`voice-region-leading-chunk.md`](../re/structures/voice-region-leading-chunk.md). + +Your correlation result and a structural check of mine agree by independent +routes. Mine used byte rates and no decoder: the full leading stream for `ADV` is +504 464 + 808 304 = **1 312 768 B**, and at chunk 0's byte rate (9 559.7 B/s) that +is **137.323 s** against chunk 1's measured **137.324 s** — 1 ms over 137 s. + +✅ **And the byte structure settles the shape disc-wide.** Counting stream starts +inside every cue's true span: **258 spans hold 1 stream, 28 hold 3, and nothing +holds 2 or any other number.** All 20 spans ≥ 1.5 MB are 3-stream. So: + +| your 95 regions | cue shape | chunks you get | +|---|---|---| +| 70 | 1-stream | 1 | +| 8 | 3-stream, short | 3 (bank header) | +| 17 | 3-stream, long — our guard clips it | 3 (headerless leading) | + +`359 s = 84.55 + 137.32 + 137.32`. **The 2.6× is three presentations of one +take**, one clipped by our own guard. That was the last open one and it is shut. + +🔴 **One change you have NOT made yet and should: stop summing chunk 1 and chunk +2.** They are the same take, not two stems — `ADV` chunk 2 is 0.60 × chunk 1 +(residual 26.8 dB down), `S00A` chunk 2 is digital silence. Summing a take with a +scaled copy of itself adds ~4 dB and colours it. **Take one stream**, the +highest-rate one (chunk 1). Dropping silent chunks was the right instinct; this +is the same defect one step further out. ❔ *Why* the disc stores three +presentations is not answered. + +⚠️ On your offer to convert the 504 464 B constant: **don't spend the decode.** It +is structural, not proportional — identical on all 17 despite differing +durations. `ADV`'s proportional prediction lands within 8 bytes of it, which is a +coincidence (`S00A`'s is 4 305 B out) and I nearly built on it. 🔴 **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 diff --git a/docs/re/data/voice-stream-cue-map.txt b/docs/re/data/voice-stream-cue-map.txt index 5f0f6e60..1ce55828 100644 --- a/docs/re/data/voice-stream-cue-map.txt +++ b/docs/re/data/voice-stream-cue-map.txt @@ -48,3 +48,8 @@ cue span vs the 1.5 MB guard, and what the region actually starts at: cues over the 1.5 MB guard: 17, of which stream-opening: 17 cues under the guard: 78, of which stream-opening: 0 + +stream starts inside each cue's TRUE span (desc(N-1)..desc(N)): + + all inter-descriptor spans: {1: 258, 3: 28} + spans >= 1.5 MB (the long cues): {3: 20} diff --git a/docs/re/structures/voice-region-leading-chunk.md b/docs/re/structures/voice-region-leading-chunk.md index 79e5abbb..3ed68c15 100644 --- a/docs/re/structures/voice-region-leading-chunk.md +++ b/docs/re/structures/voice-region-leading-chunk.md @@ -132,29 +132,99 @@ 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 +### ✅ RESOLVED 2026-08-29 (later) — a long cue is stored as THREE presentations of one take -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. +The section that stood here declined to convert bytes into seconds and left open +*why one cue's byte span decodes to ~2.6× the movie*. That is now answered, by +two independent routes that agree. -❔ **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. +**The port's route — envelope cross-correlation, with controls.** Sliding with +overhang at both ends and normalised over the overlap only: + +| | best r | at lag | overlap | +|---|---|---|---| +| `ADV` chunk 0 → chunk 1 | **0.998** | +52.8 s | 84.5 s | +| `S00A` chunk 0 → chunk 1 | **0.932** | +25.6 s | 68.0 s | +| control — `ADV` chunk 0 → itself | 1.000 | 0.0 s | — | +| control — `ADV` chunk 0 → `S00A` chunk 1 | **0.289** | — | 28.2 s | + +Both lags put chunk 0 flush against the **end** of chunk 1 (52.8 + 84.55 = +137.35 against 137.324; 25.6 + 68.07 = 93.67 against 93.694). In the sample +domain, after refining the lag and best-fitting a scalar, the residual is +**16.70 dB** below the target over 84.5 s (`ADV`) and 23.15 dB over 68.1 s +(`S00A`) — 98–99.5 % of the energy is a scaled copy. + +⚠️ Their earlier 0.768 is **withdrawn by them**: that search only tried lags where +the shorter chunk fitted wholly inside the longer, and scored best on the +boundary of its own range — where a statistic lands when it has found nothing. + +**My route — byte rates, from the disc, with no decoder.** If the leading chunk +is the tail of a *full-length* first stream, then the whole leading stream +(the part before the region's anchor plus the part after) should be one complete +take of chunk 1's duration. Using only the port's durations and the disc's byte +counts: + +| | | +|---|---| +| full leading stream, `ADV` | 504 464 + 808 304 = **1 312 768 B** | +| its byte rate, from chunk 0 | 808 304 B / 84.553 s = **9 559.7 B/s** | +| implied duration of the whole leading stream | **137.323 s** | +| chunk 1's measured duration | **137.324 s** | + +**Agreement to 1 ms over 137 s**, from a quantity (byte rate) independent of the +one the port measured (envelope correlation). And it confirms their point that +bytes-per-second is not a constant: the three streams run at **9 560 / 8 143 / +8 531 B/s** for the same 137.324 s. + +### ✅ And the byte structure says three, disc-wide + +The reading above predicts a fixed number of stream starts inside a cue's **true** +span `[desc(N-1) .. desc(N)]`. Counted directly over every inter-descriptor span +in the 116.2 MB window — RIFFs, plus a leading headerless run when it is not a +bank header: + +| streams in the span | spans | +|---|---| +| **1** | **258** | +| **3** | **28** | +| 2, or anything else | **0** | + +**Bimodal, with nothing in between**, and *all 20* spans ≥ 1.5 MB are 3-stream. +So a cue is stored as **one** stream or as **three**, never two. + +That closes the account, and the three routes agree on the arithmetic: + +| the 95 movie regions | cue shape | what the region yields | +|---|---|---| +| **70** | 1-stream | 1 chunk, bank header | +| **8** | 3-stream, span < 1.5 MB | 3 chunks, bank header | +| **17** | 3-stream, span ≥ 1.5 MB — the guard fires | 3 chunks, headerless leading one | + +70 + 8 + 17 = 95, and the 8 here are independently the same 8 the first census +found as "bank header, 3 chunks". `359 s = 84.55 + 137.32 + 137.32` — the region +catches the tail of stream 1 and the whole of streams 2 and 3. **The 2.6× is +three presentations of one take, one of them clipped by our own guard.** + +⚠️ **The 504 464 B constant is structural, not proportional.** It is the same +number on all 17 despite their differing durations. For `ADV` a proportional +prediction lands within 8 bytes of it, which is a coincidence and should not be +built on — the same prediction for `S00A` is 4 305 B out. ## What a consumer should do meanwhile -🔴 **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. +✅ **Drop the leading chunk. It is a DUPLICATE, not a truncation.** It is the +cutscene's own dialogue — but it is the *tail of the stream that follows it*, so +removing it loses nothing any other chunk does not carry. An earlier version of +this page said "I would not change the exporter yet"; that hedge is lifted, and +the reason the exporter's existing behaviour is right is now on the record. + +🔴 **But DO NOT sum chunk 1 and chunk 2 — they are the same take, not two +stems.** `ADV` chunk 2 is `0.60 ×` chunk 1 with the residual 26.8 dB down; +`S00A` chunk 2 is digital silence. Summing a take with a scaled copy of itself +adds ~4 dB and colours it; summing it with silence at `1/n` costs 6.02 dB. **Take +one stream.** On the evidence so far the highest-rate, highest-gain one is chunk +1. ❔ *Why* the disc stores three presentations — quality tiers, a mix the engine +selects between, an authoring artefact — is not answered here. ⚠️ **Do not "fix" it by concatenating.** The port measured a concatenated region at 359 s against a 137 s movie.