#!/usr/bin/env bash # verify-x68000-audio.sh - X68000 audio gate, read off the chips. # # Boots a built example on a Human68k floppy pair under headless MAME and # watches the two sound devices through memory taps, so the checks are on # what the hardware was actually told, not on a log line: # # 1. PPI port C ($E9A005) low nibble. That nibble IS the MSM6258's # sample rate (bits 3,2) and channel gating (bits 1,0): the gate # wants rate index 2, divider 512, 7812 Hz -- the rate the mixer # renders at -- and pan 0, both channels. IOCS's own mode byte does # not set these correctly (measured: mode 0x05 leaves 0x01, which is # 3906 Hz out of the right speaker only), so audioPcm.c writes the # register itself and this checks that it stuck. # 2. Every YM2151 noise write ($E90000 register 0x0F). The low five # bits must be a value from kOpmNoiseFreq in src/x68000/audio.c, # which is read out of the source here rather than duplicated: a # pitch that escapes the table is the bug this gate exists for # (a plain `31 - pitch` ran the generator an octave bright). # 3. The ADPCM byte feed rate WHILE A SOUND IS PLAYING. Two samples per # byte at 7812 Hz is 3906 bytes/s; half that means the divider is # wrong again and every sample the port plays is an octave low. It # is the rate inside one burst of feeding that counts, not the # average: the port stops the chip between sounds, so an average # over a mostly silent run would say nothing. # # Space Taxi is the default because its attract demo exercises the whole # noise curve (all seventeen distinct table values) and speaks. # # Usage: scripts/verify-x68000-audio.sh [example] [seconds] # Env: X68K_DIR dir holding HUMAN302.XDF + roms/ (default toolchains/x68mame) # Requires: the example built (make -f make/x68000.mk), MAME with x68000. set -uo pipefail prog=${1:-staxi} seconds=${2:-160} PROG=$(echo "$prog" | tr '[:lower:]' '[:upper:]') repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) bin=$repo/build/x68000/bin/$PROG.X x68=${X68K_DIR:-$repo/toolchains/x68mame} template=$x68/HUMAN302.XDF source=$repo/src/x68000/audio.c MAME="${MAME:-$repo/toolchains/cache/mame-mame0264/x68k}" if [[ ! -x $MAME ]]; then MAME="$(command -v mame || true)" fi [[ -n ${MAME:-} && -x $MAME ]] || { echo "verify-x68000-audio: no MAME (set MAME=...)" >&2; exit 2; } [[ -f $bin ]] || { echo "verify-x68000-audio: $bin not built" >&2; exit 2; } [[ -f $template ]] || { echo "verify-x68000-audio: missing $template" >&2; exit 2; } # The noise table, straight out of the port so the two cannot drift apart. table=$(python3 - "$source" <<'PY' import re, sys src = open(sys.argv[1]).read() m = re.search(r'kOpmNoiseFreq\[32\]\s*=\s*\{(.*?)\}', src, re.S) if not m: sys.exit("kOpmNoiseFreq not found") print(",".join(v.strip() for v in m.group(1).split(",") if v.strip())) PY ) || { echo "verify-x68000-audio: cannot read kOpmNoiseFreq from $source" >&2; exit 2; } work=$(mktemp -d -t joey-x68audio.XXXXXX) trap 'rm -rf "$work"' EXIT printf 'B:\r\nB:\\%s.X\r\n\x1a' "$PROG" > "$work/AUTOEXEC.BAT" cp "$template" "$work/boot.xdf" cp "$template" "$work/data.xdf" python3 "$repo/tools/xdftool.py" empty "$work/data.xdf" >/dev/null for f in USKCG.SYS BEEP.SYS KEY.SYS STARTUP.ENV; do python3 "$repo/tools/xdftool.py" delete "$work/boot.xdf" "$f" >/dev/null 2>&1 || true done for d in SYS HIS BIN BASIC2 ASK ETC; do python3 "$repo/tools/xdftool.py" deltree "$work/boot.xdf" "$d" >/dev/null 2>&1 || true done python3 "$repo/tools/xdftool.py" add "$work/boot.xdf" "$work/AUTOEXEC.BAT" AUTOEXEC.BAT >/dev/null python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$bin" "$PROG.X" >/dev/null data_dir=$repo/build/x68000/bin/DATA if [[ -d $data_dir ]]; then while IFS= read -r f; do rel=${f#"$data_dir"/} python3 "$repo/tools/xdftool.py" add "$work/data.xdf" "$f" \ "DATA/$(echo "$rel" | tr '[:lower:]' '[:upper:]')" >/dev/null done < <(find "$data_dir" -type f | sort) fi # The taps keep to arithmetic and file writes: a tap that reads memory # takes MAME down with it. cat > "$work/tap.lua" <> 8) & 0xFF end if offset < 0xE90002 then sel = v elseif sel == 0x0F then out:write(string.format("NOISE %02X\\n", v)) end return data end) ppiTap = mem:install_write_tap(0xE9A004, 0xE9A005, "ppi", function(offset, data, mask) local v = data & 0xFF if (mask & 0xFF) == 0 then v = (data >> 8) & 0xFF end out:write(string.format("PORTC %02X\\n", v)) return data end) -- The port stops the chip between sounds, so the honest figure is the rate -- inside one burst of feeding, not an average across the silence. A gap of -- more than 100 ms ends a burst; bursts shorter than 300 ms are too short to -- time accurately and are skipped. local function closeBurst() if burstLast > burstStart + 0.3 and burstCount > 0 then local r = burstCount / (burstLast - burstStart) if r > bestRate then bestRate = r end end burstCount = 0 end pcmTap = mem:install_write_tap(0xE92000, 0xE92003, "pcm", function(offset, data, mask) local t = manager.machine.time:as_double() pcmWrites = pcmWrites + 1 if burstLast < 0 or (t - burstLast) > 0.1 then closeBurst() burstStart = t end burstLast = t burstCount = burstCount + 1 return data end) emu.register_frame_done(function() local t = manager.machine.time:as_double() if t >= stop then closeBurst() out:write(string.format("PCMTOTAL %d\\n", pcmWrites)) out:write(string.format("PCMRATE %.0f\\n", bestRate)) out:write("DONE\\n") out:close() manager.machine:exit() end end) LUA ( cd "$work" && SDL_AUDIODRIVER=dummy timeout 1800 "$MAME" x68000 \ -bios ipl10 -rompath "$x68/roms" \ -flop1 boot.xdf -flop2 data.xdf \ -video none -sound none -nothrottle -skip_gameinfo \ -autoboot_script tap.lua mame.log 2>&1 ) || true [[ -f $work/audio.txt ]] || { echo "verify-x68000-audio: FAIL (MAME produced no tap output)" >&2; exit 1; } python3 - "$work/audio.txt" "$table" "$PROG" <<'PY' import sys path, table, prog = sys.argv[1], sys.argv[2], sys.argv[3] allowed = {int(v) for v in table.split(",")} noise, portc, rate, done = [], [], None, False for line in open(path): p = line.split() if not p: continue if p[0] == "NOISE": noise.append(int(p[1], 16)) elif p[0] == "PORTC": portc.append(int(p[1], 16)) elif p[0] == "PCMRATE": rate = float(p[1]) elif p[0] == "DONE": done = True fails = [] if not done: fails.append("the run never reached its time limit") # 1. The ADPCM rate/pan nibble. The last value written wins; the ROM's own # boot value (0x0B) does not count as ours. ours = [v for v in portc if v != 0x0B] if not ours: fails.append("PPI port C was never programmed, so the ADPCM never played") else: got = ours[-1] & 0x0F if got != 0x08: fails.append(f"port C low nibble {got:#04x}, want 0x08 " f"(rate index {(got >> 2) & 3} pan {got & 3}; want rate 2 = 7812 Hz, pan 0 = both)") # 2. Every enabled noise write must name a table entry. on = {v & 0x1F for v in noise if v & 0x80} if not on: fails.append("no noise writes seen at all") stray = sorted(on - allowed) if stray: fails.append(f"noise codes outside kOpmNoiseFreq: {stray}") # 3. Byte feed rate: two samples per byte at 7812 Hz. if rate is None: fails.append("the ADPCM channel was never fed") elif not (3500.0 <= rate <= 4300.0): fails.append(f"ADPCM in-burst feed {rate:.0f} bytes/s, want ~3906 " f"({'half rate, the divider is wrong' if rate < 2600 else 'off rate'})") portcText = "/".join(f"{v:02X}" for v in ours[-2:]) if ours else "never set" rateText = f"{rate:.0f} bytes/s in-burst" if rate is not None else "silent" print(f"verify-x68000-audio: {prog} noise writes {len(noise)} " f"({len(on)} distinct codes, {len(allowed - on)} table entries unused), " f"port C {portcText}, ADPCM {rateText}") if fails: for f in fails: print(f"verify-x68000-audio: FAIL ({f})", file=sys.stderr) sys.exit(1) print("verify-x68000-audio: PASS") PY