Settles the conflict by timing the loop instead of converting it. A tailing probe stamps read_offset with the wall clock as each log line arrives, so the period needs no bits-to-time step -- the step already shown to be invalid. Three wraps, each exactly loop_end -> loop_start, and BOTH CONTEXTS WRAP AT THE SAME INSTANT all three times. That is the property two stems of one performance must have and the one the linear conversion could not deliver (62.34 vs 63.29 s would drift a second per cycle). Cycle 61.56 and 62.06 s, mean 61.81, against the audio autocorrelation's 61.93 -- 0.2 % apart from instruments sharing nothing. Linearity refuted a second time and internally: the fitted rate over 10..60 s is 341 394 bits/s while the cycle covers 22 034 741 bits in 61.81 s = 356 491 bits/s, 4.4 % apart inside one stream. My own audio locator's PLACEMENT is refuted. loop_start at 3.6 M bits is 11.6 % of the stream by any reading, ~10.1 s at the cycle's own mean rate, against the 0.25 s that page reported -- for the reason already suspected, that its control matched slices cut from the wave itself and never tested the aliasing the real problem has. The length was right and the span was wrong. Still not measured: loop_start in seconds. Offsets below it play exactly once and this trace stamped that whole stretch at t=0.002, swallowing the log backlog in one read, because it started after the music. The fix is to start the trace before tapping into the menu -- one line, not done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
6.5 KiB
🟡 The loop point IS a runtime field — and reading it contradicts the audio measurement
Classification: decoded (the fields and their values) plus an unresolved conflict (what they mean in seconds). Xenia Canary, 2026-08-30, one boot, 45 s on the menu. The port should change nothing on the strength of this page.
✅ The loop point is not absent from the format
bgm-two-stems.md says "no loop-point field has been
identified in the XMA header, so a menu loop is authored". That is true of the
file header and it left the wrong impression. The loop lives in the XMA
decoder context, set at runtime by XMASetLoopData, and Xenia's
UpdateLoopStatus already logs it — no patch was needed, only the Apu log
category (--log_mask=13 --log_level=3).
| ctx | wave | loop_start |
loop_end |
loop_count |
|---|---|---|---|---|
| 0 | 3 876 864 B | 3 605 682 | 25 640 423 | 255 (infinite) |
| 1 | 3 930 112 B | 3 539 158 | 26 216 351 | 255 |
Bit offsets. 8 734 records, every one after BGM_103's contexts appear — the
movie's three ADV streams log none at all, i.e. they do not loop.
✅ And the semantics are visible in the trajectory. input_buffer_read_offset
runs from 32 (the first packet header) upward, and 20 % of samples sit below
loop_start — so the stream plays from the beginning, and loop_start is where
it returns after loop_end. The first pass is longer than the cycles after it.
⚠️ No wrap was observed. Max read offset was 16.9 M / 17.2 M against a
loop_end of 25.6 M / 26.2 M — the 45 s hold was too short. The jump itself is
inferred from the field semantics, not watched.
🔴 Two things I predicted are refuted
1. "loop_start ≈ 0" — no. It is ~3.5–3.6 M bits, 11.4–11.6 % into the stream.
I registered that prediction before the run and it is wrong.
2. A linear bits→seconds conversion — invalid, and the data proves it. Applied
to each stem with its own byte_size:
| linear loop duration | |
|---|---|
| ctx0 | 62.34 s |
| ctx1 | 63.29 s |
Two stems that play sample-synchronously cannot have loop durations 0.95 s apart — they would drift a second per cycle. So the assumption fails on its own output. XMA frames are variable-length in bits, which is exactly why.
🔴 The conflict, stated rather than resolved
loop_start at 11.6 % of the stream implies a cycle of roughly [10 s, 72 s] of
the 87.744 s wave. But
menu-bgm-loop-measured.md put the observed wave
offsets at 0.25 … 57.18 s. Both cannot be true.
⚠️ And the weakness is probably mine. That page's locator control used slices cut from the wave itself, which are exact copies — a far easier matching problem than a real capture, which differs by decoder, gain and mix. A control that is easier than the measurement does not bound the measurement's error, and music with repeated phrases is exactly where a locator aliases. The clean +5.00 s stepping shows the locator is self-consistent; it does not show it locked to the right phrase.
So the honest position is:
| claim | status |
|---|---|
| the loop is a runtime field, with these values | ✅ decoded |
| the movie streams do not loop | ✅ decoded |
| the cycle is 61.93 s | 🟡 measured from audio, and its control was too easy |
| where the cycle starts in the wave | 🔴 contested — 0.25 s from audio, ~10 s from the field |
| bits → seconds | ❔ needs an XMA frame walk; not done |
✅ RESOLVED (2026-08-30, later) — the wrap was watched, and the length is confirmed
The conflict is settled by timing the loop instead of converting it. Tailing the Apu
debug log and stamping input_buffer_read_offset as it arrives
(../data/menu-bgm-wrap-timing.txt):
| wraps observed | three |
| each | exactly its own loop_end → its own loop_start |
| the two contexts | wrap at the same instant, all three times |
| cycle | 61.56 s and 62.06 s → 61.81 s |
✅ The field semantics are now watched, not inferred. And the two stems wrapping together is the property the linear conversion could not deliver — 62.34 vs 63.29 s would have drifted them a second per cycle.
✅ 61.81 s against the audio measurement's 61.93 s — 0.2 % apart, from instruments sharing nothing: one is a wall clock between decoder events, the other an autocorrelation that never touched the wave.
🔴 Linearity refuted a second time, internally. The fitted rate over the clean stretch 10–60 s is 341 394 bits/s; the cycle covers 22 034 741 bits in 61.81 s = 356 491 bits/s. 4.4 % apart inside one stream — no single rate converts these offsets.
🔴 And my audio locator's placement is refuted
loop_start at 3.6 M bits is 11.6 % of the stream by any reading, and ~10.1 s
at the cycle's own mean rate. menu-bgm-loop-measured.md
put the loop's start at 0.25 s. That is wrong, and the reason is the one already
suspected: its control matched slices cut from the wave itself, which never tested
the aliasing the real problem has.
So the length was right and the placement was wrong — which is why the port's shipped loop sounds correct: it has the right duration, over the wrong span.
🟡 Where loop_start sits in seconds is still not measured
Offsets below loop_start play exactly once, before the first wrap, and this
trace stamped that whole stretch at t=0.002 — 616 samples spanning offsets
32…2 559 033 in a single batch, because the trace started after the music and
swallowed the log's backlog in one read. A back-extrapolation suggests ~9–13 s, but
it is a linear back-extrapolation and linearity is what the same run refutes.
The fix is one line of scheduling: start the trace before tapping into the menu, so the first pass is sampled at 0.5 s like every later cycle. Not done.
What the port should do: keep 61.93, and know the span is wrong
✅ The 61.93 s length is now confirmed twice over — keep it. ⚠️ But the span
is wrong: the game loops a 61.8 s window that begins ~10 s into the wave, not the
first 61.93 s. A trim to [0, 61.93] therefore replays the intro every cycle and
omits the tail the game does play. 🟡 The exact start is not measured, so do not
re-cut on a guess — what is needed is the trace started before the music, which is
one line of scheduling and is not done.