port: withdraw my own "two stems" reading of a voice region, and stop summing silence
The Decoder asked me to decode a voice region's leading chunk -- it has no XMA1
decoder in its container -- and the decoder run refuted a claim of mine that it
had already adopted into `docs/re/structures/voice-region-leading-chunk.md`.
I wrote that a region's two equal-length chunks are HANDOFF Q10's decoded
two-stem shape. Equal duration was a SHAPE match and I carried the music census
across on the strength of it. The content does not support it:
S00A chunk 2 is DIGITAL SILENCE -- 4497300 samples, peak -inf.
ADV chunk 2 is 0.60x chunk 1, best-fit scalar, residual 26.8 dB below the
target: about 95% of its energy is a -4.4 dB copy of the first chunk.
That cost real level. Summing chunk 1 with silence at 1/n put S00A's dialogue
6.02 dB down for nothing -- the exported file peaked at -16.2 dBFS against a
source chunk peaking at -4.2. `export_voice` now drops a digitally silent chunk
before the sum, which is arithmetic and not a judgement about content.
WHAT ADV'S NEAR-DUPLICATE SECOND CHUNK IS REMAINS OPEN AND IT IS STILL SUMMED.
Whether the game plays both is a decoding question, 26.8 dB of residual is not
nothing, and dropping a chunk because it correlates with another would be
answering it.
The leading chunk, answered as far as a measurement goes: ADV region + 1392, 394
packets, 84.553 s, stereo 48 kHz, peak -2.48 dBFS, 6 silent gaps over 0.4 s
totalling 45.3 s -- 54% silence, the same duty cycle as the full-length chunks.
Speech-structured, so not a header and not padding. "Cutscene or mission" is an
identification and this agent has no ears and no oracle; envelope correlation
peaks at 0.768 at the last lag in the search range, which is where a statistic
lands when it has found nothing, and it is not an answer.
Not taken yet, and said so in BLOCKED: the discriminator should be
`bank_header_len`, not a duration tie. This exporter never used `riffs.len()`, so
it already handles both of the Decoder's cases, but a tie is an observation and
`bank_header_len` is decoded. It switches when `c1f3608` reaches `main`;
`sylpheed-formats` is a path dependency and merging another agent's topic branch
is not the port's to do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF
This commit is contained in:
@@ -33,7 +33,14 @@ fn main() {
|
||||
.output()
|
||||
.unwrap();
|
||||
let dur = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
let _ = std::fs::remove_file(&w);
|
||||
if std::env::var("KEEP_WAV").is_ok() {
|
||||
let keep = std::path::Path::new(&std::env::var("KEEP_WAV").unwrap())
|
||||
.join(format!("{movie}_chunk{i}.wav"));
|
||||
let _ = std::fs::rename(&w, &keep);
|
||||
println!(" kept -> {}", keep.display());
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&w);
|
||||
}
|
||||
println!(" chunk {i}: {} bytes -> {dur} s", r.len());
|
||||
let _ = std::fs::remove_file(&p);
|
||||
}
|
||||
|
||||
@@ -535,18 +535,43 @@ pub fn export_voice<S: DiscSource + ?Sized>(
|
||||
// Classify before mixing. XMA declares no duration, so each chunk is decoded
|
||||
// and timed -- the only way to tell a stem from the leading region, and the
|
||||
// measurement that showed concatenation to be wrong here.
|
||||
let lengths: Vec<f32> = all.iter().map(|p| decoded_seconds(p).unwrap_or(0.0)).collect();
|
||||
let longest = lengths.iter().cloned().fold(0.0f32, f32::max);
|
||||
let probed: Vec<(f32, f32)> = all.iter().map(|p| decoded_chunk(p)).collect();
|
||||
let lengths: Vec<f32> = probed.iter().map(|&(d, _)| d).collect();
|
||||
// A DIGITALLY SILENT chunk is dropped before anything else, and that is
|
||||
// arithmetic rather than a judgement about content: it contributes nothing
|
||||
// to a mix, and counting it in the 1/n normalisation costs 6.02 dB for
|
||||
// nothing. `S00A`'s second full-length chunk is exactly this -- 4 497 300
|
||||
// samples of zeroes, peak -inf -- and summing it is why that movie's voice
|
||||
// came out at -16.2 dBFS against a source peaking at -4.2.
|
||||
let silent: Vec<usize> = (0..all.len()).filter(|&i| probed[i].1 <= -90.0).collect();
|
||||
let longest = (0..all.len())
|
||||
.filter(|i| !silent.contains(i))
|
||||
.map(|i| lengths[i])
|
||||
.fold(0.0f32, f32::max);
|
||||
// A tie at 1 ms. The two stems agree to six decimals and the chunk that is
|
||||
// not one of them misses by tens of seconds, so nothing sits near this
|
||||
// bound: it separates the measured cases without being a tuned threshold.
|
||||
let keep: Vec<usize> = (0..all.len())
|
||||
.filter(|&i| (longest - lengths[i]).abs() < 0.001)
|
||||
.filter(|&i| !silent.contains(&i) && (longest - lengths[i]).abs() < 0.001)
|
||||
.collect();
|
||||
let dropped: Vec<String> = (0..all.len())
|
||||
.filter(|i| !keep.contains(i))
|
||||
.map(|i| format!("chunk {i} ({:.3} s, {} B)", lengths[i], riffs[i].len()))
|
||||
.map(|i| {
|
||||
format!(
|
||||
"chunk {i} ({:.3} s, {} B, peak {})",
|
||||
lengths[i],
|
||||
riffs[i].len(),
|
||||
if silent.contains(&i) {
|
||||
"SILENT".to_string()
|
||||
} else {
|
||||
format!("{:.1} dBFS", probed[i].1)
|
||||
}
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if keep.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let staged: Vec<PathBuf> = keep.iter().map(|&i| all[i].clone()).collect();
|
||||
|
||||
// The fold is chosen from what the stream declares, because `pan` silently
|
||||
@@ -674,13 +699,17 @@ pub fn probe_duration(path: &Path) -> Option<f32> {
|
||||
String::from_utf8_lossy(&out.stdout).trim().parse().ok()
|
||||
}
|
||||
|
||||
/// Seconds one staged XMA `RIFF` decodes to.
|
||||
/// Seconds and peak dBFS that one staged XMA `RIFF` decodes to.
|
||||
///
|
||||
/// XMA carries no duration in its header, so this decodes the chunk to PCM and
|
||||
/// measures the result. That is expensive and it is the only instrument that can
|
||||
/// tell a stem from the leading region: `ffprobe` on the `RIFF` itself returns
|
||||
/// `N/A`, which a caller that trusted it would read as zero.
|
||||
fn decoded_seconds(riff: &Path) -> Option<f32> {
|
||||
/// XMA carries no duration in its header, so the chunk is decoded to PCM and the
|
||||
/// result measured. That is expensive and it is the only instrument that can
|
||||
/// separate these chunks at all: `ffprobe` on the `RIFF` itself returns `N/A`
|
||||
/// for duration, which a caller that trusted it would read as zero, and the
|
||||
/// corpus records `sylpheed-cli audio info` mis-reading the same headers as
|
||||
/// 16 channels at 4 310 Hz.
|
||||
///
|
||||
/// A silent chunk returns `-inf`, which the caller drops.
|
||||
fn decoded_chunk(riff: &Path) -> (f32, f32) {
|
||||
let wav = riff.with_extension("probe.wav");
|
||||
let ok = Command::new("ffmpeg")
|
||||
.args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
|
||||
@@ -689,7 +718,12 @@ fn decoded_seconds(riff: &Path) -> Option<f32> {
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false);
|
||||
let out = if ok { probe_duration(&wav) } else { None };
|
||||
let out = if ok {
|
||||
let (peak, dur) = measure(&wav);
|
||||
(dur.unwrap_or(0.0), peak.unwrap_or(f32::NEG_INFINITY))
|
||||
} else {
|
||||
(0.0, f32::NEG_INFINITY)
|
||||
};
|
||||
let _ = std::fs::remove_file(&wav);
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user