237 lines
9.6 KiB
Bash
Executable file
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)"
|