Take the player through a branch with sound, and find the predictor does not seek
FINDINGS 71, ROADMAP P6d. 70.3 named exactly what was missing -- packed.s starts PG_AK/PG_AKF at lump 0 and has no audio seek path -- and priced its absence at a mean 416.5 ms of silence over the arcade's 409 within-container seek targets. pg_aseek is that path: the lump index, the stream position, the remainder accumulator and the byte offset into the group, then the second READ(10) at the lump's own LBA and a re-arm part way into the buffer. Measured off a real volume: 132,162 B of spliced stream accounted for byte by byte in MAME's own capture, across a branch at frame 37 -- four frames into lump 3, deliberately NOT on a group boundary -- in both chip configurations. Skip computed 2,604 B, cadence says 2,604. THE PREDICTOR DOES NOT SEEK, AND THE ERROR IS DC. The MSM6258's accumulator is a pure integrator with no leakage term, so a branch that hands the chip bytes chosen for a state it is not in produces an offset that does not decay. Playing through: DC -355 of 511 with AC 0.00 -- the right shape from the wrong ground -- still -108 four seconds later. STOP and re-PLAY: all 62,500 post-seek samples are EXACTLY a decode from the container's own init, and the whole error is the single constant -65. A re-PLAY is 5.5x better and neither is zero, so PG_ARST is a mailbox with a number under it. The host computes -65 out of the container's bytes and the gate asserts the equality rather than printing both. AND THE ONLY FIX THAT REACHES ZERO IS THE ENCODER'S. A player cannot set the chip's accumulator, only reset it. Resetting the encoder's predictor every frame makes all 119 of the container's branch points exact for 0.33 dB (21.99 -> 21.66), because the step table's floor is a constant 16. That is a DLXP3 and it is deliberately not in tools/encoder. TWO SILENT BUGS, BOTH CAUGHT BY THE CAPTURE. pg_udiv32 trashes d4 and pg_aseek held hz there, so the offset came out 1 byte instead of 2,604 -- 166 ms of the wrong part of the scene at exactly the right rate, every counter agreeing. And one already in the tree that had passed this gate three times: pg_ainit waited on a READ-BACK MTC before PLAY, which is the same test as "a byte has left RAM" only if no byte leaves in between. One does, and the chip then plays the scene one byte in, forever. Found by locating the capture's opening samples in the container image: sector 1 + 1. The witness is now the count that was written. ALL GREEN, two new stages included. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
+36
-1
@@ -770,7 +770,7 @@ echo "--- session 36: THE CONTAINER'S OWN AUDIO, OUT OF THE CHIP (FINDINGS 68) -
|
||||
# The run is part of packed_run.sh's gate half, so DLX_PK_GATE_ONLY takes it.
|
||||
if [ -f tmp/packed_gate_stage.log ] && \
|
||||
grep -aq -- "--- 5. THE AUDIO" tmp/packed_gate_stage.log; then
|
||||
sed -n '/--- 5. THE AUDIO/,$p' tmp/packed_gate_stage.log | \
|
||||
sed -n '/--- 5. THE AUDIO/,/--- 7. THE SEEK/p' tmp/packed_gate_stage.log | \
|
||||
grep -aE "^(OK|FAIL) |^ AUDIO:|^ {5}(nibbles per|worst|payload|-> |the player)"
|
||||
else
|
||||
echo " SKIPPED: no x68000 romset, or the container is silent"
|
||||
@@ -846,6 +846,41 @@ python3 tools/analysis/35_audio_level.py --gate > tmp/audio_level.log 2>&1 \
|
||||
grep -aE "DISC PEAK|LOUDEST PASSAGE|THE CLAMP|LEVEL GATE|NO AUDIO TRACK|^ 1\.0000|^ 0\.5" \
|
||||
tmp/audio_level.log
|
||||
|
||||
echo "--- session 39: THE SEEK, on the machine, with sound across it (FINDINGS 71) ---"
|
||||
# Run 7 of packed_run.sh, split out of the same log for the same reason session
|
||||
# 36's stage is split out of it: three verifiers in one file all emit OK lines,
|
||||
# and one grep over the whole thing prints this session's result under session
|
||||
# 31's heading.
|
||||
if [ -f tmp/packed_gate_stage.log ] && \
|
||||
grep -aq -- "--- 7. THE SEEK" tmp/packed_gate_stage.log; then
|
||||
sed -n '/--- 7. THE SEEK/,$p' tmp/packed_gate_stage.log | \
|
||||
grep -aE "^(OK|FAIL) |^ {4}-- the chip|^ {5}(the chip was|error against|-> )|DC +-?[0-9.]+ +AC"
|
||||
else
|
||||
echo " SKIPPED: no x68000 romset, or the container is silent"
|
||||
fi
|
||||
|
||||
echo "--- session 39: the audio seek path, run across a real branch (FINDINGS 71) ---"
|
||||
# ROADMAP P6d, and it is the item session 38's handoff put first: 70.3 named
|
||||
# what was missing -- "src/player/packed.s starts PG_AK/PG_AKF at lump 0 and has
|
||||
# no audio seek path at all" -- and priced its absence at a mean 416.5 ms of
|
||||
# silence over the arcade's 409 within-container seek targets.
|
||||
#
|
||||
# The MACHINE half of this is run 7 of tools/bench/packed_run.sh, above: two
|
||||
# passes over the container with the second starting at frame 37, which is four
|
||||
# frames into lump 3 and therefore NOT on a group boundary, so the byte offset
|
||||
# inside the lump is load-bearing. It runs in both chip configurations.
|
||||
#
|
||||
# THIS STAGE IS THE HOST HALF, and what it adds is the census the one branch
|
||||
# point on the machine cannot give. The chip's accumulator is an integrator with
|
||||
# NO LEAKAGE TERM, so the state error a branch creates is a DC offset that never
|
||||
# decays -- and the only fix that makes a branch free is in the ENCODER, not the
|
||||
# player. Both are priced here, and section 1 checks the host's arithmetic for
|
||||
# the machine's own branch point against what MAME's capture measured.
|
||||
python3 tools/analysis/37_audio_seek.py > tmp/audio_seek.log 2>&1 \
|
||||
|| { cat tmp/audio_seek.log; exit 1; }
|
||||
grep -aE "^(OK|FAIL) |RE-PLAYED BRANCH|\|DC\| against|step index the encoder|frame 37, the branch|reset every|never \(shipped\)|frames \(the cadence\)|1 frame |ONLY FIX" \
|
||||
tmp/audio_seek.log
|
||||
|
||||
echo "--- session 38: the refill climb with a second consumer, through a real branch point (FINDINGS 70) ---"
|
||||
# ROADMAP P6, the oldest item in it: 65.6 and 67.6 both recorded that the slack
|
||||
# table existed and that 51.3's climb had never met a branch point with audio on
|
||||
|
||||
+58
-4
@@ -36,8 +36,16 @@
|
||||
-- DLX_PK_PACE 1 = the machine holds itself to the container's fps off
|
||||
-- V-DISP (default). 0 free-runs, which tests the CHAIN with
|
||||
-- the clock out of the way.
|
||||
-- DLX_PK_ITER passes over the scene (default 1). >1 exercises the seek,
|
||||
-- which for this container is arithmetic and nothing else.
|
||||
-- DLX_PK_ITER passes over the scene (default 1). >1 exercises the SEEK.
|
||||
-- DLX_PK_SEEK the frame passes after the first start at (default 0, i.e.
|
||||
-- a replay). On the video path the seek IS arithmetic and
|
||||
-- nothing else; on the audio path it is a second read at a
|
||||
-- separate LBA, because a DLXP2 group puts lump k in FRONT of
|
||||
-- its records and a branch lands `f mod F` frames into it
|
||||
-- (FINDINGS 70.3). Setting this to a frame that is NOT a
|
||||
-- multiple of the cadence is the point: 36 of the arcade's
|
||||
-- 409 within-container seek targets land on a boundary and
|
||||
-- 373 do not.
|
||||
-- DLX_PK_NFR play only the first N frames (default: all of them)
|
||||
-- DLX_PK_FPS pace at this rate instead of the container's. NOT a
|
||||
-- cosmetic knob and not a way to make a number look better:
|
||||
@@ -76,6 +84,9 @@ local PG_GSPIN, PG_LOSTV, PG_ARRN = 0x18954, 0x18958, 0x1895C
|
||||
-- ROADMAP P6c: the audio path's inputs and its own account of what it did.
|
||||
local PG_AFPS, PG_AHZ, PG_ALBA0 = 0x18960, 0x18964, 0x18968
|
||||
local PG_NLUMP, PG_ABYTES, PG_APRE = 0x1896C, 0x18970, 0x18974
|
||||
local PG_SEEKF, PG_ARST = 0x18978, 0x1897C
|
||||
local PG_APOS, PG_ASKIP = 0x189C8, 0x189CC
|
||||
local PG_ASKN, PG_ASKB, PG_TSEQ = 0x189D0, 0x189D4, 0x189DC
|
||||
local PG_AARM, PG_AFET, PG_ABYT = 0x18980, 0x18984, 0x18988
|
||||
local PG_ADRY, PG_ASEAM, PG_ASRV = 0x1898C, 0x18990, 0x18994
|
||||
local PG_ACSR, PG_ACER, PG_ALATE = 0x18998, 0x1899C, 0x189A0
|
||||
@@ -90,6 +101,11 @@ local CRTC_R20 = 0xE80028
|
||||
local HELD = (os.getenv("DLX_PK_HELD") or "1") == "1"
|
||||
local PACED = (os.getenv("DLX_PK_PACE") or "1") == "1"
|
||||
local ITERS = tonumber(os.getenv("DLX_PK_ITER") or "") or 1
|
||||
local SEEKF = tonumber(os.getenv("DLX_PK_SEEK") or "") or 0
|
||||
-- ROADMAP P6d. 1 = STOP and re-PLAY the chip at a branch. Not a tidiness
|
||||
-- knob: the chip's accumulator has no leak, so the two settings are a large
|
||||
-- decaying error against a small permanent one (FINDINGS 71.3).
|
||||
local ARST = (os.getenv("DLX_PK_ARST") or "0") == "1"
|
||||
local NFR = tonumber(os.getenv("DLX_PK_NFR") or "") or META.nframes
|
||||
local FPS = tonumber(os.getenv("DLX_PK_FPS") or "") or META.fps
|
||||
local CSV = os.getenv("DLX_PK_CSV")
|
||||
@@ -123,6 +139,8 @@ local function setup()
|
||||
SP:write_u32(PG_HELD, HELD and 1 or 0)
|
||||
SP:write_u32(PG_PACEON, PACED and 1 or 0)
|
||||
SP:write_u32(PG_ITER, ITERS)
|
||||
SP:write_u32(PG_SEEKF, SEEKF)
|
||||
SP:write_u32(PG_ARST, ARST and 1 or 0)
|
||||
-- DLXP2's cadence. Zero for a silent container, and the 68000 branches on the
|
||||
-- zero: a player told the wrong cadence does not fail, it reads an audio lump
|
||||
-- as a record and paints it.
|
||||
@@ -139,6 +157,23 @@ local function setup()
|
||||
SP:write_u32(PG_NLUMP, META.n_lumps or 0)
|
||||
SP:write_u32(PG_ABYTES, META.aud_bytes or 0)
|
||||
SP:write_u32(PG_APRE, APRE)
|
||||
if ITERS > 1 then
|
||||
local grp = (META.cad_f or 0) > 0 and (SEEKF % META.cad_f) or 0
|
||||
P(string.format("SEEK: %d passes, and passes 2..%d start at FRAME %d%s",
|
||||
ITERS, ITERS, SEEKF,
|
||||
(META.cad_f or 0) == 0 and " (silent container)"
|
||||
or string.format(" -- lump %d, %d frame(s) into its group "
|
||||
.."of %d, so the audio needs a second read and a byte "
|
||||
.."offset (FINDINGS 70.3)%s",
|
||||
SEEKF // META.cad_f, grp, META.cad_f,
|
||||
grp == 0 and " -- ON a group boundary, the free case"
|
||||
or "")))
|
||||
P(ARST and " the chip is STOPPED and re-PLAYED at the branch: its "
|
||||
.."accumulator goes to the container's own init and its step "
|
||||
.."index to 0"
|
||||
or " the chip PLAYS THROUGH the branch: it keeps the predictor "
|
||||
.."state the previous scene's audio left it in")
|
||||
end
|
||||
P(string.format("packed.bin=%d B, %dx%d %d fps, %d of %d frames, %d passes",
|
||||
#code, META.W, META.H, META.fps, NFR, META.nframes, ITERS))
|
||||
local cad = ""
|
||||
@@ -338,6 +373,20 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
P(string.format(" re-arms with MTC still non-zero: %d (bytes "
|
||||
.."fetched and never played; 0 is the correct value)",
|
||||
late))
|
||||
-- THE SEEK'S OWN ACCOUNT. PG_ASKN is the second reads and PG_ASKB the
|
||||
-- bytes they skipped at the head of a lump: a seek path with no offset
|
||||
-- term would report the first and zero for the second, and would be
|
||||
-- indistinguishable from a correct one on any target that happened to
|
||||
-- land on a group boundary.
|
||||
local skn, skb = SP:read_u32(PG_ASKN), SP:read_u32(PG_ASKB)
|
||||
if skn > 0 then
|
||||
P(string.format(" SEEK: %d audio seek(s), %d B skipped into "
|
||||
.."the head of a lump. Stream position ended at %d B "
|
||||
.."and the chip was handed %d -- they differ BY the "
|
||||
.."skip, which is the whole reason they are two cells "
|
||||
.."(FINDINGS 71).", skn, skb, SP:read_u32(PG_APOS),
|
||||
byt))
|
||||
end
|
||||
local ferr = SP:read_u32(PG_AFERR)
|
||||
if ferr ~= 0 then
|
||||
P(string.format(" A LUMP FETCH FAILED on lump %d: %s -- the "
|
||||
@@ -366,10 +415,15 @@ SUB = emu.add_machine_frame_notifier(function()
|
||||
local f = io.open(AJSON, "w")
|
||||
f:write(string.format('{"armed":%d,"fetched":%d,"bytes":%d,'
|
||||
..'"starve":%d,"seam":%d,"serv":%d,"late":%d,"acc":%d,'
|
||||
..'"csr":%d,"cer":%d,"held":%s,"fps":%d,"shown":%d}\n',
|
||||
..'"csr":%d,"cer":%d,"held":%s,"fps":%d,"shown":%d,'
|
||||
..'"seekn":%d,"seekb":%d,"pos":%d,"seekf":%d,"iters":%d,'
|
||||
..'"arst":%s}\n',
|
||||
armed, fet, byt, dry, seam, srv, late, acc,
|
||||
SP:read_u32(PG_ACSR), SP:read_u32(PG_ACER),
|
||||
HELD and "true" or "false", FPS, shown))
|
||||
HELD and "true" or "false", FPS, shown,
|
||||
SP:read_u32(PG_ASKN), SP:read_u32(PG_ASKB),
|
||||
SP:read_u32(PG_APOS), SEEKF, ITERS,
|
||||
ARST and "true" or "false"))
|
||||
f:close()
|
||||
P("audio counters -> "..AJSON)
|
||||
end
|
||||
|
||||
@@ -158,6 +158,65 @@ if [ "$AUDIO_ON" = "1" ]; then
|
||||
sound (FINDINGS 67.4, and it is the bug session 36 shipped and caught)."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. THE SEEK, WITH SOUND ON IT. FINDINGS 70.3 asked for this and named what
|
||||
# was missing: "src/player/packed.s starts PG_AK/PG_AKF at lump 0 and has no
|
||||
# audio seek path at all". This is that path, run.
|
||||
#
|
||||
# THE FRAME IS CHOSEN NOT TO BE A MULTIPLE OF THE CADENCE, and that is the whole
|
||||
# design of the run. A DLXP2 group is `lump k, then F records`, so a branch
|
||||
# that lands on a group boundary needs no offset and no second read -- and 36 of
|
||||
# the arcade's 409 within-container seek targets do land on one. A run that
|
||||
# picked one of those would exercise the arithmetic that was already there and
|
||||
# report success. DLX_PK_SEEK defaults below to a frame `f mod F != 0`, so the
|
||||
# byte offset inside the lump is load-bearing: get it wrong and the chip is fed
|
||||
# a stream that starts up to F frames early, which is not an error, it is a
|
||||
# rate, and only verify_packed_audio.py's spliced walk can see it.
|
||||
#
|
||||
# AND THE SECOND RESULT IS ONE NO COUNTER CAN REACH: the chip's predictor does
|
||||
# not seek. Every byte can arrive, in order, exactly -- and the samples still
|
||||
# be wrong, because the encoder chose them for a state a continuous play would
|
||||
# have been in. The verifier measures that against its own control.
|
||||
if [ "$AUDIO_ON" = "1" ] && [ "${DLX_PK_NOSEEK:-0}" != "1" ]; then
|
||||
SEEKF=${DLX_PK_SEEK:-37}
|
||||
CADF=$(sed -n 's/^ cad_f = \([0-9]*\),.*/\1/p' tmp/packed_meta.lua)
|
||||
[ $((SEEKF % CADF)) -ne 0 ] || \
|
||||
fail "the seek frame $SEEKF is a multiple of the cadence $CADF, so it lands
|
||||
ON a group boundary -- the one case that needs no byte offset and would
|
||||
pass with the offset arithmetic deleted (FINDINGS 70.3)."
|
||||
echo
|
||||
echo "--- 7. THE SEEK: two passes, the second starting at frame $SEEKF --"
|
||||
echo " lump $((SEEKF / CADF)), $((SEEKF % CADF)) frame(s) into its group"
|
||||
echo " of $CADF (FINDINGS 70.3/71) ---"
|
||||
TOTFR=$(( NFR + NFR - SEEKF ))
|
||||
SECS=$(( TOTFR / 8 + 35 ))
|
||||
# BOTH CONFIGURATIONS, and they are not two speeds of one thing. The chip's
|
||||
# accumulator is an integrator with no leak, so what a branch costs is set by
|
||||
# the state it lands in: play THROUGH and the chip keeps whatever the previous
|
||||
# scene left it in; STOP and re-PLAY and it goes to the container's own `init`
|
||||
# with the step index at 0. Neither is zero and they are 5.5x apart, so the
|
||||
# run measures both and FINDINGS 71.3 chooses.
|
||||
for M in 0 1; do
|
||||
TAG=seek; [ "$M" = 1 ] && TAG=seek_rst
|
||||
echo " -- the chip $([ "$M" = 1 ] && echo 'STOPPED and re-PLAYED' \
|
||||
|| echo 'PLAYING THROUGH') the branch"
|
||||
SOUNDARGS="-samplerate 15625 -wavwrite packed_$TAG.wav"
|
||||
run $TAG DLX_PK_HELD=0 DLX_PK_PACE=1 DLX_PK_FPS="$CFPS" DLX_PK_NFR=$NFR \
|
||||
DLX_PK_AUD=1 DLX_PK_AJSON=packed_$TAG.json \
|
||||
DLX_PK_ITER=2 DLX_PK_SEEK=$SEEKF DLX_PK_ARST=$M
|
||||
SOUNDARGS="-sound none"
|
||||
grep -aq "^\[PK\] FLAG=\$FF" "tmp/packed_$TAG.log" || \
|
||||
fail "the $TAG run did not reach the end of the second pass."
|
||||
python3 tools/bench/verify_packed_audio.py "$DLXP" "tmp/packed_$TAG.wav" \
|
||||
"tmp/packed_$TAG.json" --seek "$SEEKF" --iters 2 || \
|
||||
fail "the player did not play the container ACROSS A BRANCH ($TAG).
|
||||
Nothing here parses anything (FINDINGS 67.4): a seek that fetched the
|
||||
wrong lump plays 7,168 B of the wrong part of the scene, and one that
|
||||
dropped the byte offset plays the right lump from up to $((CADF-1))
|
||||
frames too early -- neither is an error and both are a sound."
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "${DLX_PK_GATE_ONLY:-0}" = "1" ]; then exit 0; fi
|
||||
|
||||
echo
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The container's own audio, READ BACK OFF THE SPEAKER. ROADMAP P6c.
|
||||
|
||||
python3 tools/bench/verify_packed_audio.py <in.dlxp> <capture.wav> [counters.json]
|
||||
python3 tools/bench/verify_packed_audio.py <in.dlxp> <capture.wav> \
|
||||
[counters.json] [--seek FRAME --iters N]
|
||||
|
||||
WHY THIS READS THE CAPTURE AND NOT THE PLAYER'S COUNTERS. src/player/packed.s
|
||||
reports how many lumps it armed and how many payload bytes it handed the chip,
|
||||
@@ -36,6 +37,25 @@ WHAT IS CHECKED, and it is the whole scene rather than a sample of it:
|
||||
search below carries the state across the seam and reports its LENGTH,
|
||||
which is the audible cost of every design decision on the video path.
|
||||
|
||||
4. THE SEEK, when the run made one (--seek). A branch is a frame index and
|
||||
a DLXP2 group puts lump k in FRONT of its records, so the player has to
|
||||
issue a second read and enter the lump `f mod F` frames in (FINDINGS 70.3).
|
||||
The stream the chip should then have been fed is the container's own bytes
|
||||
SPLICED -- everything, then everything from byte floor(f*hz/(2*fps)) on --
|
||||
and the walk above accounts for it as one continuous run, which is the
|
||||
check: a player that dropped the byte offset feeds a stream that starts up
|
||||
to F frames early and NOTHING ABOUT IT IS AN ERROR. It is a rate.
|
||||
|
||||
AND THE THING THE PLAYER CANNOT SEE. An MSM6258 has no seek: its
|
||||
accumulator and step index are the product of every nibble since PLAY, so a
|
||||
seek hands it bytes the encoder chose for a state it is not in. The
|
||||
samples are then wrong while the recursion re-converges, and they are wrong
|
||||
WITHOUT ANY BYTE BEING WRONG -- every counter in the player stays right and
|
||||
the walk above still passes, because the walk tracks the chip rather than
|
||||
the intent. This measures it: the chip's own samples across the splice
|
||||
against the samples the ENCODER meant, which are the same bytes decoded
|
||||
from the state a continuous play would have been in.
|
||||
|
||||
THE NEGATIVE CONTROL IS BUILT IN. A seam is found by searching for the repeat
|
||||
count that makes the next lump match; if the player had fed the wrong bytes, no
|
||||
repeat count would make it match and the run fails rather than sliding.
|
||||
@@ -190,9 +210,88 @@ def _look(rec, pos, st, data, i, one, look=LOOK):
|
||||
return True
|
||||
|
||||
|
||||
def stream_pos(d, frame):
|
||||
"""The byte of the ADPCM stream frame `frame` starts at. Exact, and the
|
||||
exactness is the same one FINDINGS 67.2 is about: 15,625 samples a second
|
||||
over 24 half-frames does not divide, and a seek that rounded it would be a
|
||||
rate error rather than a byte error."""
|
||||
return frame * d.aud_hz // (2 * d.fps)
|
||||
|
||||
|
||||
def spliced(d, seek, iters):
|
||||
"""The stream the player should have fed, as (label, bytes) chunks.
|
||||
|
||||
One pass is the container's lumps in order. A pass after a SEEK starts at
|
||||
stream byte stream_pos(seek): the tail of lump k = seek//F, then every lump
|
||||
after it. The chunks are kept separate because their BOUNDARIES are what
|
||||
the stretched-byte assertion is measured against -- the chip replays its
|
||||
last byte at a lump end and at a seek, and nowhere else.
|
||||
"""
|
||||
out = [(f"lump {k}", d.lump(k)) for k in range(d.n_lumps)]
|
||||
if iters <= 1 or seek is None:
|
||||
return out
|
||||
b0 = stream_pos(d, seek)
|
||||
for it in range(1, iters):
|
||||
off = 0
|
||||
for k in range(d.n_lumps):
|
||||
lb = d.lump(k)
|
||||
if off + len(lb) <= b0:
|
||||
off += len(lb)
|
||||
continue
|
||||
cut = max(0, b0 - off)
|
||||
out.append((f"pass {it+1} lump {k}" + (f" +{cut} B" if cut else ""),
|
||||
lb[cut:]))
|
||||
off += len(lb)
|
||||
return out
|
||||
|
||||
|
||||
def walk_reset(rec, start, data, dec, sp):
|
||||
"""The walk, SPLIT AT A CHIP RESET.
|
||||
|
||||
A STOP/PLAY at the branch is a discontinuity the continuous model cannot
|
||||
cross: the accumulator, the step index and the nibble select all go back to
|
||||
the container's own start state, so no repeat count of the last byte of
|
||||
pass 1 can be followed by pass 2's first sample. The walk would report
|
||||
"byte 78,124 does not account for capture sample N", which is true and is
|
||||
the wrong complaint.
|
||||
|
||||
So pass 1 is walked without its final byte, the RESTART is searched for --
|
||||
the first capture position at which pass 2 walks cleanly from `init` -- and
|
||||
pass 2 is walked from there. The final byte's run length is then the gap,
|
||||
which is exactly what it is: the chip replaying it while the 68000 fetched
|
||||
a lump and stopped the chip.
|
||||
|
||||
The search is cheap because the first post-reset sample is DETERMINED: one
|
||||
nibble of a known byte from a known state. Only positions carrying that
|
||||
value are tried at all.
|
||||
"""
|
||||
one = stepper(dec)
|
||||
bad, pos, runs = walk(rec, start, (dec["init"], 0), data[:sp - 1], dec)
|
||||
if bad is not None:
|
||||
return bad, pos, runs, None
|
||||
first, _ = one((dec["init"], 0), data[sp] & 15)
|
||||
K = 64 # bytes of pass 2 a candidate must
|
||||
for p in range(pos, min(pos + MAXRUN, len(rec))): # survive before the
|
||||
if rec[p] != first: # full walk is run
|
||||
continue
|
||||
b2, _, _ = walk(rec, p, (dec["init"], 0), data[sp:sp + K], dec)
|
||||
if b2 is None:
|
||||
b3, pos3, runs3 = walk(rec, p, (dec["init"], 0), data[sp:], dec)
|
||||
if b3 is None:
|
||||
return None, pos3, runs + [p - pos] + runs3, p
|
||||
return sp - 1, pos, runs, None
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
argv = [a for a in sys.argv[1:]]
|
||||
seek, iters = None, 1
|
||||
while "--seek" in argv:
|
||||
i = argv.index("--seek"); seek = int(argv[i+1]); del argv[i:i+2]
|
||||
while "--iters" in argv:
|
||||
i = argv.index("--iters"); iters = int(argv[i+1]); del argv[i:i+2]
|
||||
if len(argv) < 2:
|
||||
sys.exit(__doc__)
|
||||
sys.argv = [sys.argv[0]] + argv
|
||||
d = DLXP(sys.argv[1])
|
||||
counters = json.load(open(sys.argv[3])) if len(sys.argv) > 3 else None
|
||||
if not d.has_audio:
|
||||
@@ -229,8 +328,26 @@ def main():
|
||||
# sector runs -- so a player that fed the chip whole lumps does not merely
|
||||
# score worse here, it fails to walk: the 6.54 B of zero at the end of a
|
||||
# lump are nibbles that are not in this stream.
|
||||
data = d.audio()
|
||||
bad, pos, runs = walk(rec, start, (dec["init"], 0), data, dec)
|
||||
chunks = spliced(d, seek, iters)
|
||||
data = b"".join(c for _, c in chunks)
|
||||
if seek is not None and iters > 1:
|
||||
print(f"--- THE SEEK: {iters} passes, passes 2..{iters} start at frame "
|
||||
f"{seek} = stream byte {stream_pos(d, seek):,}, which is "
|
||||
f"{seek % d.cad_f} frame(s) into lump {seek // d.cad_f} "
|
||||
f"(FINDINGS 70.3). The stream below is SPLICED and is "
|
||||
f"{len(data):,} B against the container's {d.aud_bytes:,}.")
|
||||
replay = bool(counters and counters.get("arst"))
|
||||
if replay and seek is not None and iters > 1:
|
||||
sp0 = sum(len(c) for _, c in chunks[:d.n_lumps])
|
||||
bad, pos, runs, rp = walk_reset(rec, start, data, dec, sp0)
|
||||
if bad is None:
|
||||
print(f" the chip was STOPPED and re-PLAYED at the branch: the "
|
||||
f"walk is SPLIT there, and pass 2 restarts at capture sample "
|
||||
f"{rp:,}, {runs[sp0-1]:,} samples after the last byte of pass "
|
||||
f"1 was handed over ({runs[sp0-1]/rate*1000:.0f} ms of "
|
||||
f"replayed byte and stopped chip)")
|
||||
else:
|
||||
bad, pos, runs = walk(rec, start, (dec["init"], 0), data, dec)
|
||||
ck(bad is None,
|
||||
f"all {len(data):,} bytes of the container's audio reached the chip, in "
|
||||
f"order, and every sample the chip produced from them is exact"
|
||||
@@ -278,8 +395,8 @@ def main():
|
||||
# else is the CPU losing the chip in the middle of a buffer.
|
||||
ends = set()
|
||||
off = 0
|
||||
for k in range(d.n_lumps):
|
||||
off += len(d.lump(k))
|
||||
for _, c in chunks:
|
||||
off += len(c)
|
||||
ends.add(off - 1)
|
||||
stray = [i for i, _ in seams if i not in ends]
|
||||
ck(not stray,
|
||||
@@ -310,6 +427,129 @@ def main():
|
||||
f"game's 22.8 min, and the accumulator in pg_apay is the three "
|
||||
f"lines that do not spend it")
|
||||
|
||||
# ---- 4. WHAT A SEEK COSTS THE CHIP, and it is the thing no counter in the
|
||||
# player can reach. The walk above proves every BYTE arrived; this asks
|
||||
# whether the SAMPLES the chip made out of them are the ones the encoder
|
||||
# meant. They are not, and they cannot be: the MSM6258's accumulator is a
|
||||
# pure integrator with NO LEAKAGE TERM, so a state mismatch at a branch is a
|
||||
# DC offset that does not decay -- it is not a transient with a time
|
||||
# constant, and calling it one would be the flattering reading.
|
||||
#
|
||||
# THE REFERENCE IS THE ENCODER'S OWN STATE, not a fresh one. adpcm.py
|
||||
# encoded the stream in one pass, so the state it chose byte B(f)'s nibbles
|
||||
# for is the state a CONTINUOUS play reaches at B(f). The FRESH control is
|
||||
# the other design -- STOP the chip and PLAY it again at the branch, which
|
||||
# is what DLX_PK_ARST does -- and the two numbers are what choose between
|
||||
# them. Under --replay the fresh series is not a control at all: it is the
|
||||
# prediction, and it has to be sample-exact.
|
||||
if seek is not None and iters > 1 and bad is None:
|
||||
one = stepper(dec)
|
||||
sp = sum(len(c) for _, c in chunks[:d.n_lumps])
|
||||
b0 = stream_pos(d, seek)
|
||||
whole = d.audio()
|
||||
|
||||
def run_from(st, i0, nmax):
|
||||
"""The samples the chip WOULD have made from byte i0 on, had it been
|
||||
in state `st` -- with the run lengths it actually used, so the two
|
||||
series are sample-aligned across a seam as well as a byte."""
|
||||
out, i = [], i0
|
||||
while i < len(data) and len(out) < nmax:
|
||||
b, c = data[i], runs[i]
|
||||
for j in range(c):
|
||||
smp, st = one(st, (b & 15) if j % 2 == 0 else (b >> 4))
|
||||
out.append(smp)
|
||||
i += 1
|
||||
return out
|
||||
|
||||
if replay:
|
||||
st = (dec["init"], 0) # by construction: PLAY sets
|
||||
else: # both, and the run above
|
||||
st = (dec["init"], 0) # proved it sample-exact
|
||||
for i in range(sp): # ...otherwise the chip is
|
||||
b, c = data[i], runs[i] # wherever the previous
|
||||
for j in range(c): # scene's audio left it,
|
||||
_, st = one(st, (b & 15) if j % 2 == 0 else (b >> 4))
|
||||
pos0 = start + sum(runs[:sp])
|
||||
N = min(4 * rate, len(rec) - pos0)
|
||||
|
||||
ref = (dec["init"], 0) # ...and the ENCODER's state
|
||||
for by in whole[:b0]: # at the SAME stream byte,
|
||||
for nib in (by & 15, by >> 4): # reached continuously
|
||||
_, ref = one(ref, nib)
|
||||
|
||||
got = rec[pos0:pos0 + N]
|
||||
want = run_from(ref, sp, N)
|
||||
fresh = run_from((dec["init"], 0), sp, N)
|
||||
n = min(len(got), len(want), len(fresh))
|
||||
e = [got[i] - want[i] for i in range(n)]
|
||||
|
||||
def band(lo, hi):
|
||||
seg = e[lo:min(hi, n)]
|
||||
if not seg:
|
||||
return None
|
||||
m = sum(seg) / len(seg)
|
||||
ac = (sum((x - m) ** 2 for x in seg) / len(seg)) ** 0.5
|
||||
return m, ac, max(abs(x) for x in seg)
|
||||
|
||||
lo_c, hi_c = adpcm.clamp_bounds(dec["bits"])
|
||||
print(f"--- THE PREDICTOR DOES NOT SEEK (FINDINGS 71). The chip's state "
|
||||
f"at the branch is accumulator {st[0]}, step index {st[1]}; the "
|
||||
f"encoder chose byte {b0:,}'s nibbles for accumulator {ref[0]}, "
|
||||
f"step index {ref[1]}.")
|
||||
print(f" error against the encoder's intent, DC and AC separately "
|
||||
f"(full scale is {hi_c}):")
|
||||
for lo, hi, lab in [(0, 100, "0-6 ms"), (100, 1000, "6-64 ms"),
|
||||
(1000, 5000, "64-320 ms"), (5000, rate, "0.3-1.0 s"),
|
||||
(rate, 2*rate, "1-2 s"), (2*rate, 4*rate, "2-4 s")]:
|
||||
r = band(lo, hi)
|
||||
if r:
|
||||
print(f" {lab:>10} DC {r[0]:8.1f} AC {r[1]:7.2f} "
|
||||
f"|max| {r[2]}")
|
||||
if replay:
|
||||
# THE STRONGEST FORM THIS CAN TAKE. A STOP/PLAY puts the chip in a
|
||||
# state this script knows exactly, so the prediction is not "close",
|
||||
# it is every sample. A run that claimed to reset and did not fails
|
||||
# here and passes everything else on the page.
|
||||
diff = [i for i in range(n) if got[i] != fresh[i]]
|
||||
ck(not diff,
|
||||
f"the chip was STOPPED and re-PLAYED at the branch, so all "
|
||||
f"{n:,} post-seek samples are EXACTLY a decode from the "
|
||||
f"container's own accumulator ({dec['init']}) and step index 0"
|
||||
+ ("" if not diff else f" -- {len(diff):,} differ, first at "
|
||||
f"sample {diff[0]}"))
|
||||
dc = [x for x in e]
|
||||
const = len(set(dc)) == 1
|
||||
ck(const,
|
||||
f"...and the whole error against the encoder's intent is the "
|
||||
f"SINGLE CONSTANT {dc[0]}"
|
||||
+ ("" if const else f" -- it takes {len(set(dc))} values, so the "
|
||||
f"step indices differ too and this is distortion, not offset")
|
||||
)
|
||||
if const:
|
||||
print(f" -> a re-PLAYED branch costs a PERMANENT DC offset "
|
||||
f"of {dc[0]} = {abs(dc[0])*100/hi_c:.1f}% of full scale "
|
||||
f"and {abs(dc[0])*100/511:.1f}% of the 10-bit clamp's "
|
||||
f"headroom. It is inaudible as a tone and it is not free: "
|
||||
f"it is headroom, and it clicks once at the branch.")
|
||||
else:
|
||||
r0, r4 = band(0, 100), band(2*rate, 4*rate)
|
||||
ck(band(1000, 5000)[1] < abs(band(1000, 5000)[0]),
|
||||
f"the error at the branch is an OFFSET and not distortion: over "
|
||||
f"64-320 ms its DC is {band(1000,5000)[0]:.1f} and its AC is "
|
||||
f"{band(1000,5000)[1]:.2f}, so the chip is decoding the right "
|
||||
f"shape from the wrong ground")
|
||||
if r4:
|
||||
print(f" -> playing THROUGH the branch, the offset is still "
|
||||
f"{r4[0]:.0f} four seconds later ({abs(r4[0])*100/hi_c:.0f}%"
|
||||
f" of full scale). There is no leakage term in this "
|
||||
f"predictor; what decay there is comes from the signal's "
|
||||
f"own clamping, not from the recursion forgetting.")
|
||||
# AND THE CONTROL THAT MAKES EITHER READING MEAN ANYTHING.
|
||||
ck(any(want[i] != fresh[i] for i in range(n)),
|
||||
f"the two references are distinguishable over these {n:,} samples, "
|
||||
f"so 'carry the predictor' and 'reset it' are different runs and this "
|
||||
f"measurement has a subject")
|
||||
|
||||
if counters:
|
||||
ck(counters["late"] == 0,
|
||||
f"the player never re-armed a channel that still had bytes to send "
|
||||
@@ -318,8 +558,32 @@ def main():
|
||||
f"the player never found the channel counted out with no lump ready "
|
||||
f"({counters['starve']} times it did)")
|
||||
ck(counters["bytes"] == len(data),
|
||||
f"the player's own byte count ({counters['bytes']:,}) is the "
|
||||
f"container's whole payload ({len(data):,})")
|
||||
f"the player's own byte count ({counters['bytes']:,}) is the whole "
|
||||
f"stream it should have fed ({len(data):,})")
|
||||
if seek is not None and iters > 1:
|
||||
# THE TWO CELLS, ASSERTED APART. A player that kept one counter for
|
||||
# "where the stream is" and "what the chip got" reports a number
|
||||
# that is right for neither, and the symptom is a LAST LUMP that is
|
||||
# long by the skip -- which is not an error, it is a rate.
|
||||
want_skip = (iters - 1) * (stream_pos(d, seek)
|
||||
- stream_pos(d, seek - seek % d.cad_f))
|
||||
ck(counters.get("seekn") == iters - 1,
|
||||
f"the player made {counters.get('seekn')} audio seek(s) for "
|
||||
f"{iters-1} branch point(s) -- the second read FINDINGS 70.3 "
|
||||
f"asks for, at a separate LBA")
|
||||
ck(counters.get("seekb") == want_skip,
|
||||
f"it skipped {counters.get('seekb')} B into the head of a lump "
|
||||
f"and the cadence says {want_skip} -- the byte offset is what "
|
||||
f"makes a branch land on its own frame instead of up to "
|
||||
f"{d.cad_f-1} frames early")
|
||||
ck(counters["pos"] == d.aud_bytes,
|
||||
f"the stream POSITION ended at {counters['pos']:,} B, the "
|
||||
f"container's whole stream ({d.aud_bytes:,}) -- while the chip "
|
||||
f"was handed {counters['bytes']:,}. TWO CELLS FOR TWO FACTS: the "
|
||||
f"position is where the container has got to and is what the "
|
||||
f"last lump's length is measured against; the byte count is what "
|
||||
f"the capture has to account for, and a seek moves one and not "
|
||||
f"the other")
|
||||
print(f" the player: {counters['armed']} lumps armed, "
|
||||
f"{counters['fetched']} fetched, {counters['serv']:,} service "
|
||||
f"calls over {counters['shown']} frames "
|
||||
|
||||
Reference in New Issue
Block a user