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