port: the menu music was 3.52 dB quiet -- a bank header was being summed as a stem

Checking my export against the Decoder's declared XMA1 durations turned up a
defect of mine that has been shipping since P6.

`export_bgm` summed every sub-wave `media` returned and scaled by 1/n. Decoded
and timed, all three banks have the same shape:

  BGM_103  sub-wave 0: 10300 B -> 0.009 s, peak -inf   1: 87.744 s   2: 87.744 s
  BGM_102  sub-wave 0: 10300 B -> 0.009 s, peak -inf   1: 37.482 s   2: 37.482 s
  BGM_001  sub-wave 0: 10300 B -> 0.009 s, peak -inf   1: 173.809 s  2: 173.809 s

Sub-wave 0 is DIGITALLY SILENT in all three, and 10300 B is 10240 plus a 60-byte
RIFF wrapper -- 10240 being exactly the bank header the Decoder's census
identifies. Counting it in the divisor put every real stem at 1/3 instead of 1/2:
3.52 dB on all the menu music since P6. Dropping a silent input is arithmetic,
not a decoding decision. Measured after: main_menu.ogg -7.69 -> -4.20 dBFS,
+3.49 dB against 3.52 predicted.

THIRD INSTANCE OF ONE DEFECT: a silent chunk in the voice sum, a silent channel
in the mono fold, now a silent sub-wave in the music sum. Each invisible to every
check except a level, and each time the divisor was computed from how many inputs
there are rather than how many carry signal. That is the shape, not the bug.

Closes a red row open since P6 -- "sound_bank_riffs returns three sub-waves where
Q10's census says two". The census was right, and this corroborates the Decoder's
c1f3608 by decoding rather than by counting headers. The export reports 2
sub-waves and the warning is gone.

REFUTATION ATTEMPT, conclusion survives and the reasoning does not: the Decoder
explained BGM_001 as "173.821 s declared against your decoded 167.663 s, a gap of
6.158 s -- declared is the encoded stream, decoded is where the audio stops." A
full decode yields 173.809 s of PCM, not 167.663. The 167.663 is where the music
FADES OUT, measured from the audio; the stream continues silent to its declared
end. Declared and decoded agree to 12 ms, and the trailing silence is inside the
decode rather than the difference between two methods. The cross-check is
stronger than stated -- three banks, 5-12 ms -- and the explanation should go.

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 16:19:31 +00:00
parent 6210c2e131
commit ff04fbce52
4 changed files with 150 additions and 9 deletions

View File

@@ -0,0 +1,45 @@
//! Throwaway probe: what are a music bank's sub-waves, decoded and timed?
//!
//! `export_bgm` sums every sub-wave `media` returns and scales by 1/n. If one of
//! them is not music, the divisor is wrong and every real stem is attenuated for
//! nothing -- the same defect already found and fixed in `export_voice`.
use std::process::Command;
use sylpheed_formats::media;
fn main() {
let disc = std::env::var("SYLPHEED_DISC").unwrap_or_else(|_| "/disc".into());
let src = media::DirectorySource::new(&disc);
for bank in ["BGM_103.slb", "BGM_102.slb", "BGM_001.slb"] {
match media::sound_bank_riffs(&src, bank) {
Ok(riffs) => {
println!("{bank}: {} sub-wave(s)", riffs.len());
for (i, r) in riffs.iter().enumerate() {
let p = std::env::temp_dir().join(format!("bk_{i}.xma.wav"));
std::fs::write(&p, r).unwrap();
let w = std::env::temp_dir().join(format!("bk_{i}.wav"));
let _ = Command::new("ffmpeg")
.args(["-hide_banner", "-loglevel", "error", "-y", "-i"])
.arg(&p).arg(&w).output();
let out = Command::new("ffmpeg")
.args(["-hide_banner", "-v", "info", "-i"])
.arg(&w)
.args(["-af", "astats=measure_perchannel=none", "-f", "null", "-"])
.output().unwrap();
let t = String::from_utf8_lossy(&out.stderr).into_owned();
let get = |k: &str| t.lines().find_map(|l| l.split_once(k).map(|x| x.1.trim().to_string()))
.unwrap_or_else(|| "?".into());
let dur = Command::new("ffprobe")
.args(["-v","error","-show_entries","format=duration","-of","csv=p=0"])
.arg(&w).output().ok()
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_default();
println!(" sub-wave {i}: {:>9} B -> {:>10} s peak {:>10} rms {}",
r.len(), dur, get("Peak level dB:"), get("RMS level dB:"));
let _ = std::fs::remove_file(&p);
let _ = std::fs::remove_file(&w);
}
}
Err(e) => println!("{bank}: {e}"),
}
}
}

View File

@@ -384,9 +384,35 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
let dir = out.join("audio/bgm");
std::fs::create_dir_all(&dir)?;
let mut staged = Vec::new();
let mut all = Vec::new();
for (i, riff) in riffs.iter().enumerate() {
staged.push(stage_riff(&dir, &format!("{role}.{i}"), riff)?);
all.push(stage_riff(&dir, &format!("{role}.{i}"), riff)?);
}
// DROP DIGITALLY SILENT SUB-WAVES BEFORE SUMMING -- arithmetic, not a
// decoding decision, and the same rule `export_voice` already applies.
//
// Every music bank returns THREE sub-waves where HANDOFF Q10's census says
// two, and the extra one is identical in all three banks measured:
//
// BGM_103 / BGM_102 / BGM_001 sub-wave 0: 10 300 B -> 0.009 s, peak -inf
//
// 10 300 B is 10 240 + a 60-byte RIFF wrapper, and 10 240 B is exactly what
// the Decoder's disc-wide census identifies as the BANK HEADER. So it is not
// a stem, it is silence, and counting it in the divisor attenuated every
// real stem by 1/3 instead of 1/2 -- **3.52 dB, on all the menu music this
// port has shipped since P6**. A silent input contributes nothing to a sum;
// including it in the normalisation is my error, not a judgement about
// content.
let quiet: Vec<usize> = (0..all.len())
.filter(|&i| decoded_chunk(&all[i]).1 <= -90.0)
.collect();
let staged: Vec<PathBuf> = (0..all.len())
.filter(|i| !quiet.contains(i))
.map(|i| all[i].clone())
.collect();
if staged.is_empty() {
return Ok(None);
}
let ogg = dir.join(format!("{role}.ogg"));
@@ -414,18 +440,30 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
let command = format!("ffmpeg {}", argv.join(" "));
run_ffmpeg(&argv, &ogg)?;
let (peak, dur) = measure(&ogg);
for s in &staged {
for s in &all {
std::fs::remove_file(s).ok();
}
let mut why = format!(
"{} authored/audio.json bgm.{role} names bank {}. Its {} \
sub-wave(s) are SUMMED into one file and the sum is scaled by 1/{}, \
which is the smallest constant that cannot clip.",
"{} authored/audio.json bgm.{role} names bank {}. Of its {} \
sub-wave(s), {} are SUMMED into one file and the sum is scaled by 1/{}, \
which is the smallest constant that cannot clip.{}",
spec.why,
spec.bank,
riffs.len(),
riffs.len()
staged.len(),
staged.len(),
if quiet.is_empty() {
String::new()
} else {
format!(
" DROPPED {} DIGITALLY SILENT sub-wave(s) before summing -- each 10 300 B \
decoding to 0.009 s at peak -inf, which is the 10 240-byte BANK HEADER plus \
a RIFF wrapper, not a stem. Counting them in the divisor attenuated every \
real stem by 3.52 dB. This is arithmetic, not a decoding decision.",
quiet.len()
)
}
);
if let Some(s) = &spec.stems_why {
why.push(' ');
@@ -446,7 +484,7 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
kind: "bgm",
name_match: None,
loop_mode: spec.r#loop.clone(),
sub_waves: riffs.len(),
sub_waves: staged.len(),
}))
}