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
124 lines
6.5 KiB
Python
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())
|