Measure the level off the whole disc, and find the headroom is not worth buying

FINDINGS 69, ROADMAP P6 -- the item 66.3 reopened in session 34 and sessions 35
and 36 both deferred.  The chip clamps its accumulator at 10 bits INSIDE the
recursion, and the ten seconds every audio figure in this tree is quoted on peak
at 435 of 511: it fits, and it fits by accident, because that window is a
-13.4 dBFS passage.  Nothing knew what the loudest passage of the game was.

tools/analysis/35_audio_level.py reads every stream of the unique scene footage
(00000-00201) through extract_audio.py's own chain -- 1,291.6 s, 201 of 202
streams -- and encodes windows of it with adpcm.CHIP.  The disc peaks at 946 of
2048 = -6.71 dBFS (00200 @ 2.11 s), which is 5.35 dB over the clamp, and the
census behind that peak is 687 samples of 20,182,000 (0.0034%) in 402 events,
44.0 ms, longest 0.90 ms.

THE HEADLINE IS A NEGATIVE: THE LEVEL DOES NOT CHANGE.  Forty 2 s windows drawn
over the game's timeline at six gains -- the disc's own level (gain 1.0) has the
best mean SNR (22.03 dB) and the best median, and loses the worst-window column
to -3 dB by 0.04 dB.  The gain that guarantees zero clamping disc-wide (0.5402)
costs 0.85 dB of mean SNR across the whole game to buy back 1.90 dB on the
2.11 s that clamp, because the OKI step table's floor is a constant 16 and does
not scale with the signal.

AND 66.3's MECHANISM DOES NOT SURVIVE A CONTROL.  Error after a clamp run is
elevated ~5x -- and so is the same window at a gain that never clamps, read at
the same indices, because those samples are simply loud.  Worst ratio 1.28 over
64 offsets, and the clamped encode's whole-window mean |error| is the LOWER of
the two (4.71 vs 5.05).  adpcm.encode runs the chip's clamp inside its own
sixteen-way search, so it never loses the chip's state.  The worry was right
about the mechanism and aimed one layer too late: an encoder clamping at 12 bits
while the chip clamps at 10 is exactly that divergence, and 66 closed it.

pack.py gains --audio-gain (default 1.0) so the level is a named parameter with
a measurement behind it instead of a shift buried in a list comprehension, and
prints the encoded window's peak against the clamp.  tmp/packed_singe.dlxp
rebuilds byte-identical, all 6,039,040 B.  New check.sh stage, ~18 s.

Three rig facts in 69.4, because a shipping encoder meets all three: 00176 has
no audio track at all; 00199 is 61.31 s of video with 1.25 s of audio; and 18
stream pairs share duration, peak and RMS, 7 of them byte-identical.

The 10-bit clamp is a DRIVER SETTING, not a chip constant -- x68k.cpp:1089 sets
OUTPUT_10BITS -- so it is MAME's reading of the board, and hardware item 5 is
what settles it.

Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
prosolis
2026-08-25 14:20:55 -07:00
parent 191f2b47bb
commit ab15c0749a
7 changed files with 885 additions and 5 deletions
+22 -2
View File
@@ -35,7 +35,7 @@ NOT use adpcm.py's module defaults, which are ffmpeg's on purpose so that
tools/bench/verify_adpcm.py stays a check against an independent implementation.
The axes go in the header, so a player never has to be told.
"""
import argparse, glob, os, sys
import argparse, glob, math, os, sys
import numpy as np
from PIL import Image
@@ -70,6 +70,13 @@ ap.add_argument("--audio-hz", type=int, default=15625,
help="the CHIP's rate, and the rate the .raw was resampled to")
ap.add_argument("--cadence", type=int, default=P.CADENCE_F,
help="frames between audio lumps (65.3's sweep picks 11)")
ap.add_argument("--audio-gain", type=float, default=1.0,
help="LEVEL, applied before the 12-bit requantisation. 1.0 is "
"s16>>4, the disc's own level, and it is the MEASURED "
"choice: FINDINGS 69 encoded windows drawn over the whole "
"game at six gains and every attenuation that buys "
"headroom under the chip's 10-bit clamp costs more SNR "
"than the clamping it avoids. Below 1.0 is a fallback")
a = ap.parse_args()
files = sorted(glob.glob(f"{a.frames_dir}/f*.png"))
@@ -122,7 +129,16 @@ if a.audio:
import struct as _struct
raw = open(a.audio, "rb").read()
pcm = _struct.unpack("<%dh" % (len(raw) // 2), raw)
src12 = [max(-2048, min(2047, x >> 4)) for x in pcm]
# THE LEVEL. `>> 4` maps the disc's full scale onto the 12-bit word and is
# what every container in this tree has been encoded at; the gain is a
# multiply BEFORE it, so gain 1.0 is byte-identical to what shipped. The
# chip clamps at 10 bits INSIDE the recursion (adpcm.CHIP), so anything the
# gain puts above 511 is unreachable -- and FINDINGS 69 measured that the
# attenuation which avoids that costs more than the clamping does.
g = a.audio_gain
src12 = [max(-2048, min(2047, int(math.floor(x * g)) >> 4)) for x in pcm]
lo12, hi12 = adpcm.clamp_bounds(adpcm.CHIP["bits"])
nclamp = sum(1 for v in src12 if v > hi12 or v < lo12)
need = len(files) * a.audio_hz // (2 * a.fps)
nib = adpcm.encode(src12, variant=adpcm.CHIP["variant"],
init=adpcm.CHIP["init"], bits=adpcm.CHIP["bits"])
@@ -156,6 +172,10 @@ print(f" record {rec_b:,} B = {rec_b // P.SECTOR} sectors exactly, "
if d.has_audio:
print(f" DLXP2: audio {d.aud_bytes:,} B at {d.aud_hz:,} Hz, SNR {snr:.2f} dB, "
f"cadence F={d.cad_f} A={d.cad_a} ({d.n_lumps} lumps)")
print(f" level gain {a.audio_gain:g}, source peak {max(abs(v) for v in src12)}"
f" of the chip's {hi12}: {nclamp:,} of {len(src12):,} samples "
f"({100*nclamp/len(src12):.4f}%) are above the clamp and cannot be "
f"reached (FINDINGS 69)")
print(f" the four axes, in the header: "
+ ", ".join(f"{k}={v}" for k, v in d.decoder().items()))
# STEADY STATE, not the file: the last lump of a 120-frame window feeds 4