158 lines
5.7 KiB
Bash
Executable file
158 lines
5.7 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-dvx-audio.sh - DVX audio gate, graded on recorded sound.
|
|
#
|
|
# The DVX port plays through DVX's own sound library (audio.lib, with its
|
|
# Sound Blaster driver and mixer) and music library (src/dvx/audio.c).
|
|
# Nothing in DOSBox can be tapped register by register, so, like the DOS and
|
|
# Amiga gates, this records what came out and grades it with
|
|
# tools/gradeAudioPhases.py, through a PulseAudio null sink made for this run
|
|
# (scripts/pulseRecordLib.sh: the host's own sound is never touched).
|
|
#
|
|
# AUDIO (examples/audio) is the desktop of a scratch DVX in DOSBox Staging
|
|
# with a Sound Blaster 16 (and its OPL3, where DVX plays square tones when
|
|
# it can). It is driven through the four phases the DOS gate uses:
|
|
#
|
|
# music the baked tracker song on entry (the jlAudioVoice tones), loud
|
|
# paused P pressed, near silence
|
|
# sfx SPACE during the pause, so any sound is the digital one-shot
|
|
# (DATA/test.sfx beside the program, through the DATA root)
|
|
# resumed P again, loud again
|
|
#
|
|
# Keys are HELD (keydown, pause, keyup): the demo tests jlKeyPressed, an
|
|
# edge polled once a frame. The thresholds sit at about half the levels
|
|
# measured (see the grader call below).
|
|
#
|
|
# Usage: scripts/verify-dvx-audio.sh
|
|
# Env: DVX_AUDIO_KEEP keep the work folder and the recording
|
|
# Requires: DOSBox Staging, Xvfb, xdotool, parec/pactl, python3, the dvx
|
|
# component; the audio program (make dvx).
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
source "$repo/scripts/lib/dvxGate.sh"
|
|
joeyNeed DOSBOX dosbox
|
|
joeyNeed DVX_ROOT dvx
|
|
grader=$repo/tools/gradeAudioPhases.py
|
|
pulseLib=$repo/scripts/pulseRecordLib.sh
|
|
label=verify-dvx-audio
|
|
program=audio
|
|
wall=150 # hard cap on DOSBox, whatever happens to this script
|
|
readySeconds=60 # DVX boot until its shell reports ready
|
|
settleSeconds=4 # then AUDIO opening its window and starting the song
|
|
|
|
# shellcheck source=scripts/pulseRecordLib.sh
|
|
. "$pulseLib"
|
|
for tool in Xvfb xdotool parec pactl python3; 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" >&2; exit 2; }
|
|
dvxNeedBuild "$program"
|
|
|
|
work=$(mktemp -d -t joeylib-dvx-audio.XXXXXX)
|
|
sinkModule=""
|
|
guard=""
|
|
dosbox=""
|
|
xvfb=""
|
|
parecPid=""
|
|
cleanup() {
|
|
[ -n "$parecPid" ] && kill "$parecPid" 2> /dev/null
|
|
joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null
|
|
# A leaked null sink on the user's audio server is the one side effect
|
|
# this gate could have.
|
|
pulseUnloadSink "$sinkModule"
|
|
if [ -n "${DVX_AUDIO_KEEP:-}" ]; then
|
|
echo "$label: kept $work" >&2
|
|
else
|
|
rm -rf "$work"
|
|
fi
|
|
return 0
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
sinkName="joeyDvxGate$$"
|
|
sinkModule=$(pulseCreateSink "$sinkName") || sinkModule=""
|
|
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
|
|
|
dvxStage "$work/c" "$(dvxAppDir "$program")"
|
|
dvxDosboxConf "$work/dosbox.conf" "$work/c" sb16
|
|
mkdir -p "$work/xdg"
|
|
|
|
display=":$((RANDOM % 400 + 2900))"
|
|
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 &
|
|
xvfb=$!
|
|
sleep 1
|
|
|
|
XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" PULSE_SINK="$sinkName" SDL_AUDIODRIVER=pulse \
|
|
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
|
|
--conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 &
|
|
guard=$!
|
|
dosbox=$(joeyChild "$guard")
|
|
|
|
ready=""
|
|
for ((waited = 0; waited < readySeconds; waited++)); do
|
|
if dvxDvxLog "$work/c" 200 | grep -q "DVX Shell ready"; then
|
|
ready=1
|
|
break
|
|
fi
|
|
joeyAlive "$guard" || break
|
|
sleep 1
|
|
done
|
|
if [ -z "$ready" ]; then
|
|
echo "$label: FAIL - DVX did not come up within ${readySeconds}s (DVX.LOG follows)" >&2
|
|
dvxDvxLog "$work/c" >&2
|
|
exit 1
|
|
fi
|
|
sleep "$settleSeconds"
|
|
|
|
win=$(joeyFindWindow "$display" "$dosbox")
|
|
if [ -z "$win" ]; then
|
|
echo "$label: FAIL - no DOSBox window to send keys to" >&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=$!
|
|
|
|
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 (the DOS gate's schedule).
|
|
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
|
|
|
|
# parec writes the WAV header lazily; it must exit before the file parses.
|
|
kill "$parecPid" 2> /dev/null
|
|
wait "$parecPid" 2> /dev/null
|
|
parecPid=""
|
|
dvxDvxLog "$work/c" 200 | grep -E "^Audio:" | tail -3
|
|
|
|
[ -s "$work/out.wav" ] || { echo "$label: FAIL - recorded no audio at all" >&2; exit 1; }
|
|
|
|
# Measured under DOSBox Staging 0.83 (SB16, DVX mixing at 22,050 Hz): music
|
|
# ~1260, paused 0, sfx ~670, resumed ~1245 -- the DOS port's levels, near
|
|
# enough, so the DOS gate's bounds (about half of each) apply unchanged.
|
|
python3 "$grader" "$label" "$work/out.wav" \
|
|
"music:1:5:600:" \
|
|
"paused:8:13::200" \
|
|
"sfx:16:22:330:" \
|
|
"resumed:27:32:600:"
|