#!/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())