#!/bin/sh # # pulserecorder — record a source via Pulse # # Synopsis: # pulserecorder -i [-o filename.ogg] [-f] # pulserecorder -l [-o filename.ogg] [-f] # pulserecorder [-o filename.ogg] [-f] # # The first form records sound from the sink-input with index to the # filename specified, or one generated using a UUID. # # The second form records sound from the sink-input with the highest index, # i.e. the latest one to start playing. # # The third form waits for a new sink-input to appear, and will record that. # # Copyright © 2020 martin f. krafft # Released under the teams of the Artistic Licence 2.0 # set -eu cleanup_commands= cleanup() { set +e eval $cleanup_commands 2>/dev/null trap - 1 2 3 4 5 6 7 8 10 11 12 13 14 15 } trap cleanup 1 2 3 4 5 6 7 8 10 11 12 13 14 15 cleanup_hook() { cleanup_commands="$@${cleanup_commands:+; $cleanup_commands}" } if [ -z "${TMPDIR:-}" ]; then TMPDIR=/tmp fi for i in $LOGNAME volatile; do if [ -d "${TMPDIR}/$i" ]; then TMPDIR="${TMPDIR}/$i" break fi done export TMPDIR TMPDIR=$(mktemp -dp "$TMPDIR" parec.XXXXXXXXXX) cleanup_hook rm -r $TMPDIR state= outfile= index= clobber=0 for arg in "$@"; do case "$state/$arg" in (/-i) state=i;; (i/*) index="$arg"; state=;; (/-l) index="last";; (/-o) state=o;; (o/*) outfile="$arg"; state=;; (/-f) clobber=1;; esac done pa_subscribe() { ( pactl subscribe 2>/dev/null & echo $! ) } pa_wait_for_event() { local type event id; type="${1:-*}" event="${2:-*}" id="${3:-*}" pa_subscribe | ( read pid #echo >&2 pa_subscribe started with PID $pid while read lead xevent on xtype xid; do xid=${xid#\#} #echo >&2 "pa_subscribe: $xtype/$xevent/$xid ($type/$event/$id)" case "$xtype/$xevent/$xid" in ($type/"'$event'"/$id) echo "$xtype $xevent $xid"; break;; (*) :;; esac done kill $pid ) } pa_get_next_index() { pa_wait_for_event sink-input new | { read type event id echo $id } } case "$index" in (last) index=$(pactl list short sink-inputs | sed -rne '$s,([[:digit:]]+)[[:space:]].*,\1,p') ;; (*[^0-9]*) echo >&2 "E: non-numeric index: $index" exit 1 ;; (*[0-9]*) :;; (*) echo >&2 "Listening for pulseaudio event… " index=$(pa_get_next_index) ;; esac if [ -z "$index" ]; then echo >&2 E: no index specified or discernable. exit 1 fi uuidgen() { hascmd() { command -v "$@" >/dev/null;} if hascmd uuid; then uuid elif hascmd uuidgen; then uuidgen elif hascmd python3; then python3 -c 'import uuid; print(uuid.uuid1())' elif hascmd python; then python -c 'import uuid; print(uuid.uuid1())' else dd if=/dev/urandom bs=16 count=1 status=none | base64 fi } uuid=$(uuidgen) [ -n "$outfile" ] || outfile="${uuid}.ogg" if [ -f "$outfile" ] && [ $clobber -eq 0 ]; then echo >&2 "E: file exists, and -f not given: $outfile" exit 1 fi devname="record-to-file-${uuid}" echo >&2 "Recording source $index to $outfile …" load_module() { local id mod sink desc mod="$1" sink="$2" desc="$3"; shift 3 id=$(pactl load-module "$mod" sink_name="$sink" "$@") cleanup_hook pactl unload-module $id pacmd update-sink-proplist "$sink" device.description="$desc" } move_source_to_sink() { local c; c=$(pactl list short sink-inputs | sed -rne "s,^${1}[[:space:]]+([[:digit:]]+).+,\1,p") cleanup_hook pactl move-sink-input $1 $c pactl move-sink-input $1 $2 } if false; then # This would be great, but it does not work. For instance, trying this on # audible meant that audible would play at maximum speed (3h played in 3 # minutes), but the result would be full of skips, making me think that # something in the local pipeline can't handle the throughput. Rate-limiting # might work. Have not had the time to investigate further. load_module module-pipe-sink "$devname" "$devname" file="$TMPDIR/outfifo" move_source_to_sink $index "$devname" oggenc --raw -q5 -o "$outfile" "$TMPDIR/outfifo" || : #pv -pterb "$TMPDIR/outfifo" > $TMPDIR/outfile.wav || : else # More traditional approach, which just takes 1:1 time. load_module module-null-sink "$devname" "$devname" move_source_to_sink $index "$devname" parec --format=s16le -d "${devname}.monitor" 2>/dev/null \ | oggenc --raw -q5 -o "$outfile" - & pid=$! pa_wait_for_event sink-input remove $index >/dev/null kill $pid fi cleanup