From fb1414bb84493438c4404c07172bb15da1d96a7c Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Wed, 8 Apr 2020 08:44:50 +1200 Subject: [PATCH 1/9] initial checkin --- pulserecorder | 181 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100755 pulserecorder 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 -- 2.39.2 From 7cd55a11575980fba5232c781b41fab469d00ec7 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:12:58 +1200 Subject: [PATCH 2/9] move cleanup to earlier in script Signed-off-by: martin f. krafft --- pulserecorder | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pulserecorder b/pulserecorder index 65a40ce..a9f4cdc 100755 --- a/pulserecorder +++ b/pulserecorder @@ -20,6 +20,17 @@ # 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 @@ -31,17 +42,8 @@ for i in $LOGNAME volatile; do done export TMPDIR TMPDIR=$(mktemp -dp "$TMPDIR" parec.XXXXXXXXXX) +cleanup_hook rm -r $TMPDIR -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 -- 2.39.2 From 685f85c0dadd0b1f91f17ea117ad34655cce4900 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:19:46 +1200 Subject: [PATCH 3/9] Use systemd-inhibit while recording, if available --- pulserecorder | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pulserecorder b/pulserecorder index a9f4cdc..8ad87c8 100755 --- a/pulserecorder +++ b/pulserecorder @@ -44,6 +44,10 @@ export TMPDIR TMPDIR=$(mktemp -dp "$TMPDIR" parec.XXXXXXXXXX) cleanup_hook rm -r $TMPDIR +if command -v systemd-inhibit >/dev/null; then + systemd-inhibit --who=pulserecorder --why=recording sleep 99d 2>/dev/null & + cleanup_hook kill $! +fi state= outfile= index= clobber=0 for arg in "$@"; do -- 2.39.2 From 537d0106ab8f4562db5d70a6103077d68f1db16e Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:20:44 +1200 Subject: [PATCH 4/9] More info in debug output --- pulserecorder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulserecorder b/pulserecorder index 8ad87c8..5eb3bc6 100755 --- a/pulserecorder +++ b/pulserecorder @@ -72,7 +72,7 @@ pa_wait_for_event() { type="${1:-*}" event="${2:-*}" id="${3:-*}" pa_subscribe | ( read pid - #echo >&2 pa_subscribe started with PID $pid + #echo >&2 pa_subscribe started with PID $pid, waiting for $type/$event/$id while read lead xevent on xtype xid; do xid=${xid#\#} #echo >&2 "pa_subscribe: $xtype/$xevent/$xid ($type/$event/$id)" -- 2.39.2 From 0419e8fbc114f36a087acd8ea6de65cbfcfec788 Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:21:37 +1200 Subject: [PATCH 5/9] Copyright year update --- pulserecorder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulserecorder b/pulserecorder index 5eb3bc6..211c6d5 100755 --- a/pulserecorder +++ b/pulserecorder @@ -15,7 +15,7 @@ # # The third form waits for a new sink-input to appear, and will record that. # -# Copyright © 2020 martin f. krafft +# Copyright © 2020–2021 martin f. krafft # Released under the teams of the Artistic Licence 2.0 # set -eu -- 2.39.2 From 92376471ab255fa21a70a02cb16e2436c8bd058e Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:22:23 +1200 Subject: [PATCH 6/9] Use filename instead of UUID for sink name --- pulserecorder | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/pulserecorder b/pulserecorder index 211c6d5..7d3860e 100755 --- a/pulserecorder +++ b/pulserecorder @@ -91,6 +91,28 @@ pa_get_next_index() { } } +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 +} + +if [ -z "$outfile" ]; then + uuid=$(uuidgen) + outfile="${uuid}.ogg" +fi + +if [ -f "$outfile" ] && [ $clobber -eq 0 ]; then + echo >&2 "E: file exists, and -f not given: $outfile" + exit 1 +fi case "$index" in (last) @@ -117,29 +139,7 @@ if [ -z "$index" ]; then 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}" +devname="record-to-file-$(echo -n ${outfile%.*} | tr -c '[:alnum:]' '_')" echo >&2 "Recording source $index to $outfile …" -- 2.39.2 From ca51df8a58054f318bf7fcd8e467892e1e35262e Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:23:03 +1200 Subject: [PATCH 7/9] Ensure that restore never intrudes on another recording --- pulserecorder | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pulserecorder b/pulserecorder index 7d3860e..58fdec7 100755 --- a/pulserecorder +++ b/pulserecorder @@ -152,8 +152,18 @@ load_module() { } move_source_to_sink() { - local c; + local c d; c=$(pactl list short sink-inputs | sed -rne "s,^${1}[[:space:]]+([[:digit:]]+).+,\1,p") + d="$(pactl list short sinks | sed -rne "s,^${c}[[:space:]]+([^[:space:]]+).+,\1,p")" + case "$d" in + (record-to-file-*) + # Never restore to a record-to-file destination, or it could botch + # another recording + c="@DEFAULT_SINK@" + ;; + (*) :;; + esac + echo >&2 "Moving input $1 to sink $2 (restore to $c) …" cleanup_hook pactl move-sink-input $1 $c pactl move-sink-input $1 $2 } -- 2.39.2 From 015c7497293aa96ee5f389090c3b9645f199efaf Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:23:17 +1200 Subject: [PATCH 8/9] Enable env access to pipe recording --- pulserecorder | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pulserecorder b/pulserecorder index 58fdec7..ab0d471 100755 --- a/pulserecorder +++ b/pulserecorder @@ -168,7 +168,7 @@ move_source_to_sink() { pactl move-sink-input $1 $2 } -if false; then +if [ -n "${PAREC_PIPE:-}" ]; 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 -- 2.39.2 From 4854b887e565b8cab682819affd25fdb9d8f45de Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Mon, 20 Sep 2021 09:24:04 +1200 Subject: [PATCH 9/9] Pipe recording experiments --- pulserecorder | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pulserecorder b/pulserecorder index ab0d471..5dad254 100755 --- a/pulserecorder +++ b/pulserecorder @@ -177,8 +177,11 @@ if [ -n "${PAREC_PIPE:-}" ]; then 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 || : + #oggenc --raw -q5 -o "$outfile" "$TMPDIR/outfifo" || : + pv -pterb "$TMPDIR/outfifo" > $TMPDIR/outfile.wav & + pid=$! + pa_wait_for_event sink-input remove $index >/dev/null + kill $pid else # More traditional approach, which just takes 1:1 time. -- 2.39.2