diff options
-rw-r--r-- | .asoundrc | 1 | ||||
-rw-r--r-- | README.md | 50 | ||||
-rwxr-xr-x | jackoffall | 146 | ||||
-rwxr-xr-x | jackonall | 237 | ||||
-rwxr-xr-x | jackscanall | 326 |
5 files changed, 760 insertions, 0 deletions
diff --git a/.asoundrc b/.asoundrc new file mode 100644 index 0000000..7321f6c --- /dev/null +++ b/.asoundrc | |||
@@ -0,0 +1 @@ | |||
</var/lib/JOAD/asoundrc> | |||
diff --git a/README.md b/README.md new file mode 100644 index 0000000..86dec60 --- /dev/null +++ b/README.md | |||
@@ -0,0 +1,50 @@ | |||
1 | This is the JackOnAllDevices project, JOAD for short. | ||
2 | |||
3 | The purpose is to scan for all ALSA / asound audio devices, and hook them | ||
4 | all up to JACK. Then it starts up JACK, and hooks up any joysticks it | ||
5 | finds as MIDI controllers. So any ALSA application gets routed through | ||
6 | JACK. | ||
7 | |||
8 | This is very rough for now, only just started writing it. jackoffall is | ||
9 | particularly crude and violent, lots of killall. | ||
10 | |||
11 | Since it isn't a package yet, some setup is needed. | ||
12 | |||
13 | The packages you need installed are - | ||
14 | luajit | ||
15 | jackd2 | ||
16 | jack-tools for jack-plumbing, but other patch persistance methods could be used. | ||
17 | a2jmidid | ||
18 | zita-ajbridge | ||
19 | aseqjoy | ||
20 | |||
21 | qjackctl can be used as a visual patchbay, though I prefer catia from the KXStudio repos. | ||
22 | |||
23 | You need to have the snd-aloop kernel module loaded. The jackscanall | ||
24 | script should be run at boot time, put it into /etc/boot.d/. jackonall | ||
25 | should be called on user login. Probably don't need to run jackoffall on | ||
26 | user logout. | ||
27 | |||
28 | Alas ~/.asoundrc doesn't understand ~ or $HOME, or even "try the current | ||
29 | directory" it seems. So you have to hard code the path. Make sure your | ||
30 | ~/.asoundrc includes something like this (an example is included) - | ||
31 | |||
32 | </var/lib/JOAD/asoundrc> | ||
33 | |||
34 | jackscanall scans for your sound devices and creates | ||
35 | /var/lib/JOAD/asoundrc. Run jackscanall once as root to create that | ||
36 | file, and each time you need to change your devices. | ||
37 | |||
38 | jackonall starts up JACK and friends, and creates JACK devices for all | ||
39 | the things jackscanall found. It creates the cloop and ploop devices | ||
40 | that catch everything ALSA does. Then creates MIDI devices for all your | ||
41 | joysticks. | ||
42 | |||
43 | jackoffall closes down everything jackonall started up. | ||
44 | |||
45 | NOTE - Seems both ALSA and JACK are per user. So you need to run | ||
46 | jackonall and jackoffall for each user. | ||
47 | |||
48 | TODO - Leave it running, and hotplug ALSA / asound audio devices. | ||
49 | a2jmidid takes care of hotplugging MIDI devices. | ||
50 | Though I think I still need to deal with hotplugged joysticks. | ||
diff --git a/jackoffall b/jackoffall new file mode 100755 index 0000000..6c7cec2 --- /dev/null +++ b/jackoffall | |||
@@ -0,0 +1,146 @@ | |||
1 | #!/usr/bin/env luajit | ||
2 | |||
3 | |||
4 | local APT = {} | ||
5 | |||
6 | |||
7 | APT.readCmd = function(cmd) | ||
8 | local result = {} | ||
9 | local output = io.popen(cmd) | ||
10 | if nil ~= output then | ||
11 | for l in output:lines() do | ||
12 | table.insert(result, l) | ||
13 | end | ||
14 | end | ||
15 | return result | ||
16 | end | ||
17 | |||
18 | |||
19 | APT.exe = function(c) | ||
20 | local exe = {status = 0, result = '', log = true, cmd = c .. ' ', command = c} | ||
21 | |||
22 | function exe:log() | ||
23 | self.log = true | ||
24 | return self | ||
25 | end | ||
26 | function exe:Nice(c) | ||
27 | if nil == c then | ||
28 | self.cmd = 'ionice -c3 nice -n 19 ' .. self.cmd | ||
29 | else | ||
30 | self.cmd = self.cmd .. 'ionice -c3 nice -n 19 ' .. c .. ' ' | ||
31 | end | ||
32 | return self | ||
33 | end | ||
34 | function exe:timeout(c) | ||
35 | -- 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. | ||
36 | -- --kill-after means "send KILL after TERM fails. | ||
37 | if nil == c then | ||
38 | self.cmd = 'timeout --kill-after=10.0 --foreground 42.0s ' .. self.cmd | ||
39 | else | ||
40 | self.cmd = 'timeout --kill-after=10.0 --foreground ' .. c .. ' ' .. self.cmd | ||
41 | end | ||
42 | return self | ||
43 | end | ||
44 | function exe:also(c) | ||
45 | if nil == c then c = '' else c = ' ' .. c end | ||
46 | self.cmd = self.cmd .. ';' .. c .. ' ' | ||
47 | return self | ||
48 | end | ||
49 | function exe:And(c) | ||
50 | if nil == c then c = '' else c = ' ' .. c end | ||
51 | self.cmd = self.cmd .. '&&' .. c .. ' ' | ||
52 | return self | ||
53 | end | ||
54 | function exe:Or(c) | ||
55 | if nil == c then c = '' end | ||
56 | self.cmd = self.cmd .. '|| ' .. c .. ' ' | ||
57 | return self | ||
58 | end | ||
59 | function exe:noErr() | ||
60 | self.cmd = self.cmd .. '2>/dev/null ' | ||
61 | return self | ||
62 | end | ||
63 | function exe:wait(w) | ||
64 | self.cmd = self.cmd .. '&& touch ' .. w .. ' ' | ||
65 | return self | ||
66 | end | ||
67 | function exe:Do() | ||
68 | --[[ "The condition expression of a control structure can return any | ||
69 | value. Both false and nil are considered false. All values different | ||
70 | from nil and false are considered true (in particular, the number 0 | ||
71 | and the empty string are also true)." | ||
72 | says the docs, I beg to differ.]] | ||
73 | if true == self.log then D(" executing - <code>" .. self.cmd .. "</code>") end | ||
74 | --[[ Damn os.execute() | ||
75 | Lua 5.1 says it returns "a status code, which is system-dependent" | ||
76 | Lua 5.2 says it returns true/nil, "exit"/"signal", the status code. | ||
77 | I'm getting 7168 or 0. No idea what the fuck that is. | ||
78 | local ok, rslt, status = os.execute(s) | ||
79 | ]] | ||
80 | local f = APT.readCmd(self.cmd, 'r') | ||
81 | -- The last line will be the command's returned status, collect everything else in result. | ||
82 | self.status = '' -- Otherwise the result starts with 0. | ||
83 | self.result = '\n' | ||
84 | for i,l in ipairs(f) do | ||
85 | self.result = self.result .. l .. "\n" | ||
86 | end | ||
87 | f = APT.readCmd('echo "$?"', 'r') | ||
88 | for i,l in ipairs(f) do | ||
89 | self.status = tonumber(l) | ||
90 | if (137 == self.status) or (124 == self.status) then | ||
91 | print("timeout killed " .. self.status .. ' ' .. self.command) | ||
92 | E("timeout killed " .. self.status .. ' ' .. self.command) | ||
93 | elseif (0 ~= self.status) then | ||
94 | print("status |" .. self.status .. '| ' .. self.command) | ||
95 | E("status |" .. self.status .. '| ' .. self.command) | ||
96 | end | ||
97 | end | ||
98 | return self | ||
99 | end | ||
100 | function exe:fork(host) | ||
101 | 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 | ||
102 | self.cmd = '{ ' .. self.cmd .. '; } &' | ||
103 | if true == self.log then D(" forking - <code>" .. self.cmd .. "</code>") end | ||
104 | os.execute(self.cmd) | ||
105 | return self | ||
106 | end | ||
107 | return exe | ||
108 | end | ||
109 | |||
110 | |||
111 | APT.exe("a2j_control --stop"):Do() | ||
112 | APT.exe("sleep 2"):Do() | ||
113 | APT.exe("a2j_control --exit"):Do() | ||
114 | APT.exe("sleep 2"):Do() | ||
115 | APT.exe("killall -TERM alsa_out"):Do() | ||
116 | APT.exe("killall -TERM alsa_in"):Do() | ||
117 | APT.exe("killall -TERM zita-a2j"):Do() | ||
118 | APT.exe("killall -TERM zita-j2a"):Do() | ||
119 | APT.exe("killall -TERM aseqjoy"):Do() | ||
120 | APT.exe("killall -TERM jack-plumbing"):Do() | ||
121 | APT.exe("sleep 2"):Do() | ||
122 | APT.exe("jack_control stop"):Do() | ||
123 | APT.exe("sleep 2"):Do() | ||
124 | APT.exe("jack_control exit"):Do() | ||
125 | APT.exe("sleep 2"):Do() | ||
126 | --APT.exe("a2j_control --stop; a2j_control --exit"):Do() | ||
127 | --APT.exe("sleep 2"):Do() | ||
128 | APT.exe("killall -TERM jmcore"):Do() | ||
129 | APT.exe("pkill -TERM jackdbus; pkill -TERM a2jmidid"):Do() | ||
130 | APT.exe("killall -TERM a2jmidid"):Do() | ||
131 | APT.exe("killall -KILL jackdbus"):Do() | ||
132 | APT.exe("sleep 2"):Do() | ||
133 | APT.exe("killall -KILL a2jmidid"):Do() | ||
134 | APT.exe("pkill -KILL jackdbus; pkill -KILL a2jmidid"):Do() | ||
135 | APT.exe("sleep 2"):Do() | ||
136 | APT.exe("killall -KILL a2jmidid"):Do() | ||
137 | APT.exe("killall -KILL jackdbus"):Do() | ||
138 | APT.exe("sleep 2"):Do() | ||
139 | APT.exe("killall -KILL a2jmidid"):Do() | ||
140 | APT.exe("killall -KILL jackdbus"):Do() | ||
141 | APT.exe("sleep 2"):Do() | ||
142 | APT.exe("pkill -KILL jackdbus; pkill -KILL a2jmidid"):Do() | ||
143 | APT.exe("killall -TERM qjackctl"):Do() | ||
144 | |||
145 | -- Catia is python, and no easy way to kill it. | ||
146 | APT.exe("ps auxw | grep python"):Do() | ||
diff --git a/jackonall b/jackonall new file mode 100755 index 0000000..e786188 --- /dev/null +++ b/jackonall | |||
@@ -0,0 +1,237 @@ | |||
1 | #!/usr/bin/env luajit | ||
2 | |||
3 | --[[ | ||
4 | This is part of the JackOnAllDevices project, JOAD for short. | ||
5 | |||
6 | NOTE - Seems both ALSA and JACK are per user. | ||
7 | |||
8 | ]] | ||
9 | |||
10 | |||
11 | -- CHANGE these to suit. | ||
12 | --local GUI = 'qjackctl' | ||
13 | local GUI = 'catia' | ||
14 | local alias = { | ||
15 | {name='Screen', dev='HDMI9'}, | ||
16 | } | ||
17 | |||
18 | |||
19 | -- This APT stuff was copied from apt-panopticon. | ||
20 | local APT = {} | ||
21 | |||
22 | APT.readCmd = function(cmd) | ||
23 | local result = {} | ||
24 | local output = io.popen(cmd) | ||
25 | if nil ~= output then | ||
26 | for l in output:lines() do | ||
27 | table.insert(result, l) | ||
28 | end | ||
29 | end | ||
30 | return result | ||
31 | end | ||
32 | |||
33 | |||
34 | APT.exe = function(c) | ||
35 | local exe = {status = 0, result = '', lines = {}, log = true, cmd = c .. ' ', command = c} | ||
36 | |||
37 | function exe:log() | ||
38 | self.log = true | ||
39 | return self | ||
40 | end | ||
41 | function exe:Nice(c) | ||
42 | if nil == c then | ||
43 | self.cmd = 'ionice -c3 nice -n 19 ' .. self.cmd | ||
44 | else | ||
45 | self.cmd = self.cmd .. 'ionice -c3 nice -n 19 ' .. c .. ' ' | ||
46 | end | ||
47 | return self | ||
48 | end | ||
49 | function exe:timeout(c) | ||
50 | -- 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. | ||
51 | -- --kill-after means "send KILL after TERM fails. | ||
52 | if nil == c then | ||
53 | self.cmd = 'timeout --kill-after=10.0 --foreground 42.0s ' .. self.cmd | ||
54 | else | ||
55 | self.cmd = 'timeout --kill-after=10.0 --foreground ' .. c .. ' ' .. self.cmd | ||
56 | end | ||
57 | return self | ||
58 | end | ||
59 | function exe:also(c) | ||
60 | if nil == c then c = '' else c = ' ' .. c end | ||
61 | self.cmd = self.cmd .. ';' .. c .. ' ' | ||
62 | return self | ||
63 | end | ||
64 | function exe:And(c) | ||
65 | if nil == c then c = '' else c = ' ' .. c end | ||
66 | self.cmd = self.cmd .. '&&' .. c .. ' ' | ||
67 | return self | ||
68 | end | ||
69 | function exe:Or(c) | ||
70 | if nil == c then c = '' end | ||
71 | self.cmd = self.cmd .. '|| ' .. c .. ' ' | ||
72 | return self | ||
73 | end | ||
74 | function exe:noErr() | ||
75 | self.cmd = self.cmd .. '2>/dev/null ' | ||
76 | return self | ||
77 | end | ||
78 | function exe:wait(w) | ||
79 | self.cmd = self.cmd .. '&& touch ' .. w .. ' ' | ||
80 | return self | ||
81 | end | ||
82 | function exe:Do() | ||
83 | --[[ "The condition expression of a control structure can return any | ||
84 | value. Both false and nil are considered false. All values different | ||
85 | from nil and false are considered true (in particular, the number 0 | ||
86 | and the empty string are also true)." | ||
87 | says the docs, I beg to differ.]] | ||
88 | if true == self.log then D(" executing - <code>" .. self.cmd .. "</code>") end | ||
89 | --[[ Damn os.execute() | ||
90 | Lua 5.1 says it returns "a status code, which is system-dependent" | ||
91 | Lua 5.2 says it returns true/nil, "exit"/"signal", the status code. | ||
92 | I'm getting 7168 or 0. No idea what the fuck that is. | ||
93 | local ok, rslt, status = os.execute(s) | ||
94 | ]] | ||
95 | local f = APT.readCmd(self.cmd, 'r') | ||
96 | -- The last line will be the command's returned status, collect everything else in result. | ||
97 | self.status = '' -- Otherwise the result starts with 0. | ||
98 | self.result = '\n' | ||
99 | self.lines = f | ||
100 | for i,l in ipairs(f) do | ||
101 | self.result = self.result .. l .. "\n" | ||
102 | end | ||
103 | f = APT.readCmd('echo "$?"', 'r') | ||
104 | for i,l in ipairs(f) do | ||
105 | self.status = tonumber(l) | ||
106 | if (137 == self.status) or (124 == self.status) then | ||
107 | print("timeout killed " .. self.status .. ' ' .. self.command) | ||
108 | E("timeout killed " .. self.status .. ' ' .. self.command) | ||
109 | elseif (0 ~= self.status) then | ||
110 | print("status |" .. self.status .. '| ' .. self.command) | ||
111 | E("status |" .. self.status .. '| ' .. self.command) | ||
112 | end | ||
113 | end | ||
114 | return self | ||
115 | end | ||
116 | function exe:fork(host) | ||
117 | 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 | ||
118 | self.cmd = '{ ' .. self.cmd .. '; } &' | ||
119 | if true == self.log then D(" forking - <code>" .. self.cmd .. "</code>") end | ||
120 | os.execute(self.cmd) | ||
121 | return self | ||
122 | end | ||
123 | return exe | ||
124 | end | ||
125 | |||
126 | |||
127 | |||
128 | local Cards = {} | ||
129 | |||
130 | print('Scanning for audio devices.') | ||
131 | local cards = APT.exe('ls -d1 /proc/asound/card[0-9]*'):noErr():Do() | ||
132 | for i,l in ipairs(cards.lines) do | ||
133 | local f, e = io.open(l .. '/id', "r") | ||
134 | if nil == f then print("Could not open " .. l .. '/id') else | ||
135 | Cards[l] = {path = l, name = f:read("*a"):sub(1, -2), devs = {}, captureDevs = {}, playbackDevs = {}} | ||
136 | if "Loopback" ~= Cards[l]['name'] then | ||
137 | Cards[l]['capture'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*c*'):noErr():Do() | ||
138 | for j,c in ipairs(Cards[l]['capture'].lines) do | ||
139 | local n = c:match(".*pcm(%d+).*") | ||
140 | Cards[l]['captureDevs'][j] = n | ||
141 | Cards[l]['devs'][n] = n | ||
142 | print("\tFound capture device: " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['captureDevs'][j] .. ' ' .. n) | ||
143 | end | ||
144 | Cards[l]['playback'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*p*'):noErr():Do() | ||
145 | for j,p in ipairs(Cards[l]['playback'].lines) do | ||
146 | local n = p:match(".*pcm(%d+).*") | ||
147 | Cards[l]['playbackDevs'][j] = n | ||
148 | Cards[l]['devs'][n] = n | ||
149 | print("\tFound playback device: " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['playbackDevs'][j] .. ' ' .. n) | ||
150 | end | ||
151 | end | ||
152 | end | ||
153 | end | ||
154 | |||
155 | |||
156 | print('') | ||
157 | print("Start up JACK and friends.") | ||
158 | print("jack_control") | ||
159 | APT.exe('jack_control start'):Do() | ||
160 | APT.exe('jack_control ds alsa'):Do() | ||
161 | --jack_control dps device hw:RIG,0 | ||
162 | print("sleep 5") | ||
163 | APT.exe('sleep 5'):Do() | ||
164 | if nil ~= GUI then | ||
165 | print(GUI) | ||
166 | APT.exe(GUI):fork() | ||
167 | end | ||
168 | print("jack-plumbing") | ||
169 | APT.exe('jack-plumbing -q 2>/dev/null'):fork() | ||
170 | -- Bridge ALSA ports to JACK ports. Only handles MIDI. | ||
171 | -- 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)/ | ||
172 | --a2j_control actually starts a2jmidid. | ||
173 | ----a2jmidid -e -u & | ||
174 | -- 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. | ||
175 | print("a2j_control") | ||
176 | APT.exe('a2j_control --ehw && a2j_control --start'):Do() | ||
177 | print("sleep 2") | ||
178 | APT.exe('sleep 2'):Do() | ||
179 | print("") | ||
180 | |||
181 | |||
182 | --local AIN = "alsa_in" | ||
183 | local AIN = "zita-a2j" | ||
184 | --local AOUT = "alsa_out" | ||
185 | local AOUT = "zita-j2a" | ||
186 | |||
187 | print("Basic ALSA sound devices converted to JACK.") | ||
188 | for i,C in pairs(alias) do | ||
189 | print('HW playback: ' .. C['name'] .. '\tDEVICE: ' .. C['dev']) | ||
190 | APT.exe('alsa_out -j ' .. C['name'] .. ' -d ' .. C['dev']):fork() | ||
191 | APT.exe('sleep 1'):Do() | ||
192 | end | ||
193 | print("HW playback: cloop\tDEVICE: cloop") | ||
194 | -- No idea why, cloop wont work with zita-a2j. | ||
195 | APT.exe('alsa_in -j cloop -d cloop'):fork() | ||
196 | APT.exe('sleep 1'):Do() | ||
197 | APT.exe('jack_connect cloop:capture_1 system:playback_1'):Do() | ||
198 | APT.exe('jack_connect cloop:capture_2 system:playback_2'):Do() | ||
199 | print("HW playback: ploop\tDEVICE: ploop") | ||
200 | APT.exe('alsa_out -j ploop -d ploop'):fork() | ||
201 | APT.exe('sleep 1'):Do() | ||
202 | APT.exe('jack_connect system:capture_1 ploop:playback_1'):Do() | ||
203 | APT.exe('jack_connect system:capture_2 ploop:playback_2'):Do() | ||
204 | |||
205 | |||
206 | print("") | ||
207 | |||
208 | print("Rest of ALSA sound devices converted to JACK.") | ||
209 | for i,C in pairs(Cards) do | ||
210 | for j,c in ipairs(C['playbackDevs']) do | ||
211 | print("HW playback: " .. C['name'] .. "\tDEVICE: " .. C['playbackDevs'][j]) | ||
212 | APT.exe(AOUT .. ' -j ' .. C['name'] .. "_" .. C['playbackDevs'][j] .. '-in -d ' .. C['name'] .. C['playbackDevs'][j]):fork() | ||
213 | -- APT.exe('sleep 1'):Do() | ||
214 | end | ||
215 | for j,c in ipairs(C['captureDevs']) do | ||
216 | print("HW capture: " .. C['name'] .. "\tDEVICE: " .. C['captureDevs'][j]) | ||
217 | APT.exe(AIN .. ' -j ' .. C['name'] .. "_" .. C['captureDevs'][j] .. '-out -d ' .. C['name'] .. C['captureDevs'][j]):fork() | ||
218 | -- APT.exe('sleep 1'):Do() | ||
219 | end | ||
220 | end | ||
221 | print("") | ||
222 | |||
223 | print("Scanning for joysticks.") | ||
224 | local sticks = APT.exe('ls -1 /dev/input/js[0-9]*'):noErr():Do() | ||
225 | for i,l in ipairs(sticks.lines) do | ||
226 | print("aseqjoy " .. l) | ||
227 | -- Buttons switch to that numbered MIDI channel, defaults to 1. | ||
228 | -- Axis are mapped to MIDI controllers 10 - 15 | ||
229 | -- -r means to use high resolution MIDI values. | ||
230 | APT.exe('aseqjoy -d ' .. l:sub(-1,-1) .. ' -r'):fork() | ||
231 | -- print("sleep 1") | ||
232 | -- APT.exe('sleep 1'):Do() | ||
233 | end | ||
234 | print("") | ||
235 | |||
236 | |||
237 | |||
diff --git a/jackscanall b/jackscanall new file mode 100755 index 0000000..24d87df --- /dev/null +++ b/jackscanall | |||
@@ -0,0 +1,326 @@ | |||
1 | #!/usr/bin/env luajit | ||
2 | |||
3 | --[[ | ||
4 | This is part of the JackOnAllDevices project, JOAD for short. | ||
5 | |||
6 | The purpose is to scan for all ALSA / asound audio devices, and hook them | ||
7 | all up to JACK. Then it starts up JACK, and hooks up any joysticks it | ||
8 | finds as MIDI controllers. | ||
9 | |||
10 | |||
11 | Alas ~/.asoundrc doesn't understand ~ or $HOME, or even "try the current | ||
12 | directory" it seems. So you have to hard cade the path. Make sure your | ||
13 | ~/.asoundrc or /etc/asoundrc includes something like this - | ||
14 | |||
15 | </var/lib/JOAD/asoundrc> | ||
16 | |||
17 | |||
18 | Run this once as root to create that file, and each time you need to | ||
19 | change your devices. | ||
20 | |||
21 | |||
22 | TODO - Leave it running, and hotplug ALSA / asound audio devices. | ||
23 | a2jmidid takes care of hotplugging MIDI devices. | ||
24 | Though I think I still need to deal with hotplugged joysticks. | ||
25 | |||
26 | NOTE - Seems both ALSA and JACK are per user. | ||
27 | |||
28 | ]] | ||
29 | |||
30 | |||
31 | -- CHANGE these to suit. | ||
32 | local asoundrcPath = '/var/lib/JOAD' | ||
33 | local asoundrc = 'asoundrc' | ||
34 | |||
35 | |||
36 | |||
37 | -- This APT stuff was copied from apt-panopticon. | ||
38 | local APT = {} | ||
39 | |||
40 | APT.readCmd = function(cmd) | ||
41 | local result = {} | ||
42 | local output = io.popen(cmd) | ||
43 | if nil ~= output then | ||
44 | for l in output:lines() do | ||
45 | table.insert(result, l) | ||
46 | end | ||
47 | end | ||
48 | return result | ||
49 | end | ||
50 | |||
51 | |||
52 | APT.exe = function(c) | ||
53 | local exe = {status = 0, result = '', lines = {}, log = true, cmd = c .. ' ', command = c} | ||
54 | |||
55 | function exe:log() | ||
56 | self.log = true | ||
57 | return self | ||
58 | end | ||
59 | function exe:Nice(c) | ||
60 | if nil == c then | ||
61 | self.cmd = 'ionice -c3 nice -n 19 ' .. self.cmd | ||
62 | else | ||
63 | self.cmd = self.cmd .. 'ionice -c3 nice -n 19 ' .. c .. ' ' | ||
64 | end | ||
65 | return self | ||
66 | end | ||
67 | function exe:timeout(c) | ||
68 | -- 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. | ||
69 | -- --kill-after means "send KILL after TERM fails. | ||
70 | if nil == c then | ||
71 | self.cmd = 'timeout --kill-after=10.0 --foreground 42.0s ' .. self.cmd | ||
72 | else | ||
73 | self.cmd = 'timeout --kill-after=10.0 --foreground ' .. c .. ' ' .. self.cmd | ||
74 | end | ||
75 | return self | ||
76 | end | ||
77 | function exe:also(c) | ||
78 | if nil == c then c = '' else c = ' ' .. c end | ||
79 | self.cmd = self.cmd .. ';' .. c .. ' ' | ||
80 | return self | ||
81 | end | ||
82 | function exe:And(c) | ||
83 | if nil == c then c = '' else c = ' ' .. c end | ||
84 | self.cmd = self.cmd .. '&&' .. c .. ' ' | ||
85 | return self | ||
86 | end | ||
87 | function exe:Or(c) | ||
88 | if nil == c then c = '' end | ||
89 | self.cmd = self.cmd .. '|| ' .. c .. ' ' | ||
90 | return self | ||
91 | end | ||
92 | function exe:noErr() | ||
93 | self.cmd = self.cmd .. '2>/dev/null ' | ||
94 | return self | ||
95 | end | ||
96 | function exe:wait(w) | ||
97 | self.cmd = self.cmd .. '&& touch ' .. w .. ' ' | ||
98 | return self | ||
99 | end | ||
100 | function exe:Do() | ||
101 | --[[ "The condition expression of a control structure can return any | ||
102 | value. Both false and nil are considered false. All values different | ||
103 | from nil and false are considered true (in particular, the number 0 | ||
104 | and the empty string are also true)." | ||
105 | says the docs, I beg to differ.]] | ||
106 | if true == self.log then D(" executing - <code>" .. self.cmd .. "</code>") end | ||
107 | --[[ Damn os.execute() | ||
108 | Lua 5.1 says it returns "a status code, which is system-dependent" | ||
109 | Lua 5.2 says it returns true/nil, "exit"/"signal", the status code. | ||
110 | I'm getting 7168 or 0. No idea what the fuck that is. | ||
111 | local ok, rslt, status = os.execute(s) | ||
112 | ]] | ||
113 | local f = APT.readCmd(self.cmd, 'r') | ||
114 | -- The last line will be the command's returned status, collect everything else in result. | ||
115 | self.status = '' -- Otherwise the result starts with 0. | ||
116 | self.result = '\n' | ||
117 | self.lines = f | ||
118 | for i,l in ipairs(f) do | ||
119 | self.result = self.result .. l .. "\n" | ||
120 | end | ||
121 | f = APT.readCmd('echo "$?"', 'r') | ||
122 | for i,l in ipairs(f) do | ||
123 | self.status = tonumber(l) | ||
124 | if (137 == self.status) or (124 == self.status) then | ||
125 | print("timeout killed " .. self.status .. ' ' .. self.command) | ||
126 | E("timeout killed " .. self.status .. ' ' .. self.command) | ||
127 | elseif (0 ~= self.status) then | ||
128 | print("status |" .. self.status .. '| ' .. self.command) | ||
129 | E("status |" .. self.status .. '| ' .. self.command) | ||
130 | end | ||
131 | end | ||
132 | return self | ||
133 | end | ||
134 | function exe:fork(host) | ||
135 | 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 | ||
136 | self.cmd = '{ ' .. self.cmd .. '; } &' | ||
137 | if true == self.log then D(" forking - <code>" .. self.cmd .. "</code>") end | ||
138 | os.execute(self.cmd) | ||
139 | return self | ||
140 | end | ||
141 | return exe | ||
142 | end | ||
143 | |||
144 | |||
145 | |||
146 | local Cards = {} | ||
147 | |||
148 | print('Scanning for audio devices.') | ||
149 | local cards = APT.exe('ls -d1 /proc/asound/card[0-9]*'):noErr():Do() | ||
150 | for i,l in ipairs(cards.lines) do | ||
151 | local f, e = io.open(l .. '/id', "r") | ||
152 | if nil == f then print("Could not open " .. l .. '/id') else | ||
153 | Cards[l] = {path = l, name = f:read("*a"):sub(1, -2), devs = {}, captureDevs = {}, playbackDevs = {}} | ||
154 | if "Loopback" ~= Cards[l]['name'] then | ||
155 | Cards[l]['capture'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*c*'):noErr():Do() | ||
156 | for j,c in ipairs(Cards[l]['capture'].lines) do | ||
157 | local n = c:match(".*pcm(%d+).*") | ||
158 | Cards[l]['captureDevs'][j] = n | ||
159 | Cards[l]['devs'][n] = n | ||
160 | print("\tFound capture device: " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['captureDevs'][j] .. ' ' .. n) | ||
161 | end | ||
162 | Cards[l]['playback'] = APT.exe('ls -d1 ' .. l .. '/pcm[0-9]*p*'):noErr():Do() | ||
163 | for j,p in ipairs(Cards[l]['playback'].lines) do | ||
164 | local n = p:match(".*pcm(%d+).*") | ||
165 | Cards[l]['playbackDevs'][j] = n | ||
166 | Cards[l]['devs'][n] = n | ||
167 | print("\tFound playback device: " .. Cards[l]['name'] .. "\tDEVICE: " .. Cards[l]['playbackDevs'][j] .. ' ' .. n) | ||
168 | end | ||
169 | end | ||
170 | end | ||
171 | end | ||
172 | |||
173 | APT.exe('mkdir -p ' .. asoundrcPath):Do() | ||
174 | local a, e = io.open(asoundrcPath .. '/' .. asoundrc, "w") | ||
175 | if nil == a then print("Could not open " .. asoundrcPath .. '/' .. asoundrc) else | ||
176 | for i,C in pairs(Cards) do | ||
177 | for j,c in pairs(C['devs']) do | ||
178 | a:write("pcm." .. C['name'] .. j .. " {\n") | ||
179 | a:write(" type hw\n") | ||
180 | a:write(" card " .. C['name'] .. "\n") | ||
181 | a:write(" device " .. C['devs'][j] .. "\n") | ||
182 | a:write("}\n") | ||
183 | a:write("ctl." .. C['name'] .. j .. " {\n") | ||
184 | a:write(" type hw\n") | ||
185 | a:write(" card " .. C['name'] .. "\n") | ||
186 | a:write(" device " .. C['devs'][j] .. "\n") | ||
187 | a:write("}\n\n") | ||
188 | end | ||
189 | end | ||
190 | a:write([[ | ||
191 | ################################################################################################################################# | ||
192 | |||
193 | # The complex way - https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge | ||
194 | |||
195 | # More custom version, but it didn't work for me. | ||
196 | # hardware 0,0 : used for ALSA playback | ||
197 | #pcm.loophw00 { | ||
198 | # type hw | ||
199 | # card Loopback | ||
200 | # device 0 | ||
201 | # subdevice 0 | ||
202 | # format S32_LE | ||
203 | # rate 48000 | ||
204 | #} | ||
205 | |||
206 | # playback PCM device: using loopback subdevice 0,0 | ||
207 | # Don't use a buffer size that is too small. Some apps | ||
208 | # won't like it and it will sound crappy | ||
209 | |||
210 | #pcm.amix { | ||
211 | # type dmix | ||
212 | # ipc_key 219345 | ||
213 | # slave { | ||
214 | # pcm loophw00 | ||
215 | ## period_size 4096 | ||
216 | ## periods 2 | ||
217 | # } | ||
218 | #} | ||
219 | |||
220 | # software volume | ||
221 | #pcm.asoftvol { | ||
222 | # type softvol | ||
223 | # slave.pcm "amix" | ||
224 | |||
225 | # control { name PCM } | ||
226 | |||
227 | # min_dB -51.0 | ||
228 | # max_dB 0.0 | ||
229 | #} | ||
230 | |||
231 | |||
232 | # for jack alsa_in: looped-back signal at other ends | ||
233 | #pcm.cloop { | ||
234 | # type hw | ||
235 | # card Loopback | ||
236 | # device 1 | ||
237 | # subdevice 0 | ||
238 | # format S32_LE | ||
239 | # rate 48000 | ||
240 | #} | ||
241 | |||
242 | # hardware 0,1 : used for ALSA capture | ||
243 | #pcm.loophw01 { | ||
244 | # type hw | ||
245 | # card Loopback | ||
246 | # device 0 | ||
247 | # subdevice 1 | ||
248 | # format S32_LE | ||
249 | # rate 48000 | ||
250 | #} | ||
251 | |||
252 | # for jack alsa_out: looped-back signal at other end | ||
253 | #pcm.ploop { | ||
254 | # type hw | ||
255 | # card Loopback | ||
256 | # device 1 | ||
257 | # subdevice 1 | ||
258 | # format S32_LE | ||
259 | # rate 48000 | ||
260 | #} | ||
261 | |||
262 | # duplex device combining our PCM devices defined above | ||
263 | #pcm.aduplex { | ||
264 | # type asym | ||
265 | # playback.pcm "asoftvol" | ||
266 | # capture.pcm "loophw01" | ||
267 | #} | ||
268 | |||
269 | # default device | ||
270 | #pcm.!default { | ||
271 | # type plug | ||
272 | # slave.pcm aduplex | ||
273 | |||
274 | # hint { | ||
275 | # show on | ||
276 | # description "Duplex Loopback" | ||
277 | # } | ||
278 | #} | ||
279 | |||
280 | |||
281 | |||
282 | # Generic method seems to work better. | ||
283 | # playback PCM device: using loopback subdevice 0,0 | ||
284 | pcm.amix { | ||
285 | type dmix | ||
286 | ipc_key 219345 | ||
287 | slave.pcm "hw:Loopback,0,0" | ||
288 | } | ||
289 | |||
290 | # capture PCM device: using loopback subdevice 0,1 | ||
291 | pcm.asnoop { | ||
292 | type dsnoop | ||
293 | ipc_key 219346 | ||
294 | slave.pcm "hw:Loopback,0,1" | ||
295 | } | ||
296 | |||
297 | # duplex device combining our PCM devices defined above | ||
298 | pcm.aduplex { | ||
299 | type asym | ||
300 | playback.pcm "amix" | ||
301 | capture.pcm "asnoop" | ||
302 | } | ||
303 | |||
304 | # ------------------------------------------------------ | ||
305 | # for jack alsa_in and alsa_out: looped-back signal at other ends | ||
306 | pcm.ploop { | ||
307 | type plug | ||
308 | slave.pcm "hw:Loopback,1,1" | ||
309 | } | ||
310 | |||
311 | pcm.cloop { | ||
312 | type dsnoop | ||
313 | ipc_key 219348 | ||
314 | slave.pcm "hw:Loopback,1,0" | ||
315 | } | ||
316 | |||
317 | # ------------------------------------------------------ | ||
318 | # default device | ||
319 | |||
320 | pcm.!default { | ||
321 | type plug | ||
322 | slave.pcm "aduplex" | ||
323 | } | ||
324 | ]]) | ||
325 | a:close() | ||
326 | end | ||