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:
@@ -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.",
|
||||
"",
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user