#!/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 pkill.
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/init.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.
]]
-- Most of 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
APT.exists = function(c)
if 0 == APT.exe('which ' .. c):Do().status then return true end
return false
end
APT.killEmAll = function(all)
for i,l in ipairs(all) do
local c = 0
while 0 ~= tonumber(APT.exe("pgrep -u $USER -xc " .. l):Do().result) do
local s = 'TERM'
if c > 0 then s = 'KILL'; APT.exe("sleep " .. c):Do() end
print( "pkill -" .. s .. " -u $USER -x " .. l)
APT.exe("pkill -" .. s .. " -u $USER -x " .. l):Do()
c = c + 1
end
end
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.killEmAll{'qsynth'}
APT.exe("a2j_control --stop"):Do()
APT.exe("sleep 2"):Do()
APT.exe("a2j_control --exit"):Do()
APT.exe("sleep 2"):Do()
APT.killEmAll{'alsa_in', 'alsa_out', 'zita-a2j', 'zita-j2a', 'aseqjoy', 'jack-plumbing'}
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.killEmAll{'jmcore', 'qjackctl'}
-- Catia is python, and no easy way to kill it.
-- Also it keeps jackdbus alive, no matter how hard you kill it.
APT.exe("pkill -TERM -u $USER -f catia"):Do()
APT.exe("sleep 2"):Do()
APT.killEmAll{'jackdbus', 'a2jmidid'}
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'
if APT.exists('catia') then GUI = 'catia' end
local alias = {
-- {name='Screen', dev='HDMI9'},
}
local speaker = 'espeak'
if APT.exists('espeak-ng') then speaker = 'espeak-ng' 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 " .. i - 1 .. " : " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['playbackDevs'][j] .. ' ' .. n)
if 'JACK' ~= args[1] then
print('\t\tALSA_CARD=' .. i - 1 .. ' ' .. speaker .. ' "Found playback device ' .. i - 1 .. ' : ' .. Cards[l]['name'] .. ' DEVICE: ' .. Cards[l]['playbackDevs'][j] .. ' ' .. n .. '"')
APT.exe('ALSA_CARD=' .. i - 1 .. ' ' .. speaker .. ' "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
if APT.exists('jack-plumbing') then
print("jack-plumbing")
APT.exe('jack-plumbing -o /var/lib/aataaj/jack-plumbing 2>/dev/null'):fork()
end
if APT.exists('a2j_control') then
-- 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("")
end
local AIN = "alsa_in"
if APT.exists('zita-a2j') then AIN = 'zita-a2j' end
local AOUT = "alsa_out"
if APT.exists('zita-j2a') then AOUT = 'zita-j2a' end
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("")
if APT.exists('aseqjoy') then
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("")
end
print('qsynth')
APT.exe('qsynth'):fork()
print("")
if APT.exists('jack-plumbing') then
print('Stop our jack-plumbing, eventually.')
APT.exe('sleep 4'):Do()
APT.killEmAll{"jack-plumbing"}
end
end