187 lines
6.8 KiB
Bash
Executable file
187 lines
6.8 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
|
|
# See LICENSE.md for license information.
|
|
# SPDX-License-Identifier: MIT
|
|
#
|
|
# verify-amiga-audio.sh - Amiga audio gate, graded on recorded sound.
|
|
#
|
|
# Paula cannot be tapped register-by-register here: fs-uae has no trace
|
|
# facility and no debugger this harness can drive, so unlike the IIgs and
|
|
# ST gates (which read the DOC and the YM2149 directly) this one records
|
|
# what actually came out of the emulator and grades that.
|
|
#
|
|
# Capture path: fs-uae plays through OpenAL, OpenAL is pointed at
|
|
# PulseAudio with ALSOFT_DRIVERS=pulse, and PULSE_SINK sends this ONE
|
|
# process to a null sink created for the run -- the host's default sink is
|
|
# never touched, so the gate is safe to run on a desktop that is playing
|
|
# something else. parec then records that sink's monitor.
|
|
#
|
|
# What is graded: the AUDIO example is driven through four phases whose
|
|
# expected loudness differs by a wide margin.
|
|
#
|
|
# music the baked tracker song on entry, loud
|
|
# paused P pressed, near silence
|
|
# sfx SPACE during the pause, so ANY sound must be the digital SFX
|
|
# resumed P again, loud again
|
|
#
|
|
# Firing the SFX into a paused song is the point: mixed under the music an
|
|
# 8-bit one-shot barely moves the total and "did the SFX play" becomes a
|
|
# judgment call, while against silence it is unambiguous. Coexistence is
|
|
# still covered, because the music has to come back afterward on the same
|
|
# hardware. Measured margins: music ~5700, paused ~240, sfx ~1170.
|
|
#
|
|
# Keys are HELD (keydown, pause, keyup), not tapped: the demo tests
|
|
# jlKeyPressed, an edge predicate polled once a frame, and a make/break
|
|
# delivered between two polls is never seen.
|
|
#
|
|
# --fullscreen=1 and --initial_input_grab=1 are REQUIRED under a WM-less
|
|
# Xvfb: SDL only accepts keyboard input once its window has focus, no
|
|
# window manager is there to grant it, and xdotool's XSendEvent route is
|
|
# ignored by SDL. A fullscreen SDL window takes focus itself, after which
|
|
# plain XTEST typing lands. Same reasoning as verify-amiga-input.sh.
|
|
#
|
|
# Usage: scripts/verify-amiga-audio.sh [example]
|
|
# example binary name in build/amiga/bin, default Audio
|
|
# Env: AMIGA_AUDIO_KEEP set to keep the work dir and the WAV
|
|
# Requires: fs-uae, Xvfb, xdotool, parec/pactl (PulseAudio or PipeWire),
|
|
# the Kickstart (install.sh install emulator:amiga),
|
|
# build/amiga/bin/<example>
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
joeyNeed FSUAE fsuae
|
|
source "$repo/scripts/lib/amigaGate.sh"
|
|
amigaMachine
|
|
prog=${1:-Audio}
|
|
binDir=$repo/build/amiga/bin
|
|
grader=$(joeyTool audioGate) || exit 1
|
|
pulseLib=$repo/scripts/pulseRecordLib.sh
|
|
label=verify-amiga-audio
|
|
|
|
[ -f "$pulseLib" ] || { echo "$label: missing $pulseLib" >&2; exit 2; }
|
|
# shellcheck source=scripts/pulseRecordLib.sh
|
|
. "$pulseLib"
|
|
for tool in Xvfb xdotool parec pactl; do
|
|
command -v "$tool" >/dev/null || { echo "$label: $tool not installed" >&2; exit 2; }
|
|
done
|
|
pulseEnsureServer || { echo "$label: no PulseAudio/PipeWire server to record from (tried XDG_RUNTIME_DIR and /run/user/$(id -u)/pulse/native)" >&2; exit 2; }
|
|
|
|
if [ -d "$binDir/$prog" ]; then
|
|
progDir=$binDir/$prog
|
|
elif [ -f "$binDir/$prog" ]; then
|
|
progDir=""
|
|
else
|
|
echo "$label: $binDir/$prog not built (run 'make amiga')" >&2
|
|
exit 2
|
|
fi
|
|
|
|
work=$(mktemp -d -t joeylib-amaudio.XXXXXX)
|
|
sinkModule=""
|
|
display=":$((90 + RANDOM % 9))"
|
|
xvfbPid=""
|
|
fsPid=""
|
|
parecPid=""
|
|
|
|
cleanup() {
|
|
[ -n "$parecPid" ] && kill "$parecPid" 2>/dev/null
|
|
joeyStop "$fsPid"
|
|
[ -n "$xvfbPid" ] && kill "$xvfbPid" 2>/dev/null
|
|
# Always unload the sink: leaving null sinks behind on the user's
|
|
# audio server is the one side effect this gate could have.
|
|
pulseUnloadSink "$sinkModule"
|
|
if [ -n "${AMIGA_AUDIO_KEEP:-}" ]; then
|
|
echo "$label: kept $work" >&2
|
|
else
|
|
rm -rf "$work"
|
|
fi
|
|
return 0
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
# Stage the Amiga volume the way joey-run does.
|
|
mkdir -p "$work/hd/s"
|
|
if [ -n "$progDir" ]; then
|
|
cp -r "$progDir/." "$work/hd/"
|
|
else
|
|
find "$binDir" -maxdepth 1 -type f -executable -exec cp -t "$work/hd/" {} +
|
|
[ -d "$binDir/DATA" ] && cp -r "$binDir/DATA" "$work/hd/"
|
|
fi
|
|
printf 'stack 32768\n:%s\n' "$prog" > "$work/hd/s/startup-sequence"
|
|
|
|
sinkName="joeyAudioGate$$"
|
|
sinkModule=$(pulseCreateSink "$sinkName") || sinkModule=""
|
|
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
|
|
|
Xvfb "$display" -screen 0 800x600x24 -nolisten tcp >/dev/null 2>&1 &
|
|
xvfbPid=$!
|
|
sleep 1
|
|
|
|
# --fast_memory=0 --slow_memory=0 keeps this a bare A500, the port's floor.
|
|
DISPLAY="$display" PULSE_SINK="$sinkName" ALSOFT_DRIVERS=pulse SDL_AUDIODRIVER=pulse \
|
|
"$FSUAE" \
|
|
"${AMIGA_MACHINE_ARGS[@]}" \
|
|
--hard_drive_0="$work/hd" \
|
|
--hard_drive_0_label=JOEYLIB \
|
|
--fullscreen=1 \
|
|
--initial_input_grab=1 \
|
|
--fade_out_duration=0 \
|
|
--fast_memory=0 \
|
|
--slow_memory=0 \
|
|
> "$work/fsuae.log" 2>&1 &
|
|
fsPid=$!
|
|
|
|
# Let Kickstart boot and the startup-sequence reach the program.
|
|
sleep 12
|
|
if ! kill -0 "$fsPid" 2>/dev/null; then
|
|
echo "$label: FAIL - fs-uae exited before the program started" >&2
|
|
tail -5 "$work/fsuae.log" >&2
|
|
exit 1
|
|
fi
|
|
|
|
parec --device="$sinkName.monitor" --format=s16le --rate=44100 --channels=2 \
|
|
--file-format=wav "$work/out.wav" > "$work/parec.log" 2>&1 &
|
|
parecPid=$!
|
|
|
|
win=$(DISPLAY="$display" xdotool search --name -- "FS-UAE" | head -1)
|
|
if [ -z "$win" ]; then
|
|
echo "$label: FAIL - no FS-UAE window to send keys to" >&2
|
|
exit 1
|
|
fi
|
|
DISPLAY="$display" xdotool windowactivate --sync "$win" 2>/dev/null
|
|
|
|
hold() { # $1 = key name
|
|
DISPLAY="$display" xdotool keydown --window "$win" "$1"
|
|
sleep 0.4
|
|
DISPLAY="$display" xdotool keyup --window "$win" "$1"
|
|
}
|
|
|
|
# The phase boundaries below are the timestamps handed to the grader, so
|
|
# the sleeps and the spec must stay in step.
|
|
sleep 6 # 0-6s music
|
|
hold p # -> paused
|
|
sleep 8 # 6-14s paused
|
|
for _ in 1 2 3; do # 14-24s sfx into silence
|
|
hold space
|
|
sleep 1.5
|
|
done
|
|
sleep 4
|
|
hold p # -> resumed
|
|
sleep 9 # 24-33s resumed
|
|
|
|
kill "$parecPid" 2>/dev/null
|
|
parecPid=""
|
|
sleep 1
|
|
|
|
[ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; }
|
|
|
|
# Thresholds sit far from the measured values (music ~5700, paused ~240,
|
|
# sfx ~1170) so ordinary mixing jitter cannot flip the verdict.
|
|
"$grader" phases "$label" "$work/out.wav" \
|
|
"music:1:5:2000:" \
|
|
"paused:8:13::800" \
|
|
"sfx:16:22:800:" \
|
|
"resumed:27:32:2000:"
|
|
status=$?
|
|
[ -n "${AMIGA_AUDIO_KEEP:-}" ] && cp "$work/out.wav" "$repo/build/amiga/headless-audio.wav" 2>/dev/null
|
|
exit $status
|