]> 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:

Use systemd-inhibit while recording, if available
[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 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
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 case "$index" in
95
96   (last)
97     index=$(pactl list short sink-inputs | sed -rne '$s,([[:digit:]]+)[[:space:]].*,\1,p')
98     ;;
99
100   (*[^0-9]*)
101     echo >&2 "E: non-numeric index: $index"
102     exit 1
103     ;;
104
105   (*[0-9]*) :;;
106
107   (*)
108     echo >&2 "Listening for pulseaudio event… "
109     index=$(pa_get_next_index)
110     ;;
111
112 esac
113
114 if [ -z "$index" ]; then
115
116   echo >&2 E: no index specified or discernable.
117   exit 1
118 fi
119
120 uuidgen() {
121   hascmd() { command -v "$@" >/dev/null;}
122   if   hascmd uuid; then uuid
123   elif hascmd uuidgen; then uuidgen
124   elif hascmd python3; then
125     python3 -c 'import uuid; print(uuid.uuid1())'
126   elif hascmd python; then
127     python -c 'import uuid; print(uuid.uuid1())'
128   else
129     dd if=/dev/urandom bs=16 count=1 status=none | base64
130   fi
131 }
132
133 uuid=$(uuidgen)
134
135 [ -n "$outfile" ] || outfile="${uuid}.ogg"
136
137 if [ -f "$outfile" ] && [ $clobber -eq 0 ]; then
138   echo >&2 "E: file exists, and -f not given: $outfile"
139   exit 1
140 fi
141
142 devname="record-to-file-${uuid}"
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;
156   c=$(pactl list short sink-inputs | sed -rne "s,^${1}[[:space:]]+([[:digit:]]+).+,\1,p")
157   cleanup_hook pactl move-sink-input $1 $c
158   pactl move-sink-input $1 $2
159 }
160
161 if false; then
162   # This would be great, but it does not work. For instance, trying this on
163   # audible meant that audible would play at maximum speed (3h played in 3
164   # minutes), but the result would be full of skips, making me think that
165   # something in the local pipeline can't handle the throughput. Rate-limiting
166   # might work. Have not had the time to investigate further.
167
168   load_module module-pipe-sink "$devname" "$devname" file="$TMPDIR/outfifo"
169   move_source_to_sink $index "$devname"
170   oggenc --raw -q5 -o "$outfile" "$TMPDIR/outfifo" || :
171   #pv -pterb "$TMPDIR/outfifo" > $TMPDIR/outfile.wav || :
172
173 else
174   # More traditional approach, which just takes 1:1 time.
175
176   load_module module-null-sink "$devname" "$devname"
177   move_source_to_sink $index "$devname"
178   parec --format=s16le -d "${devname}.monitor" 2>/dev/null \
179     | oggenc --raw -q5 -o "$outfile" - &
180   pid=$!
181
182   pa_wait_for_event sink-input remove $index >/dev/null
183
184   kill $pid
185 fi
186
187 cleanup