151 lines
5.4 KiB
Bash
Executable file
151 lines
5.4 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-win32-audio.sh - Win32 audio gate, graded on recorded sound.
|
|
#
|
|
# The port mixes in software (src/hosted/audio.c) and plays through waveOut.
|
|
# Under Wine that reaches PulseAudio, and PULSE_SINK sends this one run to a
|
|
# null sink made for it (scripts/pulseRecordLib.sh), so the user's
|
|
# own sound is never touched; parec records the sink's monitor and
|
|
# tools/gradeAudioPhases.py grades it through the same six phases as the
|
|
# SDL3 gate: music, paused, SFX into the pause, resumed, the noise sweep
|
|
# alone, the MOD. Wine's sound starts some seconds after the program does
|
|
# (about 8 here, in its driver's start-up, before the first waveOut buffer
|
|
# plays), so the clock starts when the song is first heard: the gate records
|
|
# from the moment AUDIO's window is up, waits for the first sound, runs the
|
|
# keys (held 0.4 s: AUDIO reads jlKeyPressed once a frame) from there, and
|
|
# grades the recording from that first sound on.
|
|
#
|
|
# Usage: scripts/verify-win32-audio.sh
|
|
# Env: WIN32_AUDIO_KEEP set to keep the recording as
|
|
# build/win32/headless-audio.wav
|
|
# Requires: Wine, Xvfb, xdotool, parec/pactl, python3;
|
|
# build/win32/bin/AUDIO.EXE (make win32).
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
source "$repo/scripts/lib/wineGate.sh"
|
|
source "$repo/scripts/pulseRecordLib.sh"
|
|
wineNeed
|
|
grader=$repo/tools/gradeAudioPhases.py
|
|
label=verify-win32-audio
|
|
|
|
program=$(joeyExampleProgram audio win32)
|
|
[ -f "$program" ] || { echo "$label: build AUDIO.EXE first (make win32)" >&2; exit 2; }
|
|
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; }
|
|
|
|
work=$(mktemp -d -t "joeylib-win32-audio.XXXXXX")
|
|
sinkModule=""
|
|
parecPid=""
|
|
cleanup() {
|
|
[ -n "$parecPid" ] && kill "$parecPid" 2> /dev/null
|
|
wineStop
|
|
pulseUnloadSink "$sinkModule"
|
|
rm -rf "$work"
|
|
return 0
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
sinkName="joeyWin32Gate$$"
|
|
RECORD_LATENCY_MS=250
|
|
sinkModule=$(pulseCreateSink "$sinkName") || sinkModule=""
|
|
[ -n "$sinkModule" ] || { echo "$label: could not create a null sink to record from" >&2; exit 2; }
|
|
|
|
wineStart "$work"
|
|
PULSE_SINK="$sinkName" wineRun "$program"
|
|
win=$(wineWindow AUDIO) || { echo "$label: FAIL - no AUDIO window" >&2; exit 1; }
|
|
xdotool windowfocus --sync "$win"
|
|
# A short record latency: the first sound must reach the file when it is
|
|
# played, as the clock starts there. Left to choose, the server can hand the
|
|
# recording over in seconds-long fragments, and every key lands seconds late
|
|
# on the timeline.
|
|
parec --latency-msec="$RECORD_LATENCY_MS" --device="$sinkName.monitor" --format=s16le --rate=44100 --channels=2 \
|
|
> "$work/out.raw" 2> "$work/parec.log" &
|
|
parecPid=$!
|
|
|
|
# The byte offset of the first sound in the recording so far, or nothing.
|
|
firstSound() {
|
|
python3 - "$work/out.raw" "$SOUND_THRESHOLD" <<'PY'
|
|
import array
|
|
import sys
|
|
|
|
data = open(sys.argv[1], "rb").read()
|
|
samples = array.array("h", data[: len(data) - len(data) % 4])
|
|
for i, v in enumerate(samples):
|
|
if abs(v) > int(sys.argv[2]):
|
|
print((i // 2) * 4)
|
|
break
|
|
PY
|
|
}
|
|
|
|
SOUND_THRESHOLD=256
|
|
SOUND_WAIT_SECONDS=30
|
|
start=""
|
|
for ((waited = 0; waited < SOUND_WAIT_SECONDS * 4; waited++)); do
|
|
start=$(firstSound)
|
|
[ -n "$start" ] && break
|
|
sleep 0.25
|
|
done
|
|
[ -n "$start" ] || { echo "$label: FAIL - no sound within ${SOUND_WAIT_SECONDS}s" >&2; exit 1; }
|
|
|
|
hold() { # $1 = key name
|
|
xdotool keydown "$1"
|
|
sleep 0.4
|
|
xdotool keyup "$1"
|
|
}
|
|
|
|
# The same timeline as verify-sdl3-audio.sh (seconds since the first sound).
|
|
sleep 5 # 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 5.6 # 24-30s resumed
|
|
hold p # -> paused again
|
|
sleep 0.6
|
|
hold n # 31-35s noise sweep alone
|
|
sleep 4.6
|
|
hold m # 36-44s the MOD
|
|
sleep 7.6
|
|
|
|
kill "$parecPid" 2> /dev/null
|
|
wait "$parecPid" 2> /dev/null
|
|
parecPid=""
|
|
# From the first sound on, as a WAV.
|
|
python3 - "$work/out.raw" "$start" "$work/out.wav" <<'PY'
|
|
import sys
|
|
import wave
|
|
|
|
data = open(sys.argv[1], "rb").read()[int(sys.argv[2]):]
|
|
with wave.open(sys.argv[3], "wb") as w:
|
|
w.setnchannels(2)
|
|
w.setsampwidth(2)
|
|
w.setframerate(44100)
|
|
w.writeframes(data)
|
|
PY
|
|
|
|
# Thresholds sit at about half the measured levels (music ~3100, paused 0,
|
|
# sfx ~1240-1520, resumed ~2770-3060, noise ~1220-1620, mod ~7390-7980, the
|
|
# grader averaging the two channels).
|
|
python3 "$grader" "$label" "$work/out.wav" \
|
|
"music:1:5:1500:" \
|
|
"paused:8:13::100" \
|
|
"sfx:15:22:700:" \
|
|
"resumed:25:29:1400:" \
|
|
"noise:31.5:34:650:" \
|
|
"mod:37:43:3700:"
|
|
status=$?
|
|
if [ -n "${WIN32_AUDIO_KEEP:-}" ]; then
|
|
cp "$work/out.wav" "$repo/build/win32/headless-audio.wav"
|
|
fi
|
|
exit $status
|