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(),
}))
}

View File

@@ -111,7 +111,7 @@ HANDOFF.
| P3/P5 — the title screen | **does the idle post-boot title show the `PRESS Ⓐ` plate?** | Q2 | 🔴 the port's boot ends on `title` (build 4), which has **no plate**, and P5 has just made Ⓐ the only way off it. Both states are captured — `live-title-build4-no-plate.png` and `live-title-press-a.png` — so the art is not the question; the **sequence** is: build 4 alone, build 4 with build 2 over it, or build 4 *then* the plate after a delay. Behavioural, so the port has no oracle for it. `press_start` (build 2) is already exported and unused. ⚠️ Fixing it also means drawing **two builds at once**, which this port has never done — a change to `ScreenView`, not a line in `flow.json`. Not blocking P5. |
| P5 — Ⓑ on the main menu | **is Ⓑ what returns to the title, or the idle timer?** | Q5 | 🟡 stated in HANDOFF, no capture behind it. The title self-returns after ~810 s idle, so one unrecorded observation cannot separate them. `authored/flow.json` implements it and marks it *authored — likely but UNPROVEN*. **Not blocking** — P5 shipped with it — but it is the only navigation rule on that screen with nothing under it. Settled by one run that presses Ⓑ well inside the idle window, timestamped. |
| P6 BGM — the sub-wave count | **is a music bank's LEADING REGION a stem, or a decoder artefact?** | Q10 | 🔴 **HANDOFF and the decoders disagree, and P6 ships the disagreement.** `media::sound_bank_riffs("BGM_103.slb")` returns **three** sub-waves; HANDOFF Q10's census says a music bank is *"exactly two waves of identical duration (32/32 banks on the disc)"*. The third comes from `slb.rs:380` `to_xma_riffs`, whose hybrid branch emits a leading headerless packet region ahead of the `RIFF` waves — and `docs/re/REFUTED.md` already records that region as what makes `BGM_106``BGM_109` *"break the two-wave rule"*. Derived at HANDOFF `9ca1eb5`. **The exporter sums all three and writes a manifest warning**, because choosing which sub-wave to drop is a decoding question and MISSION §2 forbids this exporter answering one. So the menu currently plays a sum of three things where the census predicts two. What settles it: whether that leading region carries music. Raised with the Decoder 2026-08-29. |
| ~~P6 BGM — the sub-wave count~~ | ~~is a music bank's LEADING REGION a stem, or a decoder artefact?~~ | Q10 | **CLOSED 2026-08-29 — the census was right and the port was summing a bank header into the music.** Decoded and timed, sub-wave 0 of `BGM_103`, `BGM_102` and `BGM_001` is identical: **10 300 B → 0.009 s, peak inf**, i.e. digitally silent. 10 300 B is the 10 240-byte bank header (the Decoder's disc-wide census) plus a 60-byte RIFF wrapper. So it is not a stem, and `export_bgm` had been counting it in the divisor — putting every real stem at 1/3 instead of 1/2, **3.52 dB of attenuation on all menu music shipped since P6**. Dropping a *silent* input is arithmetic, not a decoding decision, so this closed on the port's side; measured after the fix, `main_menu.ogg` goes 7.69 → **4.20 dBFS**, +3.49 dB against 3.52 predicted. Corroborates the Decoder's `c1f3608` from the other direction. The export now reports 2 sub-waves and the manifest warning is gone. |
| ~~P3 — the plate's ONSET~~ | ~~visible 2.13 s after settle, or group starts then?~~ | Q2 | ✅ **resolved 2026-08-29, and the answer is AUTHOR NOTHING.** The port's refutation held and produced a better answer than either option it offered. Correction at `5b0a6e6` on `auto/no-disc-and-menu-captures`: **both builds run on one clock, started together**, and the plate arrives at its own declared `t=238`. Checked against this export rather than taken on trust — build 4's visible build-in ends at `t=118` (`pteff01`, `pteff02`, `ptlogoall_eff` finish together), `ptbtn00` reaches alpha 255 at `t=238`, difference **120 units = 2.000 s**, against a measured 2.138 / 2.132 s at an emulator presenting 28.1 fps rather than 30. The 2.13 s constant is **deleted**. |
| ~~P3/P5 — `settle_time()`~~ | ~~`rest.t` is not when a screen settles, and the port's sequencer uses it~~ | — | ✅ **MEASURED 2026-08-29 and the row was HALF WRONG — mine.** The Decoder took it on a cold profile with no shader cache (`auto/no-disc-and-menu-captures` at `4bd4779`, `docs/re/boot-settle-times-measured.md`). The principle holds: the title's `rest.t` is 251 units = **4.183 s** where its art finishes at ~2 s. **But "everything the sequencer paces off that landmark is therefore late" does not.** Measured the port the way the game was measured — visible span, `--film` at 4 fps — the publisher wordmark runs **4.25 s** against the game's 4.297/4.604/4.370 and the developer logos **3.50 s** against 3.508/3.503/3.366. Dead on. My earlier reading compared the port's *arrival-to-arrival* timestamps against the game's *visible spans*, which differ by the exit ramp plus the black hold — the whole of the discrepancy I was about to chase. `rest.t` is still the wrong landmark; its blast radius is `_script_settled` waiting longer than it needs to, which is a slow test and not a wrong frame. `dwell_seconds` stays `null`, now for a measured reason. 🔴 **Do not author an Ⓐ→menu dwell**: it measures 3.763 s and contains a 1.53 s guest load stall, third independent reproduction. 🟡 Menu build-in 0.531 s and Ⓑ→title 0.482 s rest on one run and are not authored; the port is within ~0.1 s of both from the disc. |
| P4/P7 — the voice export is INCOMPLETE | **what are the three concurrent streams, and how do they combine?** | — | 🔴 **the premise of every earlier row here was refuted by the RUNNING GAME, 2026-08-29.** Canary's `--xma_param_probe` shows the game decoding **all three streams concurrently** in three XMA contexts, byte sizes matching the disc payloads exactly (1 294 336 / 1 118 208 / 1 171 456 against 1 294 396 / 1 118 268 / 1 171 516). So they are **not** three presentations of one take, there is no "which one" to answer, and the export — which ships one — is **missing two streams the game plays**. ⚠️ **The failure sounds like success**: one stream is clean audible dialogue. Stated as a top-level manifest warning per movie, on the console, and in `authored/audio.json`. **Behaviour deliberately unchanged**: an equal-gain `1/n` sum of channel pairs is not a downmix either (MISSION §6 pins an explicit matrix for exactly this reason) and summing cost `S00A` 6.02 dB when one stream was silence — swapping one guess for another is what produced this row twice. 🟡 "They are 5.1" is the Decoder's **hypothesis**: three stereo streams is six channels and N stereo streams is how XMA carries multichannel on the 360, but all three declare `ChannelMask = 0x0002` identically, which argues against distinct roles. 🔴 **The first attempt at that capture FAILED and the failure is measured, not suspected.** `adv-game-output-6ch.wav` (shared `1788018994-16f9d19d90b8`, taken at `68aa192`) contains neither the movie's bed, nor any of the three voice streams, nor `BGM_103`, nor `S00A`: every pairing is a plateau with a best-to-runner-up margin of **0.0010.016**, against controls that fire at r=1.000 with margins of +0.114 to +0.300 on the same instrument and the same data. Drift is excluded — 5 s windows give best lags of 4.95 / 15.30 / 119.35 / 50.75 / 29.35 / 83.95 s, scattered rather than monotonic. What that capture *is* has been handed back to the Decoder rather than guessed. **What still settles it: a recording of the game's own output over `ADV` through the PulseAudio null sink** — candidate combinations can then be correlated against what the game played. Asked 2026-08-29, emulator was up. |

View File

@@ -3312,3 +3312,61 @@ comes from the `.wmv`'s WMA track, and if the game never plays that track, then
`ADV.ogv`'s audio is wrong in a way no amount of transcode fidelity would fix. I
am not asserting that — it is a question about what the game does — but it is the
reason this is worth another boot rather than being written off.
## Every music bank was summed at 1/3 when only two sub-waves are music — 3.52 dB, since P6
The Decoder's message about `BGM_102` came with declared durations from the
corrected XMA1 `PsuedoBytesPerSec`, and checking my export against them turned up
a defect of mine that had been shipping since P6.
`export_bgm` summed every sub-wave `media` returned and scaled by `1/n`. Decoded
and timed, the three banks are identical in shape:
| bank | sub-wave 0 | sub-wave 1 | sub-wave 2 |
|---|---|---|---|
| `BGM_103` | **10 300 B → 0.009 s, peak inf** | 3 876 924 B → 87.744 s | 3 930 172 B → 87.744 s |
| `BGM_102` | **10 300 B → 0.009 s, peak inf** | 1 151 036 B → 37.482 s | 1 269 820 B → 37.482 s |
| `BGM_001` | **10 300 B → 0.009 s, peak inf** | 4 466 748 B → 173.809 s | 4 673 596 B → 173.809 s |
**Sub-wave 0 is digitally silent in all three**, and 10 300 B is 10 240 + a
60-byte RIFF wrapper — 10 240 B being exactly what the Decoder's disc-wide census
identifies as the bank header. So it is not a stem. Counting it in the divisor
put every real stem at 1/3 instead of 1/2: **3.52 dB of attenuation on all the
menu music this port has shipped since P6.**
Dropping it is **arithmetic, not a decoding decision** — a silent input
contributes nothing to a sum, and this is the same rule `export_voice` already
applies. Measured after the fix: `main_menu.ogg` goes **7.69 → 4.20 dBFS**,
**+3.49 dB** against 3.52 predicted, the remainder being Vorbis.
⚠️ **This is the third instance of one defect in this pipeline** — a silent chunk
in the voice sum, a silent channel in the mono fold, and now a silent sub-wave in
the music sum. Each was invisible in 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 to look for, not the individual bug.
### It also closes a 🔴 that has been open since P6
`docs/port/BLOCKED.md` carried *"`media::sound_bank_riffs` returns three
sub-waves where HANDOFF Q10's census says two"* as a disagreement the port shipped
deliberately. The census was right; the third was never a stem. The export now
reports **2 sub-waves** and the warning is gone — closed by measurement on my
side, corroborating the Decoder's `c1f3608` from a different direction (decoding
it, rather than counting headers).
### The declared-rate method, cross-checked a third time — and one correction
Their declared lengths against my decodes: `BGM_103` 87.750/87.749 vs **87.744**;
`BGM_102` 37.487 vs **37.482**; `BGM_001` 173.821 vs **173.809**. Agreement to
**512 ms** on three banks. The method is good for lengths.
🟢 **Refutation attempt, and the conclusion survives while the reasoning does
not.** The Decoder wrote that `BGM_001` reads *"173.821 s declared against your
decoded 167.663 s — a gap of 6.158 s"*, explaining it as *"declared is the
encoded stream, decoded is where the audio stops."* **A full decode of
`BGM_001` yields 173.809 s of PCM, not 167.663 s.** The 167.663 figure is where
the music *fades out*, measured from the audio; the stream then continues, silent,
to its declared end. So declared and decoded agree to 12 ms and the trailing
silence is *inside* the decode, not the difference between two methods. The
cross-check stands — better than stated, since it is now three banks rather than
a coincidence — and the sentence explaining it should go.