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
+181
View File
@@ -6768,3 +6768,184 @@ throughout) — and no MAME source tree was available on this machine to name it
third** that decides whether a DMAC-direct packed player shows a picture.
- **Whether B is buildable as described.** It is priced off a measured blit and
a measured ladder, and no line of it has been written.
---
## 65. Audio has an encoder, and the packed container's best property is what makes it cost (session 33)
**ROADMAP P6**, everything in the item except the bus half session 20 closed.
`tools/encoder/adpcm.py` is the codec, `tools/encoder/extract_audio.py` the
extraction, `tools/bench/verify_adpcm.py` the gate, `tools/analysis/32_audio_wire.py`
the container arithmetic. All of it is **host arithmetic and one emulator
introspection**; no board ran, and the one MAME experiment that was attempted
did not work — 65.5 says so rather than leaving it out.
### 65.1 There is no reference encoder, so the gate had to be built sideways
The X68000's ADPCM is an OKI MSM6258V: 4 bits a sample, two to a byte, 12-bit
signal word, and three rates that are 8 MHz over 512, 768 and 1024. **ffmpeg has
a decoder for the format (`adpcm_ima_oki`) and no encoder**, so there is nothing
to diff an encoder against.
What `verify_adpcm.py` gates instead is the thing that can actually be wrong:
the encoder runs a decoder **inside its own loop** to choose each nibble, and
that decoder is checked **sample-exact against ffmpeg's** over 4,268 nibbles.
An encoder that agrees with its own wrong decoder is exactly the failure mode a
round-trip test cannot see.
Two facts fell out of building it, and both were **measured rather than assumed**:
| | |
|---|---|
| nibble order | **HIGH NIBBLE FIRST** — reading low-first disagrees with ffmpeg on 3,285 of 4,268 samples, and that mismatch is carried as the gate's negative control |
| the step table | 49 entries, **BUILT** as `floor(16 * 1.1**k)` and checked against the published list, so a transcription slip is not one of the things that can be wrong |
On the window this project gates everything on (00223 @539.4 s, 10.000 s, the
same seconds as `tmp/fr_singe`): **156,250 samples → 78,125 B, SNR 21.97 dB**,
and 78,125 B / 10.000 s is **7,812.5 B/s to the byte**, which is 52's figure
arriving from the other direction.
**A negative worth having: the level is not a lever.** The disc's window peaks
at 13.4 dBFS, using 435 of the 12-bit word's 2,048. Normalising it — gain ×4.7,
one sample clipped — moves the SNR from **21.97 dB to 21.97 dB**. The step
table's adaptation covers the range, so there is no headroom win to collect and
no reason to touch the disc's level.
### 65.2 The two available references DISAGREE, and it costs 25 dB
The delta a nibble contributes has two forms in circulation:
'shift' delta = ((2*(n&7) + 1) * step) >> 3
'terms' delta = step/8 + (n&4)*step + (n&2)*step/2 + (n&1)*step/4,
each term truncated independently
`'shift'` is what ffmpeg computes — **verified sample-exact here, so that is a
measurement of the decoder that ships, not a reading of its source.** `'terms'`
is the OKI datasheet's own form, the one a chip builds out of shifts and adds,
and it is what MAME's `okim6258` is understood to compute. **That last clause is
NOT verified**: no MAME source tree is on this machine (64.4).
They differ **on 1,000 of 4,268 sampled nibbles, by at most 3 in 12-bit units**,
which reads like something nobody could hear. **It is not.**
| encoded | decoded | SNR |
|---|---|---:|
| `shift` | `shift` | **21.97 dB** |
| `shift` | `terms` | **2.88 dB** |
| `terms` | `terms` | 21.99 dB |
| `terms` | `shift` | 3.38 dB |
**The noise is louder than the signal.** Per-sample disagreement over the real
window: **max 257, mean 78.2**, against a source whose RMS is 74.4. A 3-LSB
formula difference becomes a 25 dB loss because **ADPCM is RECURSIVE** — the
delta is added to a running predictor and the nibble also moves the step index,
so a disagreement does not stay where it happens. It is the same shape as the
codec's temporal recursion, one dimension down: 64.1 used that recursion to make
one frame audit 120, and here the same property turns a rounding difference into
a broken stream.
**So "which formula does the MSM6258 run" is not a footnote. It is a
precondition on shipping any audio at all**, and it has to be answered before an
encoder's output is committed to a container.
### 65.3 The interleave, and why the obvious cadence is the wrong one
**A packed record is 49,664 B = 97 sectors and its address is `LBA0 + i*97`.
There is no index and none can be needed** — that is the format's whole claim
(63, 64.1). Audio is a stream at a rate with no arithmetic relationship to the
frame rate: at 15,625 Hz a 12 fps slot is **651.0417 B**, and the `.0417` is the
same remainder the frame clock carries (54), because 8 MHz / 512 / 2 / 12 has a
3 in the denominator that no power of two clears.
Give record *i* the audio belonging to slot *i* and the records become variable
length — and the moment records are variable length the format needs an index
and stops being the format. So audio rides a **fixed cadence**: every `F`
frames, `A` whole sectors, placed between records, leaving
LBA(i) = LBA0 + i*97 + floor(i/F)*A
which is still arithmetic. Choosing `(F, A)` is a rational approximation to
`15625/12288 = 1.271565755` from above, and **the obvious cadence is the worst
point in the space**:
| F | A | lump | needs | padding | wire adds | held (2 lumps) |
|---:|---:|---:|---:|---:|---:|---:|
| **1** — one lump a record | 2 | 1,024 B | 651.0 B | **57.29%** | **12.00 KB/s** | 2,048 B |
| 3 | 4 | 2,048 B | 1,953.1 B | 4.86% | 8.00 KB/s | 4,096 B |
| 7 | 9 | 4,608 B | 4,557.3 B | 1.11% | 7.71 KB/s | 9,216 B |
| **11** | **14** | **7,168 B** | 7,161.5 B | **0.09%** | **7.64 KB/s** | **14,336 B** |
| 81 | 103 | 52,736 B | 52,734.4 B | 0.003% | 7.63 KB/s | 105,472 B |
**F=11 is the pick.** It buys 57.2 points of padding for 12,288 B of RAM over
the naive cadence; the floor of the sweep buys the last 0.09 of a point for
91,136 B more, and on a machine where K4 already wants 99,328 B for two record
buffers that second trade is not one.
**The wire, then:** the packed container's sustained requirement was **582.0 KB/s
silent** and is **589.6 KB/s with sound** (+1.31%). A literal frame's bitrate is
geometry and cannot be talked down; the audio on top of it is 7.63 KB/s of
payload and can only be talked down by choosing a worse chip rate.
### 65.4 And this is the first price anyone has found for the packed branch's own simplification
The codec container **pays none of it**. `rc_fr_singe_scsi_span.dlx` already
carries an index and already has variable records (4,096..41,472 B, sector-aligned
since DLX5), so it can put exactly 651.0417 B of audio in record *i* and pad only
to the sector it was going to pad to anyway: **zero audio padding**, wire
440.4 → 448.1 KB/s.
"A record's length is geometry, so there is no index and none can be needed" is
what makes the packed player a page of arithmetic instead of a parser — and it
is **exactly** the property that makes a second stream at an unrelated rate cost
a cadence, a padding fraction and a 14,336 B buffer. It is a small cost and it
is not zero, and **nothing in FINDINGS 61-64 predicted it**. 64's risk list said
"a simplification that large usually hides something"; this is the first thing
it hid.
The bus half reproduces 52 exactly, which is why the tool prints it: 651.0 B a
slot at the IPL ROM's own channel-3 cost of 16..19 clk/B is **10,417..12,370
clocks = 1.25%..1.48% of a slot**. **The interaction 52 could not have had** is
with 64.2's write window: a DMAC-direct packed player holds the GVRAM window
open for the whole data phase, so an audio channel stealing the bus during that
phase makes the phase longer — audio costs **darkness**, not just clocks. It is
negligible against a dark fraction that is already 1.0, and it is not negligible
against K4's 27.3% paint. That is the third time this session the two players
have ranked differently on a column that is not clocks.
### 65.5 The experiment that did not work, stated rather than omitted
65.2's open question has an obvious apparatus: **MAME's x68000 has the chip**,
so it can be asked. Introspection got as far as fact and no further, and the
facts are worth keeping because the next attempt starts from them rather than
from folklore:
| | |
|---|---|
| device | **`:okim6258`**, shortname `okim6258` — it is there |
| registers | **`$E92001`** and **`$E92003`**, each one byte, read out of the maincpu program map by `tools/bench/probe_adpcm.lua` |
| also on the map | `$E9A000-$E9BFFF`, the PPI that carries ADPCM pan and the clock divider |
**Feeding the chip from Lua produced no audio.** `tools/bench/probe_adpcm2.lua`
writes a control byte to `$E92001` and nibble pairs to `$E92003`;
`probe_adpcm3.lua` sweeps PPI port C at `$E9A005` over all sixteen low-nibble
values with a loud burst under each. **Control 0..3 × port C 0..15: MAME's
`-wavwrite` capture is silent throughout, 0 of 567,360 samples non-zero.**
The register *semantics* are the gap — which control value starts playback,
whether port C needs the PPI's mode word set first, and whether the chip has a
clock at all until the divider is written. **None of that was guessed at further,
because guessing at it is how a rig produces a confident wrong answer.** The way
to do this is 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 — the real design, and the one path in the machine that is known to be
correct because Sharp wrote it.
### 65.6 What this does not settle
- **Which delta formula the chip runs.** 65.2, and it is worth 25 dB.
- **Anything on a board.** No hardware ran. The `-wavwrite` silence is a
statement about an apparatus, not about a chip.
- **The container.** DLXP1 has no audio section; 65.3 is the arithmetic a
DLXP2 would be built from, and no byte of one has been written.
- **What audio does to a scene change.** The slack table is here, but 51.3's
refill climb with a second consumer through a real branch point is not.