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
This commit is contained in:
@@ -91,12 +91,20 @@ print(f" round-trip: {d.nframes} records unpack and re-pack byte-identical")
|
||||
print(f" index 0 (transparency key) used {zero} times; "
|
||||
f"index 255 (black) {black:,} times in the picture")
|
||||
|
||||
kbps = d.kbps()
|
||||
# THE PICTURE'S wire, and it is the one this file is about. DLXP2 puts audio on
|
||||
# the same wire at a cadence (65.3, 67) and `d.kbps()` is both; what is asserted
|
||||
# here is that the PICTURE's share is still exactly geometry, because that is
|
||||
# 61.6's claim and a second stream is exactly the thing that could quietly
|
||||
# dilute it.
|
||||
kbps = d.video_kbps()
|
||||
geom = d.rec_bytes * d.fps / 1024
|
||||
if abs(kbps - geom) > 1e-6:
|
||||
fail.append(f"wire {kbps} != geometry {geom}")
|
||||
print(f" wire {kbps:.1f} KB/s = {d.rec_bytes:,} B x {d.fps} fps. FIXED. A codec's "
|
||||
f"bitrate is a lever and a literal frame's is geometry (61.6)")
|
||||
f"bitrate is a lever and a literal frame's is geometry (61.6)"
|
||||
+ (f"\n ...and {d.audio_kbps():.2f} KB/s of audio rides beside it on the "
|
||||
f"F={d.cad_f}/A={d.cad_a} cadence, for {d.kbps():.1f} KB/s total "
|
||||
f"(tools/analysis/34_packed_audio.py)" if d.has_audio else ""))
|
||||
print()
|
||||
|
||||
# --- 2. the picture ----------------------------------------------------------
|
||||
|
||||
@@ -134,7 +134,7 @@ print(f""" THE FLOOR OF THAT SWEEP is F={F}, A={A}: {100*w:.3f}% padding, {add:
|
||||
|
||||
=== WHAT IT DOES TO THE WIRE ===========================================""")
|
||||
|
||||
vid_kbs = d.rec_bytes * d.fps / 1024
|
||||
vid_kbs = d.video_kbps()
|
||||
for label, cad in (("F=1 (one lump a record)", f1), (f"F={F} (the floor)", best[0])):
|
||||
tot = vid_kbs + cad[5]
|
||||
print(f" {label:26s} video {vid_kbs:7.1f} + audio {cad[5]:5.2f} = {tot:7.1f} KB/s "
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user