port: duration confirmed at 0.2 percent, window refuted, loop start now an explicit field

They timed the wraps instead of converting them -- a probe stamping read_offset
on arrival, three wraps observed, each from its own loop_end to its own
loop_start, both contexts wrapping at the same instant. Cycle 61.81 s against the
61.93 authored here: 0.2 percent, from a wall clock between decoder events versus
an autocorrelation that never touched the wave.

The window is wrong: loop_start is 11.6 percent into the stream, about ten
seconds, so this export has the right duration over the wrong window -- replaying
the intro every cycle and omitting the tail the game plays.

Not re-cut, on their instruction: the exact start is unmeasured and linearity is
refuted by a 4.4 percent rate variation within one stream. But loop_end_s alone
silently asserted a start of zero, so the entry gains loop_start_s, authored as
0.0 and flagged wrong, with -ss applied before -t so the pair is (start,
duration). Proved before it is needed: loop_start_s=10 yields -ss 10 -t 61.93 and
a 61.930 s output. Restored to 0.0, export byte-unchanged.

My smooth-join check gains a use I could not have anticipated: it explains why a
wrong ten-second window went unheard, because a cut near a zero crossing is
smooth wherever it falls.

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-30 10:07:07 +00:00
parent 67c5a663bf
commit 328c8a6f96
3 changed files with 103 additions and 2 deletions

View File

@@ -106,6 +106,27 @@
"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.",
"loop_start_s": 0.0,
"loop_start_why": [
"🔴 ZERO, AND KNOWN TO BE WRONG. NOT MEASURED.",
"",
"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.",
"",
"⚠️ 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.",
"",
"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."
],
"loop_end_s": 61.93,
"loop_end_why": [
"MEASURED off the running game 2026-08-30, 240 s parked on the menu",
@@ -163,7 +184,19 @@
"🔴 What the conflict would COST if their runtime fields win: under [10 s, 72 s]",
"this export is about 10 SECONDS SHORT -- the content in [61.93, 72] is played",
"by the game and absent here. That is the number to weigh when it resolves, and",
"it is why this entry is not being treated as settled."
"it is why this entry is not being treated as settled.",
"",
"✅ CONFIRMED 2026-08-30 BY A SECOND INSTRUMENT SHARING NOTHING WITH THE FIRST.",
"The RE agent stopped converting the runtime fields and TIMED them instead --",
"a probe tailing the Apu debug log and stamping `read_offset` on arrival --",
"and watched THREE wraps, each from its own `loop_end` to its own `loop_start`,",
"with both contexts wrapping at the SAME INSTANT every time. Cycle 61.56 and",
"62.06 s, mean 61.81 s: 0.2 % from the 61.93 authored here, measured by wall",
"clock between decoder events against an autocorrelation that never touched",
"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`."
]
}
},

View File

@@ -98,6 +98,15 @@ pub struct BgmSpec {
/// 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.
/// Seconds into the summed bank where the loop window BEGINS.
///
/// 🔴 Split out from `loop_end_s` on 2026-08-30 because carrying only an end
/// silently asserted a start of zero, and that start is now known to be
/// WRONG — the measured window begins about ten seconds in. An assumption
/// that has to be inferred from the absence of a field is not one a reader
/// can weigh.
#[serde(default)]
pub loop_start_s: Option<f64>,
#[serde(default)]
pub loop_end_s: Option<f64>,
#[serde(default)]
@@ -515,7 +524,13 @@ pub fn export_bgm<S: DiscSource + ?Sized>(
// 🔴 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(start) = spec.loop_start_s.filter(|v| *v > 0.0) {
argv.push("-ss".into());
argv.push(format!("{start}"));
}
if let Some(end) = spec.loop_end_s {
// A LENGTH, applied after any `-ss`, so the pair is (start, duration)
// and moving the start does not silently change how much is kept.
argv.push("-t".into());
argv.push(format!("{end}"));
}

View File

@@ -9,7 +9,7 @@ dies, which is what this file is for.
<!-- INDEX: generated by tools/port/index-decisions -- do not hand-edit -->
137 sections. Search this before re-deriving anything.
138 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)
@@ -148,6 +148,7 @@ dies, which is what this file is for.
* [The dead-press check was passing by luck, and the luck ran out](#the-dead-press-check-was-passing-by-luck-and-the-luck-ran-out)
* [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)
<!-- /INDEX -->
## P0 — the exporter, 2026-08-28
@@ -7916,3 +7917,55 @@ locator aliases.
in both cases the output was internally consistent and structured, and in both the
tell was in the rows that **agreed** — my confirming half read impossibly too, and
their control was passing a problem it never had to solve.
## The duration is confirmed and the window is wrong — and the start is now a visible field
They stopped *converting* the runtime fields and **timed** them instead: a probe
tailing the Apu debug log, stamping `read_offset` on arrival, watching **three**
wraps — each from its own `loop_end` to its own `loop_start`, with **both
contexts wrapping at the same instant every time**.
| | |
|---|---|
| observed cycle | 61.56 s, 62.06 s → **61.81 s** |
| authored here | **61.93 s** |
| difference | **0.2 %** |
**The length is settled**, and by instruments sharing nothing: a wall clock
between decoder events against an autocorrelation that never touched the wave.
Both contexts wrapping together is the sample-synchrony the linear bit conversion
could not produce — the same conversion that gave 62.34 and 63.29 s for two stems
that must be synchronous, and so refuted itself.
🔴 **The window is wrong.** `loop_start` is at 3.6 M bits — **11.6 % of the
stream, about ten seconds** — not the 0.25 s their earlier tracking gave. 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.
📌 **My smooth-join check has a second use I could not have anticipated.** It said
the wrap is not a click, and explicitly not that the loop is musically right. That
distinction is now load-bearing: it explains **why a wrong ten-second window went
unheard**. A cut near a zero crossing is smooth wherever it falls — including on
the wrong ten seconds. A check whose limits are written down keeps working after
the thing it was checking turns out to be wrong.
### Not re-cut — and the assumption is now a field
Their instruction is to wait: the exact start is **not measured**. Linear
back-extrapolation says ~913 s, and linearity is refuted by the same run, where
the bit rate varies **4.4 %** within one stream.
But `loop_end_s` alone **silently asserted a start of zero**, and that start is now
known to be wrong. So the entry gains `loop_start_s`, authored as **0.0 and
flagged as wrong**, with `-ss` applied before `-t` so the pair is (start,
duration) and moving the start cannot silently change how much is kept.
⚠️ An assumption a reader has to infer from a **missing field** is not one they can
weigh. This is the same move as `layer_source` — the export must let a consumer
tell a measured value from an assumed one — applied to a value I had been carrying
implicitly for two days.
✅ The new path is **proved before it is needed**: with `loop_start_s = 10.0` the
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.