# 🟑 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.