Files
prosolis e3778f62b0 Put sound in the packed container, and find the padding is a rate error
ROADMAP P6b, FINDINGS 67.  DLXP2: a 64-byte header and then groups -- one
audio lump of A sectors, then F records -- so record i is at
off_frm + i*rec + (i//F)*A*512 and lump k at off_aud + k*(F*rec + A*512).
Still no index and still none needed, which is the packed branch's whole
claim surviving the one change that could have ended it.  The player carries
the third term in six instructions once a frame and zero parsing, and 120 of
120 records are still pixel-exact off a real MB89352 volume with the
interleave in, against a silent control that says no picture byte moved.

The finding is what 65.3 called padding.  A lump is 7,168 B of SPACE; eleven
frames of audio is 7,161.4583... B, so the payload alternates 7,161 and
7,162 and the rest is zero.  A player that fed the chip the whole lump --
which is what "14 sectors every 11 frames" invites -- runs 0.09% fast, and
that is not waste, it is drift: 0.84 ms a group, 1.25 s of lip-sync over the
game's 22.8 minutes.  What a player carries is one accumulator,
acc += 11*15625; n = acc//24; acc %= 24, which is clock.i's shape for
clock.i's reason and the third time this tree has met the pattern.

The four ADPCM axes ride in the header as fields rather than a version
number, and the gate flips each one to prove they earn it: nibble order
-31.99 dB, delta formula -24.86, clamp 0.00, accumulator -0.49.  Nothing
parses a packed container, so the gate partitions the whole file -- 131
spans, no overlap, no gap -- and asserts what a cadence-blind player would
read: exactly records 11..119 wrong, and frames 0..10 identical either way,
which is how an off-by-one like that survives a rig that checks frame 0.

Wire 582.0 + 7.64 = 589.6 KB/s, 65.3's prediction to the tenth.

Green light ALL GREEN before (tmp/check_s35_start.log) and after
(tmp/check_s35_end.log), with the new stage in it.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
2026-08-25 11:22:38 -07:00

218 lines
12 KiB
Python

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