+#!/bin/sh
+#
+# pulserecorder — record a source via Pulse
+#
+# Synopsis:
+# pulserecorder -i <index> [-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 <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 <madduck@madduck.net>
+# Released under the teams of the Artistic Licence 2.0
+#
+set -eu
+
+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_commands="rm -r $TMPDIR"
+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}"
+}
+
+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