Files
prosolis 6dd3fb3597 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
2026-08-25 09:50:03 -07:00

124 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""What the chip's own decoder model costs the encoder. ROADMAP P6a, after it.
tools/bench/adpcm_run.sh MEASURED four things about the MSM6258 as this machine
models it, and tools/encoder/adpcm.py had a different value for every one:
axis encoder default the chip how it was measured
feed both-high-first both-LOW-first
formula shift terms 1,678 samples, sample-exact
clamp 12-bit 10-bit one model of sixteen matched
init 0 -2
This file prices them, on the same ten seconds of the same stream every audio
figure in this project is quoted against (tmp/au_singe.raw, FINDINGS 65). It
takes an explicit source file rather than defaulting to one, for the same reason
every rate in this tree is an explicit argument (FINDINGS 50).
THE ONE THAT IS NOT A UNIT SLIP is the CLAMP. The other three are conventions:
get one wrong and the decode is wrong, get it right and nothing is lost. A
10-bit accumulator is a smaller container, and it is INSIDE the recursion -- the
predictor cannot represent what will not fit -- so it costs SNR even when the
encoder knows about it and encodes for it. That is a ceiling on this format on
this machine and it is not recoverable by encoding harder.
"""
import math, os, sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "encoder"))
import adpcm
RAW = sys.argv[1] if len(sys.argv) > 1 else "tmp/au_singe.raw"
CHIP = dict(variant="terms", order="low", bits=10, init=-2)
ENC = dict(variant="shift", order="high", bits=12, init=0)
def snr_db(ref, got):
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")
def main():
import struct
if not os.path.exists(RAW):
print(f"no {RAW} -- run tools/encoder/extract_audio.py first")
return 2
pcm = struct.unpack("<%dh" % (os.path.getsize(RAW) // 2), open(RAW, "rb").read())
src12 = [max(-2048, min(2047, x >> 4)) for x in pcm]
print(f"{RAW}: {len(src12):,} samples, peak {max(abs(v) for v in src12)} "
f"in 12-bit units")
print()
print("1. THE COST OF ENCODING FOR THE WRONG CHIP, all four axes at once")
print(" Encode under the encoder's defaults; play it on the chip. The")
print(" nibble ORDER is not a decode parameter -- it decides which nibble")
print(" of each byte the chip takes -- so it is applied by re-reading the")
print(" encoder's own packed bytes the way the chip reads them.")
nib = adpcm.encode(src12, ENC["variant"], init=ENC["init"], bits=ENC["bits"])
same = adpcm.decode(nib, ENC["variant"], init=ENC["init"], bits=ENC["bits"])
data = adpcm.pack(nib, ENC["order"])
asread = list(adpcm.unpack(data, len(nib), CHIP["order"]))
cross = adpcm.decode(asread, CHIP["variant"], init=CHIP["init"], bits=CHIP["bits"])
print(f" encoded and decoded on the encoder's model : {snr_db(src12, same):7.2f} dB")
print(f" encoded on the encoder's, played on the chip: {snr_db(src12, cross):7.2f} dB")
print()
print("2. ONE AXIS AT A TIME, so the bill is itemised rather than lumped")
for name, key, val in (("nibble order", "order", CHIP["order"]),
("delta formula", "variant", CHIP["variant"]),
("clamp", "bits", CHIP["bits"]),
("initial accumulator", "init", CHIP["init"])):
m = dict(ENC); m[key] = val
d = adpcm.pack(nib, ENC["order"])
rd = list(adpcm.unpack(d, len(nib), m["order"]))
got = adpcm.decode(rd, m["variant"], init=m["init"], bits=m["bits"])
print(f" {name:22s} wrong only here: {snr_db(src12, got):7.2f} dB")
print()
print("3. AND THE ONE THAT IS NOT A CONVENTION. Encode FOR the chip -- the")
print(" encoder knows the model and searches against it -- and compare a")
print(" 10-bit accumulator with a 12-bit one on the same seconds.")
for bits in (12, 10):
n = adpcm.encode(src12, CHIP["variant"], init=CHIP["init"], bits=bits)
r = adpcm.decode(n, CHIP["variant"], init=CHIP["init"], bits=bits)
clip = sum(1 for v in r if v in adpcm.clamp_bounds(bits))
print(f" encoded and decoded at {bits}-bit: {snr_db(src12, r):7.2f} dB"
f" ({clip:,} of {len(r):,} samples sit ON the clamp)")
print()
print("4. WHAT THE LEVEL DOES NOW, and it did nothing before (65.1).")
print(" At 12 bits the disc's -13.4 dBFS peak had headroom to spare and")
print(" normalising bought 0.00 dB. A 10-bit accumulator is 4x smaller,")
print(" so the same signal is no longer comfortably inside it.")
peak = max(abs(v) for v in src12)
for name, g in (("as recorded", 1.0),
("scaled to fit 10 bits", 500.0 / peak),
("half of that", 250.0 / peak)):
sc = [max(-512, min(511, int(round(v * g)))) for v in src12]
n = adpcm.encode(sc, CHIP["variant"], init=CHIP["init"], bits=CHIP["bits"])
r = adpcm.decode(n, CHIP["variant"], init=CHIP["init"], bits=CHIP["bits"])
print(f" {name:24s} x{g:5.2f} peak {max(abs(v) for v in sc):4d} "
f"{snr_db(sc, r):7.2f} dB")
print()
print("5. THE HEADROOM, which is the part of this that will bite later.")
hd = 20 * math.log10(511.0 / peak)
print(f" This window peaks at {peak} of the 10-bit accumulator's 511, so it")
print(f" has {hd:.1f} dB of headroom left -- and it is a QUIET passage: the")
print(" disc peaks at -13.4 dBFS here (65.1). A 10-bit accumulator is")
print(f" {20*math.log10(2047.0/511.0):.1f} dB smaller than the 12-bit word the encoder was")
print(" clamping to, so a passage only a few dB louder than this one does")
print(" not fit and the predictor CLIPS inside the recursion. Nothing in")
print(" this project has measured the loudest passage on the disc; until")
print(" something does, the audio level is an OPEN choice and not a")
print(" settled one, and 65.1's `the level is not a lever` is now wrong")
print(" in one direction: it is not a lever UPWARD.")
print()
print(" The rows in 4 are NOT comparable as absolute quality")
print(" -- each is scored against its OWN scaled reference, so what they")
print(" compare is how well the format tracks a signal of that size.")
return 0
if __name__ == "__main__":
sys.exit(main())