#!/usr/bin/env python3 """DLXP2's GATE: a packed container with sound in it. ROADMAP P6b. python3 tools/analysis/34_packed_audio.py [packed.dlxp] [--audio tmp/au_singe.raw] WHAT THIS IS FOR. FINDINGS 65.3 did the arithmetic of putting audio in the packed container and wrote no byte of one; 66 measured which of sixteen decoder models the machine's chip runs and priced the axes at up to 25.7 dB. This is the container those two produce, and the reason it needs a gate of its own is that NOTHING PARSES A PACKED CONTAINER. A DMA channel copies bytes and has no opinion about them (62), so a container whose lump is one sector out does not fail -- it plays 512 B of picture as audio and 512 B of audio as picture, both of which are things, and a gate that only checked for errors would pass it. THE FOUR CLAIMS, and each is checked against something that is not the writer: 1. THE FILE IS ITS OWN ARITHMETIC. Every byte of the container is accounted for by `off_frm + i*rec + (i//F)*A*512` and `off_aud + k*(F*rec + A*512)` with no byte left over and no byte claimed twice. A per-record read cannot catch an off-by-one that shifts everything after it; a partition can. 2. THE PICTURE DID NOT MOVE. Interleaving a second stream into a format whose whole claim is "record i is at LBA0 + i*97" is exactly the change that can break that claim, so every record is compared against a re-encode of the same frames with `--audio` off. The silent container is the control. 3. THE BYTES ARE THE ENCODER'S. The lumps, concatenated, are byte-exact against `adpcm.encode` run again on the same PCM with the same four axes. 4. THE HEADER'S AXES ARE LOAD-BEARING. The stream decodes to the source at the SNR the encoder reported, and flipping any ONE of the four axes the header carries collapses it. A header field nothing would notice being wrong is a comment. AND THE FINDING IT REPORTS (FINDINGS 67). The padding is not where a reader of 65.3 would put it. A lump is A*512 B of SPACE; F frames of audio is F*hz/(2*fps) B, which at F=11 is 7,161.4583..., so the PAYLOAD alternates 7,161 and 7,162 and the sector run is 7,168 either way. A player that handed the chip the whole lump -- the obvious implementation, and the one the phrase "14 sectors of audio every 11 frames" invites -- would be feeding it 6.54 B a group too much. That is not waste, which is what padding usually is. It is DRIFT. """ import argparse, math, os, struct, sys sys.path.insert(0, "tools/encoder") sys.path.insert(0, "tools/analysis") import adpcm import dlxp as P from dlxp import DLXP, SECTOR ap = argparse.ArgumentParser() ap.add_argument("container", nargs="?", default="tmp/packed_singe.dlxp") ap.add_argument("--audio", default="tmp/au_singe.raw") ap.add_argument("--silent", default="tmp/packed_singe_silent.dlxp", help="the control: the same frames with --audio off. Built by " "check.sh; skipped rather than faked when absent") ap.add_argument("--game-min", type=float, default=22.8, help="the game's running length, for what the drift comes to") a = ap.parse_args() fails = [] def ck(ok, msg): print((" OK " if ok else " FAIL ") + msg) if not ok: fails.append(msg) d = DLXP(a.container) # every format invariant is checked here print(f"{a.container}: DLXP{d.version} {d.W}x{d.H} {d.fps}fps {d.nframes} frames, " f"{'AUDIO' if d.has_audio else 'SILENT'}") if not d.has_audio: sys.exit(f"{a.container} carries no audio -- this gate has nothing to check. " f"Build it with tools/encoder/pack.py --audio") grp = d.cad_f * d.aud_hz / (2 * d.fps) print(f""" === THE LAYOUT ========================================================= record {d.rec_bytes:,} B = {d.rec_bytes//SECTOR} sectors, lump {d.cad_a*SECTOR:,} B = {d.cad_a} sectors, cadence F={d.cad_f} A={d.cad_a}, {d.n_lumps} lumps, {d.aud_bytes:,} B of ADPCM at {d.aud_hz:,} Hz record i = {d.off_frm:,} + i*{d.rec_bytes:,} + (i//{d.cad_f})*{d.cad_a*SECTOR:,} lump k = {d.off_aud:,} + k*{d.cad_f*d.rec_bytes + d.cad_a*SECTOR:,} and NEITHER of those is a lookup. A packed record's length is geometry and a lump's is a cadence, so DLXP2 still has no index and still needs none.""") # --- 1. the file is its own arithmetic ------------------------------------ # Every byte, partitioned. Not "does record 7 read back" -- an off-by-one that # shifts the whole stream reads back fine one record at a time. spans = [(d.frame_off(i), d.rec_bytes, f"record {i}") for i in range(d.nframes)] spans += [(d.lump_off(k), d.cad_a * SECTOR, f"lump {k}") for k in range(d.n_lumps)] spans.sort() pos, overlap, gap = SECTOR, [], [] for off, n, what in spans: if off < pos: overlap.append(what) elif off > pos: gap.append((pos, off, what)) pos = max(pos, off + n) ck(not overlap, f"nothing overlaps ({len(spans)} spans: {d.nframes} records " f"+ {d.n_lumps} lumps)" + (f" -- {overlap[:3]}" if overlap else "")) ck(not gap, "no byte between the header and the end belongs to nothing" + (f" -- {gap[:3]}" if gap else "")) ck(pos == len(d.raw), f"the arithmetic ends at {pos:,} and the file is " f"{len(d.raw):,} B") ck(all(off % SECTOR == 0 for off, _, _ in spans), "every record and every lump starts on a 512 B sector -- 58.3/60.1's " "precondition survives the interleave") # --- 1b. and the cadence term is load-bearing ----------------------------- # THE FAILURE MODE THIS FORMAT HAS AND THE CODEC'S DOES NOT. A DLX record is # found through an index and a player that read the wrong entry gets a length # word that does not parse. A packed record is found by ARITHMETIC and nothing # parses it, so a player that drops the `(i//F)*A` term reads 97 sectors # starting 14 sectors early and paints them: the last 14 sectors of the previous # record, then 83 of this one, shifted down the screen. It is a picture. Here # is what the gate would be comparing if the term were missing, and it is only # WRONG from frame F on -- the first group is exempt, which is how an off-by-one # like this survives a rig that checks frame 0. blind = [i for i in range(d.nframes) if d.raw[d.off_frm + i*d.rec_bytes:d.off_frm + (i+1)*d.rec_bytes] != d.record(i)] ck(blind == list(range(d.cad_f, d.nframes)), f"a cadence-blind player reads the wrong bytes for {len(blind)} of " f"{d.nframes} records, first at frame {blind[0] if blind else '-'} -- and " f"frames 0..{d.cad_f-1} are IDENTICAL either way, so frame 0 proves nothing") # --- 2. the picture did not move ------------------------------------------ if os.path.exists(a.silent): q = DLXP(a.silent) same = (q.nframes == d.nframes and all(q.record(i) == d.record(i) for i in range(d.nframes))) ck(same, f"all {d.nframes} records byte-exact against the SILENT control " f"({os.path.basename(a.silent)}) -- interleaving audio moved no " f"picture byte") ck(q.has_audio is False and q.off_frm == SECTOR, "and the control really is silent: no audio flag, record 0 at sector 1") else: print(f" SKIPPED: no silent control at {a.silent}") # --- 3. the bytes are the encoder's --------------------------------------- if os.path.exists(a.audio): raw = open(a.audio, "rb").read() pcm = struct.unpack("<%dh" % (len(raw) // 2), raw) src = [max(-2048, min(2047, x >> 4)) for x in pcm] axes = d.decoder() ck(axes == adpcm.CHIP, f"the header's four axes ARE adpcm.CHIP: {axes}") nib = adpcm.encode(src, variant=axes["variant"], init=axes["init"], bits=axes["bits"]) want = adpcm.pack(nib, order=axes["order"])[:d.aud_bytes] got = d.audio() ck(got == want, f"the {len(got):,} B the lumps carry are byte-exact against " f"adpcm.encode on the same PCM") ck(all(d.lump(k, padding=True)[len(d.lump(k)):] == b"\0" * ( d.cad_a * SECTOR - len(d.lump(k))) for k in range(d.n_lumps)), "and every lump's padding is zero, so a player that overruns the payload " "feeds the chip silence rather than the next lump's first sample") # --- 4. the header's axes are load-bearing ---------------------------- def snr(axes_): rec = adpcm.decode(adpcm.unpack(got, len(src), order=axes_["order"]), variant=axes_["variant"], init=axes_["init"], bits=axes_["bits"]) n = min(len(rec), len(src)) e = sum((x - y) ** 2 for x, y in zip(src[:n], rec[:n])) s = sum(x * x for x in src[:n]) return 10 * math.log10(s / e) if e else float("inf") right = snr(axes) ck(right > 20.0, f"decoded on the axes the header names: {right:.2f} dB") print(f"\n AND EVERY AXIS IS A NEGATIVE CONTROL -- flip ONE and this is what\n" f" a player that ignored the header would hear:\n") print(f" {'axis':<12} {'header':>8} {'flipped to':>11} {'SNR':>9} cost") flips = [("order", "high" if axes["order"] == "low" else "low"), ("variant", "terms" if axes["variant"] == "shift" else "shift"), ("bits", 12 if axes["bits"] == 10 else 10), ("init", 0 if axes["init"] else -2)] for k, v in flips: w = dict(axes); w[k] = v s2 = snr(w) print(f" {k:<12} {str(axes[k]):>8} {str(v):>11} {s2:9.2f} dB " f"{s2-right:+.2f} dB") if k in ("order", "variant"): ck(s2 < right - 2.0, f"axis '{k}' is load-bearing: {s2-right:+.2f} dB") else: print(f" SKIPPED: no PCM at {a.audio} -- the bytes were not re-derived") # --- the finding ---------------------------------------------------------- per = d.cad_a * SECTOR - grp print(f""" === THE PAYLOAD IS NOT THE LUMP (FINDINGS 67) ========================== A lump is {d.cad_a*SECTOR:,} B of SPACE. {d.cad_f} frames of audio is {grp:,.4f} B, so the PAYLOAD is {P.lump_bytes(0, d.cad_f, d.fps, d.aud_hz):,} or {P.lump_bytes(2, d.cad_f, d.fps, d.aud_hz):,} -- the same remainder FINDINGS 54's frame clock carries, one dimension over -- and the last {per:.4f} B are zero. A PLAYER THAT FED THE CHIP THE WHOLE LUMP would hand it {per:.2f} B a group it should not have. At {d.aud_hz:,} Hz that is {2*per/d.aud_hz*1000:.2f} ms of audio every {d.cad_f/d.fps:.4f} s, which is {100*per/grp:.3f}% -- and it does not average out, it ACCUMULATES:""") for mins in (1.0, a.game_min): print(f" {mins:5.1f} min of play -> {mins*60*(per/grp):.2f} s of lip-sync error") print(f""" so the cadence's {100*per/grp:.3f}% is not the waste figure 65.3 called it and left at that. It is waste ON THE WIRE and DRIFT IN THE PLAYER, and the second is the expensive one: {a.game_min:.1f} minutes is {a.game_min*60*(per/grp):.2f} s, which is a scene of dialogue arriving after the mouth that spoke it. WHAT A PLAYER CARRIES INSTEAD IS ONE ACCUMULATOR, and it is three instructions rather than a table: acc += {d.cad_f}*{d.aud_hz:,} ; = {d.cad_f*d.aud_hz:,} n = acc // {2*d.fps} ; the MTC for this lump's channel acc %= {2*d.fps} which is exactly clock.i's shape (54) and for exactly the same reason: a rate with a denominator of {2*d.fps} cannot be a count, so it is a remainder. === THE WIRE =========================================================== video {d.video_kbps():7.1f} KB/s FIXED by geometry audio {d.audio_kbps():7.2f} KB/s the CADENCE's, padding included -- the disc moves whole sectors and the wire pays for the zero ones total {d.kbps():7.1f} KB/s ({100*(d.kbps()/d.video_kbps()-1):+.2f}%), and 65.3 predicted {589.6:.1f} """) print(f"{'FAIL' if fails else 'OK'} 34_packed_audio: {len(fails)} failure(s)") sys.exit(1 if fails else 0)