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

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