]> git.madduck.net Git - code/pulserecorder.git/blob - pulserecorder

madduck's git repository

Every one of the projects in this repository is available at the canonical URL git://git.madduck.net/madduck/pub/<projectpath> — see each project's metadata for the exact URL.

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.

SSH access, as well as push access can be individually arranged.

If you use my repositories frequently, consider adding the following snippet to ~/.gitconfig and using the third clone URL listed for each project:

[url "git://git.madduck.net/madduck/"]
  insteadOf = madduck:

ab0d471901c7fca54bc06c7d44d92c0bb6cdbc24
[code/pulserecorder.git] / pulserecorder
1 #!/bin/sh
2 #
3 # pulserecorder — record a source via Pulse
4 #
5 # Synopsis:
6 #   pulserecorder -i <index> [-o filename.ogg] [-f]
7 #   pulserecorder -l         [-o filename.ogg] [-f]
8 #   pulserecorder            [-o filename.ogg] [-f]
9 #
10 # The first form records sound from the sink-input with index <index> to the
11 # filename specified, or one generated using a UUID.
12 #
13 # The second form records sound from the sink-input with the highest index,
14 # i.e. the latest one to start playing.
15 #
16 # The third form waits for a new sink-input to appear, and will record that.
17 #
18 # Copyright © 2020–2021 martin f. krafft <madduck@madduck.net>
19 # Released under the teams of the Artistic Licence 2.0
20 #
21 set -eu
22
23 cleanup_commands=
24 cleanup() {
25   set +e
26   eval $cleanup_commands 2>/dev/null
27   trap - 1 2 3 4 5 6 7 8 10 11 12 13 14 15
28 }
29 trap cleanup 1 2 3 4 5 6 7 8 10 11 12 13 14 15
30 cleanup_hook() {
31   cleanup_commands="$@${cleanup_commands:+; $cleanup_commands}"
32 }
33
34 if [ -z "${TMPDIR:-}" ]; then
35   TMPDIR=/tmp
36 fi
37 for i in $LOGNAME volatile; do
38   if [ -d "${TMPDIR}/$i" ]; then
39     TMPDIR="${TMPDIR}/$i"
40     break
41   fi
42 done
43 export TMPDIR
44 TMPDIR=$(mktemp -dp "$TMPDIR" parec.XXXXXXXXXX)
45 cleanup_hook rm -r $TMPDIR
46
47 if command -v systemd-inhibit >/dev/null; then
48   systemd-inhibit --who=pulserecorder --why=recording sleep 99d 2>/dev/null &
49   cleanup_hook kill $!
50 fi
51
52 state= outfile= index= clobber=0
53 for arg in "$@"; do
54   case "$state/$arg" in
55     (/-i) state=i;;
56     (i/*) index="$arg"; state=;;
57
58     (/-l) index="last";;
59
60     (/-o) state=o;;
61     (o/*) outfile="$arg"; state=;;
62
63     (/-f) clobber=1;;
64   esac
65 done
66
67 pa_subscribe() {
68   ( pactl subscribe 2>/dev/null & echo $! )
69 }
70 pa_wait_for_event() {
71   local type event id;
72   type="${1:-*}" event="${2:-*}" id="${3:-*}"
73   pa_subscribe | (
74     read pid
75     #echo >&2 pa_subscribe started with PID $pid, waiting for $type/$event/$id
76     while read lead xevent on xtype xid; do
77       xid=${xid#\#}
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;;
81         (*) :;;
82       esac
83     done
84     kill $pid
85   )
86 }
87 pa_get_next_index() {
88   pa_wait_for_event sink-input new | {
89     read type event id
90     echo $id
91   }
92 }
93
94 uuidgen() {
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())'
102   else
103     dd if=/dev/urandom bs=16 count=1 status=none | base64
104   fi
105 }
106
107 if [ -z "$outfile" ]; then
108   uuid=$(uuidgen)
109   outfile="${uuid}.ogg"
110 fi
111
112 if [ -f "$outfile" ] && [ $clobber -eq 0 ]; then
113   echo >&2 "E: file exists, and -f not given: $outfile"
114   exit 1
115 fi
116 case "$index" in
117
118   (last)
119     index=$(pactl list short sink-inputs | sed -rne '$s,([[:digit:]]+)[[:space:]].*,\1,p')
120     ;;
121
122   (*[^0-9]*)
123     echo >&2 "E: non-numeric index: $index"
124     exit 1
125     ;;
126
127   (*[0-9]*) :;;
128
129   (*)
130     echo >&2 "Listening for pulseaudio event… "
131     index=$(pa_get_next_index)
132     ;;
133
134 esac
135
136 if [ -z "$index" ]; then
137
138   echo >&2 E: no index specified or discernable.
139   exit 1
140 fi
141
142 devname="record-to-file-$(echo -n ${outfile%.*} | tr -c '[:alnum:]' '_')"
143
144 echo >&2 "Recording source $index to $outfile …"
145
146 load_module() {
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"
152 }
153
154 move_source_to_sink() {
155   local c d;
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")"
158   case "$d" in
159     (record-to-file-*)
160       # Never restore to a record-to-file destination, or it could botch
161       # another recording
162       c="@DEFAULT_SINK@"
163       ;;
164     (*) :;;
165   esac
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
169 }
170
171 if [ -n "${PAREC_PIPE:-}" ]; then
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.
177
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 || :
182
183 else
184   # More traditional approach, which just takes 1:1 time.
185
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" - &
190   pid=$!
191
192   pa_wait_for_event sink-input remove $index >/dev/null
193
194   kill $pid
195 fi
196
197 cleanup