#!/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 _ = require 'PolygLua' --[[ TODO - replace this with code to search for and print the README.md file. Since we got the path to the script in arg[0], we can split that up and follow symlinks until we find where the files are. Maybe we can find README.md there. ]] 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 and _.lua into /etc/init.d/ and activate it with - update-rc.d aataaj.lua defaults Note that _.lua might need to be in /usr/local/share/lua/5.1/ "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. ]] Version = '0.0 crap' local options = { start = {help = 'Command to start the scanning process, for Sys V init.', }, restart = {start}, ['force-reload'] = {start}, status = {help = 'Command to check the status of the scanning process, for Sys V init.', }, stop = {help = 'Command to stop the scanning process, for Sys V init.', }, JACK = {help = 'Command to start the JACK stuff, for users.', }, STOP = { help = 'Command to stop the JACK stuff, for users.', func = function(self, options, a, args, i) _.killEmAll{'qsynth'} __[[#!/bin/bash a2j_control --stop sleep 2 a2j_control --exit sleep 2 ]]:Do() _.killEmAll{'alsa_in', 'alsa_out', 'zita-a2j', 'zita-j2a', 'aseqjoy', 'jack-plumbing'} __[[#!/bin/bash sleep 2 jack_control stop sleep 2 jack_control exit sleep 2 ]]:Do() _.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. __'pkill -TERM -u $USER -f catia':Do() __'sleep 2':Do() _.killEmAll{'jackdbus', 'a2jmidid'} os.exit(0) end }, asoundrcPath = {help = 'Path to our config files and stuff.', value = '/var/lib/aataaj', }, asoundrc = {help = 'Name of asoundrc file.', value = 'asoundrc', }, aliases = {help = 'Aliases for audio devices.', value = {}, }, } _.parse(arg, options, 'aataaj') -- CHANGE these to suit. local GUI = 'qjackctl' if _.runnable('catia') then GUI = 'catia' end local speaker = 'espeak' if _.runnable('espeak-ng') then speaker = 'espeak-ng' end local Cards = {} local cnt = 0 print('Scanning for audio devices.') local cards = __'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') local g, h = io.open(l .. '/codec#0', 'r') -- Test to see if it's a real audio device. I think this is a real test. lol local a, b = io.open(l .. '/stream0', 'r') -- Test to see if it's a real audio device. I think this is a real test. lol if nil == f then print('Could not open ' .. l .. '/id - ' .. e) else if (nil ~= g) or (nil ~= a) then local n = f:read('*a'):sub(1, -2) Cards[n] = {path = l, name = n, devs = {}, captureDevs = {}, playbackDevs = {}, card=i} cnt = cnt + 1 if 'Loopback' ~= Cards[n]['name'] then Cards[n]['capture'] = __('ls -d1 ' .. l .. '/pcm[0-9]*c*'):noErr():Do() for j,c in ipairs(Cards[n]['capture'].lines) do local m = c:match('.*pcm(%d+).*') Cards[n]['captureDevs'][j] = m Cards[n]['devs'][m] = m print('\tFound capture device: ' .. Cards[n]['name'] .. '\tDEVICE: ' .. Cards[n]['captureDevs'][j] .. ' ' .. m) -- io.flush() end Cards[n]['playback'] = __('ls -d1 ' .. l .. '/pcm[0-9]*p*'):noErr():Do() for j,p in ipairs(Cards[n]['playback'].lines) do local m = p:match('.*pcm(%d+).*') Cards[n]['playbackDevs'][j] = m Cards[n]['devs'][m] = m print('\tFound playback device ' .. Cards[n].card - 1 .. ' : ' .. Cards[n]['name'] .. '\tDEVICE: ' .. Cards[n]['playbackDevs'][j] .. ' ' .. m) -- io.flush() if 'JACK' ~= arg[1] then -- print('\t\tALSA_CARD=' .. Cards[n].card - 1 .. ' ' .. speaker .. ' "Found playback device ' .. Cards[n].card - 1 .. ' : ' .. Cards[n]['name'] .. ' DEVICE: ' .. Cards[n]['playbackDevs'][j] .. ' ' .. m .. '"') -- io.flush() end end end end end end table.sort(Cards, function(a, b) return a.card < b.card end) print('') io.flush() local speak = function(card, subdevice, device, words, printIt, forkIt) if printIt then print(words) end if forkIt then __('#!/bin/bash\necho "' .. words .. '" | ' .. speaker .. ' -d ' .. card .. subdevice):noErr():noOut():fork() else __('#!/bin/bash\necho "' .. words .. '" | ' .. speaker .. ' -d ' .. card .. subdevice):noErr():noOut():Do() end -- io.flush() end if 'start' == arg[1] then print('Your ' .. cnt .. ' audio devices are - ') for k,C in pairs(Cards) do for j,c in ipairs(C['playbackDevs']) do speak(C.name, C['playbackDevs'][j], C.card - 1, 'Your ' .. cnt .. ' audio devices are - ', false, true) end end -- TODO - should do a proper "wait for speakers to finish" here. Have fork(write a file), think that's what :wait(file) does. __'sleep 6':Do() for k,C in pairs(Cards) do for j,c in ipairs(C['playbackDevs']) do speak(C.name, C['playbackDevs'][j], C.card - 1, 'Device number ' .. C.card - 1 .. ', ' .. C['playbackDevs'][j] .. ' : ' .. C.name, true, false) end __'sleep 1':Do() end io.flush() print('Please type the device number you heard best - ') for k,C in pairs(Cards) do for j,c in ipairs(C['playbackDevs']) do speak(C.name, C['playbackDevs'][j], C.card - 1, 'Please type the device number you heard best - ', false, true) end end local choice = tonumber(io.read()) -- Lua has no way of just checking IF there is ANY input, so can't do "check if there was a keypress, continue if not". local ourCard = '' for k,C in pairs(Cards) do if (C.card - 1) == choice then print('Your choice is ' .. choice .. ' ' .. C.name) ourCard = C.name end end __('mkdir -p ' .. options.asoundrcPath.value):Do() local a, e = io.open(options.asoundrcPath.value .. '/jack-plumbing', 'w') if nil == a then print('Could not open ' .. options.asoundrcPath.value .. '/jack-plumbing - ' .. e) 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 outCard = function(a, C, j) 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 local a, e = io.open(options.asoundrcPath.value .. '/' .. options.asoundrc.value, 'w') if nil == a then print('Could not open ' .. options.asoundrcPath.value .. '/' .. options.asoundrc.value .. ' - ' .. e) else print('Writing suggested ALSA configuration file to ' .. options.asoundrcPath.value .. '/' .. options.asoundrc.value) if '' ~= ourCard then outCard(a, Cards[ourCard], '0') -- How the fuck is that a string? end for i,C in pairs(Cards) do for j,c in pairs(C['devs']) do if C.name ~= ourCard then outCard(a, C, j) end 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' == arg[1] then print('') print('Start up JACK and friends.') print('jack_control') __[[jack_control start jack_control ds alsa]]:Do() --jack_control dps device hw:RIG,0 while 0 ~= __'jack_control status':Do().status do print('Waiting for JACK') __'sleep 1':Do() end if nil ~= GUI then print(GUI) __(GUI):noErr():noOut():forkOnce() end if _.runnable'jack-plumbing' then print('jack-plumbing') __'jack-plumbing -o /var/lib/aataaj/jack-plumbing 2>/dev/null':fork() end if _.runnable'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') __'a2j_control --ehw && a2j_control --start':Do() -- print('sleep 2') -- __('sleep 2'):Do() print("") end local AIN = 'alsa_in' if _.runnable'zita-a2j' then AIN = 'zita-a2j' end local AOUT = 'alsa_out' if _.runnable'zita-j2a' then AOUT = 'zita-j2a' end print('Basic ALSA sound devices converted to JACK.') for k,C in pairs(options.aliases.value) do print('HW playback: ' .. C['name'] .. '\tDEVICE: ' .. C['dev']) __(AOUT .. ' -j ' .. C['name'] .. ' -d ' .. C['dev']):fork() end print('HW playback: cloop\tDEVICE: cloop') -- No idea why, cloop wont work with zita-a2j. __'alsa_in -j cloop -d cloop':fork() --__[[sleep 1 -- jack_connect cloop:capture_1 system:playback_1o() -- jack_connect cloop:capture_2 system:playback_2]]:Do() print('HW playback: ploop\tDEVICE: ploop') __'alsa_out -j ploop -d ploop':fork() --__[[sleep 1 -- jack_connect system:capture_1 ploop:playback_1 -- jack_connect system:capture_2 ploop:playback_2]]:Do() print('') print('Rest of ALSA sound devices converted to JACK.') for k,C in pairs(Cards) do for j,c in ipairs(C['playbackDevs']) do print('HW playback: ' .. C['name'] .. '\tDEVICE: ' .. C['playbackDevs'][j]) __(AOUT .. ' -j ' .. C['name'] .. '_' .. C['playbackDevs'][j] .. '-in -d ' .. C['name'] .. C['playbackDevs'][j]):fork() -- __'sleep 1':Do() -- __('jack_connect cloop:capture_1 ' .. C['name'] .. '_' .. C['playbackDevs'][j] .. '-in' .. ':playback_1'):Do() -- __('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]) __(AIN .. ' -j ' .. C['name'] .. '_' .. C['captureDevs'][j] .. '-out -d ' .. C['name'] .. C['captureDevs'][j]):fork() end end print('') if _.runnable('aseqjoy') then print('Scanning for joysticks.') local sticks = __'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. __('aseqjoy -d ' .. l:sub(-1,-1) .. ' -r'):fork() end print('') end if _.runnable('jack-plumbing') then print('Stop our jack-plumbing, eventually.') __'sleep 4':Do() _.killEmAll{'jack-plumbing'} end if _.runnable('~/.aataaj_JACK.lua') then print('Running users aataaj_JACK.lua') __'~/.aataaj_JACK.lua':Do() end end