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

7d3860e56f3efcedcaee992a56bad6152cd5c297
[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;
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