joeylib2/scripts/verify-iigs-audio.sh

237 lines
9.6 KiB
Bash
Executable file

#!/usr/bin/env bash
# verify-iigs-audio.sh - Headless gate proving NTP is actually running on
# the IIgs, modelled on verify-iigs.sh (same Finder launch timeline, same
# "read emulated memory, not a snapshot" approach).
#
# Sound cannot be graded from a -sound none run, so this grades the two
# pieces of state that make sound possible:
#
# irqVec $E1002C is the GS sound-interrupt vector. NTPprepare writes
# $5C (JML) + the address of its interrupt_handler + its own
# bank there (setup_interrupt), and NTPstop puts the previous
# vector back. $5C plus a RAM bank therefore means "NTP's DOC
# interrupt is installed". Before the ntpArm change nothing
# ever called NTPprepare unless the app played a .NTP module,
# so an SFX-only app left this at the system value and every
# streamed sample reached the DOC exactly once, unrefilled.
#
# docVol $E100CA is the Sound Tool Set's copy of the sound control
# register, and its LOW NIBBLE is the DOC master volume. NTP
# copies this byte into $C03C before every DOC access and
# never writes it, so if nothing started the Sound Tool Set it
# is whatever GS/OS left behind -- and a zero nibble means the
# DOC streams perfectly while outputting SILENCE. Everything
# below passed in exactly that state ("I don't hear any sound
# other than the IIgs startup beep"), which is why this check
# exists: streaming is necessary, not sufficient.
#
# slotN MAME exposes the Ensoniq 5503's 64KB wavetable RAM as the
# ":doc" device's "rom" space. JoeyLib's SFX slots live at DOC
# pages 0xC0, 0xC2, ... (SFX_BASE_DOC_PAGE / SFX_DOC_PAGE_STEP
# in src/iigs/audio_full.c), and bytes only land there when
# NTPstreamsound runs. Every slot is hashed twice, frames
# apart. Which slots an application uses is its own business
# -- AUDIO's tracker takes 0-2, Space Taxi only ever reaches
# the noise slot 3 -- so the gate asks that SOME slot carry a
# waveform and that SOME slot's hash move between the probes.
# A moving hash is the live-interrupt evidence: NTP streams
# the sample through the page in 256-byte halves, so a dead
# interrupt leaves whatever the one initial copy wrote.
#
# Usage: scripts/verify-iigs-audio.sh [example]
# example lowercase name, default "audio". A single-example
# disk is built for it (joey.2mg cannot hold them all).
# Requires: toolchains/env.sh sourced; the example built under build/iigs/bin.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
example=${1:-audio}
NAME=${example^^}
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
probeA="${MAME_PROBE_A:-5400}"
probeB="${MAME_PROBE_B:-9000}"
# Mirrors JOEY_AUDIO_SFX_SLOTS / SFX_BASE_DOC_PAGE / SFX_DOC_PAGE_STEP
# in src/iigs/audio_full.c.
slots=5
basePage=0xC0
pageStep=2
[ -f "$sys_disk" ] || { echo "verify-iigs-audio: missing $sys_disk" >&2; exit 2; }
[ -f "$repo/build/iigs/bin/$NAME" ] || { echo "verify-iigs-audio: missing build/iigs/bin/$NAME (run 'make iigs-disk')" >&2; exit 2; }
work=$(mktemp -d -t joeylib-audio.XXXXXX)
trap 'rm -rf "$work"' EXIT
# One-example volume: the 800KB floppy cannot hold every example, and the
# shared joey.2mg drops AUDIO and STAXI first (see make-iigs-disk.sh).
JOEY_DISK_EXAMPLES="$NAME" BINDIR="$repo/build/iigs/bin" \
NTP_BIN="$repo/build/iigs/audio/ntpplayer.bin" \
"$repo/scripts/make-iigs-disk.sh" "$work/joey.2mg" >/dev/null
cp "$sys_disk" "$work/boot.po"
cat > "$work/audio.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
-- The DOC's wavetable RAM. Guarded: if a MAME build ever renames the
-- device or its space, say so instead of silently reporting zeroes.
local docSpace = nil
do
local ok = pcall(function()
docSpace = manager.machine.devices[":doc"].spaces["rom"]
end)
if not ok then docSpace = nil end
end
-- FNV-1a over a DOC page pair (one JoeyLib SFX slot = 512 bytes), plus a
-- count of bytes that are neither 0x00 (never written) nor 0x80 (DOC
-- silence), i.e. bytes that carry an actual waveform.
local function docSlot(page)
if docSpace == nil then return 0, -1 end
local h = 2166136261
local nz = 0
for i = 0, 511 do
local b = docSpace:read_u8((page * 256) + i)
if b ~= 0 and b ~= 0x80 then nz = nz + 1 end
h = (h ~ b) & 0xFFFFFFFF
h = (h * 16777619) & 0xFFFFFFFF
end
return h, nz
end
local function report(tag)
local v0 = mem:read_u8(0xE1002C)
local v1 = mem:read_u8(0xE1002D)
local v2 = mem:read_u8(0xE1002E)
local v3 = mem:read_u8(0xE1002F)
local scb0 = mem:read_u8(0xE19D00)
local docVol = mem:read_u8(0xE100CA)
local line = string.format(
"VERIFY-AUDIO name=$NAME tag=%s frame=%d scb0=%02X irqVec=%02X%02X%02X%02X docVol=%02X",
tag, frame, scb0, v3, v2, v1, v0, docVol)
for slot = 0, $slots - 1 do
local h, n = docSlot($basePage + slot * $pageStep)
line = line .. string.format(" slot%d=%08X slot%dNZ=%d", slot, h, slot, n)
end
io.write(line .. "\n")
io.flush()
end
local steps = {
{3000, function() nat:post("J") end},
{3120, function() press(key_cmd) end},
{3126, function() nat:post("o") end},
{3180, function() release(key_cmd) end},
{3540, function() nat:post("$NAME") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
{$probeA, function() report("A") end},
{$probeB, function() report("B"); manager.machine:exit() end},
}
emu.register_frame_done(function()
frame = frame + 1
while idx <= #steps and frame >= steps[idx][1] do
steps[idx][2]()
idx = idx + 1
end
end)
LUA
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 400 mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/audio.lua" </dev/null 2>&1) || true
lineA=$(echo "$out" | grep -E '^VERIFY-AUDIO .* tag=A ' | tail -1)
lineB=$(echo "$out" | grep -E '^VERIFY-AUDIO .* tag=B ' | tail -1)
echo "$lineA"
echo "$lineB"
if [ -z "$lineA" ] || [ -z "$lineB" ]; then
echo "verify-iigs-audio: FAIL - no probe report (boot/launch failed)" >&2
echo "$out" | tail -15 >&2
exit 1
fi
field() { echo "$2" | sed -E "s/.*$1=([0-9A-Fa-f]+).*/\1/"; }
# Launch gate first, exactly as verify-iigs.sh: the Finder desktop alone
# would otherwise be graded as if it were the example.
scb0=$(field scb0 "$lineB")
if [ $((16#$scb0)) -ge 16 ]; then
echo "verify-iigs-audio: FAIL ($NAME never launched - SHR SCB[0]=0x$scb0 is the Finder's 640-mode fill)" >&2
exit 1
fi
vec=$(field irqVec "$lineB")
if [ "${vec:6:2}" != "5C" ]; then
echo "verify-iigs-audio: FAIL (sound interrupt vector is $vec, not a JML - NTPprepare never ran, so there is no interrupt to refill a streamed sample)" >&2
exit 1
fi
# The vector the ROM leaves in place is a JML too ($5C 62 CD FC on this
# machine), so "is it a JML" is not enough -- the control run with the
# pre-ntpArm audio_full.c passed that test. NTP lives in a Memory
# Manager handle, always in RAM, so a ROM bank ($F0 and up) means the
# system vector is still installed and NTPprepare never ran.
bank=${vec:0:2}
if [ $((16#$bank)) -ge 240 ]; then
echo "verify-iigs-audio: FAIL (sound interrupt vector $vec still points into ROM bank \$$bank - NTPprepare never ran)" >&2
exit 1
fi
# Audible-output precondition: the DOC master volume must not be zero.
docVol=$(field docVol "$lineB")
if [ $(( 16#$docVol & 0x0F )) -eq 0 ]; then
echo "verify-iigs-audio: FAIL (DOC master volume is 0 -- \$E100CA=0x$docVol. The Sound Tool Set was never started, so NTP copies a zero volume into \$C03C: the DOC streams but nothing is audible)" >&2
exit 1
fi
if [ "$(field slot0NZ "$lineA")" = "-1" ]; then
echo "verify-iigs-audio: FAIL (MAME exposed no :doc/rom space - the DOC RAM probe read nothing)" >&2
exit 1
fi
loud=""
moved=""
slot=0
while [ "$slot" -lt "$slots" ]; do
nz=$(field "slot${slot}NZ" "$lineB")
if [ "$nz" -gt 0 ]; then
loud="$loud $slot"
fi
if [ "$(field "slot$slot" "$lineA")" != "$(field "slot$slot" "$lineB")" ]; then
moved="$moved $slot"
fi
slot=$((slot + 1))
done
if [ -z "$loud" ]; then
echo "verify-iigs-audio: FAIL (no SFX slot's DOC page holds a waveform - NTPstreamsound never reached the DOC)" >&2
exit 1
fi
# A live NTP interrupt keeps rewriting the DOC page it streams through.
# A slot parked on the resident tone square (toneSquareEnsure) hashes
# the same every time -- every refill writes the same square -- so this
# only warns when NO slot moved at all.
if [ -z "$moved" ]; then
echo "verify-iigs-audio: WARN (no DOC slot changed between frames $probeA and $probeB - samples are present but nothing is refilling them)"
fi
echo "verify-iigs-audio: PASS ($NAME: NTP interrupt installed at bank $bank; DOC slots with a waveform:$loud; refilled between probes:$moved)"