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.
+46 -8
View File
@@ -115,6 +115,24 @@ loudest passage on the disc is unmeasured. **(2) The transport is P6b's, not
scaffolding**, and it worked first time. **P6b is next and its bytes are
decided: DLXP2 encodes with `adpcm.CHIP`.**
Amended end of session 35: **P6b IS DONE AND ITS PADDING WAS DRIFT (FINDINGS
67).** DLXP2 exists: a 64-byte header, groups of one `A`-sector audio lump then
`F` records, `record i = off_frm + i*rec + (i//F)*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 it in **six instructions
once a frame**, and 120 of 120 records are still pixel-exact off a real volume
with the interleave in, against a **silent control** that says no picture byte
moved. **65.3's 0.09% was not waste, it was a rate error**: a lump is 7,168 B of
space and eleven frames of audio is 7,161.4583… B, so a player that fed the chip
the whole lump would run 0.09% fast — **1.25 s of lip-sync over the game's 22.8
minutes**. The payload is a remainder, `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 and the gate
flips each one to prove they earn it (**order 31.99 dB, formula 24.86**). The
wire is **589.6 KB/s**, 65.3's figure to the tenth. **What is left of P6 is the
last quarter: no audio has been played out of this container on any layer, and
two DMA channels have never run at once.**
**THE COMPLETION TARGET IS M3, THE VERTICAL SLICE** (USER DECISION): one scene
tree — a decision point, two outcomes, a death clip — with audio, streaming from
a real SCSI volume on a stock 2 MB machine, playable. That is the point at which
@@ -747,10 +765,28 @@ throughout (65.5). The gap is the register semantics, and the way to close it is
machine that is known-correct because Sharp wrote it. That is also the real
design, so it is not scaffolding.
**P6b. A CONTAINER WITH SOUND IN IT.** DLXP1 has no audio section. 65.3 is the
arithmetic a DLXP2 is built from and no byte of one is written. It waits on P6a,
because a container full of audio encoded against the wrong formula is 25 dB of
work to redo.
**P6b. A CONTAINER WITH SOUND IN IT. DONE, session 35 — FINDINGS 67.**
~~DLXP1 has no audio section. 65.3 is the arithmetic a DLXP2 is built from and no
byte of one is written.~~ DLXP2 is written, gated and carried by the player:
`tools/encoder/dlxp.py`, `pack.py --audio`, `tools/analysis/34_packed_audio.py`,
the third LBA term in `src/player/packed.s`. The cadence arithmetic came through
unchanged (F=11, A=14, 589.6 KB/s) and the **payload** did not: a lump's audio is
7,161 or 7,162 B of a 7,168 B sector run, and feeding the chip the whole lump
drifts 1.25 s over the game.
**P6c. AUDIO OUT OF THE CONTAINER, ON THE MACHINE (new, session 35).** Every
piece exists and none is joined up. The container carries the bytes (67); the
transport is `src/player/adpcm.i`'s channel-3 configuration, which is the IPL
ROM's own and worked first time (66.1). What is missing is **the lump buffer**
— 14,336 B, double-buffered, allocated by nobody — and **the remainder
accumulator**, which is three instructions and is not optional.
**And the interaction neither half has met: TWO CHANNELS AT ONCE.** The video
channel holds the bus and halts the 68000, which already costs the frame clock
half its ticks without the clock being able to tell (64.3). An audio channel
that must be serviced *during* that hold has never been run. 52's 1.25%..1.48%
of a frame is a figure measured in isolation, and `32_audio_wire.py` names what
it turns into here: audio does not merely cost clocks, it costs **darkness**.
**E6. Container v2** — audio interleave, per-record index, scene table. Depends
on P6's answer and on P5's index.
@@ -878,10 +914,12 @@ B2 blanking ─┬─ NOT blanked ─> K3's DMAC-DIRECT player is the one: 54.9%
picks a different player.
B1 BURST rate (NEW, 64.2) ──> which of the two K3/K4 wins, if B2 blanks
P6 audio DONE bar one (65): encoder gated, cadence F=11/A=14, 589.6 KB/s
└─> P6a WHICH DELTA FORMULA? ─┬─ needs no board: MAME has :okim6258
worth 25 dB, so it is a │ at $E92001/$E92003 (65.5)
PRECONDITION not a tweak └─> then P6b, a container with sound in it
P6 audio: encoder gated (65), P6a the chip's own decoder measured (66),
P6b DLXP2 written and gated, 589.6 KB/s on the wire (67)
└─> P6c AUDIO OUT OF THE CONTAINER ─┬─ the bytes exist, the transport
needs no board: the transport │ exists, the lump buffer does not
is the IPL ROM's own (66.1) └─> TWO CHANNELS AT ONCE, which is
the one thing nothing has run
```
**Read that top-left branch as the project's live question.** Everything else
+112 -1
View File
@@ -1,4 +1,115 @@
# Status & next-session handoff — end of session 34 (2026-08-25)
# Status & next-session handoff — end of session 35 (2026-08-25)
## Session 35: the packed container gets sound, and the padding is drift
**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any
of this (`tmp/check_s35_start.log`) and ALL GREEN after
(`tmp/check_s35_end.log`)** — the same stages, plus one new one.
**FINDINGS 67. ROADMAP P6b is DONE.** `tools/encoder/dlxp.py` (DLXP2),
`tools/encoder/pack.py --audio`, `tools/analysis/34_packed_audio.py`,
`src/player/packed.s`, `tools/bench/prep_packed.py`, `tools/bench/packed.lua`,
`tools/analysis/30_packed_container.py`, `tools/bench/check.sh`.
**NAME THE LAYER.** The container is **host arithmetic**. What ran on the
emulated 68000 is its *video* consequence: `packed.s` now carries the third LBA
term and fetches 120 records out of an interleaved container off a real MB89352
volume, all 120 pixel-exact. **No audio byte has been played out of this
container on any layer.**
**1. THE FORMAT.** 64-byte header; from sector 1, groups of one `A`-sector audio
lump then `F` records. `record i = off_frm + i*rec + (i//F)*A*512`,
`lump k = off_aud + k*(F*rec + A*512)`. **Still no index and still none needed**,
which was the packed branch's whole claim and is the thing a second stream could
have ended. The lump goes **before** the group it feeds; 65.3's formula put it
after, and a stream is read forwards. On the 68000 it costs **six instructions
once a frame** and zero parsing. The four ADPCM axes ride in the header as
fields, not a version number.
**2. THE HEADLINE, AND 65.3 CALLED IT THE WRONG THING.** A lump is 7,168 B of
*space*; eleven frames of audio is **7,161.4583… B**, so the payload alternates
**7,161 / 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` — the same shape as `clock.i` and for
the same reason. **Third time this tree has met it** (54, 65.3, now).
**3. THE GATE, AND ITS THREE NEGATIVE CONTROLS.** Nothing parses a packed
container, so a lump one sector out does not fail, it paints. `34_packed_audio.py`
therefore (a) **partitions the whole file** — 131 spans, no overlap, no gap,
ending on the last byte — because a per-record read cannot see an off-by-one
that shifts everything after it; (b) asserts a **cadence-blind player** gets
exactly records 11..119 wrong, **109 of 120, and frames 0..10 identical either
way**, which is how such a bug survives a rig that checks frame 0; (c) **flips
each of the four ADPCM axes one at a time** — nibble order **31.99 dB**, delta
formula **24.86 dB**, clamp 0.00, accumulator 0.49 — so the header fields are
load-bearing rather than documentation. And the picture is gated against a
**silent control** built from the same frames: all 120 records byte-identical.
**4. THE WIRE IS THE PREDICTION.** 582.0 + 7.64 = **589.6 KB/s**, which is
65.3's figure to the tenth, because none of 66's four axes changes a byte count.
**RISKS IN THIS SESSION'S RESULT:**
- **Nothing has been heard.** The container has audio in it; no transport has
fed it to a chip, on any layer.
- **The lump buffer is not allocated anywhere.** 65.3 charges 14,336 B for
double-buffering the cadence and no player holds it.
- **Two channels have never run at once.** The audio DMA is 1.25%..1.48% of a
frame *in isolation* (52); the video channel holds the bus (64.2/64.3).
- **The level is still open downward** (66.3) and the loudest passage on the
disc is still unmeasured.
## HANDOFF — start here
**THE TREE IS ALL GREEN**, session 35's stage included.
### The work, in the order it should be done
**1. THE LAST QUARTER OF P6 — audio out of the container, on the machine.**
Everything it needs now exists and none of it has been joined up: the container
carries the bytes (67), the transport is `adpcm.i`'s channel-3 configuration
which worked first time (66.1), and the missing piece is **the lump buffer and
the remainder accumulator in a player**. Do the accumulator as 67.2 states it or
the run drifts 1.25 s over a game.
**2. AND THEN THE THING NEITHER HALF HAS MET: TWO CHANNELS AT ONCE.** The video
channel holds the bus and halts the 68000 (64.3 — it costs the frame clock half
its ticks *without the clock being able to tell*). An audio channel that has to
be serviced during that hold has never been run. This is the interaction 52
could not have had, and `32_audio_wire.py` names it: audio does not merely cost
clocks, it costs **darkness**.
**3. THE REFILL CLIMB WITH A SECOND CONSUMER** through a real branch point
(51.3, 55.4). The slack table is in `32_audio_wire.py`; nothing has been run.
**4. THE AUDIO LEVEL, which 66.3 reopened.** Measure the loudest passage on the
disc before choosing a level.
### What is still BLOCKED, so it is not picked up by mistake
**K4 — the packed player that is on screen — is conditional on B2**, a board
question. **E7, E4 and C1** are parked (61.8), and **P4a's wiring** is parked
with the ring K3 deleted.
**The hardware list is unchanged and is the user's**: B1 (sustained AND the
data-phase BURST rate, 64.2 — and the acceptance figure is now **589.6 KB/s**,
not 582.0), B2 (blanking), B3 (`#EXREQ`), B4 (a byte write to a palette
register), and session 34's fifth: play a known nibble stream on a real
MSM6258V and record the line out.
### Reproducing this session
./tools/bench/check.sh # ALL GREEN
python3 tools/encoder/pack.py tmp/fr_singe tmp/packed_singe.dlxp \
--nframes 120 --audio tmp/au_singe.raw
python3 tools/analysis/34_packed_audio.py # the DLXP2 gate on its own
**WHAT IS NEXT.** P6's last quarter: the container's own audio, out of a
channel, beside the video channel that holds the bus.
---
## Session 34: the chip is asked, and the encoder was wrong on four axes