Files
Sylpheed/tools/audio-capture
MechaCat02 7e643c242c audio: actually install the capture path I kept deferring
The audio work was three parts and I shipped two. The transcode-fidelity method
and the pinned 5.1 downmix landed; the null sink -- the only one that answers
"what does the GAME play" -- I deferred to "the next natural rebuild window" and
then rebuilt both images four times without doing it.

pulseaudio-utils is now in both, with tools/audio-capture wrapping it: a null
sink is a real device as far as an application is concerned, so Canary and Godot
open it normally and parec records what they emit.

This unblocks the decoder's Q8. The cue-to-event bindings are currently a name
match against the authors' own identifiers -- a plausible guess, not a
measurement -- and capturing what the game plays on a menu move converts them.

`audio-capture run` reports the peak level and warns when the capture is silent,
because silence is the failure that looks like success: a WAV of exactly the
right duration, full of zeroes, because the application opened a different sink.
A duration check alone passes it, which is how a confident wrong number gets
made.
2026-08-29 13:41:02 +02:00

65 lines
2.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Record what an application plays, with no sound card present.
#
# audio-capture start bring up a null sink named `cap`
# audio-capture record OUT.wav & record from it until killed
# audio-capture run OUT.wav -- CMD… start, run CMD, stop, leave OUT.wav
#
# A null sink is a real device as far as the application is concerned: the
# emulator and Godot open it exactly as they would hardware, and its monitor
# source is what `parec` reads. This is the difference between "the file decodes"
# and "the game played this" -- the second is a measurement, the first is not.
#
# Nothing here says the audio SOUNDS right; it says what was emitted. A human
# listening still answers something none of this does.
set -euo pipefail
SINK="${SYLPH_SINK:-cap}"
ensure_daemon() {
pulseaudio --check 2>/dev/null || pulseaudio --start --exit-idle-time=-1 2>/dev/null || true
for _ in $(seq 30); do pactl info >/dev/null 2>&1 && return 0; sleep 0.2; done
echo "audio-capture: no PulseAudio daemon" >&2; return 1
}
case "${1:-}" in
start)
ensure_daemon
pactl list short sinks | grep -q "^[0-9]*[[:space:]]*$SINK[[:space:]]" \
|| pactl load-module module-null-sink sink_name="$SINK" \
sink_properties=device.description="$SINK" >/dev/null
pactl set-default-sink "$SINK"
echo "audio-capture: sink '$SINK' ready; point apps at it with PULSE_SINK=$SINK"
;;
record)
out="${2:?usage: audio-capture record OUT.wav}"
ensure_daemon
exec parec -d "${SINK}.monitor" --file-format=wav "$out"
;;
run)
out="${2:?usage: audio-capture run OUT.wav -- COMMAND...}"; shift 2
[ "${1:-}" = "--" ] && shift
"$0" start
parec -d "${SINK}.monitor" --file-format=wav "$out" & rec=$!
# Kill the recorder on any exit path, or a failed run leaves it holding the
# monitor and the next capture silently records nothing.
trap 'kill "$rec" 2>/dev/null || true' EXIT
PULSE_SINK="$SINK" "$@" || true
sleep 1; kill "$rec" 2>/dev/null || true; wait "$rec" 2>/dev/null || true
trap - EXIT
if [ -s "$out" ]; then
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$out" 2>/dev/null || echo "?")
echo "audio-capture: $out (${dur}s)"
# Silence is the failure that looks like success: a WAV of the right
# length, full of zeroes, because the app opened a different sink.
peak=$(ffmpeg -hide_banner -i "$out" -af astats=measure_perchannel=none -f null - 2>&1 \
| grep -m1 "Peak level" || true)
echo " ${peak:-no level measured}"
case "$peak" in *"-inf"*) echo " WARNING: silent -- did the app use this sink?" ;; esac
else
echo "audio-capture: nothing recorded" >&2; exit 1
fi
;;
*) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' ;;
esac