The README still opened by calling this a video codec problem and led with the
codec player's stills and clips, which have been PARKED since session 29
(FINDINGS 61.8). Everything measured since says the opposite: at the 9 clk/B
dual-address floor the codec is 110.4% of a 12 fps frame and the decoder-free
packed player is 54.9%, so decoding 37,585 bytes costs more than not decoding
49,152. The document now says that in its first paragraph.
871 -> 473 lines. "Where it stands" was an append-only pile of session
headlines -- which is what docs/STATUS.md is for -- and is now a STATE: what
runs end to end, the four resources every item is priced in, the one open
question that decides which player gets built (A vs B, blocked on B2), and the
machine facts a player has to carry. The codec keeps a section as the parked
branch, with its media and the reason it is parked.
Drift struck while restructuring:
- "Current encode: 496.7 KB/s at 29.19 dB" was session 14's candidate, not
the DLX5 gate container, and was presented as the project's current state.
- The Encoder section documented only the parked encoder. It now leads with
pack.py/dlxp.py/adpcm.py and uses the gate window the tree actually
encodes (00223, 539.4 s, 10.0 s) rather than 00020.
- Analysis scripts 25, 30 and 34 were described under tools/bench/gvpack/
and tools/import/. They are tools/analysis/.
- make_packed_media.py was never mentioned.
- check.sh was "~6 min" in prose and "~3 min" in the code block, and its own
header says ~2 min. No figure is quoted now rather than a fourth one.
AND THE HERO IS A SEQUENCE NOW, BECAUSE A PACKED FRAME IS A LITERAL. A still
cannot show the property that separates this branch from the codec's: there is
no recursion here, so frame 119 says nothing about frame 60 and 120 frames are
120 separate claims. make_packed_media.py grew a clip path that gates all of
them before writing any -- and refuses to write if one differs, because a clip
of the player is a claim that the player drew it.
The clip is a COMPOSITE of two runs of packed_run.sh and it says so on the
page: the picture is run 1, paced at half rate so the snapshot lands inside
the write window; the sound is run 5, MAME's own -wavwrite capture off the
speaker at 12 fps. They cannot be one run -- audio cut at 12 fps played at 6
is not this scene. What it is NOT is a real-time capture of the shipping
configuration, which at this container's own burst rate would show a blank
layer for 99.5% of every slot (FINDINGS 64.2).
The audio cut is GATED, not eyeballed. The capture opens with the machine
booting, so the stream starts at the first non-zero sample -- exactly the kind
of alignment that is off by one byte forever with every counter agreeing
(71.5). Lump 0 is decoded with the four ADPCM axes out of the container's own
header and required to be sample-exact from the cut: 50,603 samples in, 3.24 s
of boot dropped, terms/low, 10-bit, init -2.
check.sh is ALL GREEN with this in the tree (tmp/check_s40.log, exit 0, 101 OK
lines). It does not gate the README or the media builder, so that run confirms
the tree the README describes and not the description.
Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
236 lines
11 KiB
Python
236 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""The README still for the PACKED player -- ROADMAP K3, FINDINGS 64.
|
|
|
|
python3 tools/media/make_packed_media.py [container.dlxp]
|
|
[--snap tmp/snap_packed_gate] [--map tmp/packed_snaps_gate.csv]
|
|
[--src tmp/fr_singe] [--frame N] [--out docs/img/packed-player.png]
|
|
|
|
Blu-ray source | what the emulated 68000 actually put on screen. The right-hand
|
|
panel is MAME's own snapshot, de-double-scanned and cropped to the picture -- not
|
|
a re-render, not `dlxp.render`. It is the same rule the codec's still is built on
|
|
(tools/media/make_readme_media.py) and it is the only reason the picture is worth
|
|
printing: an encoder can be checked against its own inverse, and a screen cannot.
|
|
|
|
THE FRAME IS CHOSEN, NOT PICKED. --frame defaults to the one whose PSNR against
|
|
the source is CLOSEST TO THE MEAN over the whole gated window, so the still is
|
|
representative rather than flattering. The chosen frame and its distance from the
|
|
mean are printed, so a reader can see it was not the best one.
|
|
"""
|
|
import argparse, csv, os, shutil, struct, subprocess, sys, wave
|
|
sys.path.insert(0, "tools/encoder")
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
from dlxp import DLXP
|
|
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("container", nargs="?", default="tmp/packed_singe.dlxp")
|
|
ap.add_argument("--snap", default="tmp/snap_packed_gate")
|
|
ap.add_argument("--map", default="tmp/packed_snaps_gate.csv")
|
|
ap.add_argument("--src", default="tmp/fr_singe")
|
|
ap.add_argument("--frame", type=int, default=None)
|
|
ap.add_argument("--out", default="docs/img/packed-player.png")
|
|
ap.add_argument("--webm", default="docs/img/packed-player.webm")
|
|
ap.add_argument("--wav", default="tmp/packed_aud.wav",
|
|
help="MAME's own -wavwrite capture from run 5 of "
|
|
"packed_run.sh; --no-audio drops it")
|
|
ap.add_argument("--no-audio", action="store_true")
|
|
ap.add_argument("--no-webm", action="store_true")
|
|
ap.add_argument("--fps", type=float, default=None,
|
|
help="clip rate; defaults to the container's own")
|
|
a = ap.parse_args()
|
|
|
|
d = DLXP(a.container)
|
|
SNAP_W, SNAP_H = 256, 512
|
|
with open(a.map) as fh:
|
|
shot = {int(r["frame"]): r["snapshot"] for r in csv.DictReader(fh)}
|
|
if not shot:
|
|
sys.exit(f"{a.map} is empty -- run tools/bench/packed_run.sh first")
|
|
|
|
|
|
def screen(fr):
|
|
"""The 256x192 picture out of one MAME native snapshot."""
|
|
p = f"{a.snap}/x68000/{shot[fr]}.png"
|
|
s = np.asarray(Image.open(p).convert("RGB"))
|
|
if s.shape[:2] != (SNAP_H, SNAP_W):
|
|
sys.exit(f"{p}: expected {SNAP_W}x{SNAP_H}, got {s.shape[1]}x{s.shape[0]}")
|
|
g = s[0::2] # undo the double scan
|
|
y = (g.shape[0] - d.H) // 2 # the picture is centred
|
|
return g[y:y + d.H]
|
|
|
|
|
|
def source(fr):
|
|
p = f"{a.src}/f{fr+1:04d}.png"
|
|
if not os.path.exists(p):
|
|
sys.exit(f"missing {p} -- re-extract the frames the container was built "
|
|
f"from, or point --src at them")
|
|
return np.asarray(Image.open(p).convert("RGB"))
|
|
|
|
|
|
def psnr(x, y):
|
|
e = ((x.astype(float) - y.astype(float)) ** 2).mean()
|
|
return float("inf") if e == 0 else 10 * np.log10(255.0 ** 2 / e)
|
|
|
|
|
|
frames = sorted(shot)
|
|
scores = {f: psnr(source(f), screen(f)) for f in frames}
|
|
mean = float(np.mean(list(scores.values())))
|
|
if a.frame is None:
|
|
pick = min(scores, key=lambda f: abs(scores[f] - mean))
|
|
else:
|
|
pick = a.frame
|
|
if pick not in scores:
|
|
sys.exit(f"frame {pick} was not sampled by that run")
|
|
|
|
# THE PANEL IS GATED, not just drawn. A still of the player is a claim that the
|
|
# player drew it, and the snapshot has to still be pixel-exact against the
|
|
# container for that claim to hold -- verify_packed.py checks all of them and
|
|
# this checks the one being printed, so the picture cannot outlive the result.
|
|
ref = d.render(pick)
|
|
if not np.array_equal(screen(pick), ref):
|
|
sys.exit(f"frame {pick} is NOT pixel-exact against {a.container}. The still "
|
|
f"is not being written: it would be a picture of a failure with a "
|
|
f"caption saying otherwise.")
|
|
|
|
Z, BAR = 2, 22
|
|
|
|
|
|
def captioned(img, text):
|
|
up = np.repeat(np.repeat(img, Z, 0), Z, 1)
|
|
out = Image.new("RGB", (up.shape[1], up.shape[0] + BAR), (16, 16, 18))
|
|
out.paste(Image.fromarray(up), (0, BAR))
|
|
ImageDraw.Draw(out).text((6, 6), text, fill=(190, 190, 196))
|
|
return out
|
|
|
|
|
|
left = captioned(source(pick), "Blu-ray source, cropped 256x192")
|
|
right = captioned(screen(pick),
|
|
"emulated 68000, MAME's own snapshot, no decoder")
|
|
out = Image.new("RGB", (left.width + right.width + 8, left.height), (16, 16, 18))
|
|
out.paste(left, (0, 0))
|
|
out.paste(right, (left.width + 8, 0))
|
|
os.makedirs(os.path.dirname(a.out), exist_ok=True)
|
|
out.save(a.out)
|
|
print(f"{a.out}: frame {pick} of {d.nframes}, {scores[pick]:.2f} dB against the "
|
|
f"24-bit source")
|
|
print(f" chosen as the frame CLOSEST TO THE MEAN ({mean:.2f} dB over "
|
|
f"{len(frames)} gated frames), {abs(scores[pick]-mean):.3f} dB from it -- "
|
|
f"best in the window is {max(scores.values()):.2f}, worst "
|
|
f"{min(scores.values()):.2f}")
|
|
print(f" and it is pixel-exact against {a.container}, checked before writing")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# THE CLIP. ROADMAP K3's result is a SEQUENCE -- 120 records off a real volume,
|
|
# every one of them a literal -- and a still cannot show the one property that
|
|
# distinguishes this branch from the codec's: there is no recursion here, so
|
|
# frame 119 says nothing about frame 60 and every frame has to be its own claim.
|
|
# This writes all of them, and gates all of them before writing any (64.1).
|
|
#
|
|
# WHAT IT IS, EXACTLY, because two runs of packed_run.sh are in it:
|
|
# picture run 1, the GATE run -- paced at half the container's rate so the
|
|
# snapshot lands inside the write window, cycle stealing, no sound.
|
|
# Every panel is MAME's own snapshot, 2x nearest, no filtering.
|
|
# sound run 5, the AUDIO run -- the same container at 12 fps with the
|
|
# MSM6258 on channel 3, captured by MAME's -wavwrite off the
|
|
# speaker. It is the chip's stream, not the encoder's.
|
|
# They are two runs because they have to be: the gate run is at 6 fps and audio
|
|
# cut at 12 fps played at 6 is not this scene. The clip is therefore a
|
|
# COMPOSITE, and saying so is the point -- what it is NOT is a real-time capture
|
|
# of the shipping configuration, which at the container's own burst rate would
|
|
# show a blank layer for 99.5% of every slot (FINDINGS 64.2).
|
|
if not a.no_webm:
|
|
if not shutil.which("ffmpeg"):
|
|
sys.exit("ffmpeg not found -- needed for the webm (--no-webm skips it)")
|
|
fps = a.fps or d.fps
|
|
|
|
# EVERY frame gated, not the printed one. A packed frame is a literal: the
|
|
# codec's last-frame test audits 120 through its own recursion and nothing
|
|
# here does, so a clip of 120 frames is 120 separate claims.
|
|
bad = [f for f in frames if not np.array_equal(screen(f), d.render(f))]
|
|
if bad:
|
|
sys.exit(f"{len(bad)} of {len(frames)} frames are NOT pixel-exact "
|
|
f"against {a.container} (first {bad[0]}). The clip is not "
|
|
f"being written: it would be a recording of a failure.")
|
|
|
|
aud = None
|
|
if not a.no_audio and d.has_audio and os.path.exists(a.wav):
|
|
# THE ALIGNMENT IS FOUND, NOT ASSUMED, and then CHECKED. The capture
|
|
# opens with the machine booting, so the stream starts at the first
|
|
# non-zero sample -- and "first non-zero" is exactly the kind of thing
|
|
# that is off by one byte forever with every counter in the player
|
|
# agreeing (FINDINGS 71.5). So lump 0 is decoded with the FOUR AXES OUT
|
|
# OF THE CONTAINER'S OWN HEADER and required to be sample-exact from
|
|
# there. Only lump 0: past it the seams need the walk in
|
|
# tools/bench/verify_packed_audio.py, which is what gates the whole
|
|
# stream in check.sh. This gates the cut.
|
|
sys.path.insert(0, "tools/encoder")
|
|
import adpcm
|
|
SCALE = 8 # okim6258's signal<<4 at gain 0.50
|
|
w = wave.open(a.wav)
|
|
nfr, ch, rate, sw = (w.getnframes(), w.getnchannels(),
|
|
w.getframerate(), w.getsampwidth())
|
|
if rate != d.aud_hz or sw != 2:
|
|
sys.exit(f"{a.wav}: {rate} Hz / {sw*8}-bit -- the capture has to be "
|
|
f"the chip's own {d.aud_hz} Hz or a resampler is in the "
|
|
f"measurement")
|
|
raw = w.readframes(nfr)
|
|
left = struct.unpack("<%dh" % (nfr * ch), raw)[0::ch]
|
|
rec = [round(v / SCALE) for v in left]
|
|
start = next((i for i, v in enumerate(rec) if v), None)
|
|
if start is None:
|
|
sys.exit(f"{a.wav} is silent -- run 5 of packed_run.sh writes it")
|
|
dec = d.decoder()
|
|
l0 = d.lump(0)
|
|
want = adpcm.decode(adpcm.unpack(l0, order=dec["order"]),
|
|
variant=dec["variant"], init=dec["init"],
|
|
bits=dec["bits"])
|
|
got = rec[start:start + len(want)]
|
|
if got != list(want):
|
|
n = sum(x != y for x, y in zip(got, want))
|
|
sys.exit(f"the cut at capture sample {start:,} does not decode as "
|
|
f"lump 0: {n:,} of {len(want):,} samples differ. The clip "
|
|
f"is not being written -- the sound would be the right "
|
|
f"scene from the wrong byte.")
|
|
n_out = int(round(len(frames) / fps * rate))
|
|
aud = "tmp/_packed_media_audio.wav"
|
|
ow = wave.open(aud, "wb")
|
|
ow.setnchannels(ch); ow.setsampwidth(sw); ow.setframerate(rate)
|
|
ow.writeframes(raw[start * ch * sw:(start + n_out) * ch * sw])
|
|
ow.close()
|
|
print(f" audio: {a.wav}, cut at sample {start:,} "
|
|
f"({start/rate:.2f} s of boot dropped), {n_out/rate:.2f} s -- "
|
|
f"lump 0 sample-exact against the container's own axes "
|
|
f"({dec['variant']}/{dec['order']}, {dec['bits']}-bit, "
|
|
f"init {dec['init']})")
|
|
elif not a.no_audio:
|
|
print(f" no audio: {a.wav} is missing or the container is silent")
|
|
|
|
tmpd = "tmp/_packed_media_frames"
|
|
shutil.rmtree(tmpd, ignore_errors=True)
|
|
os.makedirs(tmpd)
|
|
for n, f in enumerate(frames):
|
|
l = captioned(source(f), "Blu-ray source, cropped 256x192")
|
|
r = captioned(screen(f), "emulated 68000, MAME's own snapshot, "
|
|
"no decoder in the machine")
|
|
im = Image.new("RGB", (l.width + r.width + 8, l.height), (16, 16, 18))
|
|
im.paste(l, (0, 0)); im.paste(r, (l.width + 8, 0))
|
|
im.save(f"{tmpd}/{n:04d}.png")
|
|
|
|
# VP9 near-lossless: this is 256x192 palettised pixel art scaled by an
|
|
# integer, and a codec that smooths a colour boundary would be editorialising
|
|
# about the one thing the picture is evidence of.
|
|
cmd = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", f"{fps:g}",
|
|
"-i", f"{tmpd}/%04d.png"]
|
|
if aud:
|
|
cmd += ["-i", aud, "-c:a", "libopus", "-b:a", "96k", "-shortest"]
|
|
cmd += ["-c:v", "libvpx-vp9", "-crf", "12", "-b:v", "0",
|
|
"-pix_fmt", "yuv444p", "-row-mt", "1", a.webm]
|
|
subprocess.run(cmd, check=True)
|
|
shutil.rmtree(tmpd)
|
|
if aud:
|
|
os.remove(aud)
|
|
print(f"{a.webm}: {len(frames)} frames @ {fps:g} fps, "
|
|
f"{os.path.getsize(a.webm)/1024:.0f} KB, every frame pixel-exact "
|
|
f"against {a.container}"
|
|
+ (" -- with the chip's own audio" if aud else " -- silent"))
|