From: martin f. krafft Date: Tue, 7 Apr 2020 20:44:50 +0000 (+1200) Subject: initial checkin X-Git-Url: https://git.madduck.net/code/pulserecorder.git/commitdiff_plain/fb1414bb84493438c4404c07172bb15da1d96a7c initial checkin --- fb1414bb84493438c4404c07172bb15da1d96a7c diff --git a/pulserecorder b/pulserecorder new file mode 100755 index 0000000..65a40ce --- /dev/null +++ b/pulserecorder @@ -0,0 +1,181 @@ +#!/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 + +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