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

move cleanup to earlier in script
[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
48 state= outfile= index= clobber=0
49 for arg in "$@"; do
50   case "$state/$arg" in
51     (/-i) state=i;;
52     (i/*) index="$arg"; state=;;
53
54     (/-l) index="last";;
55
56     (/-o) state=o;;
57     (o/*) outfile="$arg"; state=;;
58
59     (/-f) clobber=1;;
60   esac
61 done
62
63 pa_subscribe() {
64   ( pactl subscribe 2>/dev/null & echo $! )
65 }
66 pa_wait_for_event() {
67   local type event id;
68   type="${1:-*}" event="${2:-*}" id="${3:-*}"
69   pa_subscribe | (
70     read pid
71     #echo >&2 pa_subscribe started with PID $pid
72     while read lead xevent on xtype xid; do
73       xid=${xid#\#}
74       #echo >&2 "pa_subscribe: $xtype/$xevent/$xid ($type/$event/$id)"
75       case "$xtype/$xevent/$xid" in
76         ($type/"'$event'"/$id) echo "$xtype $xevent $xid"; break;;
77         (*) :;;
78       esac
79     done
80     kill $pid
81   )
82 }
83 pa_get_next_index() {
84   pa_wait_for_event sink-input new | {
85     read type event id
86     echo $id
87   }
88 }
89
90 case "$index" in
91
92   (last)
93     index=$(pactl list short sink-inputs | sed -rne '$s,([[:digit:]]+)[[:space:]].*,\1,p')
94     ;;
95
96   (*[^0-9]*)
97     echo >&2 "E: non-numeric index: $index"
98     exit 1
99     ;;
100
101   (*[0-9]*) :;;
102
103   (*)
104     echo >&2 "Listening for pulseaudio event… "
105     index=$(pa_get_next_index)
106     ;;
107
108 esac
109
110 if [ -z "$index" ]; then
111
112   echo >&2 E: no index specified or discernable.
113   exit 1
114 fi
115
116 uuidgen() {
117   hascmd() { command -v "$@" >/dev/null;}
118   if   hascmd uuid; then uuid
119   elif hascmd uuidgen; then uuidgen
120   elif hascmd python3; then
121     python3 -c 'import uuid; print(uuid.uuid1())'
122   elif hascmd python; then
123     python -c 'import uuid; print(uuid.uuid1())'
124   else
125     dd if=/dev/urandom bs=16 count=1 status=none | base64
126   fi
127 }
128
129 uuid=$(uuidgen)
130
131 [ -n "$outfile" ] || outfile="${uuid}.ogg"
132
133 if [ -f "$outfile" ] && [ $clobber -eq 0 ]; then
134   echo >&2 "E: file exists, and -f not given: $outfile"
135   exit 1
136 fi
137
138 devname="record-to-file-${uuid}"
139
140 echo >&2 "Recording source $index to $outfile …"
141
142 load_module() {
143   local id mod sink desc
144   mod="$1" sink="$2" desc="$3"; shift 3
145   id=$(pactl load-module "$mod" sink_name="$sink" "$@")
146   cleanup_hook pactl unload-module $id
147   pacmd update-sink-proplist "$sink" device.description="$desc"
148 }
149
150 move_source_to_sink() {
151   local c;
152   c=$(pactl list short sink-inputs | sed -rne "s,^${1}[[:space:]]+([[:digit:]]+).+,\1,p")
153   cleanup_hook pactl move-sink-input $1 $c
154   pactl move-sink-input $1 $2
155 }
156
157 if false; then
158   # This would be great, but it does not work. For instance, trying this on
159   # audible meant that audible would play at maximum speed (3h played in 3
160   # minutes), but the result would be full of skips, making me think that
161   # something in the local pipeline can't handle the throughput. Rate-limiting
162   # might work. Have not had the time to investigate further.
163
164   load_module module-pipe-sink "$devname" "$devname" file="$TMPDIR/outfifo"
165   move_source_to_sink $index "$devname"
166   oggenc --raw -q5 -o "$outfile" "$TMPDIR/outfifo" || :
167   #pv -pterb "$TMPDIR/outfifo" > $TMPDIR/outfile.wav || :
168
169 else
170   # More traditional approach, which just takes 1:1 time.
171
172   load_module module-null-sink "$devname" "$devname"
173   move_source_to_sink $index "$devname"
174   parec --format=s16le -d "${devname}.monitor" 2>/dev/null \
175     | oggenc --raw -q5 -o "$outfile" - &
176   pid=$!
177
178   pa_wait_for_event sink-input remove $index >/dev/null
179
180   kill $pid
181 fi
182
183 cleanup