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