Put sound in the packed container, and find the padding is a rate error

ROADMAP P6b, FINDINGS 67.  DLXP2: a 64-byte header and then groups -- one
audio lump of A sectors, then F records -- so record i is at
off_frm + i*rec + (i//F)*A*512 and lump k at off_aud + k*(F*rec + A*512).
Still no index and still none needed, which is the packed branch's whole
claim surviving the one change that could have ended it.  The player carries
the third term in six instructions once a frame and zero parsing, and 120 of
120 records are still pixel-exact off a real MB89352 volume with the
interleave in, against a silent control that says no picture byte moved.

The finding is what 65.3 called padding.  A lump is 7,168 B of SPACE; eleven
frames of audio is 7,161.4583... B, so the payload alternates 7,161 and
7,162 and the rest is zero.  A player that fed the chip the whole lump --
which is what "14 sectors every 11 frames" invites -- runs 0.09% fast, and
that is not waste, it is drift: 0.84 ms a group, 1.25 s of lip-sync over the
game's 22.8 minutes.  What a player carries is one accumulator,
acc += 11*15625; n = acc//24; acc %= 24, which is clock.i's shape for
clock.i's reason and the third time this tree has met the pattern.

The four ADPCM axes ride in the header as fields rather than a version
number, and the gate flips each one to prove they earn it: nibble order
-31.99 dB, delta formula -24.86, clamp 0.00, accumulator -0.49.  Nothing
parses a packed container, so the gate partitions the whole file -- 131
spans, no overlap, no gap -- and asserts what a cadence-blind player would
read: exactly records 11..119 wrong, and frames 0..10 identical either way,
which is how an off-by-one like that survives a rig that checks frame 0.

Wire 582.0 + 7.64 = 589.6 KB/s, 65.3's prediction to the tenth.

Green light ALL GREEN before (tmp/check_s35_start.log) and after
(tmp/check_s35_end.log), with the new stage in it.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 11:22:38 -07:00
parent 6dd3fb3597
commit e3778f62b0
13 changed files with 1001 additions and 51 deletions
+144
View File
@@ -7075,3 +7075,147 @@ write*.
`adpcm.CHIP`, and 65.3's cadence arithmetic (F=11, A=14, wire 582.0 → 589.6
KB/s) is untouched by any of this — it is a byte count, and none of the four
axes changes how many bytes a second the format needs.
---
## 67. DLXP2 — a packed container with sound in it, and the padding turns out to be drift
**ROADMAP P6b. Session 35.** `tools/encoder/dlxp.py` (DLXP2),
`tools/encoder/pack.py --audio`, `tools/analysis/34_packed_audio.py`,
`src/player/packed.s` (the third LBA term), `tools/bench/prep_packed.py`,
`tools/bench/packed.lua`.
**NAME THE LAYER.** The container is **host arithmetic**, gated against itself
and against a silent control. The one thing that ran on the emulated 68000 is
the *video* consequence: `packed.s` fetches records out of an interleaved
container and all 120 are still pixel-exact off a real volume. **No audio byte
has been fed to a chip out of this container**, on any layer — the chip gate
(66) feeds a designed stream, not this one.
### 67.1 The format, and what it costs the player: one `divu` and one `mulu`
A DLXP2 is a DLXP1 with a 64-byte header and, from sector 1, **groups**: one
audio lump of `A` sectors, then `F` records.
record i = off_frm + i*rec_bytes + (i//F)*A*512
lump k = off_aud + k*(F*rec_bytes + A*512)
Neither is a lookup. **The format still has no index and still needs none**
which was 63/64.1's whole claim for the packed branch, and a second stream at an
unrelated rate is exactly the thing that could have ended it. The lump goes
**before** the group it feeds rather than after it (65.3's formula put it after):
a stream is read forwards, so bytes that arrive after the slot they belong to
are bytes a player had to fetch early anyway.
On the 68000 the third term is **six instructions in `pg_frame`** — a `divu`, an
`andi` to drop the remainder `divu` leaves in the high half, a `mulu` and an
`add` — and **zero instructions of parsing**, because the cadence is two numbers
in the header rather than a table in the stream.
**Measured, on the emulated machine, off a real MB89352 volume: 120 of 120
frames pixel-exact** out of the interleaved container, every one compared. The
interleave moved no picture byte — asserted against a **silent control** built
from the same frames with `--audio` off, all 120 records byte-identical.
### 67.2 The finding: the payload is not the lump
65.3 chose the cadence F=11, A=14 and called the 0.09% "padding". It is padding
on the wire. **It is drift in the player**, and that is a different thing.
A lump is `A*512` = **7,168 B of space**. Eleven frames of audio is
`11*15625/24` = **7,161.4583… B**. So the *payload* alternates **7,161 and
7,162** — the same remainder FINDINGS 54's frame clock carries, one dimension
over — and the last 6.54 B of the sector run are zero.
**A player that fed the chip the whole lump** — the obvious implementation, and
the one the phrase "14 sectors of audio every 11 frames" invites — hands it
6.54 B a group it should not have. That is **0.84 ms of extra audio every
0.9167 s**, and it does not average out:
| play | lip-sync error |
|---|---:|
| 1 min | 0.05 s |
| **22.8 min (the game)** | **1.25 s** |
A second and a quarter is a scene of dialogue arriving after the mouth that
spoke it. So `lump_bytes(k)` is **part of the format**, not a convenience, and
what a player carries is the same shape `clock.i` carries and for the same
reason — a rate whose denominator is 24 cannot be a count:
acc += 11*15625 ; = 171,875
n = acc // 24 ; the MTC for this lump's channel
acc %= 24
**The general shape, and it is the third time this tree has hit it**: 54's frame
clock, 65.3's `.0417` B a slot, and now this. A ratio with a remainder that is
rounded *once* is a rounding error; rounded *every period* it is a rate error,
and a rate error integrates.
### 67.3 The four ADPCM axes are in the header, and the gate proves they earn it
66 measured which decoder MAME's chip runs and priced getting it wrong at up to
25.74 dB. DLXP2 carries all four — nibble order, delta formula, clamp width,
the accumulator at PLAY — as **three header fields rather than a version
number**, so a mismatch is legible in a hexdump instead of inferred from a
container's age.
`34_packed_audio.py` decodes the container's own lumps and **flips one axis at a
time**, which is the negative control that makes the fields load-bearing rather
than documentation:
| axis | header | flipped to | SNR | cost |
|---|---|---|---:|---:|
| — | | | **21.99 dB** | |
| nibble order | low | high | 10.00 dB | **31.99 dB** |
| delta formula | terms | shift | 2.87 dB | **24.86 dB** |
| clamp | 10 | 12 | 21.99 dB | +0.00 dB |
| accumulator at PLAY | 2 | 0 | 21.50 dB | 0.49 dB |
These are **decode-side** flips on bytes that were encoded correctly, where
66.2's table encoded and decoded on the wrong model together; the two agree on
which axes are expensive and disagree by a few dB on how expensive, which is
what different experiments on the same fact look like. The clamp is still free
**on this window only** and for 66.3's reason: it peaks at 435 of 511.
### 67.4 The failure mode this format has and the codec's does not
A DLX record is found through an index, so a player that reads the wrong entry
gets a length word that does not parse. **A packed record is found by
arithmetic and nothing parses it.** A player that drops the `(i//F)*A` term
reads 97 sectors starting 14 sectors early and *paints them*: the tail of the
previous record, then most of this one, shifted down the screen. It is a
picture. Nothing errors.
And it is invisible where a gate usually looks: **frames 0..10 are byte-identical
either way**. The gate asserts the whole shape — that a cadence-blind read is
wrong for exactly records `F..n-1`, 109 of 120, first at frame 11 — rather than
that some frame differs.
The same argument is why the gate **partitions the file** instead of reading
records back one at a time: an off-by-one that shifts everything after it reads
back fine record by record. 131 spans, no overlap, no gap, ending exactly at the
file's last byte.
### 67.5 The wire, unchanged from the prediction
video 582.0 KB/s geometry, no lever
audio 7.64 KB/s the cadence's, padding included
total 589.6 KB/s +1.31%
**65.3 predicted 589.6 and the container is 589.6** — which is what a byte count
should do, since none of 66's four axes changes how many bytes a second the
format needs. The audio figure charges the padding on purpose: the disc moves
whole sectors and the wire pays for the zero ones.
### 67.6 What is still open
* **No audio has been played out of this container**, on any layer. The chip
gate (66) fed a designed nibble stream through the IPL ROM's channel-3
configuration; wiring *this* stream to *that* transport, and running it beside
the video channel, is P6's remaining quarter.
* **The second consumer has not met a branch point.** 51.3's refill climb with
audio on the wire is arithmetic in `32_audio_wire.py` and has not been run.
* **The level is still open downward** (66.3), and the loudest passage on the
disc is still unmeasured.
* **The lump buffer is not allocated anywhere.** 65.3 charges 14,336 B for
double-buffering the cadence and no player holds it.