livepilot 1.9.8 → 1.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +45 -0
- package/CODE_OF_CONDUCT.md +27 -0
- package/CONTRIBUTING.md +131 -0
- package/README.md +119 -395
- package/SECURITY.md +48 -0
- package/bin/livepilot.js +41 -1
- package/livepilot/.Codex-plugin/plugin.json +8 -0
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/commands/beat.md +18 -6
- package/livepilot/commands/sounddesign.md +6 -5
- package/livepilot/skills/livepilot-core/SKILL.md +30 -7
- package/livepilot/skills/livepilot-core/references/overview.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +7 -4
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/LivePilot_Analyzer.maxpat +378 -4
- package/m4l_device/livepilot_bridge.js +413 -184
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/connection.py +35 -0
- package/mcp_server/m4l_bridge.py +55 -5
- package/mcp_server/server.py +16 -29
- package/mcp_server/tools/_perception_engine.py +26 -3
- package/mcp_server/tools/_theory_engine.py +36 -5
- package/mcp_server/tools/analyzer.py +62 -17
- package/mcp_server/tools/automation.py +4 -1
- package/mcp_server/tools/devices.py +199 -10
- package/mcp_server/tools/generative.py +4 -1
- package/mcp_server/tools/harmony.py +16 -3
- package/mcp_server/tools/notes.py +4 -1
- package/mcp_server/tools/perception.py +27 -1
- package/mcp_server/tools/scenes.py +4 -1
- package/mcp_server/tools/theory.py +15 -7
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +13 -116
- package/remote_script/LivePilot/diagnostics.py +81 -5
- package/remote_script/LivePilot/router.py +22 -0
- package/remote_script/LivePilot/server.py +25 -13
- package/remote_script/LivePilot/tracks.py +17 -2
|
@@ -34,6 +34,7 @@ var detected_scale = "";
|
|
|
34
34
|
var capture_active = false;
|
|
35
35
|
var capture_timer = null;
|
|
36
36
|
var capture_filename = "";
|
|
37
|
+
var capture_file_path = "";
|
|
37
38
|
var current_sample_rate = 44100; // Updated by dspstate~ via inlet 1
|
|
38
39
|
|
|
39
40
|
// Base64 encoding table
|
|
@@ -67,7 +68,7 @@ function anything() {
|
|
|
67
68
|
// OSC messages arrive as messagename — strip leading / if present
|
|
68
69
|
var cmd = messagename;
|
|
69
70
|
if (cmd.charAt(0) === "/") cmd = cmd.substring(1);
|
|
70
|
-
var args = arrayfromargs(arguments);
|
|
71
|
+
var args = _decode_arg_strings(arrayfromargs(arguments));
|
|
71
72
|
|
|
72
73
|
// Defer to low-priority thread for LiveAPI safety
|
|
73
74
|
var task = new Task(function() {
|
|
@@ -83,7 +84,7 @@ function anything() {
|
|
|
83
84
|
function dispatch(cmd, args) {
|
|
84
85
|
switch(cmd) {
|
|
85
86
|
case "ping":
|
|
86
|
-
send_response({"ok": true, "version": "1.9.
|
|
87
|
+
send_response({"ok": true, "version": "1.9.10"});
|
|
87
88
|
break;
|
|
88
89
|
case "get_params":
|
|
89
90
|
cmd_get_params(args);
|
|
@@ -188,36 +189,40 @@ function cmd_get_params(args) {
|
|
|
188
189
|
var param_count = cursor_a.getcount("parameters");
|
|
189
190
|
var params = [];
|
|
190
191
|
|
|
191
|
-
|
|
192
|
-
var batch_size = 4;
|
|
192
|
+
var batch_size = 8;
|
|
193
193
|
var current = 0;
|
|
194
194
|
|
|
195
195
|
function read_batch() {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
current = end;
|
|
196
|
+
try {
|
|
197
|
+
var end = Math.min(current + batch_size, param_count);
|
|
198
|
+
for (var i = current; i < end; i++) {
|
|
199
|
+
cursor_b.goto(path + " parameters " + i);
|
|
200
|
+
params.push({
|
|
201
|
+
index: i,
|
|
202
|
+
name: cursor_b.get("name").toString(),
|
|
203
|
+
value: parseFloat(cursor_b.get("value")),
|
|
204
|
+
min: parseFloat(cursor_b.get("min")),
|
|
205
|
+
max: parseFloat(cursor_b.get("max")),
|
|
206
|
+
is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
|
|
207
|
+
automation_state: parseInt(cursor_b.get("automation_state")),
|
|
208
|
+
state: parseInt(cursor_b.get("state"))
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
current = end;
|
|
214
212
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
213
|
+
if (current < param_count) {
|
|
214
|
+
var next_task = new Task(read_batch);
|
|
215
|
+
next_task.schedule(20);
|
|
216
|
+
} else {
|
|
217
|
+
send_response({"track": track_idx, "device": device_idx, "params": params});
|
|
218
|
+
}
|
|
219
|
+
} catch (e) {
|
|
220
|
+
send_response({
|
|
221
|
+
"error": "Failed reading parameter " + current + ": " + String(e),
|
|
222
|
+
"track": track_idx,
|
|
223
|
+
"device": device_idx,
|
|
224
|
+
"partial_params": params
|
|
225
|
+
});
|
|
221
226
|
}
|
|
222
227
|
}
|
|
223
228
|
|
|
@@ -226,8 +231,7 @@ function cmd_get_params(args) {
|
|
|
226
231
|
|
|
227
232
|
function cmd_get_hidden_params(args) {
|
|
228
233
|
// Returns ALL parameters including hidden ones not in ControlSurface API
|
|
229
|
-
//
|
|
230
|
-
// accessible from the standard API
|
|
234
|
+
// Includes value_string (human-readable) via str_for_value where safe
|
|
231
235
|
var track_idx = parseInt(args[0]);
|
|
232
236
|
var device_idx = parseInt(args[1]);
|
|
233
237
|
var path = build_device_path(track_idx, device_idx);
|
|
@@ -235,39 +239,51 @@ function cmd_get_hidden_params(args) {
|
|
|
235
239
|
cursor_a.goto(path);
|
|
236
240
|
var param_count = cursor_a.getcount("parameters");
|
|
237
241
|
var device_name = cursor_a.get("name").toString();
|
|
242
|
+
var device_class = cursor_a.get("class_name").toString();
|
|
238
243
|
var params = [];
|
|
239
244
|
var current = 0;
|
|
240
|
-
var batch_size =
|
|
245
|
+
var batch_size = 8;
|
|
241
246
|
|
|
242
247
|
function read_batch() {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
248
|
+
try {
|
|
249
|
+
var end = Math.min(current + batch_size, param_count);
|
|
250
|
+
for (var i = current; i < end; i++) {
|
|
251
|
+
cursor_b.goto(path + " parameters " + i);
|
|
252
|
+
var val = parseFloat(cursor_b.get("value"));
|
|
253
|
+
params.push({
|
|
254
|
+
index: i,
|
|
255
|
+
name: cursor_b.get("name").toString(),
|
|
256
|
+
value: val,
|
|
257
|
+
value_string: _safe_display_string(cursor_b, val, device_class),
|
|
258
|
+
min: parseFloat(cursor_b.get("min")),
|
|
259
|
+
max: parseFloat(cursor_b.get("max")),
|
|
260
|
+
default_value: parseFloat(cursor_b.get("default_value")),
|
|
261
|
+
is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
|
|
262
|
+
automation_state: parseInt(cursor_b.get("automation_state")),
|
|
263
|
+
state: parseInt(cursor_b.get("state"))
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
current = end;
|
|
267
|
+
|
|
268
|
+
if (current < param_count) {
|
|
269
|
+
var next_task = new Task(read_batch);
|
|
270
|
+
next_task.schedule(20);
|
|
271
|
+
} else {
|
|
272
|
+
send_response({
|
|
273
|
+
"track": track_idx,
|
|
274
|
+
"device": device_idx,
|
|
275
|
+
"device_name": device_name,
|
|
276
|
+
"total_params": param_count,
|
|
277
|
+
"params": params
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
} catch (e) {
|
|
265
281
|
send_response({
|
|
282
|
+
"error": "Failed reading parameter " + current + ": " + String(e),
|
|
266
283
|
"track": track_idx,
|
|
267
284
|
"device": device_idx,
|
|
268
285
|
"device_name": device_name,
|
|
269
|
-
"
|
|
270
|
-
"params": params
|
|
286
|
+
"partial_params": params
|
|
271
287
|
});
|
|
272
288
|
}
|
|
273
289
|
}
|
|
@@ -285,31 +301,40 @@ function cmd_get_auto_state(args) {
|
|
|
285
301
|
var param_count = cursor_a.getcount("parameters");
|
|
286
302
|
var results = [];
|
|
287
303
|
var current = 0;
|
|
288
|
-
var batch_size =
|
|
304
|
+
var batch_size = 8;
|
|
289
305
|
|
|
290
306
|
function read_batch() {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
307
|
+
try {
|
|
308
|
+
var end = Math.min(current + batch_size, param_count);
|
|
309
|
+
for (var i = current; i < end; i++) {
|
|
310
|
+
cursor_b.goto(path + " parameters " + i);
|
|
311
|
+
var state = parseInt(cursor_b.get("automation_state"));
|
|
312
|
+
if (state > 0) {
|
|
313
|
+
results.push({
|
|
314
|
+
index: i,
|
|
315
|
+
name: cursor_b.get("name").toString(),
|
|
316
|
+
automation_state: state,
|
|
317
|
+
state_label: state === 1 ? "active" : "overridden"
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
current = end;
|
|
322
|
+
|
|
323
|
+
if (current < param_count) {
|
|
324
|
+
var next_task = new Task(read_batch);
|
|
325
|
+
next_task.schedule(20);
|
|
326
|
+
} else {
|
|
327
|
+
send_response({
|
|
328
|
+
"track": track_idx,
|
|
329
|
+
"device": device_idx,
|
|
330
|
+
"total_params": param_count,
|
|
331
|
+
"automated_params": results,
|
|
332
|
+
"automated_count": results.length
|
|
303
333
|
});
|
|
304
334
|
}
|
|
305
|
-
}
|
|
306
|
-
current = end;
|
|
307
|
-
|
|
308
|
-
if (current < param_count) {
|
|
309
|
-
var next_task = new Task(read_batch);
|
|
310
|
-
next_task.schedule(50);
|
|
311
|
-
} else {
|
|
335
|
+
} catch (e) {
|
|
312
336
|
send_response({
|
|
337
|
+
"error": "Failed reading automation state at param " + current + ": " + String(e),
|
|
313
338
|
"track": track_idx,
|
|
314
339
|
"device": device_idx,
|
|
315
340
|
"total_params": param_count,
|
|
@@ -391,62 +416,73 @@ function walk_device(path, depth) {
|
|
|
391
416
|
}
|
|
392
417
|
|
|
393
418
|
function cmd_get_chains_deep(args) {
|
|
394
|
-
// Get detailed chain info including all devices in each chain
|
|
395
419
|
var track_idx = parseInt(args[0]);
|
|
396
420
|
var device_idx = parseInt(args[1]);
|
|
397
421
|
var path = build_device_path(track_idx, device_idx);
|
|
398
422
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
for (var c = 0; c < chain_count; c++) {
|
|
404
|
-
var chain_path = path + " chains " + c;
|
|
405
|
-
cursor_b.goto(chain_path);
|
|
406
|
-
var chain = {
|
|
407
|
-
index: c,
|
|
408
|
-
name: cursor_b.get("name").toString(),
|
|
409
|
-
volume: parseFloat(cursor_b.get("volume")),
|
|
410
|
-
panning: parseFloat(cursor_b.get("panning")),
|
|
411
|
-
mute: parseInt(cursor_b.get("mute")) === 1,
|
|
412
|
-
solo: parseInt(cursor_b.get("solo")) === 1,
|
|
413
|
-
devices: []
|
|
414
|
-
};
|
|
423
|
+
try {
|
|
424
|
+
cursor_a.goto(path);
|
|
425
|
+
var chain_count = cursor_a.getcount("chains");
|
|
426
|
+
var chains = [];
|
|
415
427
|
|
|
416
|
-
var
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
chain
|
|
420
|
-
index:
|
|
421
|
-
name:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
428
|
+
for (var c = 0; c < chain_count; c++) {
|
|
429
|
+
var chain_path = path + " chains " + c;
|
|
430
|
+
cursor_b.goto(chain_path);
|
|
431
|
+
var chain = {
|
|
432
|
+
index: c,
|
|
433
|
+
name: cursor_b.get("name").toString(),
|
|
434
|
+
volume: parseFloat(cursor_b.get("volume")),
|
|
435
|
+
panning: parseFloat(cursor_b.get("panning")),
|
|
436
|
+
mute: parseInt(cursor_b.get("mute")) === 1,
|
|
437
|
+
solo: parseInt(cursor_b.get("solo")) === 1,
|
|
438
|
+
devices: []
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
var dev_count = cursor_b.getcount("devices");
|
|
442
|
+
for (var d = 0; d < dev_count; d++) {
|
|
443
|
+
cursor_a.goto(chain_path + " devices " + d);
|
|
444
|
+
chain.devices.push({
|
|
445
|
+
index: d,
|
|
446
|
+
name: cursor_a.get("name").toString(),
|
|
447
|
+
class_name: cursor_a.get("class_name").toString(),
|
|
448
|
+
is_active: parseInt(cursor_a.get("is_active")) === 1,
|
|
449
|
+
param_count: cursor_a.getcount("parameters")
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
chains.push(chain);
|
|
426
453
|
}
|
|
427
|
-
chains.push(chain);
|
|
428
|
-
}
|
|
429
454
|
|
|
430
|
-
|
|
455
|
+
send_response({"track": track_idx, "device": device_idx, "chains": chains});
|
|
456
|
+
} catch (e) {
|
|
457
|
+
send_response({"error": "Failed reading chains: " + String(e), "track": track_idx, "device": device_idx});
|
|
458
|
+
}
|
|
431
459
|
}
|
|
432
460
|
|
|
433
461
|
function cmd_get_track_cpu(args) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
462
|
+
try {
|
|
463
|
+
var results = [];
|
|
464
|
+
cursor_a.goto("live_set");
|
|
465
|
+
var track_count = cursor_a.getcount("tracks");
|
|
466
|
+
|
|
467
|
+
for (var t = 0; t < track_count; t++) {
|
|
468
|
+
cursor_b.goto("live_set tracks " + t);
|
|
469
|
+
var cpu = 0;
|
|
470
|
+
try {
|
|
471
|
+
cpu = parseFloat(cursor_b.get("performance_impact") || 0);
|
|
472
|
+
} catch (e) {
|
|
473
|
+
cpu = -1;
|
|
474
|
+
}
|
|
475
|
+
results.push({
|
|
476
|
+
index: t,
|
|
477
|
+
name: cursor_b.get("name").toString(),
|
|
478
|
+
cpu: cpu
|
|
479
|
+
});
|
|
480
|
+
}
|
|
448
481
|
|
|
449
|
-
|
|
482
|
+
send_response({"tracks": results, "count": track_count});
|
|
483
|
+
} catch (e) {
|
|
484
|
+
send_response({"error": "Failed reading track CPU: " + String(e)});
|
|
485
|
+
}
|
|
450
486
|
}
|
|
451
487
|
|
|
452
488
|
function cmd_get_selected() {
|
|
@@ -599,12 +635,12 @@ function send_response(obj) {
|
|
|
599
635
|
}
|
|
600
636
|
|
|
601
637
|
function base64_encode(str) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
}
|
|
638
|
+
// UTF-8 encode first, then base64 encode the byte sequence.
|
|
639
|
+
// This preserves non-ASCII characters (accented names, CJK, emoji)
|
|
640
|
+
// that would otherwise be truncated by charCodeAt & 0xFF.
|
|
641
|
+
var bytes = _utf8_encode(str);
|
|
607
642
|
|
|
643
|
+
var result = "";
|
|
608
644
|
for (var i = 0; i < bytes.length; i += 3) {
|
|
609
645
|
var b0 = bytes[i];
|
|
610
646
|
var b1 = (i + 1 < bytes.length) ? bytes[i + 1] : 0;
|
|
@@ -623,6 +659,130 @@ function base64_encode(str) {
|
|
|
623
659
|
return result;
|
|
624
660
|
}
|
|
625
661
|
|
|
662
|
+
function _utf8_encode(str) {
|
|
663
|
+
// Convert a JavaScript string to a UTF-8 byte array.
|
|
664
|
+
// Handles codepoints U+0000..U+FFFF (BMP) which covers all
|
|
665
|
+
// characters Max JS can produce from LiveAPI get() calls.
|
|
666
|
+
var bytes = [];
|
|
667
|
+
for (var i = 0; i < str.length; i++) {
|
|
668
|
+
var c = str.charCodeAt(i);
|
|
669
|
+
if (c < 0x80) {
|
|
670
|
+
bytes.push(c);
|
|
671
|
+
} else if (c < 0x800) {
|
|
672
|
+
bytes.push(0xC0 | (c >> 6));
|
|
673
|
+
bytes.push(0x80 | (c & 0x3F));
|
|
674
|
+
} else {
|
|
675
|
+
bytes.push(0xE0 | (c >> 12));
|
|
676
|
+
bytes.push(0x80 | ((c >> 6) & 0x3F));
|
|
677
|
+
bytes.push(0x80 | (c & 0x3F));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return bytes;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function base64_decode(str) {
|
|
684
|
+
var clean = String(str || "").replace(/=/g, "");
|
|
685
|
+
var bytes = [];
|
|
686
|
+
|
|
687
|
+
for (var i = 0; i < clean.length; i += 4) {
|
|
688
|
+
var c0 = B64.indexOf(clean.charAt(i));
|
|
689
|
+
var c1 = B64.indexOf(clean.charAt(i + 1));
|
|
690
|
+
var c2 = (i + 2 < clean.length) ? B64.indexOf(clean.charAt(i + 2)) : -1;
|
|
691
|
+
var c3 = (i + 3 < clean.length) ? B64.indexOf(clean.charAt(i + 3)) : -1;
|
|
692
|
+
|
|
693
|
+
if (c0 < 0 || c1 < 0 || (c2 < 0 && i + 2 < clean.length) || (c3 < 0 && i + 3 < clean.length)) {
|
|
694
|
+
throw new Error("Invalid base64 input");
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
bytes.push(((c0 << 2) | (c1 >> 4)) & 0xFF);
|
|
698
|
+
if (c2 !== -1) {
|
|
699
|
+
bytes.push((((c1 & 15) << 4) | (c2 >> 2)) & 0xFF);
|
|
700
|
+
}
|
|
701
|
+
if (c3 !== -1) {
|
|
702
|
+
bytes.push((((c2 & 3) << 6) | c3) & 0xFF);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return bytes;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function _utf8_decode(bytes) {
|
|
710
|
+
// Convert a UTF-8 byte array back to a JavaScript string.
|
|
711
|
+
// Handles BMP codepoints which covers the text LivePilot exchanges.
|
|
712
|
+
var result = "";
|
|
713
|
+
for (var i = 0; i < bytes.length;) {
|
|
714
|
+
var b0 = bytes[i];
|
|
715
|
+
if (b0 < 0x80) {
|
|
716
|
+
result += String.fromCharCode(b0);
|
|
717
|
+
i += 1;
|
|
718
|
+
} else if ((b0 & 0xE0) === 0xC0 && i + 1 < bytes.length) {
|
|
719
|
+
var b1 = bytes[i + 1];
|
|
720
|
+
result += String.fromCharCode(((b0 & 0x1F) << 6) | (b1 & 0x3F));
|
|
721
|
+
i += 2;
|
|
722
|
+
} else if (i + 2 < bytes.length) {
|
|
723
|
+
var b2 = bytes[i + 1];
|
|
724
|
+
var b3 = bytes[i + 2];
|
|
725
|
+
result += String.fromCharCode(
|
|
726
|
+
((b0 & 0x0F) << 12) |
|
|
727
|
+
((b2 & 0x3F) << 6) |
|
|
728
|
+
(b3 & 0x3F)
|
|
729
|
+
);
|
|
730
|
+
i += 3;
|
|
731
|
+
} else {
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function _decode_b64_arg(arg) {
|
|
739
|
+
if (arg === null || arg === undefined) {
|
|
740
|
+
return arg;
|
|
741
|
+
}
|
|
742
|
+
var text = String(arg);
|
|
743
|
+
if (text.indexOf("b64:") !== 0) {
|
|
744
|
+
return arg;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
return _utf8_decode(base64_decode(text.substring(4)));
|
|
748
|
+
} catch (e) {
|
|
749
|
+
return arg;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function _decode_arg_strings(args) {
|
|
754
|
+
var decoded = [];
|
|
755
|
+
for (var i = 0; i < args.length; i++) {
|
|
756
|
+
decoded.push(_decode_b64_arg(args[i]));
|
|
757
|
+
}
|
|
758
|
+
return decoded;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function _to_posix_path(path) {
|
|
762
|
+
if (!path || path.length < 2) return path;
|
|
763
|
+
|
|
764
|
+
// Keep Windows-style drive paths unchanged.
|
|
765
|
+
if (path.length >= 3 && path.charAt(1) === ":" &&
|
|
766
|
+
(path.charAt(2) === "/" || path.charAt(2) === "\\")) {
|
|
767
|
+
return path;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
var colon = path.indexOf(":");
|
|
771
|
+
var slash = path.indexOf("/");
|
|
772
|
+
if (colon <= 0 || (slash !== -1 && colon > slash)) {
|
|
773
|
+
return path;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
var rest = path.substring(colon + 1);
|
|
777
|
+
if (rest.indexOf(":") !== -1) {
|
|
778
|
+
rest = rest.replace(/:/g, "/");
|
|
779
|
+
}
|
|
780
|
+
if (rest.charAt(0) !== "/") {
|
|
781
|
+
rest = "/" + rest.replace(/^[/\\]+/, "");
|
|
782
|
+
}
|
|
783
|
+
return rest;
|
|
784
|
+
}
|
|
785
|
+
|
|
626
786
|
// ── Phase 2: Sample Operations ────────────────────────────────────────
|
|
627
787
|
|
|
628
788
|
function cmd_get_clip_file_path(args) {
|
|
@@ -641,7 +801,7 @@ function cmd_get_clip_file_path(args) {
|
|
|
641
801
|
send_response({
|
|
642
802
|
"track": track_idx,
|
|
643
803
|
"clip": clip_idx,
|
|
644
|
-
"file_path": sample_path,
|
|
804
|
+
"file_path": _to_posix_path(sample_path),
|
|
645
805
|
"length": parseFloat(cursor_a.get("length")),
|
|
646
806
|
"name": cursor_a.get("name").toString()
|
|
647
807
|
});
|
|
@@ -653,7 +813,8 @@ function cmd_get_clip_file_path(args) {
|
|
|
653
813
|
function cmd_replace_simpler_sample(args) {
|
|
654
814
|
var track_idx = parseInt(args[0]);
|
|
655
815
|
var device_idx = parseInt(args[1]);
|
|
656
|
-
//
|
|
816
|
+
// Keep the join for backward compatibility with older clients.
|
|
817
|
+
// Current clients send file paths as a single decoded b64: arg.
|
|
657
818
|
var parts = [];
|
|
658
819
|
for (var i = 2; i < args.length; i++) parts.push(args[i].toString());
|
|
659
820
|
var file_path = parts.join(" ");
|
|
@@ -914,6 +1075,28 @@ function cmd_stop_scrub(args) {
|
|
|
914
1075
|
}
|
|
915
1076
|
}
|
|
916
1077
|
|
|
1078
|
+
// Device classes where str_for_value freezes Max JS (uncatchable hang).
|
|
1079
|
+
// Auto Filter is the confirmed case; others may exist.
|
|
1080
|
+
var STR_FOR_VALUE_BLACKLIST = ["AutoFilter"];
|
|
1081
|
+
|
|
1082
|
+
function _safe_display_string(cursor, val, device_class) {
|
|
1083
|
+
// Return the human-readable UI string for a parameter value.
|
|
1084
|
+
// Falls back to raw value string for blacklisted device classes
|
|
1085
|
+
// where str_for_value hangs Max's JS engine.
|
|
1086
|
+
if (STR_FOR_VALUE_BLACKLIST.indexOf(device_class) !== -1) {
|
|
1087
|
+
return String(val);
|
|
1088
|
+
}
|
|
1089
|
+
try {
|
|
1090
|
+
var result = cursor.call("str_for_value", val);
|
|
1091
|
+
if (result !== undefined && result !== null && String(result) !== "") {
|
|
1092
|
+
return String(result);
|
|
1093
|
+
}
|
|
1094
|
+
} catch(e) {
|
|
1095
|
+
// str_for_value not available on this parameter
|
|
1096
|
+
}
|
|
1097
|
+
return String(val);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
917
1100
|
function cmd_get_display_values(args) {
|
|
918
1101
|
var track_idx = parseInt(args[0]);
|
|
919
1102
|
var device_idx = parseInt(args[1]);
|
|
@@ -922,34 +1105,46 @@ function cmd_get_display_values(args) {
|
|
|
922
1105
|
cursor_a.goto(path);
|
|
923
1106
|
var param_count = cursor_a.getcount("parameters");
|
|
924
1107
|
var device_name = cursor_a.get("name").toString();
|
|
1108
|
+
var device_class = cursor_a.get("class_name").toString();
|
|
925
1109
|
var params = [];
|
|
926
1110
|
var current = 0;
|
|
927
|
-
var batch_size =
|
|
1111
|
+
var batch_size = 8;
|
|
928
1112
|
|
|
929
1113
|
function read_batch() {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1114
|
+
try {
|
|
1115
|
+
var end = Math.min(current + batch_size, param_count);
|
|
1116
|
+
for (var i = current; i < end; i++) {
|
|
1117
|
+
cursor_b.goto(path + " parameters " + i);
|
|
1118
|
+
var state = parseInt(cursor_b.get("state"));
|
|
1119
|
+
if (state !== 2) {
|
|
1120
|
+
var val = parseFloat(cursor_b.get("value"));
|
|
1121
|
+
params.push({
|
|
1122
|
+
index: i,
|
|
1123
|
+
name: cursor_b.get("name").toString(),
|
|
1124
|
+
display_value: _safe_display_string(cursor_b, val, device_class),
|
|
1125
|
+
value: val
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
current = end;
|
|
1130
|
+
if (current < param_count) {
|
|
1131
|
+
var next_task = new Task(read_batch);
|
|
1132
|
+
next_task.schedule(20);
|
|
1133
|
+
} else {
|
|
1134
|
+
send_response({
|
|
1135
|
+
"track": track_idx,
|
|
1136
|
+
"device": device_idx,
|
|
1137
|
+
"device_name": device_name,
|
|
1138
|
+
"params": params
|
|
940
1139
|
});
|
|
941
1140
|
}
|
|
942
|
-
}
|
|
943
|
-
current = end;
|
|
944
|
-
if (current < param_count) {
|
|
945
|
-
var next_task = new Task(read_batch);
|
|
946
|
-
next_task.schedule(50);
|
|
947
|
-
} else {
|
|
1141
|
+
} catch (e) {
|
|
948
1142
|
send_response({
|
|
1143
|
+
"error": "Failed reading parameter " + current + ": " + String(e),
|
|
949
1144
|
"track": track_idx,
|
|
950
1145
|
"device": device_idx,
|
|
951
1146
|
"device_name": device_name,
|
|
952
|
-
"
|
|
1147
|
+
"partial_params": params
|
|
953
1148
|
});
|
|
954
1149
|
}
|
|
955
1150
|
}
|
|
@@ -977,16 +1172,16 @@ function cmd_capture_audio(args) {
|
|
|
977
1172
|
+ pad2(d.getDate()) + "_"
|
|
978
1173
|
+ pad2(d.getHours()) + pad2(d.getMinutes()) + pad2(d.getSeconds());
|
|
979
1174
|
capture_filename = requested_name.length > 0 ? requested_name : ("capture_" + ts + ".wav");
|
|
1175
|
+
capture_file_path = _join_path(_get_patcher_dir(), capture_filename);
|
|
980
1176
|
|
|
981
1177
|
// Calculate sample count from duration and current sample rate
|
|
982
1178
|
var num_samples = Math.ceil((duration_ms / 1000.0) * current_sample_rate);
|
|
983
1179
|
|
|
984
1180
|
capture_active = true;
|
|
985
1181
|
|
|
986
|
-
// Tell the Max patch to start recording
|
|
987
|
-
//
|
|
988
|
-
|
|
989
|
-
outlet(1, "capture_start", capture_filename, num_samples);
|
|
1182
|
+
// Tell the Max patch to start recording the incoming stereo signal.
|
|
1183
|
+
// Message: "capture_start <absolute_path> <num_samples>"
|
|
1184
|
+
outlet(1, "capture_start", capture_file_path, num_samples);
|
|
990
1185
|
|
|
991
1186
|
// Set a timer to call cmd_capture_write_done after duration_ms.
|
|
992
1187
|
// If the buffer~ fires its bang first (via a connected message), that
|
|
@@ -1008,12 +1203,18 @@ function cmd_capture_write_done() {
|
|
|
1008
1203
|
}
|
|
1009
1204
|
|
|
1010
1205
|
var written = capture_filename;
|
|
1206
|
+
var written_path = capture_file_path;
|
|
1011
1207
|
capture_filename = "";
|
|
1208
|
+
capture_file_path = "";
|
|
1209
|
+
|
|
1210
|
+
// Stop the recorder before reporting completion so the file is flushed.
|
|
1211
|
+
outlet(1, "capture_stop");
|
|
1012
1212
|
|
|
1013
1213
|
// Send /capture_complete back to the MCP server via outlet 0.
|
|
1014
1214
|
var encoded = base64_encode(JSON.stringify({
|
|
1015
1215
|
"ok": true,
|
|
1016
1216
|
"file": written,
|
|
1217
|
+
"file_path": _to_posix_path(written_path),
|
|
1017
1218
|
"sample_rate": current_sample_rate
|
|
1018
1219
|
}));
|
|
1019
1220
|
outlet(0, "/capture_complete", encoded);
|
|
@@ -1036,9 +1237,11 @@ function cmd_capture_stop() {
|
|
|
1036
1237
|
|
|
1037
1238
|
capture_active = false;
|
|
1038
1239
|
var written = capture_filename;
|
|
1240
|
+
var written_path = capture_file_path;
|
|
1039
1241
|
capture_filename = "";
|
|
1242
|
+
capture_file_path = "";
|
|
1040
1243
|
|
|
1041
|
-
send_response({"ok": true, "stopped": true, "file": written});
|
|
1244
|
+
send_response({"ok": true, "stopped": true, "file": written, "file_path": _to_posix_path(written_path)});
|
|
1042
1245
|
}
|
|
1043
1246
|
|
|
1044
1247
|
function pad2(n) {
|
|
@@ -1069,37 +1272,48 @@ function cmd_get_plugin_params(args) {
|
|
|
1069
1272
|
var param_count = cursor_a.getcount("parameters");
|
|
1070
1273
|
var params = [];
|
|
1071
1274
|
var current = 0;
|
|
1072
|
-
var batch_size =
|
|
1275
|
+
var batch_size = 8;
|
|
1073
1276
|
|
|
1074
1277
|
function read_batch() {
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1278
|
+
try {
|
|
1279
|
+
var end = Math.min(current + batch_size, param_count);
|
|
1280
|
+
for (var i = current; i < end; i++) {
|
|
1281
|
+
cursor_b.goto(path + " parameters " + i);
|
|
1282
|
+
var val = parseFloat(cursor_b.get("value"));
|
|
1283
|
+
params.push({
|
|
1284
|
+
index: i,
|
|
1285
|
+
name: cursor_b.get("name").toString(),
|
|
1286
|
+
value: val,
|
|
1287
|
+
min: parseFloat(cursor_b.get("min")),
|
|
1288
|
+
max: parseFloat(cursor_b.get("max")),
|
|
1289
|
+
default_value: parseFloat(cursor_b.get("default_value")),
|
|
1290
|
+
is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
|
|
1291
|
+
value_string: String(val)
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
current = end;
|
|
1295
|
+
|
|
1296
|
+
if (current < param_count) {
|
|
1297
|
+
var next_task = new Task(read_batch);
|
|
1298
|
+
next_task.schedule(20);
|
|
1299
|
+
} else {
|
|
1300
|
+
send_response({
|
|
1301
|
+
"track": track_idx,
|
|
1302
|
+
"device": device_idx,
|
|
1303
|
+
"name": device_name,
|
|
1304
|
+
"class_name": class_name,
|
|
1305
|
+
"is_plugin": true,
|
|
1306
|
+
"parameter_count": param_count,
|
|
1307
|
+
"parameters": params
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
} catch (e) {
|
|
1095
1311
|
send_response({
|
|
1312
|
+
"error": "Failed reading plugin param " + current + ": " + String(e),
|
|
1096
1313
|
"track": track_idx,
|
|
1097
1314
|
"device": device_idx,
|
|
1098
1315
|
"name": device_name,
|
|
1099
|
-
"
|
|
1100
|
-
"is_plugin": true,
|
|
1101
|
-
"parameter_count": param_count,
|
|
1102
|
-
"parameters": params
|
|
1316
|
+
"partial_params": params
|
|
1103
1317
|
});
|
|
1104
1318
|
}
|
|
1105
1319
|
}
|
|
@@ -1133,13 +1347,10 @@ function cmd_map_plugin_param(args) {
|
|
|
1133
1347
|
return;
|
|
1134
1348
|
}
|
|
1135
1349
|
|
|
1136
|
-
// Navigate to the parameter and read its name
|
|
1137
|
-
cursor_b.goto(path + " parameters " + param_idx);
|
|
1138
|
-
var param_name = cursor_b.get("name").toString();
|
|
1139
|
-
|
|
1140
|
-
// Select the parameter — this is how Ableton's Configure mode works
|
|
1141
|
-
// via LiveAPI. The parameter becomes visible in the device's macro panel.
|
|
1142
1350
|
try {
|
|
1351
|
+
cursor_b.goto(path + " parameters " + param_idx);
|
|
1352
|
+
var param_name = cursor_b.get("name").toString();
|
|
1353
|
+
|
|
1143
1354
|
cursor_a.set("selected_parameter", param_idx);
|
|
1144
1355
|
cursor_a.call("store_chosen_bank");
|
|
1145
1356
|
send_response({
|
|
@@ -1149,9 +1360,8 @@ function cmd_map_plugin_param(args) {
|
|
|
1149
1360
|
});
|
|
1150
1361
|
} catch(e) {
|
|
1151
1362
|
send_response({
|
|
1152
|
-
"error": "Failed to map parameter: " + e
|
|
1153
|
-
"parameter_index": param_idx
|
|
1154
|
-
"parameter_name": param_name
|
|
1363
|
+
"error": "Failed to map parameter: " + String(e),
|
|
1364
|
+
"parameter_index": param_idx
|
|
1155
1365
|
});
|
|
1156
1366
|
}
|
|
1157
1367
|
}
|
|
@@ -1227,3 +1437,22 @@ function build_device_path(track_idx, device_idx) {
|
|
|
1227
1437
|
return "live_set tracks " + track_idx + " devices " + device_idx;
|
|
1228
1438
|
}
|
|
1229
1439
|
}
|
|
1440
|
+
|
|
1441
|
+
function _get_patcher_dir() {
|
|
1442
|
+
try {
|
|
1443
|
+
var filepath = this.patcher && this.patcher.filepath ? this.patcher.filepath.toString() : "";
|
|
1444
|
+
if (!filepath) return "";
|
|
1445
|
+
var slash = Math.max(filepath.lastIndexOf("/"), filepath.lastIndexOf("\\"));
|
|
1446
|
+
if (slash < 0) return "";
|
|
1447
|
+
return filepath.substring(0, slash + 1);
|
|
1448
|
+
} catch (e) {
|
|
1449
|
+
return "";
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function _join_path(dir, file) {
|
|
1454
|
+
if (!dir) return file;
|
|
1455
|
+
var last = dir.charAt(dir.length - 1);
|
|
1456
|
+
if (last !== "/" && last !== "\\") dir += "/";
|
|
1457
|
+
return dir + file;
|
|
1458
|
+
}
|