All patches and comments are welcome. Please squash your changes to logical
commits before using git-format-patch and git-send-email to
patches@git.madduck.net.
If you'd read over the Git project's submission guidelines and adhered to them,
I'd be especially grateful.
   3 # pulserecorder — record a source via Pulse
 
   6 #   pulserecorder -i <index> [-o filename.ogg] [-f]
 
   7 #   pulserecorder -l         [-o filename.ogg] [-f]
 
   8 #   pulserecorder            [-o filename.ogg] [-f]
 
  10 # The first form records sound from the sink-input with index <index> to the
 
  11 # filename specified, or one generated using a UUID.
 
  13 # The second form records sound from the sink-input with the highest index,
 
  14 # i.e. the latest one to start playing.
 
  16 # The third form waits for a new sink-input to appear, and will record that.
 
  18 # Copyright © 2020–2021 martin f. krafft <madduck@madduck.net>
 
  19 # Released under the teams of the Artistic Licence 2.0
 
  26   eval $cleanup_commands 2>/dev/null
 
  27   trap - 1 2 3 4 5 6 7 8 10 11 12 13 14 15
 
  29 trap cleanup 1 2 3 4 5 6 7 8 10 11 12 13 14 15
 
  31   cleanup_commands="$@${cleanup_commands:+; $cleanup_commands}"
 
  34 if [ -z "${TMPDIR:-}" ]; then
 
  37 for i in $LOGNAME volatile; do
 
  38   if [ -d "${TMPDIR}/$i" ]; then
 
  44 TMPDIR=$(mktemp -dp "$TMPDIR" parec.XXXXXXXXXX)
 
  45 cleanup_hook rm -r $TMPDIR
 
  47 if command -v systemd-inhibit >/dev/null; then
 
  48   systemd-inhibit --who=pulserecorder --why=recording sleep 99d 2>/dev/null &
 
  52 state= outfile= index= clobber=0
 
  56     (i/*) index="$arg"; state=;;
 
  61     (o/*) outfile="$arg"; state=;;
 
  68   ( pactl subscribe 2>/dev/null & echo $! )
 
  72   type="${1:-*}" event="${2:-*}" id="${3:-*}"
 
  75     #echo >&2 pa_subscribe started with PID $pid, waiting for $type/$event/$id
 
  76     while read lead xevent on xtype xid; do
 
  78       #echo >&2 "pa_subscribe: $xtype/$xevent/$xid ($type/$event/$id)"
 
  79       case "$xtype/$xevent/$xid" in
 
  80         ($type/"'$event'"/$id) echo "$xtype $xevent $xid"; break;;
 
  88   pa_wait_for_event sink-input new | {
 
  95   hascmd() { command -v "$@" >/dev/null;}
 
  96   if   hascmd uuid; then uuid
 
  97   elif hascmd uuidgen; then uuidgen
 
  98   elif hascmd python3; then
 
  99     python3 -c 'import uuid; print(uuid.uuid1())'
 
 100   elif hascmd python; then
 
 101     python -c 'import uuid; print(uuid.uuid1())'
 
 103     dd if=/dev/urandom bs=16 count=1 status=none | base64
 
 107 if [ -z "$outfile" ]; then
 
 109   outfile="${uuid}.ogg"
 
 112 if [ -f "$outfile" ] && [ $clobber -eq 0 ]; then
 
 113   echo >&2 "E: file exists, and -f not given: $outfile"
 
 119     index=$(pactl list short sink-inputs | sed -rne '$s,([[:digit:]]+)[[:space:]].*,\1,p')
 
 123     echo >&2 "E: non-numeric index: $index"
 
 130     echo >&2 "Listening for pulseaudio event… "
 
 131     index=$(pa_get_next_index)
 
 136 if [ -z "$index" ]; then
 
 138   echo >&2 E: no index specified or discernable.
 
 142 devname="record-to-file-$(echo -n ${outfile%.*} | tr -c '[:alnum:]' '_')"
 
 144 echo >&2 "Recording source $index to $outfile …"
 
 147   local id mod sink desc
 
 148   mod="$1" sink="$2" desc="$3"; shift 3
 
 149   id=$(pactl load-module "$mod" sink_name="$sink" "$@")
 
 150   cleanup_hook pactl unload-module $id
 
 151   pacmd update-sink-proplist "$sink" device.description="$desc"
 
 154 move_source_to_sink() {
 
 156   c=$(pactl list short sink-inputs | sed -rne "s,^${1}[[:space:]]+([[:digit:]]+).+,\1,p")
 
 157   d="$(pactl list short sinks | sed -rne "s,^${c}[[:space:]]+([^[:space:]]+).+,\1,p")"
 
 160       # Never restore to a record-to-file destination, or it could botch
 
 166   echo >&2 "Moving input $1 to sink $2 (restore to $c) …"
 
 167   cleanup_hook pactl move-sink-input $1 $c
 
 168   pactl move-sink-input $1 $2
 
 172   # This would be great, but it does not work. For instance, trying this on
 
 173   # audible meant that audible would play at maximum speed (3h played in 3
 
 174   # minutes), but the result would be full of skips, making me think that
 
 175   # something in the local pipeline can't handle the throughput. Rate-limiting
 
 176   # might work. Have not had the time to investigate further.
 
 178   load_module module-pipe-sink "$devname" "$devname" file="$TMPDIR/outfifo"
 
 179   move_source_to_sink $index "$devname"
 
 180   oggenc --raw -q5 -o "$outfile" "$TMPDIR/outfifo" || :
 
 181   #pv -pterb "$TMPDIR/outfifo" > $TMPDIR/outfile.wav || :
 
 184   # More traditional approach, which just takes 1:1 time.
 
 186   load_module module-null-sink "$devname" "$devname"
 
 187   move_source_to_sink $index "$devname"
 
 188   parec --format=s16le -d "${devname}.monitor" 2>/dev/null \
 
 189     | oggenc --raw -q5 -o "$outfile" - &
 
 192   pa_wait_for_event sink-input remove $index >/dev/null