From be5a982340c558a38776d17bf5fc9bbcc02be909 Mon Sep 17 00:00:00 2001 From: Sylpheed port agent Date: Sun, 30 Aug 2026 10:17:51 +0000 Subject: [PATCH] port: the loop window is measured -- ss 9.44 t 61.87 -- and near-silence tracked the error Region [9.44, 71.31] of an 87.744 s wave: first 9.44 s an intro played once, last 16.4 s a fade never played. 61.87 replaces 61.93, 0.1 percent apart; the measured value has the loop's own endpoints under it. The port's near-silence count tracked the window's correctness without being designed to: 36 frames spanning 3.4 s untrimmed, 5 with the right length over the wrong window, 0 with the measured window. The old window kept part of the quiet intro; the measured one excludes intro and fade both. Wrap continuity is unchanged and still not evidence -- the silence count discriminated where the step count could not. Also fixes a stale why that reached the manifest for two days: corrections went into loop_end_why and loop_start_why while loop_why, the field the exporter concatenates, still said the loop would be audibly wrong at the seam and that no loop-point field existed. A correction that does not reach the artifact a consumer reads has not been made. My first check of that fix reported the stale text still present, because the replacement quotes the refuted sentences in order to name them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01N7FiFFFwbvG2uxdcEh8HyF --- authored/audio.json | 45 ++++++++++++++++++-------------- docs/port/DECISIONS.md | 59 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 20 deletions(-) diff --git a/authored/audio.json b/authored/audio.json index d5d28397..909c9dc5 100644 --- a/authored/audio.json +++ b/authored/audio.json @@ -103,31 +103,31 @@ "loop": "restart", "kind": "measured", "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.", + "loop_why": "MEASURED, and this field's own history is why it says so first. The bed loops; the loop is a RUNTIME field -- `loop_start`/`loop_end` in the XMA decoder context, set by `XMASetLoopData` and logged by Xenia -- and the cycle was watched directly: three wraps, both contexts wrapping at the same instant every time, mean 61.81 s against the 61.93 s authored in `loop_end_s`, 0.2 % apart from instruments sharing nothing. The export is TRIMMED to that window, because Godot loops a whole file and a loop region therefore has to BE the file. ⚠️ The window's START is not measured and is authored as 0, which is known to be wrong -- see `loop_start_why`. 🔴 EVERY SENTENCE THAT PRECEDED THIS ONE WAS REFUTED, and the previous text survived in the manifest for two days after the corrections were written. It said the loop would be `AUDIBLY WRONG AT THE SEAM`, that `no loop-point field has been identified anywhere`, and that trimming `would INVENT a loop point`. All three are false: the field exists, the 3.4 s of near-silence was the PORT'S loop and not the game's, and the trim is now what the measurement says. The corrections went into `loop_end_why` and `loop_start_why`; this field is the one the exporter concatenates into `manifest.json`, so the export went on telling readers the refuted story. A correction that does not reach the artifact a consumer reads has not been made.", "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.", - "loop_start_s": 0.0, + "loop_start_s": 9.44, "loop_start_why": [ - "🔴 ZERO, AND KNOWN TO BE WRONG. NOT MEASURED.", + "MEASURED 2026-08-30 -- 9.44 s. The loop region is [9.44 s, 71.31 s] of an", + "87.744 s wave: the first 9.44 s is an intro played ONCE, and the last 16.4 s", + "is a fade-out never played at all.", "", - "The loop window begins about TEN SECONDS into the bank, not at 0:", - "`loop_start` sits at 3.6 M bits, 11.6 % of the stream (RE agent", - "2026-08-30, menu-bgm-loop-fields-conflict.md, RESOLVED section). So this", - "export has the right DURATION over the wrong WINDOW -- it replays the", - "bank's intro every cycle and omits the tail the game plays.", + "Two derivations, both stems, and NEITHER converts bits to seconds -- the", + "conversion that refuted itself earlier by giving two sample-synchronous stems", + "62.34 and 63.29 s. (a) time to `read_offset` crossing `loop_start`, plus a", + "1.33 s head correction at a LOCALLY measured rate; (b) first pass minus cycle.", + "9.44 s on both stems either way.", "", - "⚠️ It is still 0 because the exact start is NOT MEASURED and a guess", - "would be worse than a flagged wrong value. Linear back-extrapolation", - "gives ~9-13 s, and linearity is refuted by the same run: the bit rate", - "varies 4.4 % within one stream. Their fix is one line -- start the trace", - "before tapping into the menu -- and it is their next iteration.", + "⚠️ ONE BOOT, ONE BANK. The decoder reads ahead of playback, but both endpoints", + "are `read_offset` events so the lead cancels in the difference.", "", - "This field exists SEPARATELY from `loop_end_s` so the assumption is", - "visible. Carrying only an end silently asserted a start of zero, and an", - "assumption a reader has to infer from a missing field is not one they", - "can weigh. When the start is measured this becomes a one-value edit." + "🔴 THIS FIELD WAS 0.0 AND FLAGGED WRONG FOR ONE ITERATION, deliberately. The", + "value was not guessable -- linear back-extrapolation said 9-13 s and linearity", + "is refuted by a 4.4 % rate variation within one stream. What made the wait", + "cheap was that the field EXISTED and the `-ss`/`-t` ordering had been proved", + "with a stand-in value, so arriving at 9.44 was a one-value edit." ], - "loop_end_s": 61.93, + "loop_end_s": 61.87, "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", @@ -196,7 +196,14 @@ "the wave. Both contexts wrapping together is the sample-synchrony the linear", "bit conversion could not produce.", "", - "So the LENGTH is settled and the WINDOW is not. See `loop_start_why`." + "So the LENGTH is settled and the WINDOW is not. See `loop_start_why`.", + "", + "✅ 61.87 ADOPTED 2026-08-30, replacing 61.93. Their wrap timing gives 61.87 --", + "wraps at 96.46 / 158.33 / 220.21 s, gaps 61.87 and 61.87 -- against the 61.93", + "this port's autocorrelation gave. 0.1 % apart. The measured value is taken", + "because it is the one with the loop's own endpoints under it; the", + "autocorrelation never touched the wave and agreed to a tenth of a percent,", + "which is what makes both worth having." ] } }, diff --git a/docs/port/DECISIONS.md b/docs/port/DECISIONS.md index 88c0b908..b8127875 100644 --- a/docs/port/DECISIONS.md +++ b/docs/port/DECISIONS.md @@ -9,7 +9,7 @@ dies, which is what this file is for. -138 sections. Search this before re-deriving anything. +139 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) @@ -149,6 +149,7 @@ dies, which is what this file is for. * [Independent confirmation of the 1.5 MB cap — the mechanism, not just the conclusion](#independent-confirmation-of-the-15-mb-cap--the-mechanism-not-just-the-conclusion) * [The loop is a runtime field, the two readings conflict, and the port keeps what it shipped](#the-loop-is-a-runtime-field-the-two-readings-conflict-and-the-port-keeps-what-it-shipped) * [The duration is confirmed and the window is wrong — and the start is now a visible field](#the-duration-is-confirmed-and-the-window-is-wrong--and-the-start-is-now-a-visible-field) +* [The loop window is measured — `-ss 9.44 -t 61.87` — and the near-silence count tracked the error](#the-loop-window-is-measured---ss-944--t-6187--and-the-near-silence-count-tracked-the-error) ## P0 — the exporter, 2026-08-28 @@ -7969,3 +7970,59 @@ implicitly for two days. command carries `-ss 10 -t 61.93` and the output stays 61.930 s — a window, not a truncation. Restored to 0.0; the export is byte-unchanged. When the start is measured this is a one-value edit, not a code change. + +## The loop window is measured — `-ss 9.44 -t 61.87` — and the near-silence count tracked the error + +The region is **[9.44 s, 71.31 s]** of an 87.744 s wave: the first 9.44 s is an +intro played **once**, the last 16.4 s a fade-out **never played**. Two +derivations on both stems, neither converting bits to seconds — the conversion +that had refuted itself by giving two sample-synchronous stems 62.34 and 63.29 s. + +✅ **61.87 replaces 61.93**, 0.1 % apart. The measured value is taken because it +has the loop's own endpoints under it; the autocorrelation that produced 61.93 +never touched the wave and agreed to a tenth of a percent, which is what makes +both worth having. + +### The port's own near-silence count tracked the window's correctness + +| window | near-silent 50 ms frames in ~127 s | +|---|---| +| no trim, `restart` at the wave's end | **36**, spanning 3.4 s | +| `[0, 61.93]` — right length, wrong window | **5**, no run ≥0.2 s | +| **`[9.44, 71.31]` — measured** | **0** | + +That is a real corroboration from this side and it was not designed as one. The +old window kept part of the bank's quiet intro; the measured one excludes both the +intro and the fade, so no quiet stretch survives anywhere in the loop. The count +fell monotonically as the window got closer to right. + +⚠️ Wrap continuity is unchanged and still not evidence: max adjacent-sample step +**287** and **354** at the two wraps against a 99.9th percentile of **3 812**. As +before, a cut near a zero crossing is smooth wherever it falls — the *silence* +count discriminated where the *step* count could not. + +### 🔴 A stale `why` reached the manifest for two days + +Correcting `loop_end_why` and `loop_start_why` left `loop_why` — **the field the +exporter concatenates into `manifest.json`** — still asserting that the loop would +be *"AUDIBLY WRONG AT THE SEAM"*, that *"no loop-point field has been identified +anywhere"*, and that trimming *"would INVENT a loop point"*. All three refuted; +all three shipped to any consumer reading the export. + +**A correction that does not reach the artifact a consumer reads has not been +made.** The corrections existed, were accurate, and were in the wrong fields. + +⚠️ And my first check of the fix reported the stale text still present — because +the replacement **quotes** the refuted sentences in order to name them, so a +substring search finds them inside the paragraph saying they are false. I had to +read the context to see it. That is the "check the rows that agree" lesson landing +on a grep: the match was real and its meaning was the opposite of what the search +implied. + +### Why the wait for 9.44 was cheap + +Their note is worth keeping: it was not that the field predicted the value, but +that `loop_end_s` alone was **asserting** a start of zero in a form no reader +could weigh or find — and that proving `10.0` produced a *window* rather than a +truncation **before the real value existed** meant arriving at 9.44 was a +one-value edit with a clean baseline behind it.