Put sound on the wire, and find three LSBs are worth 25 dB

ROADMAP P6, everything in the item except the bus half session 20 closed.
tools/encoder/adpcm.py is an MSM6258 codec, tools/encoder/extract_audio.py
takes the same seconds of the same stream the frames come from,
tools/bench/verify_adpcm.py is the gate, tools/analysis/32_audio_wire.py the
container arithmetic.

There is no reference encoder -- ffmpeg has a decoder for this format and none
the other way -- so what is gated is the decoder the encoder runs INSIDE its
own nibble search, sample-exact against ffmpeg's over 4,268 nibbles. An
encoder that agrees with its own wrong decoder is what that catches. The Singe
window: 156,250 samples -> 78,125 B at 21.97 dB, which is 7,812.5 B/s to the
byte. Normalising the disc's -13.4 dBFS level moves the SNR 21.97 -> 21.97, so
the level is not a lever.

And the two published delta formulas are not the same codec. They differ by at
most 3 in 12-bit units; encode for one and decode on the other and the SNR
goes 21.97 -> -2.88 dB, the noise louder than the signal, because ADPCM is
recursive the way the video codec is temporally recursive. Which one the chip
runs is now P6a and it is a precondition on shipping any audio.

And audio is the first thing the packed branch's simplification has cost
anything for. A record has no index BY DESIGN, so audio cannot be per-record
without making records variable; it rides a fixed cadence (F, A), the obvious
F=1 wastes 57.3% of every audio sector, and the pick is F=11 A=14 -- 0.09%
padding, 14,336 B held, wire 582.0 -> 589.6 KB/s. The codec container, which
kept its index, pays zero.

The MAME experiment did not work and 65.5 says so: :okim6258 is there at
$E92001/$E92003, read out of the machine's own program map, and feeding it
from Lua recorded silence across control 0..3 x port C 0..15. The register
semantics were not guessed at further.

FINDINGS 65. check.sh ALL GREEN before and after, with a new stage.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 09:11:03 -07:00
parent 6f698ca226
commit f925a1dd9a
12 changed files with 1069 additions and 6 deletions
+171 -1
View File
@@ -1,4 +1,174 @@
# Status & next-session handoff — end of session 32 (2026-08-25)
# Status & next-session handoff — end of session 33 (2026-08-25)
## Session 33: audio gets an encoder, and the packed container's best property gets a bill
**Green light first and last: `./tools/bench/check.sh` was ALL GREEN before any
of this (`tmp/check_s33_start.log`) and ALL GREEN after** — the same stages, plus
one new one.
**FINDINGS 65. ROADMAP P6 is three-quarters done and has grown two sub-items.**
`tools/encoder/adpcm.py`, `tools/encoder/extract_audio.py`,
`tools/bench/verify_adpcm.py`, `tools/analysis/32_audio_wire.py`, and three
throwaway MAME probes kept because their failure is a finding
(`tools/bench/probe_adpcm*.lua`).
**Everything below is HOST ARITHMETIC plus one MAME introspection.** No board
ran, and the one MAME *experiment* attempted did not work — 65.5 says so.
**0. SESSION 32'S WORK WAS UNCOMMITTED AND ITS STATUS BLOCK HAD NO HANDOFF.**
Both fixed before anything new was written: the tree was re-gated ALL GREEN,
the handoff written, and the whole of session 32 committed as one change.
**1. THE ENCODER, AND THE GATE IT NEEDED INSTEAD.** ffmpeg has a DECODER for
this format (`adpcm_ima_oki`) and **no encoder**, so there is nothing to diff
against. What is gated instead is the decoder the encoder runs **inside its own
nibble search** — sample-exact against ffmpeg's over 4,268 nibbles. An encoder
that agrees with its own wrong decoder is what that catches.
| | |
|---|---|
| the window | 00223 @539.4 s, 10.000 s — **the same seconds as `tmp/fr_singe`** |
| encoded | 156,250 samples → **78,125 B, SNR 21.97 dB** |
| the rate | 78,125 B / 10 s = **7,812.5 B/s to the byte** — 52's figure, from the other end |
| nibble order | **HIGH FIRST, measured** — low-first mismatches ffmpeg on 3,285 of 4,268, and that is the gate's negative control |
| the level | disc peaks at **13.4 dBFS**; normalising ×4.7 moves the SNR **21.97 → 21.97**. Not a lever |
**2. THE HEADLINE, AND IT IS A THREE-LSB DIFFERENCE THAT COSTS 25 dB.** The two
published delta formulas — ffmpeg's `((2*(n&7)+1)*step)>>3` and the OKI
datasheet's per-term truncation — differ **by at most 3 in 12-bit units**.
Encode for one, decode on the other:
| encoded | decoded | SNR |
|---|---|---:|
| `shift` | `shift` | **21.97 dB** |
| `shift` | `terms` | **2.88 dB** |
**The noise comes out louder than the signal** (mean disagreement 78.2 against a
source RMS of 74.4). **ADPCM is recursive**, so a rounding difference does not
stay where it happens — the same property, one dimension down, that let 64.1's
gate audit 120 frames by comparing one. **Which formula the chip runs is
therefore a PRECONDITION on shipping any audio**, and it is ROADMAP P6a.
**3. AND THE PACKED CONTAINER'S BEST PROPERTY IS WHAT MAKES AUDIO COST.** A
packed record is 97 sectors and its address is `LBA0 + i*97` because a literal
frame's length is geometry — **no index, and none can be needed** (63, 64.1).
Audio at 15,625 Hz is **651.0417 B a slot**, and the `.0417` is the same
remainder the frame clock carries (54). Per-record audio makes records variable
length, which needs an index, which ends the format. So audio rides a **fixed
cadence** — every `F` frames, `A` whole sectors — and `(F, A)` is a rational
approximation to 15625/12288 from above:
| F | A | lump | padding | wire adds | held |
|---:|---:|---:|---:|---:|---:|
| **1 — the obvious one** | 2 | 1,024 B | **57.29%** | 12.00 KB/s | 2,048 B |
| **11 — the pick** | **14** | **7,168 B** | **0.09%** | **7.64 KB/s** | **14,336 B** |
| 81 — the floor | 103 | 52,736 B | 0.003% | 7.63 KB/s | 105,472 B |
**Wire: 582.0 KB/s silent → 589.6 KB/s with sound (+1.31%).** F=11 buys 57.2
points of padding for 12,288 B of RAM; the floor buys the last 0.09 of a point
for 91,136 B more, and K4 already wants 99,328 B.
**4. THE CODEC CONTAINER PAYS NONE OF IT.** It already has an index and already
has variable records, so it puts exactly 651.0417 B in record *i* and pads to
the sector it was padding to anyway: **zero audio padding**, 440.4 → 448.1 KB/s.
**That is the first price anyone has found for the packed branch's own
simplification.** 64's risk list said a deletion that large usually hides
something; this is the first thing it hid, and it is small — a cadence, a
padding fraction and 14,336 B.
**5. THE EXPERIMENT THAT DID NOT WORK, and the facts it did leave.** MAME's
x68000 **has the chip**`:okim6258`, reached at **`$E92001` and `$E92003`**,
both read out of the machine's own program map rather than from folklore, with
the PPI at `$E9A000-$E9BFFF`. **Feeding it from Lua produced silence**: control
0..3 against port C 0..15, `-wavwrite` capture **0 of 567,360 samples non-zero**.
The gap is register semantics and it was **not guessed at further**. The way to
close it is from 68000 code with the IPL ROM's own channel-3 configuration,
which `21_iplrom_dmac.py` already reads out of the ROM.
**RISKS IN THIS SESSION'S RESULT, stated rather than left to be found:**
- **The encoder is greedy, not optimal.** Each nibble is chosen by exhaustive
search over all sixteen minimising THIS sample's error; a nibble also moves
the step index, so a locally worse choice can pay later. 21.97 dB is a floor
for this format, not its ceiling, and no lookahead was tried.
- **21.97 dB is against the 12-bit word**, not against the disc's 16-bit PCM,
and the source was already resampled and downmixed to mono by ffmpeg. The
downmix matrix is ffmpeg's default and was not chosen.
- **P6a is open and it is worth 25 dB**, so every byte `adpcm.py` has produced
is provisional.
- **Nothing has played.** No audio has left an emulated machine, let alone a
board.
## HANDOFF — start here
**THE TREE IS ALL GREEN**, session 33's stage included.
### The work, in the order it should be done
**1. P6a — WHICH DELTA FORMULA, and it needs no board.** It is worth **25 dB**
(65.2), so every byte the encoder has produced is provisional until it is
answered, and it is the cheapest open item in the project by a distance. MAME's
x68000 has the chip and this session found where it lives — `:okim6258` at
**`$E92001`/`$E92003`**, PPI at `$E9A000-$E9BFFF`, read out of the machine's own
program map (65.5).
**Do NOT retry the Lua feed.** It was swept — control 0..3 × port C 0..15 —
and recorded silence, and the gap is register semantics that nobody here should
be guessing at. **Do it from 68000 code with the IPL ROM's own channel-3 DMAC
configuration**, which `tools/analysis/21_iplrom_dmac.py` already reads out of
the ROM: dual address, 8-bit port, cycle steal without hold, external request.
That is the one ADPCM path in this machine that is known-correct because Sharp
wrote it, and it is also the real design — so it is not scaffolding, it is P6b's
transport arriving early.
The discriminating stream is already worked out: **twelve loud nibbles to climb
the step index, then every nibble in turn**, whose two reconstructions differ
first at sample 3 (523 against 522). Capture with `-wavwrite` and compare against
`adpcm.decode(nibs, 'shift')` and `adpcm.decode(nibs, 'terms')`. **Watch the
clipping** — the sequence above saturates at 2,047 within six samples, which
destroys discrimination; build one that alternates sign to hold the signal
mid-range while the step index climbs.
**2. THEN P6b — DLXP2, a container with sound in it.** 65.3 is all the
arithmetic: cadence **F=11, A=14**, `LBA(i) = LBA0 + i*97 + floor(i/11)*14`,
14,336 B held, wire 589.6 KB/s. It waits on P6a, because a container full of
audio encoded against the wrong formula is 25 dB of work to redo.
**3. WHAT IS LEFT OF P6 AFTER THAT** is the fourth quarter: 51.3's refill climb
with a second consumer through a real branch point. The slack table is in
`32_audio_wire.py`; nothing has been run.
### 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. If buffer mode does NOT blank, K3's player is already on screen the
whole slot and K4's paint is 27.3% of a frame spent on nothing. **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), B2 (blanking — the five-minute half), B3 (`#EXREQ`),
B4 (a byte write to a palette register).
### Risks that are OURS, not hardware
1. **P6a is open and it is worth 25 dB.** Everything in `adpcm.py`'s output is
provisional.
2. **The encoder is greedy.** Exhaustive per-sample search, no lookahead;
21.97 dB is this format's floor here, not its ceiling.
3. **Nothing has played.** No audio has left an emulated machine.
4. **The packed branch's simplification has now cost something once** (65.4).
It was predicted in the abstract and it was small. It may not be the only one.
### Reproducing this session
./tools/bench/check.sh # ALL GREEN
python3 tools/encoder/extract_audio.py 00223 tmp/au_singe.raw 15625 539.4 10.0
python3 tools/bench/verify_adpcm.py tmp/au_singe.raw
python3 tools/analysis/32_audio_wire.py tmp/packed_singe.dlxp
**WHAT IS NEXT.** P6a: ask the chip which formula it runs, from 68000 code.
---
## Session 32: the packed player runs, and the write window turns out to be the frame