#!/usr/bin/env luajit --[[ This is part of the JackOnAllDevices project, JOAD for short. The purpose is to scan for all ALSA / asound audio devices, and hook them all up to JACK. Then it starts up JACK, and hooks up any joysticks it finds as MIDI controllers. Alas ~/.asoundrc doesn't understand ~ or $HOME, or even "try the current directory" it seems. So you have to hard cade the path. Make sure your ~/.asoundrc or /etc/asoundrc includes something like this - Run this once as root to create that file, and each time you need to change your devices. 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. ]] -- CHANGE these to suit. local asoundrcPath = '/var/lib/JOAD' local asoundrc = 'asoundrc' -- 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 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: " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['playbackDevs'][j] .. ' ' .. n) end end end end APT.exe('mkdir -p ' .. asoundrcPath):Do() 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