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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +45 -0
  4. package/CODE_OF_CONDUCT.md +27 -0
  5. package/CONTRIBUTING.md +131 -0
  6. package/README.md +119 -395
  7. package/SECURITY.md +48 -0
  8. package/bin/livepilot.js +41 -1
  9. package/livepilot/.Codex-plugin/plugin.json +8 -0
  10. package/livepilot/.claude-plugin/plugin.json +1 -1
  11. package/livepilot/commands/beat.md +18 -6
  12. package/livepilot/commands/sounddesign.md +6 -5
  13. package/livepilot/skills/livepilot-core/SKILL.md +30 -7
  14. package/livepilot/skills/livepilot-core/references/overview.md +1 -1
  15. package/livepilot/skills/livepilot-release/SKILL.md +7 -4
  16. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  17. package/m4l_device/LivePilot_Analyzer.maxpat +378 -4
  18. package/m4l_device/livepilot_bridge.js +413 -184
  19. package/mcp_server/__init__.py +1 -1
  20. package/mcp_server/connection.py +35 -0
  21. package/mcp_server/m4l_bridge.py +55 -5
  22. package/mcp_server/server.py +16 -29
  23. package/mcp_server/tools/_perception_engine.py +26 -3
  24. package/mcp_server/tools/_theory_engine.py +36 -5
  25. package/mcp_server/tools/analyzer.py +62 -17
  26. package/mcp_server/tools/automation.py +4 -1
  27. package/mcp_server/tools/devices.py +199 -10
  28. package/mcp_server/tools/generative.py +4 -1
  29. package/mcp_server/tools/harmony.py +16 -3
  30. package/mcp_server/tools/notes.py +4 -1
  31. package/mcp_server/tools/perception.py +27 -1
  32. package/mcp_server/tools/scenes.py +4 -1
  33. package/mcp_server/tools/theory.py +15 -7
  34. package/package.json +1 -1
  35. package/remote_script/LivePilot/__init__.py +1 -1
  36. package/remote_script/LivePilot/arrangement.py +13 -116
  37. package/remote_script/LivePilot/diagnostics.py +81 -5
  38. package/remote_script/LivePilot/router.py +22 -0
  39. package/remote_script/LivePilot/server.py +25 -13
  40. 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.8"});
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
- // Chunked reading: 4 params per batch
192
- var batch_size = 4;
192
+ var batch_size = 8;
193
193
  var current = 0;
194
194
 
195
195
  function read_batch() {
196
- var end = Math.min(current + batch_size, param_count);
197
- for (var i = current; i < end; i++) {
198
- cursor_b.goto(path + " parameters " + i);
199
- var p = {
200
- index: i,
201
- name: cursor_b.get("name").toString(),
202
- value: parseFloat(cursor_b.get("value")),
203
- min: parseFloat(cursor_b.get("min")),
204
- max: parseFloat(cursor_b.get("max")),
205
- is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
206
- automation_state: parseInt(cursor_b.get("automation_state")),
207
- state: parseInt(cursor_b.get("state"))
208
- };
209
- // state: 0=enabled, 1=disabled, 2=irrelevant
210
- // automation_state: 0=none, 1=active, 2=overridden
211
- params.push(p);
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
- if (current < param_count) {
216
- // Schedule next batch in 50ms
217
- var next_task = new Task(read_batch);
218
- next_task.schedule(50);
219
- } else {
220
- send_response({"track": track_idx, "device": device_idx, "params": params});
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
- // Same as get_params but also includes value_string and whether it's
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 = 4;
245
+ var batch_size = 8;
241
246
 
242
247
  function read_batch() {
243
- var end = Math.min(current + batch_size, param_count);
244
- for (var i = current; i < end; i++) {
245
- cursor_b.goto(path + " parameters " + i);
246
- params.push({
247
- index: i,
248
- name: cursor_b.get("name").toString(),
249
- value: parseFloat(cursor_b.get("value")),
250
- min: parseFloat(cursor_b.get("min")),
251
- max: parseFloat(cursor_b.get("max")),
252
- default_value: parseFloat(cursor_b.get("default_value")),
253
- is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
254
- value_string: String(cursor_b.call("str_for_value", parseFloat(cursor_b.get("value")))),
255
- automation_state: parseInt(cursor_b.get("automation_state")),
256
- state: parseInt(cursor_b.get("state"))
257
- });
258
- }
259
- current = end;
260
-
261
- if (current < param_count) {
262
- var next_task = new Task(read_batch);
263
- next_task.schedule(50);
264
- } else {
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
- "total_params": param_count,
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 = 4;
304
+ var batch_size = 8;
289
305
 
290
306
  function read_batch() {
291
- var end = Math.min(current + batch_size, param_count);
292
- for (var i = current; i < end; i++) {
293
- cursor_b.goto(path + " parameters " + i);
294
- var state = parseInt(cursor_b.get("automation_state"));
295
- // Only include params that HAVE automation (skip state=0)
296
- if (state > 0) {
297
- results.push({
298
- index: i,
299
- name: cursor_b.get("name").toString(),
300
- automation_state: state,
301
- // 1 = automation active, 2 = automation overridden (user moved knob)
302
- state_label: state === 1 ? "active" : "overridden"
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
- cursor_a.goto(path);
400
- var chain_count = cursor_a.getcount("chains");
401
- var chains = [];
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 dev_count = cursor_b.getcount("devices");
417
- for (var d = 0; d < dev_count; d++) {
418
- cursor_a.goto(chain_path + " devices " + d);
419
- chain.devices.push({
420
- index: d,
421
- name: cursor_a.get("name").toString(),
422
- class_name: cursor_a.get("class_name").toString(),
423
- is_active: parseInt(cursor_a.get("is_active")) === 1,
424
- param_count: cursor_a.getcount("parameters")
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
- send_response({"track": track_idx, "device": device_idx, "chains": chains});
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
- // Get CPU performance impact per track
435
- var results = [];
436
- cursor_a.goto("live_set");
437
- var track_count = cursor_a.getcount("tracks");
438
-
439
- for (var t = 0; t < track_count; t++) {
440
- cursor_b.goto("live_set tracks " + t);
441
- results.push({
442
- index: t,
443
- name: cursor_b.get("name").toString(),
444
- // performance_impact is 0.0-1.0 representing CPU load
445
- cpu: parseFloat(cursor_b.get("performance_impact") || 0)
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
- send_response({"tracks": results, "count": track_count});
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
- var result = "";
603
- var bytes = [];
604
- for (var i = 0; i < str.length; i++) {
605
- bytes.push(str.charCodeAt(i) & 0xFF);
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
- // Reconstruct file path spaces in path split into multiple OSC args
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 = 4;
1111
+ var batch_size = 8;
928
1112
 
929
1113
  function read_batch() {
930
- var end = Math.min(current + batch_size, param_count);
931
- for (var i = current; i < end; i++) {
932
- cursor_b.goto(path + " parameters " + i);
933
- var state = parseInt(cursor_b.get("state"));
934
- if (state !== 2) {
935
- params.push({
936
- index: i,
937
- name: cursor_b.get("name").toString(),
938
- display_value: String(cursor_b.call("str_for_value", parseFloat(cursor_b.get("value")))),
939
- value: parseFloat(cursor_b.get("value"))
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
- "params": params
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 into buffer~ via outlet 1.
987
- // The patch is expected to connect outlet 1 to a buffer~ / record~ rig.
988
- // Message: "capture_start <filename> <num_samples>"
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 = 4;
1275
+ var batch_size = 8;
1073
1276
 
1074
1277
  function read_batch() {
1075
- var end = Math.min(current + batch_size, param_count);
1076
- for (var i = current; i < end; i++) {
1077
- cursor_b.goto(path + " parameters " + i);
1078
- params.push({
1079
- index: i,
1080
- name: cursor_b.get("name").toString(),
1081
- value: parseFloat(cursor_b.get("value")),
1082
- min: parseFloat(cursor_b.get("min")),
1083
- max: parseFloat(cursor_b.get("max")),
1084
- default_value: parseFloat(cursor_b.get("default_value")),
1085
- is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
1086
- value_string: String(cursor_b.call("str_for_value", parseFloat(cursor_b.get("value"))))
1087
- });
1088
- }
1089
- current = end;
1090
-
1091
- if (current < param_count) {
1092
- var next_task = new Task(read_batch);
1093
- next_task.schedule(50);
1094
- } else {
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
- "class_name": class_name,
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.message,
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
+ }