Ask the chip which decoder it is, and find four wrong axes where one was expected

ROADMAP P6a, on the machine. 68000 code programs HD63450 channel 3 with the
IPL ROM's own ADPCM bytes -- dual address, 8-bit port, cycle steal, external
request -- and feeds the MSM6258 a designed 1,678-nibble stream at the chip's
own pace: 839 B in 0.1074 s = 7,811.4 B/s against the format's 7,812.5, CER=$00.
That transport is P6b's, not scaffolding.

Sixteen candidate decoder models, three capture decimations and a searched
prologue are fitted to MAME's capture. Exactly one reproduces it sample-exact
over all 1,678 samples, and every axis carries a negative control: flip it
alone and the closest survivor disagrees on 826, 1,504, 156 and 1,522 samples.

The chip runs 'terms', takes the LOW nibble of a byte first, clamps the
accumulator at 10 bits and starts it at -2. tools/encoder/adpcm.py defaulted to
the opposite of all four, and 65.2 named the wrong axis as the risk: the delta
formula is worth -2.88 dB and the NIBBLE ORDER is worth -25.74 dB. 65.1's "high
first, measured" was a measurement of ffmpeg, i.e. of the VOX file convention,
which is a different question from what a chip does with a byte in its data
register.

The 10-bit clamp is free on the Singe window and only because that window peaks
at 435 of 511 -- 1.4 dB of headroom on a -13.4 dBFS passage, 12.1 dB below where
the encoder was clamping, and inside the recursion. So the audio level is an
open choice again, downward, and the loudest passage on the disc is unmeasured.

Session 33's silence had two ordinary causes: the PPI's port C is an input until
control word $92 says otherwise, and $01 is COMMAND_STOP. And a rig fact worth
the space: the 8 MHz ADPCM clock is CT1 in the YM2151's $1B, delivered on the
sound system's schedule rather than at the store, so a transfer started in the
same breath as the setup plays its first ~17 ms at the old clock and no model
fits a stream that changed rate part way through.

Name the layer: this is MAME 0.277's okim6258 device model measured end to end
through the machine's real transport. It settles the rig and not the silicon.
Also struck: 64.4's "no MAME source tree is on this machine" -- there is none on
disk, but the machine has network and the upstream tag fetches.

check.sh ALL GREEN before (tmp/check_s34_start.log) and after
(tmp/check_s34_end.log), with one new stage.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 09:50:03 -07:00
parent f925a1dd9a
commit 6dd3fb3597
15 changed files with 1240 additions and 22 deletions
+69 -18
View File
@@ -43,6 +43,48 @@ SAMPLE_MIN, SAMPLE_MAX = -2048, 2047 # the 12-bit DAC word
VARIANTS = ("shift", "terms")
# ---------------------------------------------------------------------------
# THE THREE AXES THAT WERE FIXED CONSTANTS UNTIL SESSION 34, and every one of
# them turned out to be a real choice that a decoder can get wrong. FINDINGS 65
# priced the `variant` axis at 25 dB and left the other three unnamed; MAME's
# okim6258 disagrees with this file on ALL THREE, so they are parameters now and
# tools/bench/adpcm_run.sh measures which values the emulated chip runs.
#
# ORDER which nibble of a byte is played FIRST. 'high' is the Dialogic VOX
# file convention and is what ffmpeg's adpcm_ima_oki reads, which is
# what 65.1 measured. That is a fact about a FILE FORMAT. What the
# chip does with a byte handed to its data register is a different
# question and MAME answers it 'low'.
# INIT the accumulator at the instant the chip is told to PLAY. This file
# started it at 0; MAME's okim6258 resets it to -2.
# BITS where the accumulator CLAMPS. This file clamped at the 12-bit ADPCM
# word; the MSM6258's own D/A is 10-bit and MAME clamps there, INSIDE
# the recursion, so it is not a post-hoc output scaling.
#
# Defaults are unchanged, so tools/bench/verify_adpcm.py still measures exactly
# what it measured in session 33: ffmpeg's decoder, high nibble first.
ORDERS = ("high", "low")
# WHAT THE MACHINE'S OWN CHIP DOES, MEASURED -- tools/bench/adpcm_run.sh, one
# model of sixteen reproducing 1,678 consecutive samples of a MAME capture
# sample-exact, with a negative control on every axis (FINDINGS 66). It is a
# measurement of MAME's device model driven through the real transport, not of
# an MSM6258; the silicon is still a hardware item.
#
# THE DEFAULTS ABOVE ARE DELIBERATELY *NOT* THESE. The defaults are ffmpeg's
# adpcm_ima_oki, because tools/bench/verify_adpcm.py's whole value is that it
# checks this file against an independent implementation, and a default that
# had drifted to match the thing under test would end that. Anything that
# ENCODES FOR THE MACHINE passes CHIP explicitly.
CHIP = dict(variant="terms", order="low", bits=10, init=-2)
def clamp_bounds(bits):
"""The accumulator's clamp, as MAME's okim6258 computes it: max = 2^(b-1)-1,
min = -2^(b-1). Note it is NOT symmetric, and the asymmetry is load-bearing
on a signal that saturates."""
return -(1 << (bits - 1)), (1 << (bits - 1)) - 1
def delta(nibble, step, variant):
"""The reconstruction step for one nibble, in 12-bit units."""
@@ -58,21 +100,21 @@ def delta(nibble, step, variant):
return -d if nibble & 8 else d
def decode(nibbles, variant="shift"):
"""Nibbles -> 12-bit signed samples. State is (signal, step index), both
zero at the start of a stream, which is what the chip resets to."""
signal, idx, out = 0, 0, []
def decode(nibbles, variant="shift", init=0, bits=12):
"""Nibbles -> signed samples. State is (signal, step index); the step index
is 0 at the start of a stream and `init` is where the accumulator starts."""
lo, hi = clamp_bounds(bits)
signal, idx, out = init, 0, []
for n in nibbles:
signal += delta(n, STEP[idx], variant)
signal = SAMPLE_MIN if signal < SAMPLE_MIN else (
SAMPLE_MAX if signal > SAMPLE_MAX else signal)
signal = lo if signal < lo else (hi if signal > hi else signal)
idx += INDEX_ADJUST[n & 7]
idx = 0 if idx < 0 else (48 if idx > 48 else idx)
out.append(signal)
return out
def encode(samples, variant="shift"):
def encode(samples, variant="shift", init=0, bits=12):
"""12-bit signed samples -> nibbles.
The nibble is chosen by EXHAUSTIVE SEARCH over all sixteen, minimising the
@@ -82,37 +124,46 @@ def encode(samples, variant="shift"):
nothing offline. The decoder is run INSIDE the loop, so the encoder can
never drift away from what the decoder will reconstruct.
"""
signal, idx, out = 0, 0, bytearray()
lo, hi = clamp_bounds(bits)
signal, idx, out = init, 0, bytearray()
for s in samples:
step = STEP[idx]
best, best_err = 0, None
for n in range(16):
v = signal + delta(n, step, variant)
v = SAMPLE_MIN if v < SAMPLE_MIN else (SAMPLE_MAX if v > SAMPLE_MAX else v)
v = lo if v < lo else (hi if v > hi else v)
err = (v - s) ** 2
if best_err is None or err < best_err:
best, best_err = n, err
signal += delta(best, step, variant)
signal = SAMPLE_MIN if signal < SAMPLE_MIN else (
SAMPLE_MAX if signal > SAMPLE_MAX else signal)
signal = lo if signal < lo else (hi if signal > hi else signal)
idx += INDEX_ADJUST[best & 7]
idx = 0 if idx < 0 else (48 if idx > 48 else idx)
out.append(best)
return bytes(out)
def pack(nibbles):
"""Nibbles -> bytes, HIGH NIBBLE FIRST. An odd count pads with a 0 nibble,
which is the quietest one the format has (delta = step/8)."""
def pack(nibbles, order="high"):
"""Nibbles -> bytes. `order` names which nibble of a byte is played FIRST;
'high' is the VOX file convention. An odd count pads with a 0 nibble, which
is the quietest one the format has (delta = step/8)."""
if order not in ORDERS:
raise ValueError(f"unknown nibble order {order!r}")
n = bytes(nibbles)
if len(n) & 1:
n += b"\0"
return bytes((n[i] << 4) | n[i + 1] for i in range(0, len(n), 2))
if order == "high":
return bytes((n[i] << 4) | n[i + 1] for i in range(0, len(n), 2))
return bytes((n[i + 1] << 4) | n[i] for i in range(0, len(n), 2))
def unpack(data, count=None):
def unpack(data, count=None, order="high"):
if order not in ORDERS:
raise ValueError(f"unknown nibble order {order!r}")
out = bytearray()
for b in data:
out.append(b >> 4)
out.append(b & 15)
if order == "high":
out.append(b >> 4); out.append(b & 15)
else:
out.append(b & 15); out.append(b >> 4)
return bytes(out[:count] if count is not None else out)