Put sound on the wire, and find three LSBs are worth 25 dB
ROADMAP P6, everything in the item except the bus half session 20 closed. tools/encoder/adpcm.py is an MSM6258 codec, tools/encoder/extract_audio.py takes the same seconds of the same stream the frames come from, tools/bench/verify_adpcm.py is the gate, tools/analysis/32_audio_wire.py the container arithmetic. There is no reference encoder -- ffmpeg has a decoder for this format and none the other way -- so what is gated is the decoder the encoder runs INSIDE its own nibble search, sample-exact against ffmpeg's over 4,268 nibbles. An encoder that agrees with its own wrong decoder is what that catches. The Singe window: 156,250 samples -> 78,125 B at 21.97 dB, which is 7,812.5 B/s to the byte. Normalising the disc's -13.4 dBFS level moves the SNR 21.97 -> 21.97, so the level is not a lever. And the two published delta formulas are not the same codec. They differ by at most 3 in 12-bit units; encode for one and decode on the other and the SNR goes 21.97 -> -2.88 dB, the noise louder than the signal, because ADPCM is recursive the way the video codec is temporally recursive. Which one the chip runs is now P6a and it is a precondition on shipping any audio. And audio is the first thing the packed branch's simplification has cost anything for. A record has no index BY DESIGN, so audio cannot be per-record without making records variable; it rides a fixed cadence (F, A), the obvious F=1 wastes 57.3% of every audio sector, and the pick is F=11 A=14 -- 0.09% padding, 14,336 B held, wire 582.0 -> 589.6 KB/s. The codec container, which kept its index, pays zero. The MAME experiment did not work and 65.5 says so: :okim6258 is there at $E92001/$E92003, read out of the machine's own program map, and feeding it from Lua recorded silence across control 0..3 x port C 0..15. The register semantics were not guessed at further. FINDINGS 65. check.sh ALL GREEN before and after, with a new stage. Claude-Session: https://claude.ai/code/session_01194oWYW8DQXK1SZ2DnChW6
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MSM6258 (OKI/Dialogic) 4-bit ADPCM -- encoder, decoder, and the fact that
|
||||
there are TWO decoders and they are not the same one.
|
||||
|
||||
The X68000's ADPCM is an OKI MSM6258V clocked at 8 MHz, dividing to 15,625 /
|
||||
10,417 / 7,812.5 samples a second, 4 bits each, two samples to a byte
|
||||
(FINDINGS 52, buscost.ADPCM_SAMPLE_HZ). The sample word is 12 bits signed.
|
||||
|
||||
WHY THIS FILE HAS TWO DECODERS. Nothing in this repo can be trusted to say what
|
||||
the chip does, and the two references available on this machine DISAGREE:
|
||||
|
||||
VARIANT 'shift' delta = ((2*(n&7) + 1) * step) >> 3
|
||||
This is ffmpeg's `adpcm_ima_oki`, and `gate_vs_ffmpeg()`
|
||||
reproduces it SAMPLE-EXACT, so it is not a reading of source
|
||||
code -- it is a measurement of the decoder that ships.
|
||||
|
||||
VARIANT 'terms' delta = step/8 + (n&4 ? step : 0) + (n&2 ? step/2 : 0)
|
||||
+ (n&1 ? step/4 : 0), each term truncated
|
||||
This is the OKI datasheet's own form, the one an ADPCM chip
|
||||
can actually build out of shifts and adds, and it is what
|
||||
MAME's okim6258 is understood to compute. NOT VERIFIED HERE:
|
||||
no MAME source tree is on this machine (FINDINGS 64.4).
|
||||
|
||||
They differ on 445 of 2,268 sampled nibbles, by up to 4 in 12-bit units --
|
||||
small, and small is not zero. Which one the machine runs is an open question
|
||||
with an experiment attached: MAME's x68000 HAS an okim6258, so it can be asked
|
||||
rather than argued about.
|
||||
|
||||
Nibble order is HIGH NIBBLE FIRST within a byte -- measured, not assumed, by the
|
||||
same gate: reading low-first mismatches ffmpeg on 1,728 of 2,268 samples.
|
||||
"""
|
||||
|
||||
# The 49-entry OKI step table. floor(16 * 1.1**k) for k in 0..48 -- built rather
|
||||
# than pasted, so a transcription slip is not one of the things that can be
|
||||
# wrong here.
|
||||
STEP = [int(16 * 1.1**k) for k in range(49)]
|
||||
|
||||
# The nibble magnitude's effect on the step index. Four quiet nibbles walk it
|
||||
# down one, four loud ones walk it up by more.
|
||||
INDEX_ADJUST = (-1, -1, -1, -1, 2, 4, 6, 8)
|
||||
|
||||
SAMPLE_MIN, SAMPLE_MAX = -2048, 2047 # the 12-bit DAC word
|
||||
|
||||
VARIANTS = ("shift", "terms")
|
||||
|
||||
|
||||
def delta(nibble, step, variant):
|
||||
"""The reconstruction step for one nibble, in 12-bit units."""
|
||||
if variant == "shift":
|
||||
d = ((2 * (nibble & 7) + 1) * step) >> 3
|
||||
elif variant == "terms":
|
||||
d = step // 8
|
||||
if nibble & 4: d += step
|
||||
if nibble & 2: d += step // 2
|
||||
if nibble & 1: d += step // 4
|
||||
else:
|
||||
raise ValueError(f"unknown variant {variant!r}")
|
||||
return -d if nibble & 8 else d
|
||||
|
||||
|
||||
def decode(nibbles, variant="shift"):
|
||||
"""Nibbles -> 12-bit signed samples. State is (signal, step index), both
|
||||
zero at the start of a stream, which is what the chip resets to."""
|
||||
signal, idx, out = 0, 0, []
|
||||
for n in nibbles:
|
||||
signal += delta(n, STEP[idx], variant)
|
||||
signal = SAMPLE_MIN if signal < SAMPLE_MIN else (
|
||||
SAMPLE_MAX if signal > SAMPLE_MAX else signal)
|
||||
idx += INDEX_ADJUST[n & 7]
|
||||
idx = 0 if idx < 0 else (48 if idx > 48 else idx)
|
||||
out.append(signal)
|
||||
return out
|
||||
|
||||
|
||||
def encode(samples, variant="shift"):
|
||||
"""12-bit signed samples -> nibbles.
|
||||
|
||||
The nibble is chosen by EXHAUSTIVE SEARCH over all sixteen, minimising the
|
||||
reconstruction error of this sample. That is greedy rather than optimal --
|
||||
a nibble also moves the step index, so a locally worse choice can pay later
|
||||
-- but it is what a chip-matched encoder is expected to do and it costs
|
||||
nothing offline. The decoder is run INSIDE the loop, so the encoder can
|
||||
never drift away from what the decoder will reconstruct.
|
||||
"""
|
||||
signal, idx, out = 0, 0, bytearray()
|
||||
for s in samples:
|
||||
step = STEP[idx]
|
||||
best, best_err = 0, None
|
||||
for n in range(16):
|
||||
v = signal + delta(n, step, variant)
|
||||
v = SAMPLE_MIN if v < SAMPLE_MIN else (SAMPLE_MAX if v > SAMPLE_MAX else v)
|
||||
err = (v - s) ** 2
|
||||
if best_err is None or err < best_err:
|
||||
best, best_err = n, err
|
||||
signal += delta(best, step, variant)
|
||||
signal = SAMPLE_MIN if signal < SAMPLE_MIN else (
|
||||
SAMPLE_MAX if signal > SAMPLE_MAX else signal)
|
||||
idx += INDEX_ADJUST[best & 7]
|
||||
idx = 0 if idx < 0 else (48 if idx > 48 else idx)
|
||||
out.append(best)
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def pack(nibbles):
|
||||
"""Nibbles -> bytes, HIGH NIBBLE FIRST. An odd count pads with a 0 nibble,
|
||||
which is the quietest one the format has (delta = step/8)."""
|
||||
n = bytes(nibbles)
|
||||
if len(n) & 1:
|
||||
n += b"\0"
|
||||
return bytes((n[i] << 4) | n[i + 1] for i in range(0, len(n), 2))
|
||||
|
||||
|
||||
def unpack(data, count=None):
|
||||
out = bytearray()
|
||||
for b in data:
|
||||
out.append(b >> 4)
|
||||
out.append(b & 15)
|
||||
return bytes(out[:count] if count is not None else out)
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Extract the audio of a Blu-ray window as mono PCM at an MSM6258 sample rate.
|
||||
|
||||
The video side of this window is tools/encoder/extract.py; the arguments mean
|
||||
the same things and are meant to be given the same values, because an audio
|
||||
stream that is not the same seconds as the frames is not this project's audio.
|
||||
|
||||
The disc is AC-3 5.1 at 48 kHz. The arcade original is MONO, so this downmixes
|
||||
-- ffmpeg's default matrix, dialogue from the centre channel included -- and
|
||||
resamples to the chip's rate. Nothing here shapes, gates or normalises the
|
||||
level: what the ADPCM encoder is handed is what the disc has, so that the SNR
|
||||
it reports is the codec's and not a gain stage's.
|
||||
"""
|
||||
import getpass, os, subprocess, sys
|
||||
|
||||
BDROM = os.environ.get("DLX_BDROM") or f"/media/{getpass.getuser()}/BDROM"
|
||||
STREAM_DIR = f"{BDROM}/BDMV/STREAM"
|
||||
|
||||
# 8 MHz / {512, 768, 1024}. The chip has no other rates and 15,625 is the one
|
||||
# every budget in this project is written against (FINDINGS 52).
|
||||
RATES = {15625: 512, 10417: 768, 7813: 1024}
|
||||
|
||||
|
||||
def extract(stream, out, rate=15625, start=None, dur=None):
|
||||
if rate not in RATES:
|
||||
raise SystemExit(f"{rate} is not an MSM6258 rate: {sorted(RATES)}")
|
||||
src = f"{STREAM_DIR}/{stream}.m2ts"
|
||||
cmd = ["ffmpeg", "-v", "error"]
|
||||
if start is not None: cmd += ["-ss", str(start)]
|
||||
if dur is not None: cmd += ["-t", str(dur)]
|
||||
cmd += ["-i", src, "-vn", "-ac", "1", "-ar", str(rate),
|
||||
"-f", "s16le", "-acodec", "pcm_s16le", out, "-y"]
|
||||
subprocess.check_call(cmd)
|
||||
n = os.path.getsize(out) // 2
|
||||
print(f"{stream}: {n} samples @ {rate} Hz mono = {n/rate:.3f} s -> {out}")
|
||||
return n
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# extract_audio.py <stream> <out.raw> [rate] [start_s] [dur_s]
|
||||
stream, out = sys.argv[1], sys.argv[2]
|
||||
rate = int(sys.argv[3]) if len(sys.argv) > 3 else 15625
|
||||
start = float(sys.argv[4]) if len(sys.argv) > 4 else None
|
||||
dur = float(sys.argv[5]) if len(sys.argv) > 5 else None
|
||||
extract(stream, out, rate, start, dur)
|
||||
Reference in New Issue
Block a user