#!/usr/bin/env luajit --[[ This block is what system V LSB expects. ### BEGIN INIT INFO # Provides: aataaj # Required-Start: $local_fs # Required-Stop: # X-Start-Before: alsa-utils espeakup # Default-Start: S # Default-Stop: # Short-Description: Scan for audio devices. # Description: Scan for all audio devices and produce an # asound.conf include file fragment. ### END INIT INFO ]] local Help = [[ This is part of the AllAudioToALSAandJACK project, aataaj for short, pronounced like "attach". The purpose is to scan for all ALSA / asound audio devices, and hook them all up to ALSA and JACK. Then it starts up JACK, and hooks up any joysticks it finds as MIDI controllers. So any ALSA application gets routed through JACK. This is very rough for now, only just started writing it. The stop command is particularly crude and violent, lots of killall. Since it isn't a package yet, some setup is needed. The packages you need installed are - luajit jackd2 jack-tools for jack-plumbing, but other patch persistance methods could be used. a2jmidid zita-ajbridge aseqjoy qjackctl can be used as a visual patchbay, though I prefer catia from the KXStudio repos. You need to have the snd-aloop kernel module loaded. The aataaj.lua script should be run at boot time, put it into /etc/boot.d/ and activate it with - update-rc.d aataaj.lua defaults It scans for your sound devices and creates /var/lib/aataaj/asoundrc. You can run it manually with "aataaj start" each time you need to change your devices. "aataaj JACK" should be called on user login. Probably don't need to run "aataaj stop" on user logout. It starts up JACK and friends, and creates JACK devices for all the things "aataaj start" found. It creates the cloop and ploop devices that catch everything ALSA does. Then creates MIDI devices for all your joysticks. Alas ~/.asoundrc doesn't understand ~ or $HOME, or even "try the current directory" it seems. So you have to hard code the path. Make sure your ~/.asoundrc or /etc/asoundrc includes something like this - "aataaj stop" closes down everything "aataaj JACK" started up. TODO - Leave it running, and hotplug ALSA / asound audio devices. a2jmidid takes care of hotplugging MIDI devices. Though I think I still need to deal with hotplugged joysticks. NOTE - Seems both ALSA and JACK are per user. So you need to run "aataaj JACK" for each user. ]] -- This APT stuff was copied from apt-panopticon. local APT = {} APT.readCmd = function(cmd) local result = {} local output = io.popen(cmd) if nil ~= output then for l in output:lines() do table.insert(result, l) end end return result end APT.exe = function(c) local exe = {status = 0, result = '', lines = {}, log = true, cmd = c .. ' ', command = c} function exe:log() self.log = true return self end function exe:Nice(c) if nil == c then self.cmd = 'ionice -c3 nice -n 19 ' .. self.cmd else self.cmd = self.cmd .. 'ionice -c3 nice -n 19 ' .. c .. ' ' end return self end function exe:timeout(c) -- 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. -- --kill-after means "send KILL after TERM fails. if nil == c then self.cmd = 'timeout --kill-after=10.0 --foreground 42.0s ' .. self.cmd else self.cmd = 'timeout --kill-after=10.0 --foreground ' .. c .. ' ' .. self.cmd end return self end function exe:also(c) if nil == c then c = '' else c = ' ' .. c end self.cmd = self.cmd .. ';' .. c .. ' ' return self end function exe:And(c) if nil == c then c = '' else c = ' ' .. c end self.cmd = self.cmd .. '&&' .. c .. ' ' return self end function exe:Or(c) if nil == c then c = '' end self.cmd = self.cmd .. '|| ' .. c .. ' ' return self end function exe:noErr() self.cmd = self.cmd .. '2>/dev/null ' return self end function exe:wait(w) self.cmd = self.cmd .. '&& touch ' .. w .. ' ' return self end function exe:Do() --[[ "The condition expression of a control structure can return any value. Both false and nil are considered false. All values different from nil and false are considered true (in particular, the number 0 and the empty string are also true)." says the docs, I beg to differ.]] if true == self.log then D(" executing -   " .. self.cmd .. "") end --[[ Damn os.execute() Lua 5.1 says it returns "a status code, which is system-dependent" Lua 5.2 says it returns true/nil, "exit"/"signal", the status code. I'm getting 7168 or 0. No idea what the fuck that is. local ok, rslt, status = os.execute(s) ]] local f = APT.readCmd(self.cmd, 'r') -- The last line will be the command's returned status, collect everything else in result. self.status = '' -- Otherwise the result starts with 0. self.result = '\n' self.lines = f for i,l in ipairs(f) do self.result = self.result .. l .. "\n" end f = APT.readCmd('echo "$?"', 'r') for i,l in ipairs(f) do self.status = tonumber(l) if (137 == self.status) or (124 == self.status) then print("timeout killed " .. self.status .. ' ' .. self.command) E("timeout killed " .. self.status .. ' ' .. self.command) elseif (0 ~= self.status) then print("status |" .. self.status .. '| ' .. self.command) E("status |" .. self.status .. '| ' .. self.command) end end return self end function exe:fork(host) 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 self.cmd = '{ ' .. self.cmd .. '; } &' if true == self.log then D(" forking -   " .. self.cmd .. "") end os.execute(self.cmd) return self end return exe end local args = {...} if 0 ~= #args then -- for i,a in pairs(args) do -- print('Argument ' .. i .. ' = ' .. a) -- end if 'start' == args[1] then elseif 'stop' == args[1] then APT.exe("killall -TERM qsynth"):Do() APT.exe("a2j_control --stop"):Do() APT.exe("sleep 2"):Do() APT.exe("a2j_control --exit"):Do() APT.exe("sleep 2"):Do() APT.exe("killall -TERM alsa_out"):Do() APT.exe("killall -TERM alsa_in"):Do() APT.exe("killall -TERM zita-a2j"):Do() APT.exe("killall -TERM zita-j2a"):Do() APT.exe("killall -TERM aseqjoy"):Do() APT.exe("killall -TERM jack-plumbing"):Do() APT.exe("sleep 2"):Do() APT.exe("jack_control stop"):Do() APT.exe("sleep 2"):Do() APT.exe("jack_control exit"):Do() APT.exe("sleep 2"):Do() --APT.exe("a2j_control --stop; a2j_control --exit"):Do() --APT.exe("sleep 2"):Do() APT.exe("killall -TERM jmcore"):Do() APT.exe("pkill -TERM jackdbus; pkill -TERM a2jmidid"):Do() APT.exe("killall -TERM a2jmidid"):Do() APT.exe("killall -KILL jackdbus"):Do() APT.exe("sleep 2"):Do() APT.exe("killall -KILL a2jmidid"):Do() APT.exe("pkill -KILL jackdbus; pkill -KILL a2jmidid"):Do() APT.exe("sleep 2"):Do() APT.exe("killall -KILL a2jmidid"):Do() APT.exe("killall -KILL jackdbus"):Do() APT.exe("sleep 2"):Do() APT.exe("killall -KILL a2jmidid"):Do() APT.exe("killall -KILL jackdbus"):Do() APT.exe("sleep 2"):Do() APT.exe("pkill -KILL jackdbus; pkill -KILL a2jmidid"):Do() APT.exe("killall -TERM qjackctl"):Do() -- Catia is python, and no easy way to kill it. APT.exe("ps auxw | grep python"):Do() return(0) elseif 'JACK' == args[1] then elseif 'restart' == args[1] then args[1] = 'start' elseif 'force-reload' == args[1] then args[1] = 'start' elseif 'status' == args[1] then return(0) elseif 'help' == args[1] then print(Help) return(0) elseif '--help' == args[1] then print(Help) return(0) else print("Usage: /etc/init.d/aataaj.lua {start|stop|restart|force-reload|status}") return(1) end else print("Usage: /etc/init.d/aataaj.lua {help|start|stop|restart|force-reload|status|JACK}") return(1) end -- CHANGE these to suit. local asoundrcPath = '/var/lib/aataaj' local asoundrc = 'asoundrc' local GUI = 'qjackctl' --local GUI = 'catia' local alias = { -- {name='Screen', dev='HDMI9'}, } local Cards = {} print('Scanning for audio devices.') local cards = APT.exe('ls -d1 /proc/asound/card[0-9]*'):noErr():Do() for i,l in ipairs(cards.lines) do local f, e = io.open(l .. '/id', "r") if nil == f then print("Could not open " .. l .. '/id') else Cards[l] = {path = l, name = f:read("*a"):sub(1, -2), devs = {}, captureDevs = {}, playbackDevs = {}} if "Loopback" ~= Cards[l]['name'] then Cards[l]['capture'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*c*'):noErr():Do() for j,c in ipairs(Cards[l]['capture'].lines) do local n = c:match(".*pcm(%d+).*") Cards[l]['captureDevs'][j] = n Cards[l]['devs'][n] = n print("\tFound capture device: " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['captureDevs'][j] .. ' ' .. n) end Cards[l]['playback'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*p*'):noErr():Do() for j,p in ipairs(Cards[l]['playback'].lines) do local n = p:match(".*pcm(%d+).*") Cards[l]['playbackDevs'][j] = n Cards[l]['devs'][n] = n print("\tFound playback device " .. i - 1 .. " : " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['playbackDevs'][j] .. ' ' .. n) if 'JACK' ~= args[1] then -- print('\t\tALSA_CARD=' .. i - 1 .. ' espeak "Found playback device ' .. i - 1 .. ' : ' .. Cards[l]['name'] .. ' DEVICE: ' .. Cards[l]['playbackDevs'][j] .. ' ' .. n .. '"') APT.exe('ALSA_CARD=' .. i - 1 .. ' espeak "Found playback device ' .. i - 1 .. ' : ' .. Cards[l]['name'] .. ' DEVICE: ' .. Cards[l]['playbackDevs'][j] .. ' ' .. n .. '"'):noErr():Do() APT.exe('sleep 1') end end end end end if 'start' == args[1] then APT.exe('mkdir -p ' .. asoundrcPath):Do() local a, e = io.open(asoundrcPath .. '/jack-plumbing', "w") if nil == a then print("Could not open " .. asoundrcPath .. '/jack-plumbing') else a:write([[ (connect "system:capture_1" "ploop:playback_1") (connect "system:capture_2" "ploop:playback_2") (connect "cloop:capture_1" "system:playback_1") (connect "cloop:capture_2" "system:playback_2") (connect "cloop:capture_1" "(.*)-in:playback_1") (connect "cloop:capture_2" "(.*)-in:playback_2") (connect "qsynth:left" "(.*)-in:playback_1") (connect "qsynth:right" "(.*)-in:playback_2") ]]) a:close() end local a, e = io.open(asoundrcPath .. '/' .. asoundrc, "w") if nil == a then print("Could not open " .. asoundrcPath .. '/' .. asoundrc) else for i,C in pairs(Cards) do for j,c in pairs(C['devs']) do a:write("pcm." .. C['name'] .. j .. " {\n") a:write(" type hw\n") a:write(" card " .. C['name'] .. "\n") a:write(" device " .. C['devs'][j] .. "\n") a:write("}\n") a:write("ctl." .. C['name'] .. j .. " {\n") a:write(" type hw\n") a:write(" card " .. C['name'] .. "\n") a:write(" device " .. C['devs'][j] .. "\n") a:write("}\n\n") end end a:write([[ ################################################################################################################################# # The complex way - https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge # More custom version, but it didn't work for me. # hardware 0,0 : used for ALSA playback #pcm.loophw00 { # type hw # card Loopback # device 0 # subdevice 0 # format S32_LE # rate 48000 #} # playback PCM device: using loopback subdevice 0,0 # Don't use a buffer size that is too small. Some apps # won't like it and it will sound crappy #pcm.amix { # type dmix # ipc_key 219345 # slave { # pcm loophw00 ## period_size 4096 ## periods 2 # } #} # software volume #pcm.asoftvol { # type softvol # slave.pcm "amix" # control { name PCM } # min_dB -51.0 # max_dB 0.0 #} # for jack alsa_in: looped-back signal at other ends #pcm.cloop { # type hw # card Loopback # device 1 # subdevice 0 # format S32_LE # rate 48000 #} # hardware 0,1 : used for ALSA capture #pcm.loophw01 { # type hw # card Loopback # device 0 # subdevice 1 # format S32_LE # rate 48000 #} # for jack alsa_out: looped-back signal at other end #pcm.ploop { # type hw # card Loopback # device 1 # subdevice 1 # format S32_LE # rate 48000 #} # duplex device combining our PCM devices defined above #pcm.aduplex { # type asym # playback.pcm "asoftvol" # capture.pcm "loophw01" #} # default device #pcm.!default { # type plug # slave.pcm aduplex # hint { # show on # description "Duplex Loopback" # } #} # Generic method seems to work better. # playback PCM device: using loopback subdevice 0,0 pcm.amix { type dmix ipc_key 219345 slave.pcm "hw:Loopback,0,0" } # capture PCM device: using loopback subdevice 0,1 pcm.asnoop { type dsnoop ipc_key 219346 slave.pcm "hw:Loopback,0,1" } # duplex device combining our PCM devices defined above pcm.aduplex { type asym playback.pcm "amix" capture.pcm "asnoop" } # ------------------------------------------------------ # for jack alsa_in and alsa_out: looped-back signal at other ends pcm.ploop { type plug slave.pcm "hw:Loopback,1,1" } pcm.cloop { type dsnoop ipc_key 219348 slave.pcm "hw:Loopback,1,0" } # ------------------------------------------------------ # default device pcm.!default { type plug slave.pcm "aduplex" } ]]) a:close() end elseif 'JACK' == args[1] then print('') print("Start up JACK and friends.") print("jack_control") APT.exe('jack_control start'):Do() APT.exe('jack_control ds alsa'):Do() --jack_control dps device hw:RIG,0 local r = APT.exe('jack_control status'):Do().status while r ~= 0 do -- if 0 ~= r then print("Waiting for JACK - sleep 1") APT.exe('sleep 1'):Do() r = APT.exe('jack_control status'):Do().status -- end end if nil ~= GUI then print(GUI) APT.exe(GUI):fork() end print("jack-plumbing") APT.exe('jack-plumbing -o /var/lib/aataaj/jack-plumbing 2>/dev/null'):fork() -- Bridge ALSA ports to JACK ports. Only handles MIDI. -- 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)/ --a2j_control actually starts a2jmidid. ----a2jmidid -e -u & -- 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. print("a2j_control") APT.exe('a2j_control --ehw && a2j_control --start'):Do() print("sleep 2") APT.exe('sleep 2'):Do() print("") --local AIN = "alsa_in" local AIN = "zita-a2j" --local AOUT = "alsa_out" local AOUT = "zita-j2a" print("Basic ALSA sound devices converted to JACK.") for i,C in pairs(alias) do print('HW playback: ' .. C['name'] .. '\tDEVICE: ' .. C['dev']) APT.exe('alsa_out -j ' .. C['name'] .. ' -d ' .. C['dev']):fork() end print("HW playback: cloop\tDEVICE: cloop") -- No idea why, cloop wont work with zita-a2j. APT.exe('alsa_in -j cloop -d cloop'):fork() --APT.exe('sleep 1'):Do() --APT.exe('jack_connect cloop:capture_1 system:playback_1'):Do() --APT.exe('jack_connect cloop:capture_2 system:playback_2'):Do() print("HW playback: ploop\tDEVICE: ploop") APT.exe('alsa_out -j ploop -d ploop'):fork() --APT.exe('sleep 1'):Do() --APT.exe('jack_connect system:capture_1 ploop:playback_1'):Do() --APT.exe('jack_connect system:capture_2 ploop:playback_2'):Do() print("") print("Rest of ALSA sound devices converted to JACK.") for i,C in pairs(Cards) do for j,c in ipairs(C['playbackDevs']) do print("HW playback: " .. C['name'] .. "\tDEVICE: " .. C['playbackDevs'][j]) APT.exe(AOUT .. ' -j ' .. C['name'] .. "_" .. C['playbackDevs'][j] .. '-in -d ' .. C['name'] .. C['playbackDevs'][j]):fork() -- APT.exe('sleep 1'):Do() -- APT.exe('jack_connect cloop:capture_1 ' .. C['name'] .. '_' .. C['playbackDevs'][j] .. '-in' .. ':playback_1'):Do() -- APT.exe('jack_connect cloop:capture_2 ' .. C['name'] .. '_' .. C['playbackDevs'][j] .. '-in' .. ':playback_2'):Do() end for j,c in ipairs(C['captureDevs']) do print("HW capture: " .. C['name'] .. "\tDEVICE: " .. C['captureDevs'][j]) APT.exe(AIN .. ' -j ' .. C['name'] .. "_" .. C['captureDevs'][j] .. '-out -d ' .. C['name'] .. C['captureDevs'][j]):fork() end end print("") print("Scanning for joysticks.") local sticks = APT.exe('ls -1 /dev/input/js[0-9]*'):noErr():Do() for i,l in ipairs(sticks.lines) do print("aseqjoy " .. l) -- Buttons switch to that numbered MIDI channel, defaults to 1. -- Axis are mapped to MIDI controllers 10 - 15 -- -r means to use high resolution MIDI values. APT.exe('aseqjoy -d ' .. l:sub(-1,-1) .. ' -r'):fork() end print('qsynth') APT.exe('qsynth'):fork() print("") print('Stop our jack-plumbing, eventually.') APT.exe('sleep 4'):Do() APT.exe("killall -TERM jack-plumbing"):Do() end