#!/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