#!/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 # tests/gate/audioGate.c, 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, 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=$(joeyTool audioGate) || exit 1 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; 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), after # the 2026-10-16 mix: music ~3030, paused 0, sfx ~170, resumed ~3040; the # bounds are about half of each. "$grader" phases "$label" "$work/out.wav" \ "music:1:5:1500:" \ "paused:8:13::200" \ "sfx:16:22:80:" \ "resumed:27:32:1500:"