From 93b823eedfe826f65e6061de8ee1b8895e16d13f Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Sun, 30 Aug 2026 09:39:52 +0000 Subject: [PATCH] port: the menu bed loops at 61.93 s -- my 3.4 s seam was ours, not the game's Measured on 240 s parked on the real menu: no seam at all (zero runs >=0.3 s below median-18 dB in 232 s), and the loop is at 61.93 s, not the summed wave's 87.744 s length. Two instruments agree -- top correlation lag 61.909 s with r=-0.009 at 87.750, and slice location showing playback wrap at 61.93 three times. The final ~25.8 s, the fade-out and trailing silence, is never played. This port had authored loop: restart, measured the resulting 3.4 s of near-silence off its own Master bus, and reported it as the price of the missing loop point -- in BLOCKED and in a message to the Decoder. The measurement was correct and the attribution was wrong. Second time this fortnight I have measured something real and assigned it to the wrong side. Godot loops whole files, so the loop region has to BE the file: BgmSpec gains loop_end_s and the bed is trimmed to 61.930 s. Verified over 131 s on the port's own bus -- 5 near-silent windows, no run >=0.2 s, against 36 windows and 3.4 s. Also records their withdrawal of '8 of 10 three-chunk regions': that audit file was truncated mid-list with no summary line, so the multichannel-specificity claim is unsupported and my 25 stands. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF --- authored/audio.json | 30 ++++++++++++- crates/sylpheed-export/src/audio.rs | 18 ++++++++ docs/port/DECISIONS.md | 66 ++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/authored/audio.json b/authored/audio.json index 4f4cbe9e..5e58b836 100644 --- a/authored/audio.json +++ b/authored/audio.json @@ -105,7 +105,35 @@ "why": "MEASURED, HANDOFF Q10 -- NOT a port choice. `GamePart_Title`'s phase handler `sub_821C5580` plays cue 1103 = `BGM_103`, and `BGM_103.slb`'s two declared waves (3 876 864 / 3 930 112 B) are byte-for-byte the two streams the XMA probe saw decoding at the main menu. Static code, disc census and runtime all agree; see docs/re/menu-audio-cues.md and docs/re/structures/bgm-two-stems.md. The name carries its `.slb` extension because that is what `sound.pak` hashes -- `BGM_103` alone resolves to nothing, which is how the first draft of this file failed.", "loop_why": "AUTHORED, and it will be AUDIBLY WRONG AT THE SEAM. HANDOFF Q10: no loop-point field has been identified anywhere, so the file carries no marked restart. `restart` replays from sample 0, which means the listener hears the track's own fade-out and the silence after it before the music comes back. Trimming to the fade would sound better and would be worse: it would INVENT a loop point, and an invented one is indistinguishable from a decoded one a month later. The seam stays ugly until a loop point is measured or a capture of the real menu looping settles it. ⚠️ MEASURED 2026-08-29, and it is worse than 'ugly': the bed loops at 87.8 s against the track's own 87.7 s, and the gap before the music returns is about 3.4 SECONDS of near-silence (36 consecutive 50 ms windows below peak 300, spanning 84.40-87.80 s, after a fade from RMS 2057 to 431). Recorded off the port's own Master bus, so it is what a player hears, not a property of the file. That is long enough to read as the music having stopped rather than looped. The number is here to make the cost of the missing loop point concrete; it does NOT license trimming to the fade, which would still invent a loop point. See docs/port/DECISIONS.md.", "stems": "sum", - "stems_why": "MEASURED, HANDOFF Q10: a bank is exactly TWO waves of identical duration (32/32 banks on the disc), sample-synchronous -- transient correlation peaks at lag 0.00 s over +/-5 s and both stop at the same millisecond. Concatenating them plays the piece twice, the second time as a bass-less stem; that was the previous reading and it is refuted. Emitting two files would be wrong for a second reason: MODDING rule 1 is one logical asset, one file, and handing a modder two stems to line up by hand is the reassembly the exporter exists to have already done. WHAT IS SUMMED IS SETTLED; WHAT WAVE 1 IS, IS NOT -- HANDOFF calls it quieter, far more L/R-decorrelated and almost bass-free, so it reads as a surround-rear pair OR a second intensity layer, and `ChannelMask` is 0x0002 on both so the file will not say. A unity sum is right under either reading; a weighting would only be justified once that is settled." + "stems_why": "MEASURED, HANDOFF Q10: a bank is exactly TWO waves of identical duration (32/32 banks on the disc), sample-synchronous -- transient correlation peaks at lag 0.00 s over +/-5 s and both stop at the same millisecond. Concatenating them plays the piece twice, the second time as a bass-less stem; that was the previous reading and it is refuted. Emitting two files would be wrong for a second reason: MODDING rule 1 is one logical asset, one file, and handing a modder two stems to line up by hand is the reassembly the exporter exists to have already done. WHAT IS SUMMED IS SETTLED; WHAT WAVE 1 IS, IS NOT -- HANDOFF calls it quieter, far more L/R-decorrelated and almost bass-free, so it reads as a surround-rear pair OR a second intensity layer, and `ChannelMask` is 0x0002 on both so the file will not say. A unity sum is right under either reading; a weighting would only be justified once that is settled.", + "loop_end_s": 61.93, + "loop_end_why": [ + "MEASURED off the running game 2026-08-30, 240 s parked on the menu", + "(docs/re/structures/menu-bgm-loop-measured.md). The bed loops at 61.93 s, NOT", + "at the summed wave's 87.744 s length, and the last ~25.8 s is never played --", + "exactly the fade-out and trailing silence bgm-two-stems.md found. The game", + "loops BEFORE the fade.", + "", + "πŸ”΄ THIS CORRECTS AN AUTHORED VALUE THAT WAS WRONG IN BOTH DIRECTIONS. `restart`", + "at the wave's end produced a seam of about 3.4 SECONDS of near-silence, and", + "this port measured that seam off its own Master bus and recorded it as the", + "cost of a missing loop point. It was not the game's seam; it was OURS. Zero", + "runs of >=0.3 s below median-18 dB appear in 232 s of the real menu.", + "", + "Two instruments agree: correlation gives a top lag of 61.909 s and r = -0.009", + "at 87.750 s, and locating 30 s slices inside the decoded waves shows playback", + "advancing exactly +5.00 s per 5 s and wrapping at 61.93 s, three times, with a", + "control that finds slices cut at 10/45/70 s at 10.00/45.00/70.00.", + "", + "⚠️ The loop START is inferred, not measured: [0.0, 61.93) and [0.25, 62.18) are", + "not separated at their resolution. The port takes 0 because a bank's own start", + "is where its data begins, and records that the choice was not measured.", + "", + "⚠️ Godot loops a WHOLE FILE, so the export is TRIMMED to 61.93 s rather than", + "carrying a loop point the runtime could not honour. The trimmed tail is", + "content the game never reaches, so nothing playable is lost -- but a modder", + "replacing this file is replacing the loop region, not the whole bank." + ] } }, "voice": { diff --git a/crates/sylpheed-export/src/audio.rs b/crates/sylpheed-export/src/audio.rs index 3a8f2273..c5ace508 100644 --- a/crates/sylpheed-export/src/audio.rs +++ b/crates/sylpheed-export/src/audio.rs @@ -91,6 +91,17 @@ pub struct BgmSpec { /// It is ASSERTED rather than implemented: a weighted mix is not written, /// and inventing one would be a level decision nobody measured (HANDOFF Q10 /// settles that the two waves are summed, not what wave 1 *is*). + /// Seconds of the summed bank the game actually plays before wrapping. + /// `None` = the whole wave. + /// + /// πŸ”΄ Godot loops a WHOLE FILE, so a loop region has to BE the file. The + /// exporter therefore trims to this length rather than carrying a loop + /// point the runtime could not honour, and the trimmed tail is content the + /// game never reaches. + #[serde(default)] + pub loop_end_s: Option, + #[serde(default)] + pub loop_end_why: Option, #[serde(default)] pub stems: Option, #[serde(default)] @@ -501,6 +512,13 @@ pub fn export_bgm( n = staged.len() )); } + // πŸ”΄ TRIM TO THE MEASURED LOOP REGION. Godot loops a whole file, so the + // region has to be the file; carrying a loop point the runtime cannot + // honour would leave the fade-out playing every cycle. + if let Some(end) = spec.loop_end_s { + argv.push("-t".into()); + argv.push(format!("{end}")); + } argv.extend( ["-c:a", "libvorbis", "-q:a", VORBIS_Q, &ogg.display().to_string()] .iter() diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 98e5e4c2..63a2d136 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -133 sections. Search this before re-deriving anything. +134 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) @@ -144,6 +144,7 @@ dies, which is what this file is for. * [The positional weights are applied β€” keyed by byte size, so the key is a check](#the-positional-weights-are-applied--keyed-by-byte-size-so-the-key-is-a-check) * [πŸ”΄ Unexplained: `verify-menu-audio`'s dead-press check has started failing](#unexplained-verify-menu-audios-dead-press-check-has-started-failing) * [External ground truth for every three-chunk region β€” the movies' own durations](#external-ground-truth-for-every-three-chunk-region--the-movies-own-durations) +* [The menu bed loops at 61.93 s β€” and my 3.4 s "ugly seam" was mine, not the game's](#the-menu-bed-loops-at-6193-s--and-my-34-s-ugly-seam-was-mine-not-the-games) ## P0 β€” the exporter, 2026-08-28 @@ -7704,3 +7705,66 @@ truncated-log trap, the `--screen` default at t=0, and the checker whose input w smaller-but-valid. Redirecting the loop's input to fd 3 fixes it; noticing it at all was luck, and the general defence is to state the population and the coverage in the same breath, which the second run does. + +## The menu bed loops at 61.93 s β€” and my 3.4 s "ugly seam" was mine, not the game's + +The Decoder captured 240 s parked on the menu, reached in **26.8 s** via the +XMA-log oracle they wrote down rather than a screenshot. Two findings, and both +cut against what this port had authored: + +* πŸ”΄ **No seam.** Zero runs β‰₯0.3 s below medianβˆ’18 dB in 232 s of the real menu. +* πŸ”΄ **Not the wave's length.** r = **βˆ’0.009** at 87.750 s; top lag **61.909 s**. + A second instrument agrees β€” 30 s slices located inside the decoded waves show + playback advancing exactly +5.00 s per 5 s and wrapping at **61.93 s**, three + times, against a control that finds slices cut at 10/45/70 s at 10.00/45.00/70.00. + +The loop is **[β‰ˆ0, 61.93)** of an 87.744 s wave, so the final ~25.8 s β€” the +fade-out and trailing silence β€” is **never played**. The game loops before the +fade. + +### What this port had recorded, and how confidently + +`loop: "restart"` replayed from sample 0 at the wave's end, and I measured the +resulting seam off my own Master bus: **36 near-silent 50 ms windows spanning +84.40–87.80 s, about 3.4 seconds**. I wrote that up as *the price of a missing +loop point*, put it in `BLOCKED.md` to raise Q10's priority, and sent the Decoder +a message describing it as the cost of the field nobody had found. + +**It was our seam.** The measurement was correct and the attribution was wrong β€” +a defect in the port's own loop, reported as a property of the disc. + +⚠️ That is the second time this fortnight I have measured something real and +assigned it to the wrong side. The first was reading a start-truncated stream as +a duplicate tail. Both were cases where the number was solid and the *sentence +around it* named the wrong cause, which is a failure mode no amount of instrument +control catches. + +### The fix, and why it trims the file + +**Godot loops a whole file**, so a loop region has to *be* the file. `BgmSpec` +gains `loop_end_s` and the exporter trims: the bed is now **61.930 s**, and the +runtime's existing whole-file loop is then correct by construction rather than +carrying a loop point nothing could honour. + +βœ… Verified on the port's own bus over 131 s: **5** near-silent 50 ms windows, no +run β‰₯0.2 s β€” against 36 windows and 3.4 s before. The seam is gone. + +⚠️ Recorded limits: the loop **start** is inferred, since [0.0, 61.93) and +[0.25, 62.18) are not separated at their resolution β€” the port takes 0 because a +bank's data begins there, and says the choice was not measured. And a modder +replacing `main_menu.ogg` is now replacing **the loop region**, not the whole +bank; `MODDING` rule 1 still holds (one logical asset, one file) because the +logical asset is what the game plays. + +### πŸ”΄ And their "8 of 10" was a truncated file, not a count + +They have withdrawn it: the audit run was cut short, the committed file ends +mid-list at `S11A` **with no summary line**, and they read a partial file as a +complete one. So *"the defect is specific to multichannel regions"* is now +**unsupported β€” possibly true, not shown**, and my 25 stands unopposed. + +πŸ“Œ Their tell and mine were the same on the same day, from opposite directions: +their table had **no summary line** and mine had a **population that didn't match +a count from one command earlier**. The defence that covers both is to state the +population and the coverage together β€” and theirs adds a second: **refuse to read +a table whose summary line is missing.**