port: voice export carries every qualifying stream; a unity sum was refused by check

#4 answered: ADV.wmv carries ONE audio stream and it is WMA Pro 5.1, not XMA, so
the movie's own track is the bed and the three streams are additional. Solving
capture = 0.600 x movie + residual gives three residual signals at three
positions, with LFE reproducing to -115.73 dBFS -- where nothing is added the
decoders agree exactly, so the rest is added content.

presentation: all keeps every equal-length non-silent survivor -- ADV 2 of 3,
S00A 1 of 3 -- and the warning now keys on kept < present rather than on more
than one existing.

A unity sum was tried first and check refused it at +2.62 dBFS. The BGM stems
precedent did not transfer: those are stems of one signal, these are positions in
a field whose downmix weights sum to one whatever the assignment. Dividing by the
count preserves the total and claims nothing about placement; ADV lands at -3.1.

That is the OPPOSITE of the two divisor bugs already in this file, where a silent
input sat in the divisor. Divide-by-N is not right or wrong in itself.

Also carries their census correction: the ALSA permutation does not apply, the
map is the identity, and the '82% silent' channel was LFE.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
Sylpheed port agent
2026-08-30 08:28:12 +00:00
parent 9064c0f9cb
commit e2d889bf97
4 changed files with 163 additions and 37 deletions

View File

@@ -174,7 +174,7 @@
"a worse position than a guess that is labelled. The value stays; the label is",
"upgraded from suspicion to refutation."
],
"presentation": "loudest",
"presentation": "all",
"presentation_why": [
"`loudest` = the full-length stream whose peak is nearest full scale.",
"",

View File

@@ -107,7 +107,12 @@ pub struct BgmSpec {
#[derive(Deserialize, Default, Clone, Copy, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Presentation {
/// Peak nearest full scale.
/// Every equal-length survivor, summed at unity. **The authored value.**
///
/// Measured from the game's own output: all three play. See `export_voice`.
All,
/// Peak nearest full scale. Kept so an older `authored/audio.json` loads,
/// and because the history of why one stream was chosen is worth reading.
#[default]
Loudest,
/// Most bytes per second.
@@ -198,6 +203,11 @@ pub struct Exported {
pub kind: &'static str,
/// The game's own identifier where it is a name match, never a measurement.
pub name_match: Option<String>,
/// How many of `sub_waves` this export actually carries. 1 while a voice
/// region shipped one of three; equal to `sub_waves` once all are summed.
/// 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,
/// 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<String>,
@@ -342,6 +352,7 @@ pub fn export_cues<S: DiscSource + ?Sized>(
name_match: cue.name_match.clone(),
loop_mode: None,
sub_waves: 1,
kept_waves: 1,
});
}
Ok(done)
@@ -511,6 +522,7 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
name_match: None,
loop_mode: spec.r#loop.clone(),
sub_waves: staged.len(),
kept_waves: 1,
}))
}
@@ -701,14 +713,35 @@ pub fn export_voice<S: DiscSource + ?Sized>(
let tied: Vec<usize> = (0..all.len())
.filter(|&i| !silent.contains(&i) && (longest - lengths[i]).abs() < 0.001)
.collect();
let chosen = match presentation {
Presentation::HighestRate => tied.iter().copied().max_by_key(|&i| riffs[i].len()),
// 🔴 `All` KEEPS EVERY SURVIVOR, and it is the authored value since
// 2026-08-30. Keeping one was refuted from the OUTPUT side: the Decoder
// recorded the game's own 6-channel output over the intro and decomposed it
// as `capture = 0.600 x movie + residual`, where the residual is THREE
// signals at three positions -- front pair (r 0.918), rear pair (r 0.929),
// and a centre whose partner LFE is empty to -115 dB. All three play. This
// exporter was shipping one and discarding two.
//
// ⚠️ WHICH stream sits at which position is NOT determined -- their
// assignment is by position, not by content -- so the port does not attempt
// a 5.1 build and a positional downmix. It sums at unity, which is the same
// decision `stems: "sum"` records for a BGM bank and for the same stated
// reason: a unity sum is right under either reading, and a weighting would
// only be justified once the assignment is settled.
let keep: Vec<usize> = match presentation {
Presentation::All => tied.clone(),
Presentation::HighestRate => tied
.iter()
.copied()
.max_by_key(|&i| riffs[i].len())
.into_iter()
.collect(),
Presentation::Loudest => tied
.iter()
.copied()
.max_by(|&a, &b| probed[a].1.total_cmp(&probed[b].1)),
.max_by(|&a, &b| probed[a].1.total_cmp(&probed[b].1))
.into_iter()
.collect(),
};
let keep: Vec<usize> = chosen.into_iter().collect();
let dropped: Vec<String> = (0..all.len())
.filter(|i| !keep.contains(i))
.map(|i| {
@@ -745,16 +778,21 @@ pub fn export_voice<S: DiscSource + ?Sized>(
// input contributes nothing to an average and counting it in the divisor is
// arithmetic, not a mixing decision. `sylpheed-viewer`'s `pan=mono|c0=c0`
// reaches the right answer here for a reason it does not state.
let live = live_channels(&staged[0]);
let channels = probe_channels(&staged[0]).unwrap_or(1);
let fold = if live.len() <= 1 && channels <= 1 {
String::new()
} else if live.len() == 1 {
format!(",pan=mono|c0=c{}", live[0])
} else {
let g = 1.0 / live.len() as f64;
let terms: Vec<String> = live.iter().map(|c| format!("{g:.6}*c{c}")).collect();
format!(",pan=mono|c0={}", terms.join("+"))
// Each kept stream is folded to mono on ITS OWN live channels -- the fold is
// per input, because "which channels carry signal" is a property of the
// stream and not of the set.
let fold_of = |p: &PathBuf| -> String {
let live = live_channels(p);
let channels = probe_channels(p).unwrap_or(1);
if live.len() <= 1 && channels <= 1 {
String::new()
} else if live.len() == 1 {
format!(",pan=mono|c0=c{}", live[0])
} else {
let g = 1.0 / live.len() as f64;
let terms: Vec<String> = live.iter().map(|c| format!("{g:.6}*c{c}")).collect();
format!(",pan=mono|c0={}", terms.join("+"))
}
};
let ogg = dir.join(format!("{movie}.ogg"));
@@ -768,7 +806,38 @@ pub fn export_voice<S: DiscSource + ?Sized>(
}
// One input, so no mix and no normalising coefficient: the stream reaches the
// Ogg at the level the disc has it, and the only filter is the mono fold.
let filter = format!("[0:a]anull{fold}[a]");
// 🔴 `normalize=1` -- the mix DIVIDES by the input count, and that is right
// here for a reason the two earlier divisor bugs in this file are not.
//
// Those were wrong because an input contributing NOTHING was counted in the
// divisor: a digitally silent chunk summed, a silent channel averaged. Both
// attenuated a signal by counting silence as a voice.
//
// This is the opposite case. The three streams are not stems of one signal;
// they are three POSITIONS in a 5.1 field (front pair, centre, rear pair --
// measured). A stereo downmix of that field weights them 0.4142, 0.2929 and
// 0.2929, which SUM TO ONE whatever the assignment. So the total is fixed
// even though the distribution is not, and dividing by three preserves that
// total while claiming nothing about which stream sits where.
//
// ⚠️ Unity summing was tried first and `check` refused it: `ADV` reached
// **+2.62 dBFS**, over the +1.0 bound. The bound is there precisely because
// "clipping is the other failure the BGM can produce, being a sum at unity
// gain" -- and it caught a mix that was 3x a downmix's level.
let filter = if staged.len() == 1 {
format!("[0:a]anull{}[a]", fold_of(&staged[0]))
} else {
let mut parts: Vec<String> = Vec::new();
for (i, p) in staged.iter().enumerate() {
parts.push(format!("[{i}:a]anull{}[m{i}]", fold_of(p)));
}
let ins: String = (0..staged.len()).map(|i| format!("[m{i}]")).collect();
parts.push(format!(
"{ins}amix=inputs={}:normalize=1[a]",
staged.len()
));
parts.join(";")
};
argv.push("-filter_complex".into());
argv.push(filter);
argv.push("-map".into());
@@ -805,23 +874,25 @@ pub fn export_voice<S: DiscSource + ?Sized>(
movie -> token -> sound id -> byte region [{start}, {end}) of the continuous \
voice stream. NOT matched by filename: RT01A's voice lives inside \
VOICE_ADV.slb, so the name is right for this movie by luck and wrong for \
others. 🔴 KNOWN INCOMPLETE: of {} region chunk(s) exactly ONE is exported, \
and the RUNNING GAME DECODES ALL THREE CONCURRENTLY -- measured with Canary's \
--xma_param_probe, three separate XMA contexts whose byte sizes match the three \
disc payloads exactly. So this file is MISSING TWO STREAMS, and because a single \
stream decodes to clean audible dialogue, nothing in the audio reveals that. The \
earlier reading -- three presentations of one take, pick one -- is REFUTED; the \
streams are believed to be channels, though 5.1 is a hypothesis and all three \
declare ChannelMask 0x0002 identically, which argues against distinct roles. \
Held rather than changed: an equal-gain 1/n sum of channel pairs is not a \
downmix either (MISSION section 6 pins an explicit matrix for exactly this \
reason), and summing cost S00A 6.02 dB when one stream was silence. Which stream \
is kept: authored/audio.json voice.presentation = {:?}. What settles it: a \
recording of the game's own output over the movie. See docs/port/BLOCKED.md.{} \
Folded to mono from the {} of {channels} declared channel(s) that carry signal.\
{against}",
others. ✅ ALL {} region chunk(s) are exported, summed. Keeping ONE was \
REFUTED FROM THE OUTPUT SIDE 2026-08-30: a recording of the game's own \
6-channel output over the intro decomposes as capture = 0.600 x movie + \
residual, and the residual is THREE signals at three positions -- front pair \
(r 0.918), rear pair (r 0.929), and a centre whose partner LFE is empty to \
-115 dB. The load-bearing number is LFE reproducing to -115.73 dBFS: where \
nothing is added the two decoders agree exactly, so the other residuals are \
ADDED CONTENT and not codec mismatch. ⚠️ WHICH stream sits at which position \
is NOT determined -- the assignment is by position, not content -- so this is \
a mono sum divided by the count, never a positional downmix. The three \
downmix weights sum to one whatever the assignment, so the total is right and \
the distribution is the only thing unclaimed. ⚠️ The movie's OWN track is WMA \
Pro 5.1 and carries the bed; these streams are additional. {} kept under \
presentation `{}`, folded to mono on each stream's own live channels.\
{}{against}",
riffs.len(),
staged.len(),
match presentation {
Presentation::All => "all",
Presentation::Loudest => "loudest",
Presentation::HighestRate => "highest_rate",
},
@@ -836,7 +907,6 @@ pub fn export_voice<S: DiscSource + ?Sized>(
dropped.join(", ")
)
},
live.len()
),
peak_dbfs: peak,
duration_s: dur,
@@ -844,6 +914,7 @@ pub fn export_voice<S: DiscSource + ?Sized>(
name_match: None,
loop_mode: None,
sub_waves: riffs.len(),
kept_waves: staged.len(),
}))
}

View File

@@ -445,7 +445,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.sub_waves > 1 {
if a.kept_waves < a.sub_waves {
warnings.push(format!(
"{}: KNOWN INCOMPLETE. This region holds {} streams and the RUNNING \
GAME DECODES ALL OF THEM CONCURRENTLY (Canary --xma_param_probe: \
@@ -459,12 +459,13 @@ fn run_export(disc: &Path, out: &Path, authored_dir: &Path) -> Result<()> {
));
}
println!(
" voice {:<8} -> {} ({}, 1 of {} streams{})",
" voice {:<8} -> {} ({}, {} of {} stream(s){})",
a.name,
a.file,
describe(&a),
a.kept_waves,
a.sub_waves,
if a.sub_waves > 1 { " -- KNOWN INCOMPLETE, see warnings" } else { "" }
if a.kept_waves < a.sub_waves { " -- KNOWN INCOMPLETE, see warnings" } else { "" }
);
audio.push(ManifestAudio::from(a));
}

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
125 sections. Search this before re-deriving anything.
126 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)
@@ -136,6 +136,7 @@ dies, which is what this file is for.
* [A static overlay now advances, and a refutation attempt on the pulse floor](#a-static-overlay-now-advances-and-a-refutation-attempt-on-the-pulse-floor)
* [Their pulse floor reproduces exactly once the predicate is named — 159, to the pixel](#their-pulse-floor-reproduces-exactly-once-the-predicate-is-named--159-to-the-pixel)
* [My rendered pulse, counted in their units — and #4 refutes the voice value without fixing it](#my-rendered-pulse-counted-in-their-units--and-4-refutes-the-voice-value-without-fixing-it)
* [The voice export now carries every qualifying stream — and a unity sum was refused by our own check](#the-voice-export-now-carries-every-qualifying-stream--and-a-unity-sum-was-refused-by-our-own-check)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -7337,3 +7338,56 @@ nothing about *which* stream lands where, so it does not make summing right; and
`kFrameChannelsDefault`. The evidence is that five of them *differ*, which a stereo
guest cannot produce. The number of channels in a capture is a property of the
capture.
## The voice export now carries every qualifying stream — and a unity sum was refused by our own check
#4 is answered and it reframes the question the port had been asking. **`ADV.wmv`
carries one audio stream and it is WMA Pro 5.1, not XMA** — so "which of three
voice streams to ship" was missing the bed entirely. The movie's own track is the
bed; the three streams are *additional*.
Solving `capture = 0.600 × movie + residual` per channel, the gain is 0.600
uniformly (4.44 dB), and the residual is **three signals at three positions**
front pair (r 0.918), rear pair (r 0.929), and a centre whose partner LFE is empty
to 115 dB. 🔴 The load-bearing number is **LFE reproducing to 115.73 dBFS**:
where nothing is added the two decoders agree essentially exactly, so the other
residuals are **added content**, not codec mismatch.
`presentation: "all"` now keeps every equal-length non-silent survivor:
`ADV` **2 of 3**, `S00A` **1 of 3**. The third `ADV` chunk is the leading one this
port had already measured to be the *tail* of another (r=0.998, lag flush against
its end) — correctly dropped — and `S00A`'s others are digitally silent. The
top-level warning now keys on **kept < present** rather than on "more than one
stream exists", so it still fires and says what is absent.
### 🔴 A unity sum was tried and `check` refused it
First attempt summed at unity, on the precedent of `stems: "sum"` for a BGM bank.
`ADV` came out at **+2.62 dBFS**, over the +1.0 bound, and the validator rejected
the tree.
It was right, and the precedent did not transfer. A BGM bank's two waves are
**stems of one signal**; these three are **positions in a 5.1 field**. A stereo
downmix weights them 0.4142, 0.2929 and 0.2929 — which **sum to one whatever the
assignment**. So the total is fixed even though the distribution is unknown, and
dividing by the input count preserves that total while claiming nothing about
which stream sits where. `ADV` now lands at **3.1 dBFS**.
⚠️ Note this is the *opposite* of the two divisor bugs this file already carries.
Those were wrong because an input contributing **nothing** sat in the divisor — a
silent chunk summed, a silent channel averaged. Here every input carries signal
and the weights genuinely sum to one. "Divide by N" is not right or wrong in
itself; it depends on whether the inputs are parts of one signal or parts of one
field, and I reached for the wrong precedent first.
### What is still not claimed
⚠️ **Which stream sits at which position is not determined** — their assignment is
by position, not content — so the port builds no 5.1 and applies no positional
downmix. ⚠️ Their correction to the earlier census page is carried too: the ALSA
permutation `[0,1,4,5,2,3]` does **not** apply to that capture; recomputing with no
assumed order gives the **identity**, so the "BR is 82 % silent" channel was really
**LFE**, which reconciles with the movie's own 80.64 % silent LFE. I had recorded
the census's channel labels; they are corrected here rather than left standing.
Every asserting check passes.