diff --git a/Cargo.lock b/Cargo.lock index aa808c11..e655ebf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4627,7 +4627,7 @@ dependencies = [ "image", "serde", "serde_json", - "sylpheed-formats 0.1.0 (git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-29d)", + "sylpheed-formats 0.1.0 (git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-30)", ] [[package]] @@ -4651,7 +4651,7 @@ dependencies = [ [[package]] name = "sylpheed-formats" version = "0.1.0" -source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-29d#e2640338e149acc903fac551f400a5319950afb5" +source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-08-30#c8d3a6a15d5feb0416b2b589cb8e0c442f7422d3" dependencies = [ "anyhow", "binrw", diff --git a/crates/sylpheed-export/Cargo.toml b/crates/sylpheed-export/Cargo.toml index d0951923..b941fd21 100644 --- a/crates/sylpheed-export/Cargo.toml +++ b/crates/sylpheed-export/Cargo.toml @@ -81,7 +81,7 @@ license.workspace = true # geometry -- does NOT reach this port from here: `sylpheed-cli` builds from the # WORKSPACE crate, so the reference renderer stays unrotated until the tag lands # on `main`. This bump is for the parser, not for the renderer. -sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-08-29d" } +sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-08-30" } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/sylpheed-export/src/audio.rs b/crates/sylpheed-export/src/audio.rs index cab3a0da..8239d2c6 100644 --- a/crates/sylpheed-export/src/audio.rs +++ b/crates/sylpheed-export/src/audio.rs @@ -208,6 +208,12 @@ pub struct Exported { /// Exists so a "known incomplete" warning fires on the gap and not on the /// mere presence of more than one stream. pub kept_waves: usize, + /// How many of `sub_waves` carry SIGNAL. A dropped stream that is digitally + /// silent is not missing content, and a warning that fires on it is crying + /// wolf: `S00A`'s third chunk is 93.694 s of exact zeroes, so dropping it + /// costs nothing and saying "KNOWN INCOMPLETE" over it would train a reader + /// to ignore the one case that means something. + pub content_waves: usize, /// What the runtime should do at the end of the file, where that was /// authored. `None` on a cue: a cue ends. pub loop_mode: Option, @@ -353,6 +359,7 @@ pub fn export_cues( loop_mode: None, sub_waves: 1, kept_waves: 1, + content_waves: 1, }); } Ok(done) @@ -523,6 +530,7 @@ pub fn export_bgm( loop_mode: spec.r#loop.clone(), sub_waves: staged.len(), kept_waves: 1, + content_waves: 1, })) } @@ -915,6 +923,7 @@ pub fn export_voice( loop_mode: None, sub_waves: riffs.len(), kept_waves: staged.len(), + content_waves: riffs.len() - silent.len(), })) } diff --git a/crates/sylpheed-export/src/main.rs b/crates/sylpheed-export/src/main.rs index e36d5973..21557efb 100644 --- a/crates/sylpheed-export/src/main.rs +++ b/crates/sylpheed-export/src/main.rs @@ -457,7 +457,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> { // export is known to be missing audio the game plays, and // the failure sounds like success: one stream decodes to // clean dialogue, so nobody listening finds out. - if a.kept_waves < a.sub_waves { + if a.kept_waves < a.content_waves { warnings.push(format!( "{}: KNOWN INCOMPLETE. This region holds {} streams and the RUNNING \ GAME DECODES ALL OF THEM CONCURRENTLY (Canary --xma_param_probe: \ @@ -477,7 +477,7 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> { describe(&a), a.kept_waves, a.sub_waves, - if a.kept_waves < a.sub_waves { " -- KNOWN INCOMPLETE, see warnings" } else { "" } + if a.kept_waves < a.content_waves { " -- KNOWN INCOMPLETE, see warnings" } else { "" } ); audio.push(ManifestAudio::from(a)); } @@ -517,7 +517,7 @@ impl From for ManifestAudio { why: a.why, peak_dbfs: a.peak_dbfs, duration_s: a.duration_s, - incomplete: (a.kept_waves < a.sub_waves).then(|| { + incomplete: (a.kept_waves < a.content_waves).then(|| { format!( "{} of {} streams. The running game decodes all {} concurrently. \ Nothing in the audio reveals the gap -- what plays is clean dialogue. \ diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 67530bdb..b6d53e4d 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -129 sections. Search this before re-deriving anything. +130 sections. Search this before re-deriving anything. * [P0 — the exporter, 2026-08-28](#p0--the-exporter-2026-08-28) * [P1 — Godot draws the screen, 2026-08-28](#p1--godot-draws-the-screen-2026-08-28) @@ -140,6 +140,7 @@ dies, which is what this file is for. * [Their stream assignment does not fit my region — weights NOT applied](#their-stream-assignment-does-not-fit-my-region--weights-not-applied) * [The resolver starts late, and my "duplicate tail" was a real stream all along](#the-resolver-starts-late-and-my-duplicate-tail-was-a-real-stream-all-along) * [The export knew the voice was incomplete; the runtime did not say so](#the-export-knew-the-voice-was-incomplete-the-runtime-did-not-say-so) +* [The voice export is complete — new pin, and the cause was a "within one bank" cap](#the-voice-export-is-complete--new-pin-and-the-cause-was-a-within-one-bank-cap) ## P0 — the exporter, 2026-08-28 @@ -7542,3 +7543,55 @@ why is not the same story twice** and a single sentence cannot carry both. *true* only for the case it was written against. It is the failure mode of every generic warning, and it is harder to see than a wrong number because the sentence is well-formed and confident in both places. + +## The voice export is complete — new pin, and the cause was a "within one bank" cap + +`formats-pin-2026-08-30`. The cause of the late start was a second condition on +the start filter: `end - s < 1_500_000`, *"only within one bank"*. `ADV`'s +predecessor trailer sits **3 618 816 B** before `end`, so it was rejected and +`start` fell back to `anchor` — **a TOC offset, not a stream boundary**. That is +exactly why it hit regions over 1.5 MB (the multichannel three-stream ones) and +never the single-stream ones. 17 of 95 movies took the fallback. + +| | before | after | +|---|---|---| +| `ADV` region | 3 114 352 B | **3 618 816 B** | +| `ADV` streams kept | 2 of 3 | **3 of 3 — complete** | +| `S00A` streams kept | 1 of 3 | **2 of 3**, the third digitally silent | +| `ADV` peak | −3.1 dBFS | −2.84 dBFS | + +✅ **The voice export is now complete for both movies**, closing a defect that was +open for this entire session — and my re-reading of the "duplicate tail" as a +start-truncated simultaneous stream is what the fix confirms. `ADV` drops **zero** +chunks; the runtime no longer prints its incomplete line. + +### 🔴 And the incompleteness warning was crying wolf on `S00A` + +With `ADV` fixed, `S00A` still read **KNOWN INCOMPLETE** at 2 of 3 — because its +third chunk is **93.694 s of exact zeroes**. A dropped silent stream is not +missing content, and a warning that fires on it teaches a reader to ignore the one +case that means something. + +`Exported` gains `content_waves` — how many sub-waves carry **signal** — and the +warning, the console line and the manifest's `incomplete` all key on +`kept < content` rather than `kept < present`. Both movies now report no gap, +correctly. + +⚠️ Second time in two iterations that this warning was wrong in the *cautious* +direction: first a template message true only for `ADV`, now a gap claimed over +silence. Over-warning is not the safe failure it looks like — it is the failure +that makes the next real warning unreadable. + +### A second movie, in the predicted direction + +They note only `ADV` has external ground truth; the other 16 rest on their own +sweep. **`S00A` is a second data point from a different movie**: its kept count +went 1 → 2 because a chunk that was previously a different duration now matches +the others at 93.694 s — exactly what restoring a truncated first stream +predicts. + +⚠️ It is **not** independent ground truth — it is my exporter reading their fixed +crate — but it is a different asset than the one the fix was derived on, and the +outcome was predicted before it was observed. Recorded as that and nothing more. + +Oracle rows unmoved; MODDING rules pass.