#!/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