port: fold only the channels that carry signal, and measure what the leading chunk actually is

TWO DEFECTS AND ONE MEASUREMENT, all from verifying the previous commit rather
than from reading it.

Channel 2 of both voice streams is DIGITALLY SILENT -- peak -inf over the whole
file. The voice is a mono recording carried in a nominally stereo stream, and
averaging it with silence cost 5.94 dB. The doc comment directly above the code
that did it warned that "a stereo matrix applied to a mono voice track is not an
error, it is a -6 dB attenuation that nothing reports", and then the code checked
the DECLARED channel count instead of the content. `live_channels` now measures
which channels carry signal and averages only those.

Three defects this iteration were the same shape: a silent chunk in a sum, a
silent channel in a fold, and a pan matrix naming channels that do not exist.
Each is an input contributing nothing while still counting in a divisor, and none
is visible in anything but a level.

THE LEADING CHUNK IS THE TAIL OF THE FULL-LENGTH ONE. The Decoder settled by
byte-span analysis that it is the movie's own dialogue, 17 of 17 -- killing its
own hypothesis that it was an in-mission line -- and asked whether dropping it is
a truncation, having no XMA1 decoder. Sliding envelope correlation with overhang
allowed and normalised over the overlap: ADV r=0.998 at +52.8 s, S00A r=0.932 at
+25.6 s, against controls of 1.000 (self) and 0.289 (a different movie). Both
lags put chunk 0 flush against the END of chunk 1. Sample domain, lag refined to
one sample then a scalar best-fit: residuals 16.70 dB and 23.15 dB below target.

So dropping it removes a DUPLICATE and the exporter was right for a worse reason
than it gave. The manifest note is NOT rewritten to claim that -- the structural
conclusion belongs on the Decoder's page, not in my manifest -- but it no longer
equates the chunk with BGM_103's third sub-wave, which a disc-wide census showed
is a different structure, and it now says in words that the omission must not be
read as junk removal.

Not converted, deliberately: the Decoder's 504464 B anchor constant. Bytes per
second is not constant even inside one region -- chunk 1 is 1118268 B and chunk 2
is 1171516 B for the SAME 137.324 s -- so any figure in seconds off it would be
invented.

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-29 15:07:30 +00:00
parent cb8d77febc
commit 4817e5ea9e
2 changed files with 182 additions and 24 deletions

View File

@@ -485,16 +485,24 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
/// performance, played together; do not concatenate* — arriving on a different
/// asset kind, which is why they are summed at `1/n` like [`export_bgm`]'s.
///
/// ⚠️ **Chunk 0 is dropped, and what it is remains an open decoding question.**
/// Its duration matches nothing: 84.6 s under a 137 s movie, 9 ms under
/// `RT01A`. `docs/re/REFUTED.md` records `slb.rs`'s `to_xma_riffs` hybrid branch
/// emitting a **leading headerless packet region** ahead of the real `RIFF`
/// waves, and `docs/port/BLOCKED.md` already carries that as an open row against
/// `BGM_103`, where `media` likewise returns three sub-waves where Q10's census
/// says two. This is the **same signature on a second, independent asset kind** —
/// corroboration, not proof. So the rule here is stated in terms of what was
/// measured — *keep the longest duration and everything that ties with it* — and
/// every dropped chunk is named in the manifest rather than quietly discarded.
/// ⚠️ **Chunk 0 is dropped, and dropping it may be a TRUNCATION.** This comment
/// first guessed it was the same thing as `BGM_103`'s third sub-wave — a bank
/// header — and a disc-wide census over all 95 English movie-voice regions
/// showed that is a *different structure*: 78 regions open with a 10 240 B bank
/// header, 17 with a leading headerless stream at the disc's own `1392 mod 2048`
/// data offset, and the chunk count discriminates neither. Then the byte-span
/// test settled what it holds: **the leading chunk is this movie's own dialogue,
/// 17 of 17** — not an in-mission line, which was the standing hypothesis.
///
/// It is dropped anyway, and only for this reason: **the region over-covers.**
/// Taking everything measures 2.6× the movie's length, which nothing explains
/// yet. So the omission is a *bounded* choice, not junk removal, and the
/// manifest says so in those words — a consumer must not read a dropped chunk
/// here as a defect the exporter cleaned up.
///
/// The selection rule is therefore stated in terms of what was measured — *keep
/// the longest duration and everything tying with it, minus anything digitally
/// silent* — and every dropped chunk is named with its length and peak.
/// * **Mono**, with the fold chosen from the stream's own declared channel
/// count rather than by passing `-ac 1` and hoping. A voice track that is
/// already mono is passed through untouched.
@@ -574,17 +582,32 @@ pub fn export_voice<S: DiscSource + ?Sized>(
}
let staged: Vec<PathBuf> = keep.iter().map(|&i| all[i].clone()).collect();
// The fold is chosen from what the stream declares, because `pan` silently
// ignores a channel the input does not have -- so a stereo matrix applied to
// a mono voice track is not an error, it is a 6 dB attenuation nobody sees.
// The fold averages the channels that CARRY SIGNAL, not the channels the
// stream declares.
//
// This function's first version averaged all declared channels, and the doc
// comment above it warned in as many words that "a stereo matrix applied to
// a mono voice track is not an error, it is a -6 dB attenuation that nothing
// reports". It then did exactly that: **channel 2 of both voice streams is
// digitally silent** -- peak -inf over the whole file, on `ADV` and on
// `S00A` -- so this is a mono recording carried in a nominally stereo
// stream, and averaging it with silence cost 5.94 dB. Checking the declared
// count is not checking the content, and only the content is the fold.
//
// Same principle as the silent-chunk drop above, one level down: a silent
// 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 = match channels {
0 | 1 => String::new(),
n => {
let g = 1.0 / n as f64;
let terms: Vec<String> = (0..n).map(|c| format!("{g:.6}*c{c}")).collect();
format!(",pan=mono|c0={}", terms.join("+"))
}
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("+"))
};
let ogg = dir.join(format!("{movie}.ogg"));
@@ -645,7 +668,10 @@ pub fn export_voice<S: DiscSource + ?Sized>(
others. Of {} region chunk(s), {} were SUMMED at 1/{} -- they are \
equal-duration and each spans the whole movie, which is HANDOFF Q10's decoded \
two-stem shape, so joining them end to end would play the dialogue twice.{} \
Folded to mono from {channels} channel(s).{against}",
Folded to mono from the {} of {channels} declared channel(s) that carry \
signal -- averaging a silent channel in would cost 6.02 dB, and channel 2 of \
both voice streams IS silent.{against}",
live.len(),
riffs.len(),
staged.len(),
staged.len(),
@@ -653,9 +679,14 @@ pub fn export_voice<S: DiscSource + ?Sized>(
String::new()
} else {
format!(
" DROPPED, matching no duration in this region and OPEN as a decoding \
question -- the same signature as BGM_103's third sub-wave, see \
docs/port/BLOCKED.md: {}.",
" DROPPED, and NOT because it is spurious -- the leading chunk is DECODED \
to be this movie's OWN dialogue, 17 of 17 regions (the Decoder's \
voice-region-leading-chunk.md; an earlier note here wrongly equated it \
with BGM_103's third sub-wave, which a disc-wide census showed is a \
different structure). It is dropped because the region OVER-COVERS: \
including everything measures 2.6x the movie's length. So this may be a \
TRUNCATION, it is an open decoding question, and a consumer must not read \
the omission as junk removal. See docs/port/BLOCKED.md: {}.",
dropped.join(", ")
)
}
@@ -727,3 +758,52 @@ fn decoded_chunk(riff: &Path) -> (f32, f32) {
let _ = std::fs::remove_file(&wav);
out
}
/// Which channel indices of a decoded stream are not digitally silent.
///
/// `astats` reports per-channel blocks: a `Channel: N` line followed by that
/// channel's own `Peak level dB`. A channel whose peak is `-inf` carries
/// nothing, and folding it into an average is a pure loss.
///
/// Falls back to "every declared channel is live" if the parse finds nothing,
/// because the failure to prefer is the one that changes no level.
fn live_channels(riff: &Path) -> Vec<usize> {
let wav = riff.with_extension("chan.wav");
let ok = Command::new("ffmpeg")
.args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
.arg(riff)
.arg(&wav)
.output()
.map(|o| o.status.success())
.unwrap_or(false);
let mut live = Vec::new();
if ok {
if let Ok(out) = Command::new("ffmpeg")
.args(["-hide_banner", "-v", "info", "-i"])
.arg(&wav)
.args(["-af", "astats", "-f", "null", "-"])
.output()
{
let text = String::from_utf8_lossy(&out.stderr).into_owned();
let mut current: Option<usize> = None;
for line in text.lines() {
if let Some((_, n)) = line.split_once("Channel: ") {
// astats numbers channels from 1; `pan` addresses c0 upward.
current = n.trim().parse::<usize>().ok().map(|n| n.saturating_sub(1));
} else if let Some((_, v)) = line.split_once("Peak level dB: ") {
if let Some(c) = current.take() {
if v.trim().parse::<f32>().map(|p| p > -90.0).unwrap_or(false) {
live.push(c);
}
}
}
}
}
}
let _ = std::fs::remove_file(&wav);
if live.is_empty() {
live = (0..probe_channels(riff).unwrap_or(1) as usize).collect();
}
live
}