livepilot 1.4.4 → 1.5.0
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/CHANGELOG.md +151 -136
- package/README.md +136 -61
- package/m4l_device/BUILD_GUIDE.md +161 -0
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/LivePilot_Analyzer.maxpat +680 -0
- package/m4l_device/livepilot_bridge.js +942 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/connection.py +22 -16
- package/mcp_server/m4l_bridge.py +285 -0
- package/mcp_server/server.py +28 -3
- package/mcp_server/tools/analyzer.py +508 -0
- package/mcp_server/tools/clips.py +16 -12
- package/mcp_server/tools/devices.py +2 -2
- package/mcp_server/tools/mixing.py +50 -14
- package/mcp_server/tools/tracks.py +2 -2
- package/package.json +2 -3
- package/plugin/plugin.json +2 -2
- package/plugin/skills/livepilot-core/SKILL.md +52 -11
- package/plugin/skills/livepilot-core/references/overview.md +51 -5
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/mixing.py +90 -1
- package/.mcp.json +0 -9
- package/plugin/.mcp.json +0 -8
- package/plugin/skills/livepilot-core/references/device-atlas/plugins-synths.md +0 -2012
- package/plugin/skills/livepilot-core/references/device-atlas/presets-by-vibe.md +0 -727
- package/plugin/skills/livepilot-core/references/device-atlas/samples-and-irs.md +0 -598
- package/plugin/skills/livepilot-core/references/device-atlas/synths-m4l.md +0 -730
- package/plugin/skills/livepilot-core/references/device-atlas/utility-and-workflow.md +0 -843
|
@@ -0,0 +1,942 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LivePilot Analyzer Bridge — Max for Live JavaScript
|
|
3
|
+
*
|
|
4
|
+
* Handles LiveAPI commands from the MCP server via OSC/UDP.
|
|
5
|
+
* Provides deep LOM access: hidden parameters, automation state,
|
|
6
|
+
* nested rack introspection, key detection, and user action monitoring.
|
|
7
|
+
*
|
|
8
|
+
* Communication:
|
|
9
|
+
* UDP 9881 → this device (incoming commands)
|
|
10
|
+
* UDP 9880 ← this device (outgoing responses + spectral data)
|
|
11
|
+
*
|
|
12
|
+
* Design constraints (from AbletonBridge research):
|
|
13
|
+
* - Max 3 LiveAPI cursor objects (reuse via goto())
|
|
14
|
+
* - Chunk parameter reads: 4 per batch, 50ms delay
|
|
15
|
+
* - Base64 encode all JSON responses
|
|
16
|
+
* - Defer all LiveAPI operations via deferlow()
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
autowatch = 1;
|
|
20
|
+
inlets = 1;
|
|
21
|
+
outlets = 2; // 0: to udpsend (responses), 1: to status UI
|
|
22
|
+
|
|
23
|
+
// ── State ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
var cursor_a = null; // Primary LiveAPI cursor
|
|
26
|
+
var cursor_b = null; // Secondary cursor for nested walks
|
|
27
|
+
var initialized = false;
|
|
28
|
+
var pitch_history = []; // Rolling buffer for key detection
|
|
29
|
+
var MAX_PITCH_HISTORY = 128;
|
|
30
|
+
var detected_key = "";
|
|
31
|
+
var detected_scale = "";
|
|
32
|
+
|
|
33
|
+
// Base64 encoding table
|
|
34
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
35
|
+
|
|
36
|
+
// ── Initialization ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
function bang() {
|
|
39
|
+
// Called by live.thisdevice when device is ready
|
|
40
|
+
if (!initialized) {
|
|
41
|
+
cursor_a = new LiveAPI(null, "live_set");
|
|
42
|
+
cursor_b = new LiveAPI(null, "live_set");
|
|
43
|
+
initialized = true;
|
|
44
|
+
outlet(1, "status", "ready");
|
|
45
|
+
post("LivePilot Bridge: initialized\n");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Incoming OSC Message Dispatch ──────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function anything() {
|
|
52
|
+
// OSC messages arrive as messagename — strip leading / if present
|
|
53
|
+
var cmd = messagename;
|
|
54
|
+
if (cmd.charAt(0) === "/") cmd = cmd.substring(1);
|
|
55
|
+
var args = arrayfromargs(arguments);
|
|
56
|
+
|
|
57
|
+
// Defer to low-priority thread for LiveAPI safety
|
|
58
|
+
var task = new Task(function() {
|
|
59
|
+
try {
|
|
60
|
+
dispatch(cmd, args);
|
|
61
|
+
} catch(e) {
|
|
62
|
+
send_response({"error": e.message});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
task.schedule(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function dispatch(cmd, args) {
|
|
69
|
+
switch(cmd) {
|
|
70
|
+
case "ping":
|
|
71
|
+
send_response({"ok": true, "version": "1.5.0"});
|
|
72
|
+
break;
|
|
73
|
+
case "get_params":
|
|
74
|
+
cmd_get_params(args);
|
|
75
|
+
break;
|
|
76
|
+
case "get_hidden_params":
|
|
77
|
+
cmd_get_hidden_params(args);
|
|
78
|
+
break;
|
|
79
|
+
case "get_auto_state":
|
|
80
|
+
cmd_get_auto_state(args);
|
|
81
|
+
break;
|
|
82
|
+
case "walk_rack":
|
|
83
|
+
cmd_walk_rack(args);
|
|
84
|
+
break;
|
|
85
|
+
case "get_chains_deep":
|
|
86
|
+
cmd_get_chains_deep(args);
|
|
87
|
+
break;
|
|
88
|
+
case "get_track_cpu":
|
|
89
|
+
cmd_get_track_cpu(args);
|
|
90
|
+
break;
|
|
91
|
+
case "get_selected":
|
|
92
|
+
cmd_get_selected();
|
|
93
|
+
break;
|
|
94
|
+
case "get_key":
|
|
95
|
+
send_response({"key": detected_key, "scale": detected_scale, "confidence": pitch_history.length});
|
|
96
|
+
break;
|
|
97
|
+
// ── Phase 2: Sample Operations ──
|
|
98
|
+
case "get_clip_file_path":
|
|
99
|
+
cmd_get_clip_file_path(args);
|
|
100
|
+
break;
|
|
101
|
+
case "replace_simpler_sample":
|
|
102
|
+
cmd_replace_simpler_sample(args);
|
|
103
|
+
break;
|
|
104
|
+
case "get_simpler_slices":
|
|
105
|
+
cmd_get_simpler_slices(args);
|
|
106
|
+
break;
|
|
107
|
+
case "crop_simpler":
|
|
108
|
+
cmd_simpler_action(args, "crop");
|
|
109
|
+
break;
|
|
110
|
+
case "reverse_simpler":
|
|
111
|
+
cmd_simpler_action(args, "reverse");
|
|
112
|
+
break;
|
|
113
|
+
case "warp_simpler":
|
|
114
|
+
cmd_simpler_warp(args);
|
|
115
|
+
break;
|
|
116
|
+
// ── Phase 2: Warp Markers ──
|
|
117
|
+
case "get_warp_markers":
|
|
118
|
+
cmd_get_warp_markers(args);
|
|
119
|
+
break;
|
|
120
|
+
case "add_warp_marker":
|
|
121
|
+
cmd_add_warp_marker(args);
|
|
122
|
+
break;
|
|
123
|
+
case "move_warp_marker":
|
|
124
|
+
cmd_move_warp_marker(args);
|
|
125
|
+
break;
|
|
126
|
+
case "remove_warp_marker":
|
|
127
|
+
cmd_remove_warp_marker(args);
|
|
128
|
+
break;
|
|
129
|
+
// ── Phase 2: Clip & Display ──
|
|
130
|
+
case "scrub_clip":
|
|
131
|
+
cmd_scrub_clip(args);
|
|
132
|
+
break;
|
|
133
|
+
case "stop_scrub":
|
|
134
|
+
cmd_stop_scrub(args);
|
|
135
|
+
break;
|
|
136
|
+
case "get_display_values":
|
|
137
|
+
cmd_get_display_values(args);
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
send_response({"error": "Unknown command: " + cmd});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── Commands ───────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function cmd_get_params(args) {
|
|
147
|
+
// args: [track_index, device_index]
|
|
148
|
+
var track_idx = parseInt(args[0]);
|
|
149
|
+
var device_idx = parseInt(args[1]);
|
|
150
|
+
var path = build_device_path(track_idx, device_idx);
|
|
151
|
+
|
|
152
|
+
cursor_a.goto(path);
|
|
153
|
+
var param_count = cursor_a.getcount("parameters");
|
|
154
|
+
var params = [];
|
|
155
|
+
|
|
156
|
+
// Chunked reading: 4 params per batch
|
|
157
|
+
var batch_size = 4;
|
|
158
|
+
var current = 0;
|
|
159
|
+
|
|
160
|
+
function read_batch() {
|
|
161
|
+
var end = Math.min(current + batch_size, param_count);
|
|
162
|
+
for (var i = current; i < end; i++) {
|
|
163
|
+
cursor_b.goto(path + " parameters " + i);
|
|
164
|
+
var p = {
|
|
165
|
+
index: i,
|
|
166
|
+
name: cursor_b.get("name").toString(),
|
|
167
|
+
value: parseFloat(cursor_b.get("value")),
|
|
168
|
+
min: parseFloat(cursor_b.get("min")),
|
|
169
|
+
max: parseFloat(cursor_b.get("max")),
|
|
170
|
+
is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
|
|
171
|
+
automation_state: parseInt(cursor_b.get("automation_state")),
|
|
172
|
+
state: parseInt(cursor_b.get("state"))
|
|
173
|
+
};
|
|
174
|
+
// state: 0=enabled, 1=disabled, 2=irrelevant
|
|
175
|
+
// automation_state: 0=none, 1=active, 2=overridden
|
|
176
|
+
params.push(p);
|
|
177
|
+
}
|
|
178
|
+
current = end;
|
|
179
|
+
|
|
180
|
+
if (current < param_count) {
|
|
181
|
+
// Schedule next batch in 50ms
|
|
182
|
+
var next_task = new Task(read_batch);
|
|
183
|
+
next_task.schedule(50);
|
|
184
|
+
} else {
|
|
185
|
+
send_response({"track": track_idx, "device": device_idx, "params": params});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
read_batch();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function cmd_get_hidden_params(args) {
|
|
193
|
+
// Returns ALL parameters including hidden ones not in ControlSurface API
|
|
194
|
+
// Same as get_params but also includes value_string and whether it's
|
|
195
|
+
// accessible from the standard API
|
|
196
|
+
var track_idx = parseInt(args[0]);
|
|
197
|
+
var device_idx = parseInt(args[1]);
|
|
198
|
+
var path = build_device_path(track_idx, device_idx);
|
|
199
|
+
|
|
200
|
+
cursor_a.goto(path);
|
|
201
|
+
var param_count = cursor_a.getcount("parameters");
|
|
202
|
+
var device_name = cursor_a.get("name").toString();
|
|
203
|
+
var params = [];
|
|
204
|
+
var current = 0;
|
|
205
|
+
var batch_size = 4;
|
|
206
|
+
|
|
207
|
+
function read_batch() {
|
|
208
|
+
var end = Math.min(current + batch_size, param_count);
|
|
209
|
+
for (var i = current; i < end; i++) {
|
|
210
|
+
cursor_b.goto(path + " parameters " + i);
|
|
211
|
+
params.push({
|
|
212
|
+
index: i,
|
|
213
|
+
name: cursor_b.get("name").toString(),
|
|
214
|
+
value: parseFloat(cursor_b.get("value")),
|
|
215
|
+
min: parseFloat(cursor_b.get("min")),
|
|
216
|
+
max: parseFloat(cursor_b.get("max")),
|
|
217
|
+
default_value: parseFloat(cursor_b.get("default_value")),
|
|
218
|
+
is_quantized: parseInt(cursor_b.get("is_quantized")) === 1,
|
|
219
|
+
value_string: String(cursor_b.call("str_for_value", parseFloat(cursor_b.get("value")))),
|
|
220
|
+
automation_state: parseInt(cursor_b.get("automation_state")),
|
|
221
|
+
state: parseInt(cursor_b.get("state"))
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
current = end;
|
|
225
|
+
|
|
226
|
+
if (current < param_count) {
|
|
227
|
+
var next_task = new Task(read_batch);
|
|
228
|
+
next_task.schedule(50);
|
|
229
|
+
} else {
|
|
230
|
+
send_response({
|
|
231
|
+
"track": track_idx,
|
|
232
|
+
"device": device_idx,
|
|
233
|
+
"device_name": device_name,
|
|
234
|
+
"total_params": param_count,
|
|
235
|
+
"params": params
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
read_batch();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function cmd_get_auto_state(args) {
|
|
244
|
+
// args: [track_index, device_index]
|
|
245
|
+
var track_idx = parseInt(args[0]);
|
|
246
|
+
var device_idx = parseInt(args[1]);
|
|
247
|
+
var path = build_device_path(track_idx, device_idx);
|
|
248
|
+
|
|
249
|
+
cursor_a.goto(path);
|
|
250
|
+
var param_count = cursor_a.getcount("parameters");
|
|
251
|
+
var results = [];
|
|
252
|
+
var current = 0;
|
|
253
|
+
var batch_size = 4;
|
|
254
|
+
|
|
255
|
+
function read_batch() {
|
|
256
|
+
var end = Math.min(current + batch_size, param_count);
|
|
257
|
+
for (var i = current; i < end; i++) {
|
|
258
|
+
cursor_b.goto(path + " parameters " + i);
|
|
259
|
+
var state = parseInt(cursor_b.get("automation_state"));
|
|
260
|
+
// Only include params that HAVE automation (skip state=0)
|
|
261
|
+
if (state > 0) {
|
|
262
|
+
results.push({
|
|
263
|
+
index: i,
|
|
264
|
+
name: cursor_b.get("name").toString(),
|
|
265
|
+
automation_state: state,
|
|
266
|
+
// 1 = automation active, 2 = automation overridden (user moved knob)
|
|
267
|
+
state_label: state === 1 ? "active" : "overridden"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
current = end;
|
|
272
|
+
|
|
273
|
+
if (current < param_count) {
|
|
274
|
+
var next_task = new Task(read_batch);
|
|
275
|
+
next_task.schedule(50);
|
|
276
|
+
} else {
|
|
277
|
+
send_response({
|
|
278
|
+
"track": track_idx,
|
|
279
|
+
"device": device_idx,
|
|
280
|
+
"total_params": param_count,
|
|
281
|
+
"automated_params": results,
|
|
282
|
+
"automated_count": results.length
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
read_batch();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function cmd_walk_rack(args) {
|
|
291
|
+
// Recursively walk a device's chain tree (racks, drum pads, nested devices)
|
|
292
|
+
var track_idx = parseInt(args[0]);
|
|
293
|
+
var device_idx = parseInt(args[1]);
|
|
294
|
+
var path = build_device_path(track_idx, device_idx);
|
|
295
|
+
|
|
296
|
+
var tree = walk_device(path, 0);
|
|
297
|
+
send_response({"track": track_idx, "device": device_idx, "tree": tree});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function walk_device(path, depth) {
|
|
301
|
+
if (depth > 6) return {"error": "max depth reached"};
|
|
302
|
+
|
|
303
|
+
cursor_a.goto(path);
|
|
304
|
+
var result = {
|
|
305
|
+
name: cursor_a.get("name").toString(),
|
|
306
|
+
class_name: cursor_a.get("class_name").toString(),
|
|
307
|
+
is_active: parseInt(cursor_a.get("is_active")) === 1,
|
|
308
|
+
can_have_chains: parseInt(cursor_a.get("can_have_chains")) === 1,
|
|
309
|
+
can_have_drum_pads: parseInt(cursor_a.get("can_have_drum_pads")) === 1,
|
|
310
|
+
param_count: cursor_a.getcount("parameters")
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (result.can_have_chains) {
|
|
314
|
+
var chain_count = cursor_a.getcount("chains");
|
|
315
|
+
result.chains = [];
|
|
316
|
+
for (var c = 0; c < chain_count; c++) {
|
|
317
|
+
var chain_path = path + " chains " + c;
|
|
318
|
+
cursor_b.goto(chain_path);
|
|
319
|
+
var chain = {
|
|
320
|
+
index: c,
|
|
321
|
+
name: cursor_b.get("name").toString(),
|
|
322
|
+
devices: []
|
|
323
|
+
};
|
|
324
|
+
var dev_count = cursor_b.getcount("devices");
|
|
325
|
+
for (var d = 0; d < dev_count; d++) {
|
|
326
|
+
chain.devices.push(walk_device(chain_path + " devices " + d, depth + 1));
|
|
327
|
+
}
|
|
328
|
+
result.chains.push(chain);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (result.can_have_drum_pads) {
|
|
333
|
+
var pad_count = cursor_a.getcount("drum_pads");
|
|
334
|
+
result.drum_pads = [];
|
|
335
|
+
// Only report populated pads (up to 128, but most are empty)
|
|
336
|
+
for (var p = 0; p < Math.min(pad_count, 128); p++) {
|
|
337
|
+
var pad_path = path + " drum_pads " + p;
|
|
338
|
+
cursor_b.goto(pad_path);
|
|
339
|
+
var chain_count2 = cursor_b.getcount("chains");
|
|
340
|
+
if (chain_count2 > 0) {
|
|
341
|
+
result.drum_pads.push({
|
|
342
|
+
index: p,
|
|
343
|
+
note: parseInt(cursor_b.get("note")),
|
|
344
|
+
name: cursor_b.get("name").toString(),
|
|
345
|
+
chain_count: chain_count2
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function cmd_get_chains_deep(args) {
|
|
355
|
+
// Get detailed chain info including all devices in each chain
|
|
356
|
+
var track_idx = parseInt(args[0]);
|
|
357
|
+
var device_idx = parseInt(args[1]);
|
|
358
|
+
var path = build_device_path(track_idx, device_idx);
|
|
359
|
+
|
|
360
|
+
cursor_a.goto(path);
|
|
361
|
+
var chain_count = cursor_a.getcount("chains");
|
|
362
|
+
var chains = [];
|
|
363
|
+
|
|
364
|
+
for (var c = 0; c < chain_count; c++) {
|
|
365
|
+
var chain_path = path + " chains " + c;
|
|
366
|
+
cursor_b.goto(chain_path);
|
|
367
|
+
var chain = {
|
|
368
|
+
index: c,
|
|
369
|
+
name: cursor_b.get("name").toString(),
|
|
370
|
+
volume: parseFloat(cursor_b.get("volume")),
|
|
371
|
+
panning: parseFloat(cursor_b.get("panning")),
|
|
372
|
+
mute: parseInt(cursor_b.get("mute")) === 1,
|
|
373
|
+
solo: parseInt(cursor_b.get("solo")) === 1,
|
|
374
|
+
devices: []
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
var dev_count = cursor_b.getcount("devices");
|
|
378
|
+
for (var d = 0; d < dev_count; d++) {
|
|
379
|
+
cursor_a.goto(chain_path + " devices " + d);
|
|
380
|
+
chain.devices.push({
|
|
381
|
+
index: d,
|
|
382
|
+
name: cursor_a.get("name").toString(),
|
|
383
|
+
class_name: cursor_a.get("class_name").toString(),
|
|
384
|
+
is_active: parseInt(cursor_a.get("is_active")) === 1,
|
|
385
|
+
param_count: cursor_a.getcount("parameters")
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
chains.push(chain);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
send_response({"track": track_idx, "device": device_idx, "chains": chains});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function cmd_get_track_cpu(args) {
|
|
395
|
+
// Get CPU performance impact per track
|
|
396
|
+
var results = [];
|
|
397
|
+
cursor_a.goto("live_set");
|
|
398
|
+
var track_count = cursor_a.getcount("tracks");
|
|
399
|
+
|
|
400
|
+
for (var t = 0; t < track_count; t++) {
|
|
401
|
+
cursor_b.goto("live_set tracks " + t);
|
|
402
|
+
results.push({
|
|
403
|
+
index: t,
|
|
404
|
+
name: cursor_b.get("name").toString(),
|
|
405
|
+
// performance_impact is 0.0-1.0 representing CPU load
|
|
406
|
+
cpu: parseFloat(cursor_b.get("performance_impact") || 0)
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
send_response({"tracks": results, "count": track_count});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function cmd_get_selected() {
|
|
414
|
+
// What the user is currently focused on
|
|
415
|
+
cursor_a.goto("live_set view");
|
|
416
|
+
|
|
417
|
+
var result = {
|
|
418
|
+
selected_track: -1,
|
|
419
|
+
selected_track_name: "",
|
|
420
|
+
selected_scene: -1,
|
|
421
|
+
detail_clip: null,
|
|
422
|
+
appointed_device: null
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Selected track
|
|
426
|
+
try {
|
|
427
|
+
cursor_b.goto("live_set view selected_track");
|
|
428
|
+
result.selected_track_name = cursor_b.get("name").toString();
|
|
429
|
+
// Get track index by walking tracks
|
|
430
|
+
cursor_a.goto("live_set");
|
|
431
|
+
var tc = cursor_a.getcount("tracks");
|
|
432
|
+
for (var i = 0; i < tc; i++) {
|
|
433
|
+
cursor_a.goto("live_set tracks " + i);
|
|
434
|
+
if (cursor_a.get("name").toString() === result.selected_track_name) {
|
|
435
|
+
result.selected_track = i;
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch(e) {}
|
|
440
|
+
|
|
441
|
+
// Selected scene
|
|
442
|
+
try {
|
|
443
|
+
cursor_a.goto("live_set view selected_scene");
|
|
444
|
+
result.selected_scene = parseInt(cursor_a.get("scene_index") || -1);
|
|
445
|
+
} catch(e) {}
|
|
446
|
+
|
|
447
|
+
// Appointed device (blue hand)
|
|
448
|
+
try {
|
|
449
|
+
cursor_a.goto("live_set appointed_device");
|
|
450
|
+
result.appointed_device = {
|
|
451
|
+
name: cursor_a.get("name").toString(),
|
|
452
|
+
class_name: cursor_a.get("class_name").toString()
|
|
453
|
+
};
|
|
454
|
+
} catch(e) {}
|
|
455
|
+
|
|
456
|
+
send_response(result);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Key Detection ──────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
function pitch_in(midi_note, amplitude) {
|
|
462
|
+
// Called from sigmund~ via the Max patch
|
|
463
|
+
// midi_note is fractional (e.g., 69.02 for ~440 Hz)
|
|
464
|
+
if (amplitude < 0.01) return; // Skip silence
|
|
465
|
+
|
|
466
|
+
var rounded = Math.round(midi_note) % 12; // Pitch class 0-11
|
|
467
|
+
pitch_history.push(rounded);
|
|
468
|
+
if (pitch_history.length > MAX_PITCH_HISTORY) {
|
|
469
|
+
pitch_history.shift();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Only analyze when we have enough data
|
|
473
|
+
if (pitch_history.length >= 16) {
|
|
474
|
+
detect_key();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function detect_key() {
|
|
479
|
+
// Krumhansl-Schmuckler key-finding algorithm (simplified)
|
|
480
|
+
// Count occurrences of each pitch class
|
|
481
|
+
var counts = [0,0,0,0,0,0,0,0,0,0,0,0];
|
|
482
|
+
for (var i = 0; i < pitch_history.length; i++) {
|
|
483
|
+
counts[pitch_history[i]]++;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Major and minor profiles (Krumhansl)
|
|
487
|
+
var major = [6.35, 2.23, 3.48, 2.33, 4.38, 4.09, 2.52, 5.19, 2.39, 3.66, 2.29, 2.88];
|
|
488
|
+
var minor = [6.33, 2.68, 3.52, 5.38, 2.60, 3.53, 2.54, 4.75, 3.98, 2.69, 3.34, 3.17];
|
|
489
|
+
var note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
|
|
490
|
+
|
|
491
|
+
var best_corr = -999;
|
|
492
|
+
var best_key = 0;
|
|
493
|
+
var best_scale = "major";
|
|
494
|
+
|
|
495
|
+
// Test all 24 keys (12 major + 12 minor)
|
|
496
|
+
for (var k = 0; k < 12; k++) {
|
|
497
|
+
var rotated = [];
|
|
498
|
+
for (var n = 0; n < 12; n++) {
|
|
499
|
+
rotated.push(counts[(n + k) % 12]);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Correlate with major profile
|
|
503
|
+
var corr_major = correlate(rotated, major);
|
|
504
|
+
if (corr_major > best_corr) {
|
|
505
|
+
best_corr = corr_major;
|
|
506
|
+
best_key = k;
|
|
507
|
+
best_scale = "major";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Correlate with minor profile
|
|
511
|
+
var corr_minor = correlate(rotated, minor);
|
|
512
|
+
if (corr_minor > best_corr) {
|
|
513
|
+
best_corr = corr_minor;
|
|
514
|
+
best_key = k;
|
|
515
|
+
best_scale = "minor";
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
detected_key = note_names[best_key];
|
|
520
|
+
detected_scale = best_scale;
|
|
521
|
+
|
|
522
|
+
// Send to UI
|
|
523
|
+
outlet(1, "key", detected_key + " " + detected_scale);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function correlate(a, b) {
|
|
527
|
+
// Pearson correlation coefficient
|
|
528
|
+
var n = a.length;
|
|
529
|
+
var sum_a = 0, sum_b = 0, sum_ab = 0, sum_a2 = 0, sum_b2 = 0;
|
|
530
|
+
for (var i = 0; i < n; i++) {
|
|
531
|
+
sum_a += a[i];
|
|
532
|
+
sum_b += b[i];
|
|
533
|
+
sum_ab += a[i] * b[i];
|
|
534
|
+
sum_a2 += a[i] * a[i];
|
|
535
|
+
sum_b2 += b[i] * b[i];
|
|
536
|
+
}
|
|
537
|
+
var denom = Math.sqrt((n * sum_a2 - sum_a * sum_a) * (n * sum_b2 - sum_b * sum_b));
|
|
538
|
+
if (denom === 0) return 0;
|
|
539
|
+
return (n * sum_ab - sum_a * sum_b) / denom;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// ── Response Encoding ──────────────────────────────────────────────────────
|
|
543
|
+
|
|
544
|
+
function send_response(obj) {
|
|
545
|
+
var json = JSON.stringify(obj);
|
|
546
|
+
var encoded = base64_encode(json);
|
|
547
|
+
|
|
548
|
+
// Check if chunking needed (Max OSC packet limit ~8KB)
|
|
549
|
+
if (encoded.length < 1400) {
|
|
550
|
+
outlet(0, "/response", encoded);
|
|
551
|
+
} else {
|
|
552
|
+
// Split into chunks
|
|
553
|
+
var chunk_size = 1400;
|
|
554
|
+
var total = Math.ceil(encoded.length / chunk_size);
|
|
555
|
+
for (var i = 0; i < total; i++) {
|
|
556
|
+
var piece = encoded.substring(i * chunk_size, (i + 1) * chunk_size);
|
|
557
|
+
outlet(0, "/response_chunk", i, total, piece);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function base64_encode(str) {
|
|
563
|
+
var result = "";
|
|
564
|
+
var bytes = [];
|
|
565
|
+
for (var i = 0; i < str.length; i++) {
|
|
566
|
+
bytes.push(str.charCodeAt(i) & 0xFF);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
for (var i = 0; i < bytes.length; i += 3) {
|
|
570
|
+
var b0 = bytes[i];
|
|
571
|
+
var b1 = (i + 1 < bytes.length) ? bytes[i + 1] : 0;
|
|
572
|
+
var b2 = (i + 2 < bytes.length) ? bytes[i + 2] : 0;
|
|
573
|
+
|
|
574
|
+
result += B64.charAt(b0 >> 2);
|
|
575
|
+
result += B64.charAt(((b0 & 3) << 4) | (b1 >> 4));
|
|
576
|
+
if (i + 1 < bytes.length) {
|
|
577
|
+
result += B64.charAt(((b1 & 15) << 2) | (b2 >> 6));
|
|
578
|
+
}
|
|
579
|
+
if (i + 2 < bytes.length) {
|
|
580
|
+
result += B64.charAt(b2 & 63);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return result;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── Phase 2: Sample Operations ────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
function cmd_get_clip_file_path(args) {
|
|
590
|
+
var track_idx = parseInt(args[0]);
|
|
591
|
+
var clip_idx = parseInt(args[1]);
|
|
592
|
+
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
593
|
+
|
|
594
|
+
cursor_a.goto(path);
|
|
595
|
+
if (cursor_a.id === 0) {
|
|
596
|
+
send_response({"error": "No clip at track " + track_idx + " slot " + clip_idx});
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
var sample_path = cursor_a.get("file_path").toString();
|
|
602
|
+
send_response({
|
|
603
|
+
"track": track_idx,
|
|
604
|
+
"clip": clip_idx,
|
|
605
|
+
"file_path": sample_path,
|
|
606
|
+
"length": parseFloat(cursor_a.get("length")),
|
|
607
|
+
"name": cursor_a.get("name").toString()
|
|
608
|
+
});
|
|
609
|
+
} catch(e) {
|
|
610
|
+
send_response({"error": "Clip has no audio file (MIDI clip?): " + e.message});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function cmd_replace_simpler_sample(args) {
|
|
615
|
+
var track_idx = parseInt(args[0]);
|
|
616
|
+
var device_idx = parseInt(args[1]);
|
|
617
|
+
// Reconstruct file path — spaces in path split into multiple OSC args
|
|
618
|
+
var parts = [];
|
|
619
|
+
for (var i = 2; i < args.length; i++) parts.push(args[i].toString());
|
|
620
|
+
var file_path = parts.join(" ");
|
|
621
|
+
var path = build_device_path(track_idx, device_idx);
|
|
622
|
+
|
|
623
|
+
cursor_a.goto(path);
|
|
624
|
+
var class_name = cursor_a.get("class_name").toString();
|
|
625
|
+
|
|
626
|
+
if (class_name !== "OriginalSimpler") {
|
|
627
|
+
send_response({"error": "Device is " + class_name + ", not Simpler"});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
cursor_a.call("replace_sample", file_path);
|
|
633
|
+
send_response({
|
|
634
|
+
"track": track_idx,
|
|
635
|
+
"device": device_idx,
|
|
636
|
+
"sample_loaded": file_path,
|
|
637
|
+
"name": cursor_a.get("name").toString()
|
|
638
|
+
});
|
|
639
|
+
} catch(e) {
|
|
640
|
+
send_response({"error": "Failed to load sample: " + e.message + ". Ensure Simpler already has a sample loaded."});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function cmd_get_simpler_slices(args) {
|
|
645
|
+
var track_idx = parseInt(args[0]);
|
|
646
|
+
var device_idx = parseInt(args[1]);
|
|
647
|
+
var path = build_device_path(track_idx, device_idx);
|
|
648
|
+
|
|
649
|
+
cursor_a.goto(path);
|
|
650
|
+
if (cursor_a.get("class_name").toString() !== "OriginalSimpler") {
|
|
651
|
+
send_response({"error": "Not a Simpler device"});
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
var playback_mode = parseInt(cursor_a.get("playback_mode"));
|
|
656
|
+
|
|
657
|
+
// Sample metadata from SimplerDevice.sample child
|
|
658
|
+
var sample_rate = 0;
|
|
659
|
+
var length = 0;
|
|
660
|
+
try {
|
|
661
|
+
cursor_b.goto(path + " sample");
|
|
662
|
+
sample_rate = parseFloat(cursor_b.get("sample_rate"));
|
|
663
|
+
length = parseFloat(cursor_b.get("length"));
|
|
664
|
+
} catch(e) {}
|
|
665
|
+
|
|
666
|
+
// Slice points are on the Sample child object, property name is "slices"
|
|
667
|
+
var slices = [];
|
|
668
|
+
try {
|
|
669
|
+
cursor_b.goto(path + " sample");
|
|
670
|
+
var slice_data = cursor_b.get("slices");
|
|
671
|
+
if (slice_data && slice_data.length) {
|
|
672
|
+
for (var i = 0; i < slice_data.length; i++) {
|
|
673
|
+
slices.push({
|
|
674
|
+
index: i,
|
|
675
|
+
frame: parseInt(slice_data[i]),
|
|
676
|
+
seconds: sample_rate > 0 ? parseFloat(slice_data[i]) / sample_rate : 0
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch(e) {}
|
|
681
|
+
|
|
682
|
+
send_response({
|
|
683
|
+
"track": track_idx,
|
|
684
|
+
"device": device_idx,
|
|
685
|
+
"playback_mode": playback_mode,
|
|
686
|
+
"playback_mode_name": ["Classic", "One-Shot", "Slicing"][playback_mode] || "Unknown",
|
|
687
|
+
"sample_rate": sample_rate,
|
|
688
|
+
"sample_length_frames": length,
|
|
689
|
+
"sample_length_seconds": length / sample_rate,
|
|
690
|
+
"slice_count": slices.length,
|
|
691
|
+
"slices": slices
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function cmd_simpler_action(args, action) {
|
|
696
|
+
var track_idx = parseInt(args[0]);
|
|
697
|
+
var device_idx = parseInt(args[1]);
|
|
698
|
+
var path = build_device_path(track_idx, device_idx);
|
|
699
|
+
|
|
700
|
+
cursor_a.goto(path);
|
|
701
|
+
if (cursor_a.get("class_name").toString() !== "OriginalSimpler") {
|
|
702
|
+
send_response({"error": "Not a Simpler device"});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
cursor_a.call(action);
|
|
708
|
+
send_response({"track": track_idx, "device": device_idx, "action": action, "ok": true});
|
|
709
|
+
} catch(e) {
|
|
710
|
+
send_response({"error": action + " failed: " + e.message});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function cmd_simpler_warp(args) {
|
|
715
|
+
var track_idx = parseInt(args[0]);
|
|
716
|
+
var device_idx = parseInt(args[1]);
|
|
717
|
+
var beats = parseInt(args[2]);
|
|
718
|
+
var path = build_device_path(track_idx, device_idx);
|
|
719
|
+
|
|
720
|
+
cursor_a.goto(path);
|
|
721
|
+
if (cursor_a.get("class_name").toString() !== "OriginalSimpler") {
|
|
722
|
+
send_response({"error": "Not a Simpler device"});
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
try {
|
|
727
|
+
cursor_a.call("warp", beats);
|
|
728
|
+
send_response({"track": track_idx, "device": device_idx, "warped_to_beats": beats, "ok": true});
|
|
729
|
+
} catch(e) {
|
|
730
|
+
send_response({"error": "warp failed: " + e.message});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ── Phase 2: Warp Markers ─────────────────────────────────────────────
|
|
735
|
+
|
|
736
|
+
function cmd_get_warp_markers(args) {
|
|
737
|
+
var track_idx = parseInt(args[0]);
|
|
738
|
+
var clip_idx = parseInt(args[1]);
|
|
739
|
+
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
740
|
+
|
|
741
|
+
cursor_a.goto(path);
|
|
742
|
+
if (cursor_a.id === 0) {
|
|
743
|
+
send_response({"error": "No clip at track " + track_idx + " slot " + clip_idx});
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
// warp_markers is a dict property (not children) — returns JSON string
|
|
749
|
+
var raw = cursor_a.get("warp_markers");
|
|
750
|
+
var parsed;
|
|
751
|
+
try {
|
|
752
|
+
// get() may return string directly or as single-element array
|
|
753
|
+
parsed = JSON.parse(raw);
|
|
754
|
+
} catch(e1) {
|
|
755
|
+
try {
|
|
756
|
+
parsed = JSON.parse(raw[0]);
|
|
757
|
+
} catch(e2) {
|
|
758
|
+
send_response({"error": "Cannot parse warp_markers dict: raw=" + raw});
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
var markers = parsed["warp_markers"] || [];
|
|
763
|
+
var result = [];
|
|
764
|
+
for (var i = 0; i < markers.length; i++) {
|
|
765
|
+
result.push({
|
|
766
|
+
beat_time: markers[i]["beat_time"],
|
|
767
|
+
sample_time: markers[i]["sample_time"]
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
send_response({
|
|
771
|
+
"track": track_idx,
|
|
772
|
+
"clip": clip_idx,
|
|
773
|
+
"marker_count": result.length,
|
|
774
|
+
"markers": result
|
|
775
|
+
});
|
|
776
|
+
} catch(e) {
|
|
777
|
+
send_response({"error": "Cannot read warp markers (MIDI clip?): " + e.message});
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function cmd_add_warp_marker(args) {
|
|
782
|
+
var track_idx = parseInt(args[0]);
|
|
783
|
+
var clip_idx = parseInt(args[1]);
|
|
784
|
+
var beat_time = parseFloat(args[2]);
|
|
785
|
+
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
786
|
+
|
|
787
|
+
cursor_a.goto(path);
|
|
788
|
+
if (cursor_a.id === 0) {
|
|
789
|
+
send_response({"error": "No clip"});
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
cursor_a.call("add_warp_marker", beat_time);
|
|
795
|
+
send_response({"track": track_idx, "clip": clip_idx, "added_at_beat": beat_time, "ok": true});
|
|
796
|
+
} catch(e) {
|
|
797
|
+
send_response({"error": "Failed to add warp marker: " + e.message});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function cmd_move_warp_marker(args) {
|
|
802
|
+
var track_idx = parseInt(args[0]);
|
|
803
|
+
var clip_idx = parseInt(args[1]);
|
|
804
|
+
var old_beat = parseFloat(args[2]);
|
|
805
|
+
var new_beat = parseFloat(args[3]);
|
|
806
|
+
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
807
|
+
|
|
808
|
+
cursor_a.goto(path);
|
|
809
|
+
if (cursor_a.id === 0) {
|
|
810
|
+
send_response({"error": "No clip"});
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
cursor_a.call("move_warp_marker", old_beat, new_beat);
|
|
816
|
+
send_response({"track": track_idx, "clip": clip_idx, "moved_from": old_beat, "moved_to": new_beat, "ok": true});
|
|
817
|
+
} catch(e) {
|
|
818
|
+
send_response({"error": "Failed to move warp marker: " + e.message});
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function cmd_remove_warp_marker(args) {
|
|
823
|
+
var track_idx = parseInt(args[0]);
|
|
824
|
+
var clip_idx = parseInt(args[1]);
|
|
825
|
+
var beat_time = parseFloat(args[2]);
|
|
826
|
+
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
827
|
+
|
|
828
|
+
cursor_a.goto(path);
|
|
829
|
+
if (cursor_a.id === 0) {
|
|
830
|
+
send_response({"error": "No clip"});
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
try {
|
|
835
|
+
cursor_a.call("remove_warp_marker", beat_time);
|
|
836
|
+
send_response({"track": track_idx, "clip": clip_idx, "removed_at_beat": beat_time, "ok": true});
|
|
837
|
+
} catch(e) {
|
|
838
|
+
send_response({"error": "Failed to remove warp marker: " + e.message});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ── Phase 2: Clip & Display ───────────────────────────────────────────
|
|
843
|
+
|
|
844
|
+
function cmd_scrub_clip(args) {
|
|
845
|
+
var track_idx = parseInt(args[0]);
|
|
846
|
+
var clip_idx = parseInt(args[1]);
|
|
847
|
+
var beat_time = parseFloat(args[2]);
|
|
848
|
+
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
849
|
+
|
|
850
|
+
cursor_a.goto(path);
|
|
851
|
+
if (cursor_a.id === 0) {
|
|
852
|
+
send_response({"error": "No clip"});
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
try {
|
|
857
|
+
cursor_a.call("scrub", beat_time);
|
|
858
|
+
send_response({"track": track_idx, "clip": clip_idx, "scrubbing_at": beat_time, "ok": true});
|
|
859
|
+
} catch(e) {
|
|
860
|
+
send_response({"error": "Scrub failed: " + e.message});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function cmd_stop_scrub(args) {
|
|
865
|
+
var track_idx = parseInt(args[0]);
|
|
866
|
+
var clip_idx = parseInt(args[1]);
|
|
867
|
+
var path = build_track_path(track_idx) + " clip_slots " + clip_idx + " clip";
|
|
868
|
+
|
|
869
|
+
cursor_a.goto(path);
|
|
870
|
+
try {
|
|
871
|
+
cursor_a.call("stop_scrub");
|
|
872
|
+
send_response({"ok": true});
|
|
873
|
+
} catch(e) {
|
|
874
|
+
send_response({"error": e.message});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function cmd_get_display_values(args) {
|
|
879
|
+
var track_idx = parseInt(args[0]);
|
|
880
|
+
var device_idx = parseInt(args[1]);
|
|
881
|
+
var path = build_device_path(track_idx, device_idx);
|
|
882
|
+
|
|
883
|
+
cursor_a.goto(path);
|
|
884
|
+
var param_count = cursor_a.getcount("parameters");
|
|
885
|
+
var device_name = cursor_a.get("name").toString();
|
|
886
|
+
var params = [];
|
|
887
|
+
var current = 0;
|
|
888
|
+
var batch_size = 4;
|
|
889
|
+
|
|
890
|
+
function read_batch() {
|
|
891
|
+
var end = Math.min(current + batch_size, param_count);
|
|
892
|
+
for (var i = current; i < end; i++) {
|
|
893
|
+
cursor_b.goto(path + " parameters " + i);
|
|
894
|
+
var state = parseInt(cursor_b.get("state"));
|
|
895
|
+
if (state !== 2) {
|
|
896
|
+
params.push({
|
|
897
|
+
index: i,
|
|
898
|
+
name: cursor_b.get("name").toString(),
|
|
899
|
+
display_value: String(cursor_b.call("str_for_value", parseFloat(cursor_b.get("value")))),
|
|
900
|
+
value: parseFloat(cursor_b.get("value"))
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
current = end;
|
|
905
|
+
if (current < param_count) {
|
|
906
|
+
var next_task = new Task(read_batch);
|
|
907
|
+
next_task.schedule(50);
|
|
908
|
+
} else {
|
|
909
|
+
send_response({
|
|
910
|
+
"track": track_idx,
|
|
911
|
+
"device": device_idx,
|
|
912
|
+
"device_name": device_name,
|
|
913
|
+
"params": params
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
read_batch();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
921
|
+
|
|
922
|
+
function build_track_path(track_idx) {
|
|
923
|
+
if (track_idx === -1000) {
|
|
924
|
+
return "live_set master_track";
|
|
925
|
+
} else if (track_idx < 0) {
|
|
926
|
+
var ri = Math.abs(track_idx) - 1;
|
|
927
|
+
return "live_set return_tracks " + ri;
|
|
928
|
+
} else {
|
|
929
|
+
return "live_set tracks " + track_idx;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function build_device_path(track_idx, device_idx) {
|
|
934
|
+
if (track_idx === -1000) {
|
|
935
|
+
return "live_set master_track devices " + device_idx;
|
|
936
|
+
} else if (track_idx < 0) {
|
|
937
|
+
var ri = Math.abs(track_idx) - 1;
|
|
938
|
+
return "live_set return_tracks " + ri + " devices " + device_idx;
|
|
939
|
+
} else {
|
|
940
|
+
return "live_set tracks " + track_idx + " devices " + device_idx;
|
|
941
|
+
}
|
|
942
|
+
}
|