-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathjalv.js
330 lines (291 loc) · 9.77 KB
/
jalv.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
/**
* jalv is the underliying host powering calvo.
* Each plugin is loaded on a new process and connected to previous/next plugins.
* This module contains process spawn methods.
* @module jalv
*/
const Layout = require("./layout");
const { spawn } = require("child_process");
const store = require("./store");
const PubSub = require("pubsub-js");
const { settings } = require("../settings");
// Each time a new plugin is seclected query jalv for controls info.
PubSub.subscribe("selectedPlugin", (msg, plugin) => {
addToQueue(plugin, "controls", "controls");
});
/**
* Spawns a plugin. It will execute jalv and load the plugin. ALl communcations can be reachead via the process now at plugin.process
*
* @param {plugin} plugin
* @param {*} rackIndex
* @returns The nodejs child process.
*/
async function spawn_plugin(plugin, rackIndex) {
const sleep = require("util").promisify(setTimeout);
let processSpawned = false;
// We need to loose the buffer to get a fast response:
// https://gitlab.com/drobilla/jalv/-/issues/7
// const process = spawn("stdbuf", [
// "-oL",
// "-eL",
// "jalv",
// "-p",
// "-t",
// "-n",
// `calvo_${store.app.APP_ID}_${plugin.info.instanceNumber}_${plugin.info.safeName}`,
// plugin.uri,
// ]);
const process = spawn("script", [
"-q",
"-c",
`jalv -p -t -n calvo_${store.app.APP_ID}_${plugin.info.instanceNumber}_${plugin.info.safeName} ${plugin.uri}`,
"/dev/null",
]);
// const process = spawn("unbuffer", [
// "jalv",
// "-p",
// "-t",
// "-n",
// `calvo_${store.app.APP_ID}_${plugin.info.instanceNumber}_${plugin.info.safeName}`,
// plugin.uri,
// ]);
process.stdout.setEncoding("utf8");
process.stdin.setEncoding("utf8");
// process.stdout.once("data", function (msg) {
// // https://github.com/drobilla/jalv/issues/37
// process.stdin.write("presets\n");
// processSpawned = true;
// });
var result = "";
process.stdout.on("data", function (msg) {
result += msg;
});
// process.stdout.on("data", function (msg) {
// store.wlog(`[#${rackIndex}] ${msg}`);
// });
// process.stderr.on("data", function (msg) {
// store.wlogError(`[#${rackIndex}] ${msg}`);
// });
// process.on("message", function (msg) {
// store.wlog(`[#${rackIndex}] ${msg}`);
// });
let retries = 0;
while (!processSpawned && retries < 20) {
if (result.includes(">")) {
processSpawned = true;
}
await sleep(10);
}
if (!processSpawned) {
store.wlogError("Could not load plugin");
return null;
}
process.stdin.write("presets\n");
1;
plugin.info.queue = {
set: [],
controls: [],
};
plugin.process = process;
plugin.info.processQueueInterval = setInterval(
() => processQueue(plugin),
store.app.SETTINGS.JALV_POLLING_RATE
);
return process;
}
/**
* Gets the control values for a jalv process.
*
* @param {plugin} plugin Plugin instance (from rack)
* @param {string} type Type of control to get('controls'|'monitor' )
* @returns the control value as json: ctrlName: ctrlValue
*/
function getControls(plugin, type) {
addToQueue(plugin, type, type);
}
/**
* On calvo, Jalv host is spawned as a child process and we take the values from stdout.
* To provide at least, a very basic monitoring, a queue system is necesary.
* The queue will 'tick' every x ms according to JALV_POLLING_RATE setting. each tick will process a command and wait for the response.
* Different commands have different priorities:
* SET = HIGH, controls = MID, monitors = LOW
* As monitors spams the stdout buffer it was neeeded to only call it when nothing else is needing the buffer.
* @see settings
*
* @param {plugin} plugin
* @returns null if no Selected plugin or the process is busy (wating for a response)
*/
async function processQueue(plugin) {
// If this plugin is not the selected. Do not do anything.
if (store.getSelectedPlugin() !== plugin) return;
if (!plugin) return;
// If still we havent got a stdout from last command, skip this tick.
if (plugin.process.busy) return;
// Top priority for SET commands
if (plugin.info.queue.set.length > 0) {
const result = await writeWait(plugin.process, plugin.info.queue.set[0]);
plugin.info.queue.set.shift();
// store.wlogDebug(JSON.stringify(result));
// Mid priority for GET controls
} else if (plugin.info.queue.controls.length > 0) {
const result = await writeWait(plugin.process, "controls");
plugin.info.queue.controls.shift();
plugin.info.controls = result;
store.wlogDebug(JSON.stringify(result));
store.notifySubscribers("pluginControlsChanged", plugin);
} else {
// At last, if nothing else is printing output, we can now get some monitor info.
if (
store.app.SETTINGS.JALV_MONITORING &&
plugin.ports.control.output.length > 0
) {
const result = await writeWait(plugin.process, "monitors");
// Sometimes we cannot get info and we get corrupted result, lets use the previous value for now.
if (result) {
plugin.info.monitors = result;
store.notifySubscribers("pluginMonitorsChanged", plugin);
}
}
}
}
/**
* Write to a process stdin and wait for a response on stdout.
* While waiting, it will set the process.busy flag, so the queue will not advance until this function is finished.
* *
* @param {pluginJalvProcess} process The plugin instance JALV process.
* @param {string} command The command to execute on JALV
* @param {number} [maxRetries=10] How many time retry before giving up.
* @param {number} [retriesWait=5] How many ms to wait before each retry.
* @returns A JSON file with the (parsed) output for the given command.
*/
async function writeWait(process, command, maxRetries = 40, retriesWait = 5) {
const sleep = require("util").promisify(setTimeout);
process.busy = true;
let done = false;
let retries = 0;
let result = "";
process.stdin.write(command + "\n");
process.stdout.on("data", function (msg) {
result += msg;
done = true;
});
while (!done && retries < maxRetries) {
retries++;
await sleep(retriesWait);
}
if (!done) {
store.wlogError("Error in write wait, for " + command);
process.busy = false;
process.stdout.removeAllListeners(["data"]);
return null;
}
// TODO: Recheck this. 'Once' event is giving truncated output
process.stdout.removeAllListeners(["data"]);
resultJSON = jalvStdoutToJSON(result, command);
process.busy = false;
return resultJSON;
}
/**
* Add a command to the plugin instance queue
*
* @param {plugin} plugin The plugin instance.
* @param {string} type Can be 'set, controls, monitors'
* @param {string} command command to execute.
* @returns null if no plugin is specified
*/
function addToQueue(plugin, type, command) {
if (!plugin) return;
plugin.info.queue[type].push(command);
}
/**
* Set a value on a control (in queue)
*
* @param {plugin} plugin Plugin
* @param {string} control Control name. Uses `symbol` property of LV2 spec.
* @param {number} value Value to assign.
* @returns null if no plugin is specified
*/
function setControl(plugin, control, value) {
if (!plugin) return;
const command = `set ${control.symbol} ${value}`;
addToQueue(plugin, "set", command);
}
/**
* Set a preset on a plugin process (add to queue)
*
* @param {plugin} plugin Plugin instance
* @param {number} index of preset as appears on the widget
* @returns null if fails.
*/
function setPreset(plugin, index) {
if (!plugin) return;
if (!plugin.presets[index]) return;
const uri = plugin.presets[index].uri;
if (!uri) {
store.wlogError(`No preset ${index} found`);
return;
}
const command = `preset ${uri}`;
addToQueue(plugin, "set", command);
getControls(plugin, "controls");
}
/**
* Formats and convert a JALV kvp stdout (CONTROL = VALUE) into a json object.
*
* @param {string} str JALV stoud to format
* @param {string} command the command invoked which resulted in this output.
* @returns a JSON object. key: value
*/
function jalvStdoutToJSON(str, command) {
const obj = { jalv_command: command };
let result = str.replace(">", "").trim();
result.split(/\r?\n/).forEach((line, index) => {
if (index === 0) {
return;
}
const kvp = line.split("=");
const k = kvp[0].toString().replace(">", "").trim();
const v = kvp[1].replace(">", "");
obj[k] = v;
});
return obj;
}
/**
* Kills the plugin process.
*
* @param {pluginProcess} process
* @param {number} rackIndex
*/
function kill_plugin(plugin, rackIndex) {
try {
clearInterval(plugin.info.processQueueInterval);
// plugin.process.kill();
// Killing the process leaves JACK hanging, is better to send the key combination of Jalv to gracefully remove the plugin.
write(plugin.process, "\x04");
} catch (error) {
store.wlogError(`[#${rackIndex}] ${error}`);
}
}
/**
* Writes a message to a plugin process.
* @param {jalvProcess} process The jalv process to write into. (plugin.process)
* @param {string} msg The Jalv prompt supports several commands for interactive control.
* @example <caption>JALV Commands</caption>
* help Display help message
* controls Print settable control values
* monitors Print output control values
* presets Print available presets
* preset URI Set preset
* set INDEX VALUE Set control value by port index
* set SYMBOL VALUE Set control value by symbol
* SYMBOL = VALUE Set control value by symbol
*/
function write(process, msg) {
process.stdin.write(msg + "\n");
}
exports.spawn_plugin = spawn_plugin;
exports.kill_plugin = kill_plugin;
exports.write = write;
exports.getControls = getControls;
exports.setControl = setControl;
exports.setPreset = setPreset;