aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xaataaj.lua568
1 files changed, 568 insertions, 0 deletions
diff --git a/aataaj.lua b/aataaj.lua
new file mode 100755
index 0000000..1ddb45d
--- /dev/null
+++ b/aataaj.lua
@@ -0,0 +1,568 @@
1#!/usr/bin/env luajit
2
3--[[ This block is what system V LSB expects.
4### BEGIN INIT INFO
5# Provides: aataajScan
6# Required-Start: $local_fs
7# Required-Stop:
8# X-Start-Before: alsa-utils espeakup
9# Default-Start: S
10# Default-Stop:
11# Short-Description: Scan for audio devices.
12# Description: Scan for all audio devices and produce an
13# asound.conf include file fragment.
14### END INIT INFO
15]]
16
17local Help = [[
18This is part of the AllAudioToALSAandJACK project, aataaj for short,
19pronounced like "attach".
20
21The purpose is to scan for all ALSA / asound audio devices, and hook them
22all up to ALSA and JACK. Then it starts up JACK, and hooks up any
23joysticks it finds as MIDI controllers. So any ALSA application gets routed through
24JACK.
25
26This is very rough for now, only just started writing it. The stop
27command is particularly crude and violent, lots of killall.
28
29Since it isn't a package yet, some setup is needed.
30
31The packages you need installed are -
32 luajit
33 jackd2
34 jack-tools for jack-plumbing, but other patch persistance methods could be used.
35 a2jmidid
36 zita-ajbridge
37 aseqjoy
38
39 qjackctl can be used as a visual patchbay, though I prefer catia from the KXStudio repos.
40
41You need to have the snd-aloop kernel module loaded.
42
43The aataaj.lua script should be run at boot time, put it into
44/etc/boot.d/ and activate it with -
45
46update-rc.d aataaj.lua defaults
47
48It scans for your sound devices and creates /var/lib/aataaj/asoundrc.
49You can run it manually with "aataaj start" each time you need to change
50your devices.
51
52
53
54"aataaj JACK" should be called on user login. Probably don't need to run "aataaj stop" on
55user logout.
56
57It starts up JACK and friends, and creates JACK devices for all the
58things "aataaj start" found. It creates the cloop and ploop devices that
59catch everything ALSA does. Then creates MIDI devices for all your
60joysticks.
61
62
63Alas ~/.asoundrc doesn't understand ~ or $HOME, or even "try the current
64directory" it seems. So you have to hard cade the path. Make sure your
65~/.asoundrc or /etc/asoundrc includes something like this -
66
67</var/lib/aataaj/asoundrc>
68
69
70
71"aataaj stop" closes down everything "aataaj JACK" started up.
72
73
74
75TODO - Leave it running, and hotplug ALSA / asound audio devices.
76 a2jmidid takes care of hotplugging MIDI devices.
77 Though I think I still need to deal with hotplugged joysticks.
78
79NOTE - Seems both ALSA and JACK are per user. So you need to run
80"aataaj JACK" for each user.
81
82]]
83
84
85local args = {...}
86if 0 ~= #args then
87-- for i,a in pairs(args) do
88-- print('Argument ' .. i .. ' = ' .. a)
89-- end
90
91 if 'start' == args[1] then
92 elseif 'stop' == args[1] then
93 APT.exe("killall -TERM qsynth"):Do()
94 APT.exe("a2j_control --stop"):Do()
95 APT.exe("sleep 2"):Do()
96 APT.exe("a2j_control --exit"):Do()
97 APT.exe("sleep 2"):Do()
98 APT.exe("killall -TERM alsa_out"):Do()
99 APT.exe("killall -TERM alsa_in"):Do()
100 APT.exe("killall -TERM zita-a2j"):Do()
101 APT.exe("killall -TERM zita-j2a"):Do()
102 APT.exe("killall -TERM aseqjoy"):Do()
103 APT.exe("killall -TERM jack-plumbing"):Do()
104 APT.exe("sleep 2"):Do()
105 APT.exe("jack_control stop"):Do()
106 APT.exe("sleep 2"):Do()
107 APT.exe("jack_control exit"):Do()
108 APT.exe("sleep 2"):Do()
109 --APT.exe("a2j_control --stop; a2j_control --exit"):Do()
110 --APT.exe("sleep 2"):Do()
111 APT.exe("killall -TERM jmcore"):Do()
112 APT.exe("pkill -TERM jackdbus; pkill -TERM a2jmidid"):Do()
113 APT.exe("killall -TERM a2jmidid"):Do()
114 APT.exe("killall -KILL jackdbus"):Do()
115 APT.exe("sleep 2"):Do()
116 APT.exe("killall -KILL a2jmidid"):Do()
117 APT.exe("pkill -KILL jackdbus; pkill -KILL a2jmidid"):Do()
118 APT.exe("sleep 2"):Do()
119 APT.exe("killall -KILL a2jmidid"):Do()
120 APT.exe("killall -KILL jackdbus"):Do()
121 APT.exe("sleep 2"):Do()
122 APT.exe("killall -KILL a2jmidid"):Do()
123 APT.exe("killall -KILL jackdbus"):Do()
124 APT.exe("sleep 2"):Do()
125 APT.exe("pkill -KILL jackdbus; pkill -KILL a2jmidid"):Do()
126 APT.exe("killall -TERM qjackctl"):Do()
127
128 -- Catia is python, and no easy way to kill it.
129 APT.exe("ps auxw | grep python"):Do()
130 return(0)
131 elseif 'JACK' == args[1] then
132 elseif 'restart' == args[1] then args[1] = 'start'
133 elseif 'force-reload' == args[1] then args[1] = 'start'
134 elseif 'status' == args[1] then
135 return(0)
136 elseif 'help' == args[1] then
137 print(Help)
138 return(0)
139 elseif '--help' == args[1] then
140 print(Help)
141 return(0)
142 else
143 print("Usage: /etc/init.d/aataajScan.lua {start|stop|restart|force-reload|status}")
144 return(1)
145 end
146else
147 print("Usage: /etc/init.d/aataajScan.lua {help|start|stop|restart|force-reload|status|JACK}")
148 return(1)
149end
150
151
152
153-- CHANGE these to suit.
154local asoundrcPath = '/var/lib/aataaj'
155local asoundrc = 'asoundrc'
156local GUI = 'qjackctl'
157--local GUI = 'catia'
158local alias = {
159-- {name='Screen', dev='HDMI9'},
160 }
161
162
163
164
165-- This APT stuff was copied from apt-panopticon.
166local APT = {}
167
168APT.readCmd = function(cmd)
169 local result = {}
170 local output = io.popen(cmd)
171 if nil ~= output then
172 for l in output:lines() do
173 table.insert(result, l)
174 end
175 end
176 return result
177end
178
179
180APT.exe = function(c)
181 local exe = {status = 0, result = '', lines = {}, log = true, cmd = c .. ' ', command = c}
182
183 function exe:log()
184 self.log = true
185 return self
186 end
187 function exe:Nice(c)
188 if nil == c then
189 self.cmd = 'ionice -c3 nice -n 19 ' .. self.cmd
190 else
191 self.cmd = self.cmd .. 'ionice -c3 nice -n 19 ' .. c .. ' '
192 end
193 return self
194 end
195 function exe:timeout(c)
196 -- timeout returns a status of - command status if --preserve-status; "128+9" (actually 137) if --kill-after ends up being done; 124 if it had to TERM; command status if all went well.
197 -- --kill-after means "send KILL after TERM fails.
198 if nil == c then
199 self.cmd = 'timeout --kill-after=10.0 --foreground 42.0s ' .. self.cmd
200 else
201 self.cmd = 'timeout --kill-after=10.0 --foreground ' .. c .. ' ' .. self.cmd
202 end
203 return self
204 end
205 function exe:also(c)
206 if nil == c then c = '' else c = ' ' .. c end
207 self.cmd = self.cmd .. ';' .. c .. ' '
208 return self
209 end
210 function exe:And(c)
211 if nil == c then c = '' else c = ' ' .. c end
212 self.cmd = self.cmd .. '&&' .. c .. ' '
213 return self
214 end
215 function exe:Or(c)
216 if nil == c then c = '' end
217 self.cmd = self.cmd .. '|| ' .. c .. ' '
218 return self
219 end
220 function exe:noErr()
221 self.cmd = self.cmd .. '2>/dev/null '
222 return self
223 end
224 function exe:wait(w)
225 self.cmd = self.cmd .. '&& touch ' .. w .. ' '
226 return self
227 end
228 function exe:Do()
229 --[[ "The condition expression of a control structure can return any
230 value. Both false and nil are considered false. All values different
231 from nil and false are considered true (in particular, the number 0
232 and the empty string are also true)."
233 says the docs, I beg to differ.]]
234 if true == self.log then D(" executing - &nbsp; <code>" .. self.cmd .. "</code>") end
235 --[[ Damn os.execute()
236 Lua 5.1 says it returns "a status code, which is system-dependent"
237 Lua 5.2 says it returns true/nil, "exit"/"signal", the status code.
238 I'm getting 7168 or 0. No idea what the fuck that is.
239 local ok, rslt, status = os.execute(s)
240 ]]
241 local f = APT.readCmd(self.cmd, 'r')
242 -- The last line will be the command's returned status, collect everything else in result.
243 self.status = '' -- Otherwise the result starts with 0.
244 self.result = '\n'
245 self.lines = f
246 for i,l in ipairs(f) do
247 self.result = self.result .. l .. "\n"
248 end
249 f = APT.readCmd('echo "$?"', 'r')
250 for i,l in ipairs(f) do
251 self.status = tonumber(l)
252 if (137 == self.status) or (124 == self.status) then
253 print("timeout killed " .. self.status .. ' ' .. self.command)
254 E("timeout killed " .. self.status .. ' ' .. self.command)
255 elseif (0 ~= self.status) then
256 print("status |" .. self.status .. '| ' .. self.command)
257 E("status |" .. self.status .. '| ' .. self.command)
258 end
259 end
260 return self
261 end
262 function exe:fork(host)
263 if nil ~= host then self.cmd = self.cmd .. '; r=$?; if [ $r -ge 124 ]; then echo "$r ' .. host .. ' failed forked command ' .. string.gsub(self.cmd, '"', "'") .. '"; fi' end
264 self.cmd = '{ ' .. self.cmd .. '; } &'
265 if true == self.log then D(" forking - &nbsp; <code>" .. self.cmd .. "</code>") end
266 os.execute(self.cmd)
267 return self
268 end
269 return exe
270end
271
272
273
274local Cards = {}
275
276print('Scanning for audio devices.')
277local cards = APT.exe('ls -d1 /proc/asound/card[0-9]*'):noErr():Do()
278for i,l in ipairs(cards.lines) do
279 local f, e = io.open(l .. '/id', "r")
280 if nil == f then print("Could not open " .. l .. '/id') else
281 Cards[l] = {path = l, name = f:read("*a"):sub(1, -2), devs = {}, captureDevs = {}, playbackDevs = {}}
282 if "Loopback" ~= Cards[l]['name'] then
283 Cards[l]['capture'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*c*'):noErr():Do()
284 for j,c in ipairs(Cards[l]['capture'].lines) do
285 local n = c:match(".*pcm(%d+).*")
286 Cards[l]['captureDevs'][j] = n
287 Cards[l]['devs'][n] = n
288 print("\tFound capture device: " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['captureDevs'][j] .. ' ' .. n)
289 end
290 Cards[l]['playback'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*p*'):noErr():Do()
291 for j,p in ipairs(Cards[l]['playback'].lines) do
292 local n = p:match(".*pcm(%d+).*")
293 Cards[l]['playbackDevs'][j] = n
294 Cards[l]['devs'][n] = n
295 print("\tFound playback device " .. i - 1 .. " : " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['playbackDevs'][j] .. ' ' .. n)
296 if 'JACK' ~= args[1] then
297-- print('\t\tALSA_CARD=' .. i - 1 .. ' espeak "Found playback device ' .. i - 1 .. ' : ' .. Cards[l]['name'] .. ' DEVICE: ' .. Cards[l]['playbackDevs'][j] .. ' ' .. n .. '"')
298 APT.exe('ALSA_CARD=' .. i - 1 .. ' espeak "Found playback device ' .. i - 1 .. ' : ' .. Cards[l]['name'] .. ' DEVICE: ' .. Cards[l]['playbackDevs'][j] .. ' ' .. n .. '"'):noErr():Do()
299 APT.exe('sleep 1')
300 end
301 end
302 end
303 end
304end
305
306if 'start' == args[1] then
307 APT.exe('mkdir -p ' .. asoundrcPath):Do()
308 local a, e = io.open(asoundrcPath .. '/jack-plumbing', "w")
309 if nil == a then print("Could not open " .. asoundrcPath .. '/jack-plumbing') else
310 a:write([[
311(connect "system:capture_1" "ploop:playback_1")
312(connect "system:capture_2" "ploop:playback_2")
313(connect "cloop:capture_1" "system:playback_1")
314(connect "cloop:capture_2" "system:playback_2")
315(connect "cloop:capture_1" "(.*)-in:playback_1")
316(connect "cloop:capture_2" "(.*)-in:playback_2")
317
318(connect "qsynth:left" "(.*)-in:playback_1")
319(connect "qsynth:right" "(.*)-in:playback_2")
320 ]])
321 a:close()
322 end
323 local a, e = io.open(asoundrcPath .. '/' .. asoundrc, "w")
324 if nil == a then print("Could not open " .. asoundrcPath .. '/' .. asoundrc) else
325 for i,C in pairs(Cards) do
326 for j,c in pairs(C['devs']) do
327 a:write("pcm." .. C['name'] .. j .. " {\n")
328 a:write(" type hw\n")
329 a:write(" card " .. C['name'] .. "\n")
330 a:write(" device " .. C['devs'][j] .. "\n")
331 a:write("}\n")
332 a:write("ctl." .. C['name'] .. j .. " {\n")
333 a:write(" type hw\n")
334 a:write(" card " .. C['name'] .. "\n")
335 a:write(" device " .. C['devs'][j] .. "\n")
336 a:write("}\n\n")
337 end
338 end
339 a:write([[
340#################################################################################################################################
341
342# The complex way - https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge
343
344# More custom version, but it didn't work for me.
345# hardware 0,0 : used for ALSA playback
346#pcm.loophw00 {
347# type hw
348# card Loopback
349# device 0
350# subdevice 0
351# format S32_LE
352# rate 48000
353#}
354
355# playback PCM device: using loopback subdevice 0,0
356# Don't use a buffer size that is too small. Some apps
357# won't like it and it will sound crappy
358
359#pcm.amix {
360# type dmix
361# ipc_key 219345
362# slave {
363# pcm loophw00
364## period_size 4096
365## periods 2
366# }
367#}
368
369# software volume
370#pcm.asoftvol {
371# type softvol
372# slave.pcm "amix"
373
374# control { name PCM }
375
376# min_dB -51.0
377# max_dB 0.0
378#}
379
380
381# for jack alsa_in: looped-back signal at other ends
382#pcm.cloop {
383# type hw
384# card Loopback
385# device 1
386# subdevice 0
387# format S32_LE
388# rate 48000
389#}
390
391# hardware 0,1 : used for ALSA capture
392#pcm.loophw01 {
393# type hw
394# card Loopback
395# device 0
396# subdevice 1
397# format S32_LE
398# rate 48000
399#}
400
401# for jack alsa_out: looped-back signal at other end
402#pcm.ploop {
403# type hw
404# card Loopback
405# device 1
406# subdevice 1
407# format S32_LE
408# rate 48000
409#}
410
411# duplex device combining our PCM devices defined above
412#pcm.aduplex {
413# type asym
414# playback.pcm "asoftvol"
415# capture.pcm "loophw01"
416#}
417
418# default device
419#pcm.!default {
420# type plug
421# slave.pcm aduplex
422
423# hint {
424# show on
425# description "Duplex Loopback"
426# }
427#}
428
429
430
431# Generic method seems to work better.
432# playback PCM device: using loopback subdevice 0,0
433pcm.amix {
434 type dmix
435 ipc_key 219345
436 slave.pcm "hw:Loopback,0,0"
437}
438
439# capture PCM device: using loopback subdevice 0,1
440pcm.asnoop {
441 type dsnoop
442 ipc_key 219346
443 slave.pcm "hw:Loopback,0,1"
444}
445
446# duplex device combining our PCM devices defined above
447pcm.aduplex {
448 type asym
449 playback.pcm "amix"
450 capture.pcm "asnoop"
451}
452
453# ------------------------------------------------------
454# for jack alsa_in and alsa_out: looped-back signal at other ends
455pcm.ploop {
456 type plug
457 slave.pcm "hw:Loopback,1,1"
458}
459
460pcm.cloop {
461 type dsnoop
462 ipc_key 219348
463 slave.pcm "hw:Loopback,1,0"
464}
465
466# ------------------------------------------------------
467# default device
468
469pcm.!default {
470 type plug
471 slave.pcm "aduplex"
472}
473 ]])
474 a:close()
475 end
476elseif 'JACK' == args[1] then
477 print('')
478 print("Start up JACK and friends.")
479 print("jack_control")
480 APT.exe('jack_control start'):Do()
481 APT.exe('jack_control ds alsa'):Do()
482 --jack_control dps device hw:RIG,0
483 local r = APT.exe('jack_control status'):Do().status
484 while r ~= 0 do
485 -- if 0 ~= r then
486 print("Waiting for JACK - sleep 1")
487 APT.exe('sleep 1'):Do()
488 r = APT.exe('jack_control status'):Do().status
489 -- end
490 end
491 if nil ~= GUI then
492 print(GUI)
493 APT.exe(GUI):fork()
494 end
495 print("jack-plumbing")
496 APT.exe('jack-plumbing -o /var/lib/aataaj/jack-plumbing 2>/dev/null'):fork()
497 -- Bridge ALSA ports to JACK ports. Only handles MIDI.
498 -- MIDI via a2jmidid. The --ehw enables hardware ports as well, equal to using the seq MIDI drivare according to https://freeshell.de/~murks/posts/ALSA_and_JACK_MIDI_explained_(by_a_dummy_for_dummies)/
499 --a2j_control actually starts a2jmidid.
500 ----a2jmidid -e -u &
501 -- I think the jack_control start and my current alsa config means a2jmidid gets started anyway. But seem to need this bit to get the joystick covered.
502 print("a2j_control")
503 APT.exe('a2j_control --ehw && a2j_control --start'):Do()
504 print("sleep 2")
505 APT.exe('sleep 2'):Do()
506 print("")
507
508
509 --local AIN = "alsa_in"
510 local AIN = "zita-a2j"
511 --local AOUT = "alsa_out"
512 local AOUT = "zita-j2a"
513
514 print("Basic ALSA sound devices converted to JACK.")
515 for i,C in pairs(alias) do
516 print('HW playback: ' .. C['name'] .. '\tDEVICE: ' .. C['dev'])
517 APT.exe('alsa_out -j ' .. C['name'] .. ' -d ' .. C['dev']):fork()
518 end
519 print("HW playback: cloop\tDEVICE: cloop")
520 -- No idea why, cloop wont work with zita-a2j.
521 APT.exe('alsa_in -j cloop -d cloop'):fork()
522 --APT.exe('sleep 1'):Do()
523 --APT.exe('jack_connect cloop:capture_1 system:playback_1'):Do()
524 --APT.exe('jack_connect cloop:capture_2 system:playback_2'):Do()
525 print("HW playback: ploop\tDEVICE: ploop")
526 APT.exe('alsa_out -j ploop -d ploop'):fork()
527 --APT.exe('sleep 1'):Do()
528 --APT.exe('jack_connect system:capture_1 ploop:playback_1'):Do()
529 --APT.exe('jack_connect system:capture_2 ploop:playback_2'):Do()
530
531
532 print("")
533
534 print("Rest of ALSA sound devices converted to JACK.")
535 for i,C in pairs(Cards) do
536 for j,c in ipairs(C['playbackDevs']) do
537 print("HW playback: " .. C['name'] .. "\tDEVICE: " .. C['playbackDevs'][j])
538 APT.exe(AOUT .. ' -j ' .. C['name'] .. "_" .. C['playbackDevs'][j] .. '-in -d ' .. C['name'] .. C['playbackDevs'][j]):fork()
539 -- APT.exe('sleep 1'):Do()
540 -- APT.exe('jack_connect cloop:capture_1 ' .. C['name'] .. '_' .. C['playbackDevs'][j] .. '-in' .. ':playback_1'):Do()
541 -- APT.exe('jack_connect cloop:capture_2 ' .. C['name'] .. '_' .. C['playbackDevs'][j] .. '-in' .. ':playback_2'):Do()
542 end
543 for j,c in ipairs(C['captureDevs']) do
544 print("HW capture: " .. C['name'] .. "\tDEVICE: " .. C['captureDevs'][j])
545 APT.exe(AIN .. ' -j ' .. C['name'] .. "_" .. C['captureDevs'][j] .. '-out -d ' .. C['name'] .. C['captureDevs'][j]):fork()
546 end
547 end
548 print("")
549
550 print("Scanning for joysticks.")
551 local sticks = APT.exe('ls -1 /dev/input/js[0-9]*'):noErr():Do()
552 for i,l in ipairs(sticks.lines) do
553 print("aseqjoy " .. l)
554 -- Buttons switch to that numbered MIDI channel, defaults to 1.
555 -- Axis are mapped to MIDI controllers 10 - 15
556 -- -r means to use high resolution MIDI values.
557 APT.exe('aseqjoy -d ' .. l:sub(-1,-1) .. ' -r'):fork()
558 end
559
560 print('qsynth')
561 APT.exe('qsynth'):fork()
562
563 print("")
564
565 print('Stop our jack-plumbing, eventually.')
566 APT.exe('sleep 4'):Do()
567 APT.exe("killall -TERM jack-plumbing"):Do()
568end