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
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Gate tools/encoder/adpcm.py against the only independent decoder on this
machine: ffmpeg's `adpcm_ima_oki`.
There is no ffmpeg ENCODER for this format -- `adpcm_ima_oki` is decode-only --
so the encoder here cannot be checked against a reference implementation. What
CAN be checked, and is, is that the decoder our encoder runs in its own loop is
byte-for-byte the decoder that ships in ffmpeg. An encoder that agrees with its
own wrong decoder is exactly the failure this catches.
Usage: verify_adpcm.py [wav_or_raw12 ...]
"""
import os, struct, subprocess, sys, random, math
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "encoder"))
import adpcm
TMP = "tmp/adpcm_gate"
# The step table as it is printed in the OKI datasheet and in every
# implementation of this format. adpcm.py BUILDS its table from 16*1.1**k; if
# the two ever disagree, one of them is a typo and this says which.
CANON = [16,17,19,21,23,25,28,31,34,37,41,45,50,55,60,66,73,80,88,97,107,118,
130,143,157,173,190,209,230,253,279,307,337,371,408,449,494,544,598,
658,724,796,876,963,1060,1166,1282,1411,1552]
fails = []
def ck(ok, msg):
print(("OK " if ok else "FAIL ") + msg)
if not ok: fails.append(msg)
def ffmpeg_decode(data, rate=15625):
"""Decode packed OKI ADPCM through ffmpeg, by wrapping it in a WAV whose
format tag is 0x0010 (WAVE_FORMAT_OKI_ADPCM). Returns 16-bit samples."""
os.makedirs(TMP, exist_ok=True)
fmt = struct.pack("<HHIIHHH", 0x0010, 1, rate, rate, 1, 4, 0)
body = (b"WAVE" + b"fmt " + struct.pack("<I", len(fmt)) + fmt
+ b"data" + struct.pack("<I", len(data)) + data)
w = f"{TMP}/probe.wav"
open(w, "wb").write(b"RIFF" + struct.pack("<I", len(body)) + body)
raw = subprocess.check_output(
["ffmpeg", "-v", "error", "-i", w, "-f", "s16le", "-acodec", "pcm_s16le", "-"])
return list(struct.unpack("<%dh" % (len(raw) // 2), raw))
def snr_db(ref, got):
"""Signal-to-noise over the 12-bit sample word."""
num = sum(float(s) * s for s in ref)
den = sum((float(a) - b) ** 2 for a, b in zip(ref, got))
if den == 0: return float("inf")
return 10.0 * math.log10(num / den) if num else float("-inf")
print("--- the step table ---")
ck(adpcm.STEP == CANON, f"49 entries, built = published (16*1.1**k), {adpcm.STEP[0]}..{adpcm.STEP[-1]}")
print("--- our decoder vs ffmpeg's adpcm_ima_oki ---")
random.seed(1234)
nibs = ([7]*12 + [i % 16 for i in range(256)]
+ [random.randrange(16) for _ in range(4000)])
data = adpcm.pack(nibs)
ff = ffmpeg_decode(data)
ours = [v * 16 for v in adpcm.decode(adpcm.unpack(data, len(nibs)), "shift")]
ck(len(ff) == len(ours), f"sample count {len(ff)} = {len(ours)}")
ck(ff == ours, f"variant 'shift' is SAMPLE-EXACT vs ffmpeg over {len(nibs)} nibbles")
# NEGATIVE CONTROL. A gate that passes whatever it is handed proves nothing;
# reading the nibbles the other way round has to go red, or "high nibble first"
# is an assertion rather than a measurement.
lowfirst = [adpcm.unpack(data, len(nibs))[i ^ 1] for i in range(len(nibs))]
bad = [v * 16 for v in adpcm.decode(lowfirst, "shift")]
ndiff = sum(1 for a, b in zip(ff, bad) if a != b)
ck(ndiff > 0, f"low-nibble-first DISAGREES on {ndiff}/{len(ff)} -- so the order is measured, not assumed")
print("--- and the second variant is not the same decoder ---")
terms = [v * 16 for v in adpcm.decode(adpcm.unpack(data, len(nibs)), "terms")]
d = [abs(a - b) // 16 for a, b in zip(ff, terms)]
nd = sum(1 for x in d if x)
ck(nd > 0, f"variant 'terms' differs on {nd}/{len(d)} samples, max {max(d)} in 12-bit units"
" -- OPEN: which one the MSM6258 runs is unmeasured")
print("--- and getting the variant wrong is NOT a rounding error ---")
# THE MEASUREMENT THAT CHANGED THIS FROM A FOOTNOTE INTO AN OPEN ITEM. The two
# variants differ by at most 3 in 12-bit units PER SAMPLE, which reads like
# something nobody could hear. ADPCM is RECURSIVE -- the delta is added to a
# running predictor and the nibble also moves the step index -- so the
# disagreement does not stay where it happens. It is the same shape as the
# codec's temporal recursion (64.1), one dimension down.
if os.path.exists("tmp/au_singe.raw"):
raw = open("tmp/au_singe.raw", "rb").read()
pcm = struct.unpack("<%dh" % (len(raw) // 2), raw)
ref = [max(-2048, min(2047, x >> 4)) for x in pcm]
nib = adpcm.encode(ref, "shift")
same, cross = adpcm.decode(nib, "shift"), adpcm.decode(nib, "terms")
err = [abs(x - y) for x, y in zip(same, cross)]
print(f" encoded 'shift', decoded 'shift': SNR {snr_db(ref, same):6.2f} dB")
print(f" encoded 'shift', decoded 'terms': SNR {snr_db(ref, cross):6.2f} dB"
f" <- the noise is LOUDER THAN THE SIGNAL")
print(f" per-sample disagreement over {len(err):,} samples: max {max(err)}, "
f"mean {sum(err)/len(err):.1f} in 12-bit units")
ck(snr_db(ref, cross) < 0,
"a 3-LSB formula disagreement costs ~25 dB, because ADPCM is RECURSIVE")
else:
print(" SKIPPED: no tmp/au_singe.raw (tools/encoder/extract_audio.py)")
print("--- the encoder, through the gated decoder ---")
for path in (sys.argv[1:] or []):
raw = open(path, "rb").read()
if raw[:4] == b"RIFF":
raw = subprocess.check_output(["ffmpeg", "-v", "error", "-i", path,
"-f", "s16le", "-ac", "1", "-ar", "15625", "-"])
pcm16 = struct.unpack("<%dh" % (len(raw) // 2), raw)
src = [max(-2048, min(2047, s >> 4)) for s in pcm16]
nib = adpcm.encode(src, "shift")
packed = adpcm.pack(nib)
rec_ff = [v // 16 for v in ffmpeg_decode(packed)][:len(src)]
rec_us = adpcm.decode(nib, "shift")
ck(rec_ff == rec_us,
f"{os.path.basename(path)}: encoder's own reconstruction = ffmpeg's, {len(src)} samples")
print(f" {len(src)} samples, {len(packed)} B, SNR {snr_db(src, rec_us):.2f} dB "
f"(12-bit word; the source is already quantised to it)")
print("ADPCM GATE " + ("GREEN" if not fails else f"RED: {len(fails)} failed"))
sys.exit(1 if fails else 0)