Tailing the log from BEFORE the music starts cut the unsampled backlog from 616
samples spanning offsets 32..2,559,033 to 125 spanning 32..515,239, so the first
pass is sampled like any later cycle. Offsets below loop_start play exactly once,
which is why the previous run could not measure them.
Wraps at 96.46 / 158.33 / 220.21 s, gaps 61.87 / 61.87, both contexts together.
Two derivations, neither converting bits to seconds:
(a) time to read_offset crossing loop_start, plus a 1.33 s head correction at a
rate measured on 748 timestamped samples of that same stretch
(b) first pass (offset 32 -> loop_end) minus the cycle
Both give 9.44 s on both contexts -- four numbers, one value.
So the loop region is [9.44, 71.31] s of an 87.744 s wave, cycling every 61.87 s.
The first 9.44 s is an intro played once; the last 16.4 s, the fade-out
bgm-two-stems.md documents, is never played at all.
The decoder reads ahead of playback, but both endpoints are read_offset events so
the lead cancels in the difference. One boot, one bank.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
159 lines
7.7 KiB
Markdown
159 lines
7.7 KiB
Markdown
# 🟡 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`](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`](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`](../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`](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*.
|
||
|
||
### ✅ MEASURED (2026-08-30, latest): `loop_start` is at **9.44 s**
|
||
|
||
The fix was scheduling. Tailing the log from *before* the music starts cut the
|
||
unsampled backlog from 616 samples spanning offsets 32…2 559 033 down to **125
|
||
spanning 32…515 239**, and the first pass is then sampled like any other cycle
|
||
([`../data/menu-bgm-loop-start.txt`](../data/menu-bgm-loop-start.txt)).
|
||
|
||
Wraps at **96.46 / 158.33 / 220.21 s**, gaps **61.87 / 61.87**, both contexts
|
||
together.
|
||
|
||
Two derivations, neither converting bits to seconds:
|
||
|
||
| | ctx0 | ctx1 |
|
||
|---|---|---|
|
||
| (a) time to read_offset crossing `loop_start`, + the head at the **local measured** rate | **9.44 s** | **9.44 s** |
|
||
| (b) first pass (offset 32 → `loop_end`) − cycle | **9.44 s** | **9.44 s** |
|
||
|
||
Four numbers, one value. The head correction is **1.33 s** and uses a rate measured
|
||
on 748 timestamped samples of that same stretch — not the cycle mean, and not an
|
||
assumption of linearity across the stream.
|
||
|
||
**So the loop region is `[9.44 s, 71.31 s]` of an 87.744 s wave, cycling every
|
||
61.87 s.** The first **9.44 s is an intro played once**; the last **16.4 s — the
|
||
fade-out `bgm-two-stems.md` documents — is never played at all.**
|
||
|
||
⚠️ The decoder reads ahead of playback, but both endpoints are `read_offset` events,
|
||
so the lead cancels in the difference. One boot, one bank.
|
||
|
||
### The superseded position
|
||
|
||
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.
|