ROADMAP P6c, FINDINGS 68. 78,125 B of a DLXP2's audio out of channel 3, sample-exact, while the video channel fetches records off the same disc. The two pieces 67.6 said were missing: the lump buffer (pg_afill/pg_afetch, three slots and the minimum is unmeasured) and 67.2's remainder accumulator (pg_apay). The capture prices what the accumulator avoided at 1.26 s of lip-sync over the game, against 67.2's predicted 1.25. The finding is the third piece, which nothing had named: the MSM6258 has no starvation state, so the gap between a channel counting out and the next arm is a held nibble pair driving the predictor. Stealing, the seam is 0.51 ms over ten seconds because dma.i's new DM_HOOK services the chip from inside the transfer wait -- 250,000 of 250,240 looks. Held, the 68000 is halted and gets 369: every one of the ten lump boundaries has a seam, worst 72.8 ms, 2.31% of the audio. Identical bytes, different sound. 64.3 reaching the audio. Two bugs, and no counter in the player could see either. Clearing DM_BARV does not unchain a channel -- OCR bits 3-2 are what it obeys -- and the symptom is POLL TIMEOUT on the lump and every record after it. And the refill ran one lump ahead of its ring and overwrote the buffer the channel was reading: 11 of 11 armed, 11 fetched, no starve, and the sound wrong from 0.2 s in. Which is why the gate is a WAV: verify_packed_audio.py walks the stream one delivered byte at a time, because MAME's okim6258 resets the nibble select on every write and a byte is two nibbles only 99.994% of the time. check.sh ALL GREEN before (tmp/check_s36_start.log) and after (tmp/check_s36_end.log), with the new stage. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
187 lines
8.5 KiB
Python
187 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""MSM6258 (OKI/Dialogic) 4-bit ADPCM -- encoder, decoder, and the fact that
|
|
there are TWO decoders and they are not the same one.
|
|
|
|
The X68000's ADPCM is an OKI MSM6258V clocked at 8 MHz, dividing to 15,625 /
|
|
10,417 / 7,812.5 samples a second, 4 bits each, two samples to a byte
|
|
(FINDINGS 52, buscost.ADPCM_SAMPLE_HZ). The sample word is 12 bits signed.
|
|
|
|
WHY THIS FILE HAS TWO DECODERS. Nothing in this repo can be trusted to say what
|
|
the chip does, and the two references available on this machine DISAGREE:
|
|
|
|
VARIANT 'shift' delta = ((2*(n&7) + 1) * step) >> 3
|
|
This is ffmpeg's `adpcm_ima_oki`, and `gate_vs_ffmpeg()`
|
|
reproduces it SAMPLE-EXACT, so it is not a reading of source
|
|
code -- it is a measurement of the decoder that ships.
|
|
|
|
VARIANT 'terms' delta = step/8 + (n&4 ? step : 0) + (n&2 ? step/2 : 0)
|
|
+ (n&1 ? step/4 : 0), each term truncated
|
|
This is the OKI datasheet's own form, the one an ADPCM chip
|
|
can actually build out of shifts and adds, and it is what
|
|
MAME's okim6258 is understood to compute. NOT VERIFIED HERE:
|
|
no MAME source tree is on this machine (FINDINGS 64.4).
|
|
|
|
They differ on 445 of 2,268 sampled nibbles, by up to 4 in 12-bit units --
|
|
small, and small is not zero. Which one the machine runs is an open question
|
|
with an experiment attached: MAME's x68000 HAS an okim6258, so it can be asked
|
|
rather than argued about.
|
|
|
|
Nibble order is HIGH NIBBLE FIRST within a byte -- measured, not assumed, by the
|
|
same gate: reading low-first mismatches ffmpeg on 1,728 of 2,268 samples.
|
|
"""
|
|
|
|
# The 49-entry OKI step table. floor(16 * 1.1**k) for k in 0..48 -- built rather
|
|
# than pasted, so a transcription slip is not one of the things that can be
|
|
# wrong here.
|
|
STEP = [int(16 * 1.1**k) for k in range(49)]
|
|
|
|
# The nibble magnitude's effect on the step index. Four quiet nibbles walk it
|
|
# down one, four loud ones walk it up by more.
|
|
INDEX_ADJUST = (-1, -1, -1, -1, 2, 4, 6, 8)
|
|
|
|
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."""
|
|
if variant == "shift":
|
|
d = ((2 * (nibble & 7) + 1) * step) >> 3
|
|
elif variant == "terms":
|
|
d = step // 8
|
|
if nibble & 4: d += step
|
|
if nibble & 2: d += step // 2
|
|
if nibble & 1: d += step // 4
|
|
else:
|
|
raise ValueError(f"unknown variant {variant!r}")
|
|
return -d if nibble & 8 else d
|
|
|
|
|
|
def decode_state(nibbles, variant="shift", state=None, init=0, bits=12):
|
|
"""The recursion, with its STATE in and out. (samples, (signal, idx)).
|
|
|
|
`decode` is this with the state thrown away, and it is written this way
|
|
round rather than duplicated because a stream that STOPS and RESUMES is not
|
|
a hypothetical here: an MSM6258 fed by a DMA channel goes on decoding
|
|
whatever byte its data register still holds when the channel counts out
|
|
(MAME okim6258 sound_stream_update reads m_data_in unconditionally while
|
|
PLAYING), so the samples between one lump and the next are the recursion
|
|
continuing over a repeated byte. tools/bench/verify_packed_audio.py has to
|
|
carry that state across the seam to check the lump on the far side of it,
|
|
and a second copy of the loop is a second place the clamp can drift.
|
|
"""
|
|
lo, hi = clamp_bounds(bits)
|
|
signal, idx = state if state is not None else (init, 0)
|
|
out = []
|
|
for n in nibbles:
|
|
signal += delta(n, STEP[idx], variant)
|
|
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, (signal, idx)
|
|
|
|
|
|
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."""
|
|
return decode_state(nibbles, variant, None, init, bits)[0]
|
|
|
|
|
|
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
|
|
reconstruction error of this sample. That is greedy rather than optimal --
|
|
a nibble also moves the step index, so a locally worse choice can pay later
|
|
-- but it is what a chip-matched encoder is expected to do and it costs
|
|
nothing offline. The decoder is run INSIDE the loop, so the encoder can
|
|
never drift away from what the decoder will reconstruct.
|
|
"""
|
|
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 = 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 = 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, 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"
|
|
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, order="high"):
|
|
if order not in ORDERS:
|
|
raise ValueError(f"unknown nibble order {order!r}")
|
|
out = bytearray()
|
|
for b in data:
|
|
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)
|