livepilot 1.8.4 → 1.9.1
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 +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +29 -0
- package/README.md +4 -15
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +6 -6
- package/livepilot/skills/livepilot-core/references/overview.md +2 -2
- package/livepilot/skills/livepilot-release/SKILL.md +6 -6
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +170 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/tools/devices.py +95 -2
- package/mcp_server/tools/midi_io.py +10 -2
- package/mcp_server/tools/scenes.py +65 -2
- package/mcp_server/tools/tracks.py +45 -2
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +25 -0
- package/remote_script/LivePilot/scenes.py +110 -1
- package/remote_script/LivePilot/server.py +15 -2
- package/remote_script/LivePilot/tracks.py +71 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
3
|
"name": "dreamrec-LivePilot",
|
|
4
|
-
"description": "Agentic MCP production system for Ableton Live 12 —
|
|
4
|
+
"description": "Agentic MCP production system for Ableton Live 12 — 178 tools, 17 domains",
|
|
5
5
|
"owner": {
|
|
6
6
|
"name": "dreamrec",
|
|
7
7
|
"email": "dreamrec@users.noreply.github.com"
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "livepilot",
|
|
12
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
13
|
-
"version": "1.
|
|
12
|
+
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
13
|
+
"version": "1.9.1",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Pilot Studio"
|
|
16
16
|
},
|
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# LivePilot v1.
|
|
1
|
+
# LivePilot v1.9.1 — Ableton Live 12
|
|
2
2
|
|
|
3
3
|
## Project
|
|
4
4
|
- **Repo:** This directory (LivePilot)
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
## Key Rules
|
|
23
23
|
- ALL Live Object Model (LOM) calls must execute on Ableton's main thread via schedule_message queue
|
|
24
24
|
- Live 12 minimum — use modern note API (add_new_notes, get_notes_extended, apply_note_modifications)
|
|
25
|
-
-
|
|
25
|
+
- 178 tools across 17 domains: transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, analyzer, automation, theory, generative, harmony, midi_io, perception
|
|
26
26
|
- JSON over TCP, newline-delimited, port 9878
|
|
27
27
|
- Structured errors with codes: INDEX_ERROR, NOT_FOUND, INVALID_PARAM, STATE_ERROR, TIMEOUT, INTERNAL
|
|
28
28
|
|
|
@@ -43,4 +43,4 @@ When modifying .amxd attributes that Max editor won't persist (e.g., `openinpres
|
|
|
43
43
|
4. Structure: 24-byte `ampf` header + `ptch` chunk + `mx@c` header + JSON patcher + frozen deps
|
|
44
44
|
|
|
45
45
|
## Tool Count
|
|
46
|
-
Currently
|
|
46
|
+
Currently 178 tools. If adding/removing tools, update: README.md, package.json description, livepilot/.Codex-plugin/plugin.json, server.json, livepilot/skills/livepilot-core/SKILL.md, livepilot/skills/livepilot-core/references/overview.md, AGENTS.md, CHANGELOG.md, tests/test_tools_contract.py, docs/manual/index.md, docs/manual/tool-reference.md
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.1 — Rebuild .amxd with v1.9 Bridge (March 2026)
|
|
4
|
+
|
|
5
|
+
- Rebuild LivePilot_Analyzer.amxd in Max editor to freeze 3 new plugin parameter bridge commands
|
|
6
|
+
- Binary-patch `openinpresentation` for presentation mode
|
|
7
|
+
|
|
8
|
+
## 1.9.0 — Scene Matrix, Freeze/Flatten, Plugin Deep Control (March 2026)
|
|
9
|
+
|
|
10
|
+
**10 new tools (168 → 178), 3 features shipped.**
|
|
11
|
+
|
|
12
|
+
### Scene Matrix Operations (+4 tools)
|
|
13
|
+
- `get_scene_matrix` — full N×M clip grid with states (empty/stopped/playing/triggered/recording)
|
|
14
|
+
- `fire_scene_clips` — fire a scene with optional track filter for selective launching
|
|
15
|
+
- `stop_all_clips` — stop all playing clips in the session (panic button)
|
|
16
|
+
- `get_playing_clips` — return all currently playing or triggered clips
|
|
17
|
+
|
|
18
|
+
### Track Freeze/Flatten (+3 tools)
|
|
19
|
+
- `freeze_track` — freeze a track (render devices to audio for CPU savings)
|
|
20
|
+
- `flatten_track` — flatten a frozen track (commit rendered audio permanently)
|
|
21
|
+
- `get_freeze_status` — check if a track is frozen
|
|
22
|
+
|
|
23
|
+
### Plugin Parameter Mapping (+3 tools, M4L)
|
|
24
|
+
- `get_plugin_parameters` — get ALL VST/AU plugin parameters including unconfigured ones
|
|
25
|
+
- `map_plugin_parameter` — add a plugin parameter to Ableton's Configure list for automation
|
|
26
|
+
- `get_plugin_presets` — list a plugin's internal presets and banks
|
|
27
|
+
|
|
28
|
+
### Infrastructure
|
|
29
|
+
- `SLOW_WRITE_COMMANDS` set for freeze_track (35s timeout vs 15s for normal writes)
|
|
30
|
+
- Removed "Coming" section from README — all roadmap features shipped or dropped
|
|
31
|
+
|
|
3
32
|
## 1.8.4 — Bug Fix Audit (March 2026)
|
|
4
33
|
|
|
5
34
|
**5 bugs fixed (2 P1, 3 P2), verified live in Ableton.**
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
An agentic production system for Ableton Live 12.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
178 tools. Device atlas. Spectral perception. Technique memory.
|
|
13
13
|
Neo-Riemannian harmony. Euclidean rhythm. Species counterpoint.
|
|
14
14
|
|
|
15
15
|
|
|
@@ -37,7 +37,7 @@ Neo-Riemannian harmony. Euclidean rhythm. Species counterpoint.
|
|
|
37
37
|
│ └───────────────────┼───────────────────┘ │
|
|
38
38
|
│ ▼ │
|
|
39
39
|
│ ┌─────────────────┐ │
|
|
40
|
-
│ │
|
|
40
|
+
│ │ 178 MCP Tools │ │
|
|
41
41
|
│ │ 17 domains │ │
|
|
42
42
|
│ └────────┬────────┘ │
|
|
43
43
|
│ │ │
|
|
@@ -59,7 +59,7 @@ via a Max for Live device.
|
|
|
59
59
|
The memory gives it history — a searchable library of production decisions
|
|
60
60
|
that persists across sessions.
|
|
61
61
|
|
|
62
|
-
All three feed into
|
|
62
|
+
All three feed into 178 deterministic tools that execute on Ableton's main thread
|
|
63
63
|
through the official Live Object Model API. Everything is reversible with undo.
|
|
64
64
|
|
|
65
65
|
<br>
|
|
@@ -476,7 +476,7 @@ Check memory before creative decisions. Verify every mutation.
|
|
|
476
476
|
|
|
477
477
|
## Full Tool List
|
|
478
478
|
|
|
479
|
-
|
|
479
|
+
178 tools across 17 domains.
|
|
480
480
|
|
|
481
481
|
<br>
|
|
482
482
|
|
|
@@ -769,17 +769,6 @@ Check memory before creative decisions. Verify every mutation.
|
|
|
769
769
|
|
|
770
770
|
---
|
|
771
771
|
|
|
772
|
-
## Coming
|
|
773
|
-
|
|
774
|
-
```
|
|
775
|
-
□ Plugin parameter mapping — VST/AU deep control
|
|
776
|
-
□ Audio track freeze/flatten automation
|
|
777
|
-
□ Clip launch scene matrix operations
|
|
778
|
-
□ Multi-track arrangement templates
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
<br>
|
|
782
|
-
|
|
783
772
|
---
|
|
784
773
|
|
|
785
774
|
## CLI
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
3
|
+
"version": "1.9.1",
|
|
4
|
+
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pilot Studio"
|
|
7
7
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: livepilot-core
|
|
3
|
-
description: Core discipline for LivePilot — agentic production system for Ableton Live 12.
|
|
3
|
+
description: Core discipline for LivePilot — agentic production system for Ableton Live 12. 178 tools across 17 domains. Device atlas (280+ devices), M4L analyzer (spectrum/RMS/key detection), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O. Use whenever working with Ableton Live through MCP tools.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# LivePilot Core — Ableton Live 12
|
|
7
7
|
|
|
8
|
-
Agentic production system for Ableton Live 12.
|
|
8
|
+
Agentic production system for Ableton Live 12. 178 tools across 17 domains, three layers:
|
|
9
9
|
|
|
10
10
|
- **Device Atlas** — A structured knowledge corpus of 280+ instruments, 139 drum kits, and 350+ impulse responses. Consult the atlas before loading any device. It contains real browser URIs, preset names, and sonic descriptions. Never guess a device name — look it up.
|
|
11
11
|
- **M4L Analyzer** — Real-time audio analysis on the master bus (8-band spectrum, RMS/peak, key detection). Use it to verify mixing decisions, detect frequency problems, and find the key before writing harmonic content.
|
|
12
12
|
- **Technique Memory** — Persistent storage for production decisions. Consult `memory_recall` before creative tasks to understand the user's taste. Save techniques when the user likes something. The memory shapes future decisions without constraining them.
|
|
13
13
|
|
|
14
|
-
These layers sit on top of
|
|
14
|
+
These layers sit on top of 178 deterministic tools across 17 domains: transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, analyzer, automation, theory, generative, harmony, MIDI I/O, and perception.
|
|
15
15
|
|
|
16
16
|
## Golden Rules
|
|
17
17
|
|
|
@@ -32,7 +32,7 @@ These layers sit on top of 168 deterministic tools across 17 domains: transport,
|
|
|
32
32
|
Not all tools respond instantly. Know the tiers and act accordingly.
|
|
33
33
|
|
|
34
34
|
### Instant (<1s) — Use freely, no warning needed
|
|
35
|
-
All
|
|
35
|
+
All 178 core tools (transport, tracks, clips, notes, devices, scenes, mixing, browser, arrangement, memory, automation, theory, generative, harmony, midi_io, perception) plus Layer A perception tools (spectral shape, timbral profile, mel spectrum, chroma, onsets, harmonic/percussive, novelty, momentary loudness). These are the reflex tools — call them anytime without hesitation.
|
|
36
36
|
|
|
37
37
|
### Fast (1-5s) — Use freely, barely noticeable
|
|
38
38
|
`analyze_loudness` · `analyze_dynamic_range` · `compare_loudness`
|
|
@@ -117,7 +117,7 @@ Never skip levels. The user's question determines the entry point, but always st
|
|
|
117
117
|
- MIDI track with no instrument loaded
|
|
118
118
|
- Notes programmed but clip not fired
|
|
119
119
|
|
|
120
|
-
## Tool Domains (
|
|
120
|
+
## Tool Domains (178 total)
|
|
121
121
|
|
|
122
122
|
### Transport (12)
|
|
123
123
|
`get_session_info` · `set_tempo` · `set_time_signature` · `start_playback` · `stop_playback` · `continue_playback` · `toggle_metronome` · `set_session_loop` · `undo` · `redo` · `get_recent_actions` · `get_session_diagnostics`
|
|
@@ -374,7 +374,7 @@ Deep production knowledge lives in `references/`. Consult these when making crea
|
|
|
374
374
|
|
|
375
375
|
| File | What's inside | When to consult |
|
|
376
376
|
|------|--------------|-----------------|
|
|
377
|
-
| `references/overview.md` | All
|
|
377
|
+
| `references/overview.md` | All 178 tools mapped with params, units, ranges | Quick lookup for any tool |
|
|
378
378
|
| `references/midi-recipes.md` | Drum patterns by genre, chord voicings, scales, hi-hat techniques, humanization, polymetrics | Programming MIDI notes, building beats |
|
|
379
379
|
| `references/sound-design.md` | Stock instruments/effects, parameter recipes for bass/pad/lead/pluck, device chain patterns | Loading and configuring devices |
|
|
380
380
|
| `references/mixing-patterns.md` | Gain staging, parallel compression, sidechain, EQ by instrument, bus processing, stereo width | Setting volumes, panning, adding effects |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# LivePilot v1.
|
|
1
|
+
# LivePilot v1.9.1 — Architecture & Tool Reference
|
|
2
2
|
|
|
3
|
-
Agentic production system for Ableton Live 12.
|
|
3
|
+
Agentic production system for Ableton Live 12. 178 tools across 17 domains. Device atlas (280+ devices), spectral perception (M4L analyzer), technique memory, automation intelligence (16 curve types, 15 recipes), music theory (Krumhansl-Schmuckler, species counterpoint), generative algorithms (Euclidean rhythm, tintinnabuli, phase shift, additive process), neo-Riemannian harmony (PRL transforms, Tonnetz), MIDI file I/O.
|
|
4
4
|
|
|
5
5
|
## Architecture
|
|
6
6
|
|
|
@@ -27,20 +27,20 @@ Run this checklist EVERY time the user says "update everything", "push", "releas
|
|
|
27
27
|
|
|
28
28
|
## 2. Tool Count (must ALL match)
|
|
29
29
|
|
|
30
|
-
Current: **
|
|
30
|
+
Current: **178 tools across 17 domains**.
|
|
31
31
|
Core (no M4L): **139**. Analyzer (M4L): **29**. Perception (offline): **4**.
|
|
32
32
|
|
|
33
33
|
Verify: `grep -rc "@mcp.tool" mcp_server/tools/ | grep -v ":0" | awk -F: '{sum+=$2} END{print sum}'`
|
|
34
34
|
|
|
35
35
|
Files that reference tool count:
|
|
36
36
|
- [ ] `README.md` — header, PERCEPTION section ("139 core...29 analyzer"), Analyzer table header "(29)", Perception table header "(4)"
|
|
37
|
-
- [ ] `package.json` → `"description"` (
|
|
37
|
+
- [ ] `package.json` → `"description"` (178 tools, 17 domains)
|
|
38
38
|
- [ ] `server.json` → `"description"`
|
|
39
39
|
- [ ] `livepilot/.claude-plugin/plugin.json` → `"description"`
|
|
40
40
|
- [ ] `.claude-plugin/marketplace.json` → `"description"`
|
|
41
|
-
- [ ] `CLAUDE.md` → "
|
|
42
|
-
- [ ] `livepilot/skills/livepilot-core/SKILL.md` — "
|
|
43
|
-
- [ ] `livepilot/skills/livepilot-core/references/overview.md` — "
|
|
41
|
+
- [ ] `CLAUDE.md` → "178 tools across 17 domains"
|
|
42
|
+
- [ ] `livepilot/skills/livepilot-core/SKILL.md` — "178 tools across 17 domains", Analyzer (29), Perception (4)
|
|
43
|
+
- [ ] `livepilot/skills/livepilot-core/references/overview.md` — "178 tools across 17 domains"
|
|
44
44
|
- [ ] `docs/manual/index.md` — domain table: Analyzer (29), Perception (4)
|
|
45
45
|
- [ ] `docs/manual/getting-started.md` — "139 core tools...29 analyzer"
|
|
46
46
|
- [ ] `docs/manual/tool-reference.md` — all domains present with correct counts
|
|
@@ -86,7 +86,7 @@ Current: **17 domains**: transport, tracks, clips, notes, devices, scenes, mixin
|
|
|
86
86
|
|
|
87
87
|
- [ ] `README.md` — features match current capabilities, "Coming" section is accurate
|
|
88
88
|
- [ ] `docs/manual/getting-started.md` — install instructions current
|
|
89
|
-
- [ ] `docs/manual/tool-reference.md` — all 17 domains listed, all
|
|
89
|
+
- [ ] `docs/manual/tool-reference.md` — all 17 domains listed, all 178 tools present
|
|
90
90
|
- [ ] `docs/TOOL_REFERENCE.md` — all 17 domains present
|
|
91
91
|
- [ ] `docs/M4L_BRIDGE.md` — architecture accurate, core tool count correct
|
|
92
92
|
|
|
Binary file
|
|
@@ -83,7 +83,7 @@ function anything() {
|
|
|
83
83
|
function dispatch(cmd, args) {
|
|
84
84
|
switch(cmd) {
|
|
85
85
|
case "ping":
|
|
86
|
-
send_response({"ok": true, "version": "1.
|
|
86
|
+
send_response({"ok": true, "version": "1.9.1"});
|
|
87
87
|
break;
|
|
88
88
|
case "get_params":
|
|
89
89
|
cmd_get_params(args);
|
|
@@ -161,6 +161,16 @@ function dispatch(cmd, args) {
|
|
|
161
161
|
case "get_display_values":
|
|
162
162
|
cmd_get_display_values(args);
|
|
163
163
|
break;
|
|
164
|
+
// ── Plugin Parameters ──
|
|
165
|
+
case "get_plugin_params":
|
|
166
|
+
cmd_get_plugin_params(args);
|
|
167
|
+
break;
|
|
168
|
+
case "map_plugin_param":
|
|
169
|
+
cmd_map_plugin_param(args);
|
|
170
|
+
break;
|
|
171
|
+
case "get_plugin_presets":
|
|
172
|
+
cmd_get_plugin_presets(args);
|
|
173
|
+
break;
|
|
164
174
|
default:
|
|
165
175
|
send_response({"error": "Unknown command: " + cmd});
|
|
166
176
|
}
|
|
@@ -1035,6 +1045,165 @@ function pad2(n) {
|
|
|
1035
1045
|
return n < 10 ? "0" + n : "" + n;
|
|
1036
1046
|
}
|
|
1037
1047
|
|
|
1048
|
+
// ── Plugin Parameters ──────────────────────────────────────────────────────
|
|
1049
|
+
|
|
1050
|
+
function cmd_get_plugin_params(args) {
|
|
1051
|
+
// Returns all parameters for a VST/AU plugin device
|
|
1052
|
+
var track_idx = parseInt(args[0]);
|
|
1053
|
+
var device_idx = parseInt(args[1]);
|
|
1054
|
+
var path = build_device_path(track_idx, device_idx);
|
|
1055
|
+
|
|
1056
|
+
cursor_a.goto(path);
|
|
1057
|
+
var class_name = cursor_a.get("class_name").toString();
|
|
1058
|
+
|
|
1059
|
+
// Check if this is a plugin device
|
|
1060
|
+
var is_plugin = (class_name === "PluginDevice" || class_name === "AuPluginDevice");
|
|
1061
|
+
if (!is_plugin) {
|
|
1062
|
+
send_response({
|
|
1063
|
+
"error": "Device is " + class_name + ", not a plugin (PluginDevice/AuPluginDevice)"
|
|
1064
|
+
});
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
var device_name = cursor_a.get("name").toString();
|
|
1069
|
+
var param_count = cursor_a.getcount("parameters");
|
|
1070
|
+
var params = [];
|
|
1071
|
+
var current = 0;
|
|
1072
|
+
var batch_size = 4;
|
|
1073
|
+
|
|
1074
|
+
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 {
|
|
1095
|
+
send_response({
|
|
1096
|
+
"track": track_idx,
|
|
1097
|
+
"device": device_idx,
|
|
1098
|
+
"name": device_name,
|
|
1099
|
+
"class_name": class_name,
|
|
1100
|
+
"is_plugin": true,
|
|
1101
|
+
"parameter_count": param_count,
|
|
1102
|
+
"parameters": params
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (param_count > 0) {
|
|
1108
|
+
read_batch();
|
|
1109
|
+
} else {
|
|
1110
|
+
send_response({
|
|
1111
|
+
"track": track_idx,
|
|
1112
|
+
"device": device_idx,
|
|
1113
|
+
"name": device_name,
|
|
1114
|
+
"class_name": class_name,
|
|
1115
|
+
"is_plugin": true,
|
|
1116
|
+
"parameter_count": 0,
|
|
1117
|
+
"parameters": []
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function cmd_map_plugin_param(args) {
|
|
1123
|
+
// Add a plugin parameter to Ableton's Configure list
|
|
1124
|
+
var track_idx = parseInt(args[0]);
|
|
1125
|
+
var device_idx = parseInt(args[1]);
|
|
1126
|
+
var param_idx = parseInt(args[2]);
|
|
1127
|
+
var path = build_device_path(track_idx, device_idx);
|
|
1128
|
+
|
|
1129
|
+
cursor_a.goto(path);
|
|
1130
|
+
var param_count = cursor_a.getcount("parameters");
|
|
1131
|
+
if (param_idx < 0 || param_idx >= param_count) {
|
|
1132
|
+
send_response({"error": "Parameter index " + param_idx + " out of range (0.." + (param_count - 1) + ")"});
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
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
|
+
try {
|
|
1143
|
+
cursor_a.set("selected_parameter", param_idx);
|
|
1144
|
+
cursor_a.call("store_chosen_bank");
|
|
1145
|
+
send_response({
|
|
1146
|
+
"mapped": true,
|
|
1147
|
+
"parameter_index": param_idx,
|
|
1148
|
+
"parameter_name": param_name
|
|
1149
|
+
});
|
|
1150
|
+
} catch(e) {
|
|
1151
|
+
send_response({
|
|
1152
|
+
"error": "Failed to map parameter: " + e.message,
|
|
1153
|
+
"parameter_index": param_idx,
|
|
1154
|
+
"parameter_name": param_name
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function cmd_get_plugin_presets(args) {
|
|
1160
|
+
// List plugin's internal presets
|
|
1161
|
+
var track_idx = parseInt(args[0]);
|
|
1162
|
+
var device_idx = parseInt(args[1]);
|
|
1163
|
+
var path = build_device_path(track_idx, device_idx);
|
|
1164
|
+
|
|
1165
|
+
cursor_a.goto(path);
|
|
1166
|
+
var class_name = cursor_a.get("class_name").toString();
|
|
1167
|
+
var device_name = cursor_a.get("name").toString();
|
|
1168
|
+
|
|
1169
|
+
var is_plugin = (class_name === "PluginDevice" || class_name === "AuPluginDevice");
|
|
1170
|
+
if (!is_plugin) {
|
|
1171
|
+
send_response({
|
|
1172
|
+
"error": "Device is " + class_name + ", not a plugin"
|
|
1173
|
+
});
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Read presets — the presets property returns an array of preset names
|
|
1178
|
+
var presets = [];
|
|
1179
|
+
try {
|
|
1180
|
+
var preset_count = cursor_a.getcount("presets");
|
|
1181
|
+
for (var i = 0; i < preset_count; i++) {
|
|
1182
|
+
cursor_b.goto(path + " presets " + i);
|
|
1183
|
+
presets.push({
|
|
1184
|
+
index: i,
|
|
1185
|
+
name: cursor_b.get("name").toString()
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
} catch(e) {
|
|
1189
|
+
// Some plugins don't expose presets via LOM
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Try to get selected preset index
|
|
1193
|
+
var selected = -1;
|
|
1194
|
+
try {
|
|
1195
|
+
selected = parseInt(cursor_a.get("selected_preset_index"));
|
|
1196
|
+
} catch(e) {}
|
|
1197
|
+
|
|
1198
|
+
send_response({
|
|
1199
|
+
"track": track_idx,
|
|
1200
|
+
"device": device_idx,
|
|
1201
|
+
"name": device_name,
|
|
1202
|
+
"presets": presets,
|
|
1203
|
+
"selected_preset": selected
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1038
1207
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
1039
1208
|
|
|
1040
1209
|
function build_track_path(track_idx) {
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.9.1"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Device MCP tools — parameters, racks, browser loading.
|
|
1
|
+
"""Device MCP tools — parameters, racks, browser loading, plugin deep control.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
15 tools matching the Remote Script devices domain + M4L bridge.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -254,3 +254,96 @@ def get_device_presets(ctx: Context, device_name: str) -> dict:
|
|
|
254
254
|
return _get_ableton(ctx).send_command("get_device_presets", {
|
|
255
255
|
"device_name": device_name,
|
|
256
256
|
})
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ── Plugin Deep Control (M4L Bridge) ────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _get_m4l(ctx: Context):
|
|
263
|
+
"""Get M4LBridge from lifespan context."""
|
|
264
|
+
bridge = ctx.lifespan_context.get("m4l")
|
|
265
|
+
if not bridge:
|
|
266
|
+
raise RuntimeError("M4L bridge not initialized")
|
|
267
|
+
return bridge
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _get_spectral(ctx: Context):
|
|
271
|
+
"""Get SpectralCache from lifespan context."""
|
|
272
|
+
cache = ctx.lifespan_context.get("spectral")
|
|
273
|
+
if not cache:
|
|
274
|
+
raise RuntimeError("Spectral cache not initialized")
|
|
275
|
+
return cache
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _require_analyzer(cache) -> None:
|
|
279
|
+
if not cache.is_connected:
|
|
280
|
+
raise ValueError(
|
|
281
|
+
"LivePilot Analyzer not detected. "
|
|
282
|
+
"Drag 'LivePilot Analyzer' onto the master track."
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@mcp.tool()
|
|
287
|
+
async def get_plugin_parameters(
|
|
288
|
+
ctx: Context,
|
|
289
|
+
track_index: int,
|
|
290
|
+
device_index: int,
|
|
291
|
+
) -> dict:
|
|
292
|
+
"""Get ALL parameters from a VST/AU plugin including unconfigured ones.
|
|
293
|
+
|
|
294
|
+
Returns every parameter the plugin exposes — not just the 128
|
|
295
|
+
that Ableton's Configure panel shows. Includes name, value, min,
|
|
296
|
+
max, default, and display string for each.
|
|
297
|
+
Only works on PluginDevice/AuPluginDevice types.
|
|
298
|
+
Requires LivePilot Analyzer on master track.
|
|
299
|
+
"""
|
|
300
|
+
_validate_track_index(track_index)
|
|
301
|
+
_validate_device_index(device_index)
|
|
302
|
+
cache = _get_spectral(ctx)
|
|
303
|
+
_require_analyzer(cache)
|
|
304
|
+
bridge = _get_m4l(ctx)
|
|
305
|
+
return await bridge.send_command("get_plugin_params", track_index, device_index)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@mcp.tool()
|
|
309
|
+
async def map_plugin_parameter(
|
|
310
|
+
ctx: Context,
|
|
311
|
+
track_index: int,
|
|
312
|
+
device_index: int,
|
|
313
|
+
parameter_index: int,
|
|
314
|
+
) -> dict:
|
|
315
|
+
"""Add a plugin parameter to Ableton's Configure list for automation.
|
|
316
|
+
|
|
317
|
+
After mapping, the parameter becomes visible in the device's macro
|
|
318
|
+
panel and can be automated with set_device_parameter or
|
|
319
|
+
set_clip_automation like any native parameter.
|
|
320
|
+
Requires LivePilot Analyzer on master track.
|
|
321
|
+
"""
|
|
322
|
+
_validate_track_index(track_index)
|
|
323
|
+
_validate_device_index(device_index)
|
|
324
|
+
if parameter_index < 0:
|
|
325
|
+
raise ValueError("parameter_index must be >= 0")
|
|
326
|
+
cache = _get_spectral(ctx)
|
|
327
|
+
_require_analyzer(cache)
|
|
328
|
+
bridge = _get_m4l(ctx)
|
|
329
|
+
return await bridge.send_command("map_plugin_param", track_index, device_index, parameter_index)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@mcp.tool()
|
|
333
|
+
async def get_plugin_presets(
|
|
334
|
+
ctx: Context,
|
|
335
|
+
track_index: int,
|
|
336
|
+
device_index: int,
|
|
337
|
+
) -> dict:
|
|
338
|
+
"""List a VST/AU plugin's internal presets and banks.
|
|
339
|
+
|
|
340
|
+
Returns preset names and the currently selected preset index.
|
|
341
|
+
Only works on PluginDevice/AuPluginDevice types.
|
|
342
|
+
Requires LivePilot Analyzer on master track.
|
|
343
|
+
"""
|
|
344
|
+
_validate_track_index(track_index)
|
|
345
|
+
_validate_device_index(device_index)
|
|
346
|
+
cache = _get_spectral(ctx)
|
|
347
|
+
_require_analyzer(cache)
|
|
348
|
+
bridge = _get_m4l(ctx)
|
|
349
|
+
return await bridge.send_command("get_plugin_presets", track_index, device_index)
|
|
@@ -14,6 +14,7 @@ from typing import Any, Optional
|
|
|
14
14
|
|
|
15
15
|
from fastmcp import Context
|
|
16
16
|
|
|
17
|
+
from ..connection import AbletonConnectionError
|
|
17
18
|
from ..server import mcp
|
|
18
19
|
from . import _theory_engine as theory
|
|
19
20
|
|
|
@@ -177,18 +178,25 @@ def import_midi_to_clip(
|
|
|
177
178
|
|
|
178
179
|
if create_clip:
|
|
179
180
|
# Check if clip already exists — only create if the slot is empty
|
|
181
|
+
slot_has_clip = False
|
|
180
182
|
try:
|
|
181
183
|
ableton.send_command("get_clip_info", {
|
|
182
184
|
"track_index": track_index,
|
|
183
185
|
"clip_index": clip_index,
|
|
184
186
|
})
|
|
187
|
+
slot_has_clip = True
|
|
188
|
+
except AbletonConnectionError:
|
|
189
|
+
# Slot is empty — no clip to clear
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
if slot_has_clip:
|
|
185
193
|
# Clip exists — clear its notes before importing
|
|
186
194
|
ableton.send_command("remove_notes", {
|
|
187
195
|
"track_index": track_index,
|
|
188
196
|
"clip_index": clip_index,
|
|
189
197
|
})
|
|
190
|
-
|
|
191
|
-
#
|
|
198
|
+
else:
|
|
199
|
+
# Empty slot — create a new clip
|
|
192
200
|
ableton.send_command("create_clip", {
|
|
193
201
|
"track_index": track_index,
|
|
194
202
|
"clip_index": clip_index,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
"""Scene MCP tools — list, create, delete, duplicate, fire, rename, color, tempo.
|
|
1
|
+
"""Scene MCP tools — list, create, delete, duplicate, fire, rename, color, tempo, matrix.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
12 tools matching the Remote Script scenes domain.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
6
9
|
from fastmcp import Context
|
|
7
10
|
|
|
8
11
|
from ..server import mcp
|
|
@@ -87,3 +90,63 @@ def set_scene_tempo(ctx: Context, scene_index: int, tempo: float) -> dict:
|
|
|
87
90
|
"scene_index": scene_index,
|
|
88
91
|
"tempo": tempo,
|
|
89
92
|
})
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ── Scene Matrix Operations ─────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _ensure_list(value: Any) -> list:
|
|
99
|
+
if isinstance(value, str):
|
|
100
|
+
return json.loads(value)
|
|
101
|
+
return value
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@mcp.tool()
|
|
105
|
+
def get_scene_matrix(ctx: Context) -> dict:
|
|
106
|
+
"""Get the full session clip grid: every track x every scene.
|
|
107
|
+
|
|
108
|
+
Returns clip states (empty/stopped/playing/triggered/recording),
|
|
109
|
+
clip names, and colors. Use this for a bird's-eye view of the
|
|
110
|
+
entire session before making clip launch decisions.
|
|
111
|
+
"""
|
|
112
|
+
return _get_ableton(ctx).send_command("get_scene_matrix")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@mcp.tool()
|
|
116
|
+
def fire_scene_clips(
|
|
117
|
+
ctx: Context,
|
|
118
|
+
scene_index: int,
|
|
119
|
+
track_indices: Optional[Any] = None,
|
|
120
|
+
) -> dict:
|
|
121
|
+
"""Fire a scene, optionally filtering to specific tracks.
|
|
122
|
+
|
|
123
|
+
If track_indices is omitted, fires the entire scene (all tracks).
|
|
124
|
+
If provided (JSON array of ints), fires only those tracks' clip slots
|
|
125
|
+
from the scene — useful for launching drums + bass without triggering
|
|
126
|
+
the lead, or building up layers gradually.
|
|
127
|
+
"""
|
|
128
|
+
_validate_scene_index(scene_index)
|
|
129
|
+
params: dict = {"scene_index": scene_index}
|
|
130
|
+
if track_indices is not None:
|
|
131
|
+
track_indices = _ensure_list(track_indices)
|
|
132
|
+
for ti in track_indices:
|
|
133
|
+
if int(ti) < 0:
|
|
134
|
+
raise ValueError("track_indices must all be >= 0")
|
|
135
|
+
params["track_indices"] = track_indices
|
|
136
|
+
return _get_ableton(ctx).send_command("fire_scene_clips", params)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@mcp.tool()
|
|
140
|
+
def stop_all_clips(ctx: Context) -> dict:
|
|
141
|
+
"""Stop all playing clips in the session. Panic button."""
|
|
142
|
+
return _get_ableton(ctx).send_command("stop_all_clips")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@mcp.tool()
|
|
146
|
+
def get_playing_clips(ctx: Context) -> dict:
|
|
147
|
+
"""Get all currently playing or triggered clips.
|
|
148
|
+
|
|
149
|
+
Returns track index/name, clip index/name, and whether each clip
|
|
150
|
+
is actively playing or just triggered (waiting for quantization).
|
|
151
|
+
"""
|
|
152
|
+
return _get_ableton(ctx).send_command("get_playing_clips")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Track MCP tools — create, delete, rename, mute, solo, arm, group fold, monitor.
|
|
1
|
+
"""Track MCP tools — create, delete, rename, mute, solo, arm, group fold, monitor, freeze, flatten.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
17 tools matching the Remote Script tracks domain.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -173,3 +173,46 @@ def set_track_input_monitoring(ctx: Context, track_index: int, state: int) -> di
|
|
|
173
173
|
"track_index": track_index,
|
|
174
174
|
"state": state,
|
|
175
175
|
})
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── Freeze / Flatten ────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@mcp.tool()
|
|
182
|
+
def freeze_track(ctx: Context, track_index: int) -> dict:
|
|
183
|
+
"""Freeze a track — render all devices to audio for CPU savings.
|
|
184
|
+
|
|
185
|
+
Freeze is async in Ableton: this initiates the render and returns
|
|
186
|
+
immediately. Poll get_freeze_status to check when it's done.
|
|
187
|
+
Freezing a track that's already frozen is a no-op.
|
|
188
|
+
"""
|
|
189
|
+
_validate_track_index(track_index)
|
|
190
|
+
return _get_ableton(ctx).send_command("freeze_track", {
|
|
191
|
+
"track_index": track_index,
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@mcp.tool()
|
|
196
|
+
def flatten_track(ctx: Context, track_index: int) -> dict:
|
|
197
|
+
"""Flatten a frozen track — commit rendered audio permanently.
|
|
198
|
+
|
|
199
|
+
Destructive: replaces all devices with the rendered audio file.
|
|
200
|
+
The track must already be frozen. Use undo to revert.
|
|
201
|
+
"""
|
|
202
|
+
_validate_track_index(track_index)
|
|
203
|
+
return _get_ableton(ctx).send_command("flatten_track", {
|
|
204
|
+
"track_index": track_index,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@mcp.tool()
|
|
209
|
+
def get_freeze_status(ctx: Context, track_index: int) -> dict:
|
|
210
|
+
"""Check if a track is frozen.
|
|
211
|
+
|
|
212
|
+
Use after freeze_track to poll for completion, or before
|
|
213
|
+
flatten_track to verify the track is ready to flatten.
|
|
214
|
+
"""
|
|
215
|
+
_validate_track_index(track_index)
|
|
216
|
+
return _get_ableton(ctx).send_command("get_freeze_status", {
|
|
217
|
+
"track_index": track_index,
|
|
218
|
+
})
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 178 tools, 17 domains, device atlas, spectral perception, technique memory, neo-Riemannian harmony, Euclidean rhythm, species counterpoint, MIDI I/O",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.9.1"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from .server import LivePilotServer
|
|
@@ -107,6 +107,31 @@ def create_arrangement_clip(song, params):
|
|
|
107
107
|
|
|
108
108
|
clip_count += 1
|
|
109
109
|
pos += loop_length
|
|
110
|
+
|
|
111
|
+
# Trim the last clip's overshoot: if the last duplicate extends
|
|
112
|
+
# past end_pos, remove notes beyond the requested region and
|
|
113
|
+
# set loop_end so only the needed portion plays.
|
|
114
|
+
if clip_count > 0:
|
|
115
|
+
arr_clips = list(track.arrangement_clips)
|
|
116
|
+
for c in arr_clips:
|
|
117
|
+
clip_end = c.start_time + c.length
|
|
118
|
+
if c.start_time >= start_time and clip_end > end_pos + 0.01:
|
|
119
|
+
# This clip overshoots — trim its content
|
|
120
|
+
overshoot_start = end_pos - c.start_time
|
|
121
|
+
if overshoot_start > 0:
|
|
122
|
+
try:
|
|
123
|
+
c.looping = True
|
|
124
|
+
c.loop_start = 0.0
|
|
125
|
+
c.loop_end = overshoot_start
|
|
126
|
+
except (AttributeError, RuntimeError):
|
|
127
|
+
pass
|
|
128
|
+
# Remove notes beyond the trim point
|
|
129
|
+
try:
|
|
130
|
+
c.remove_notes_extended(
|
|
131
|
+
0, 128, overshoot_start, c.length
|
|
132
|
+
)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
110
135
|
finally:
|
|
111
136
|
song.end_undo_step()
|
|
112
137
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
LivePilot - Scene domain handlers (
|
|
2
|
+
LivePilot - Scene domain handlers (12 commands).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from .router import register
|
|
@@ -97,3 +97,112 @@ def set_scene_tempo(song, params):
|
|
|
97
97
|
"index": scene_index,
|
|
98
98
|
"tempo": scene.tempo,
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── Scene Matrix Operations ─────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@register("get_scene_matrix")
|
|
106
|
+
def get_scene_matrix(song, params):
|
|
107
|
+
"""Return the full clip grid: tracks x scenes with clip states."""
|
|
108
|
+
tracks = list(song.tracks)
|
|
109
|
+
scenes = list(song.scenes)
|
|
110
|
+
|
|
111
|
+
track_headers = []
|
|
112
|
+
for i, t in enumerate(tracks):
|
|
113
|
+
track_headers.append({"index": i, "name": t.name})
|
|
114
|
+
|
|
115
|
+
scene_headers = []
|
|
116
|
+
for i, s in enumerate(scenes):
|
|
117
|
+
scene_headers.append({
|
|
118
|
+
"index": i,
|
|
119
|
+
"name": s.name,
|
|
120
|
+
"tempo": s.tempo if s.tempo > 0 else None,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
matrix = []
|
|
124
|
+
for si, scene in enumerate(scenes):
|
|
125
|
+
row = []
|
|
126
|
+
for ti, track in enumerate(tracks):
|
|
127
|
+
slots = list(track.clip_slots)
|
|
128
|
+
if si >= len(slots):
|
|
129
|
+
row.append({"state": "missing"})
|
|
130
|
+
continue
|
|
131
|
+
slot = slots[si]
|
|
132
|
+
cell = {"state": "empty"}
|
|
133
|
+
if slot.has_clip and slot.clip:
|
|
134
|
+
clip = slot.clip
|
|
135
|
+
if clip.is_recording:
|
|
136
|
+
cell["state"] = "recording"
|
|
137
|
+
elif clip.is_playing:
|
|
138
|
+
cell["state"] = "playing"
|
|
139
|
+
elif clip.is_triggered:
|
|
140
|
+
cell["state"] = "triggered"
|
|
141
|
+
else:
|
|
142
|
+
cell["state"] = "stopped"
|
|
143
|
+
cell["name"] = clip.name
|
|
144
|
+
cell["color_index"] = clip.color_index
|
|
145
|
+
row.append(cell)
|
|
146
|
+
matrix.append(row)
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"tracks": track_headers,
|
|
150
|
+
"scenes": scene_headers,
|
|
151
|
+
"matrix": matrix,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@register("fire_scene_clips")
|
|
156
|
+
def fire_scene_clips(song, params):
|
|
157
|
+
"""Fire a scene with optional track filter."""
|
|
158
|
+
scene_index = int(params["scene_index"])
|
|
159
|
+
track_indices = params.get("track_indices")
|
|
160
|
+
|
|
161
|
+
scene = get_scene(song, scene_index)
|
|
162
|
+
|
|
163
|
+
if track_indices is None:
|
|
164
|
+
# Fire entire scene
|
|
165
|
+
scene.fire()
|
|
166
|
+
return {"scene_index": scene_index, "fired": "all"}
|
|
167
|
+
|
|
168
|
+
# Fire specific tracks only
|
|
169
|
+
tracks = list(song.tracks)
|
|
170
|
+
fired = []
|
|
171
|
+
for ti in track_indices:
|
|
172
|
+
ti = int(ti)
|
|
173
|
+
if ti < 0 or ti >= len(tracks):
|
|
174
|
+
raise IndexError("Track index %d out of range (0..%d)" % (ti, len(tracks) - 1))
|
|
175
|
+
slots = list(tracks[ti].clip_slots)
|
|
176
|
+
if scene_index < len(slots):
|
|
177
|
+
slots[scene_index].fire()
|
|
178
|
+
fired.append(ti)
|
|
179
|
+
|
|
180
|
+
return {"scene_index": scene_index, "fired_tracks": fired}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@register("stop_all_clips")
|
|
184
|
+
def stop_all_clips(song, params):
|
|
185
|
+
"""Stop all playing clips in the session."""
|
|
186
|
+
song.stop_all_clips()
|
|
187
|
+
return {"stopped": True}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@register("get_playing_clips")
|
|
191
|
+
def get_playing_clips(song, params):
|
|
192
|
+
"""Return all currently playing or triggered clips."""
|
|
193
|
+
tracks = list(song.tracks)
|
|
194
|
+
clips = []
|
|
195
|
+
for ti, track in enumerate(tracks):
|
|
196
|
+
for si, slot in enumerate(track.clip_slots):
|
|
197
|
+
if slot.has_clip and slot.clip:
|
|
198
|
+
clip = slot.clip
|
|
199
|
+
if clip.is_playing or clip.is_triggered:
|
|
200
|
+
clips.append({
|
|
201
|
+
"track_index": ti,
|
|
202
|
+
"track_name": track.name,
|
|
203
|
+
"clip_index": si,
|
|
204
|
+
"clip_name": clip.name,
|
|
205
|
+
"is_playing": clip.is_playing,
|
|
206
|
+
"is_triggered": clip.is_triggered,
|
|
207
|
+
})
|
|
208
|
+
return {"clips": clips}
|
|
@@ -39,6 +39,9 @@ WRITE_COMMANDS = frozenset([
|
|
|
39
39
|
# scenes
|
|
40
40
|
"create_scene", "delete_scene", "duplicate_scene", "fire_scene",
|
|
41
41
|
"set_scene_name", "set_scene_color", "set_scene_tempo",
|
|
42
|
+
"fire_scene_clips", "stop_all_clips",
|
|
43
|
+
# tracks (freeze/flatten)
|
|
44
|
+
"freeze_track", "flatten_track",
|
|
42
45
|
# mixing
|
|
43
46
|
"set_track_volume", "set_track_pan", "set_track_send",
|
|
44
47
|
"set_master_volume", "set_track_routing",
|
|
@@ -57,6 +60,11 @@ WRITE_COMMANDS = frozenset([
|
|
|
57
60
|
"clear_clip_automation",
|
|
58
61
|
])
|
|
59
62
|
|
|
63
|
+
# Commands that need longer timeouts (e.g., freeze renders audio)
|
|
64
|
+
SLOW_WRITE_COMMANDS = frozenset([
|
|
65
|
+
"freeze_track",
|
|
66
|
+
])
|
|
67
|
+
|
|
60
68
|
|
|
61
69
|
class LivePilotServer(object):
|
|
62
70
|
"""TCP server that bridges JSON commands to Ableton's main thread.
|
|
@@ -212,9 +220,14 @@ class LivePilotServer(object):
|
|
|
212
220
|
request_id = command.get("id", "unknown")
|
|
213
221
|
cmd_type = command.get("type", "")
|
|
214
222
|
|
|
215
|
-
# Determine timeout based on read vs write
|
|
223
|
+
# Determine timeout based on read vs write vs slow write
|
|
216
224
|
is_write = cmd_type in WRITE_COMMANDS
|
|
217
|
-
|
|
225
|
+
if cmd_type in SLOW_WRITE_COMMANDS:
|
|
226
|
+
timeout = 35
|
|
227
|
+
elif is_write:
|
|
228
|
+
timeout = 15
|
|
229
|
+
else:
|
|
230
|
+
timeout = 10
|
|
218
231
|
|
|
219
232
|
# Per-command response queue
|
|
220
233
|
response_queue = queue.Queue()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
LivePilot - Track domain handlers (
|
|
2
|
+
LivePilot - Track domain handlers (17 commands).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from .router import register
|
|
@@ -267,3 +267,73 @@ def set_track_input_monitoring(song, params):
|
|
|
267
267
|
"index": track_index,
|
|
268
268
|
"monitoring_state": track.current_monitoring_state,
|
|
269
269
|
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
# ── Freeze / Flatten ────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@register("get_freeze_status")
|
|
276
|
+
def get_freeze_status(song, params):
|
|
277
|
+
"""Return freeze state for a track."""
|
|
278
|
+
track_index = int(params["track_index"])
|
|
279
|
+
track = get_track(song, track_index)
|
|
280
|
+
return {
|
|
281
|
+
"track_index": track_index,
|
|
282
|
+
"is_frozen": track.is_frozen,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@register("freeze_track")
|
|
287
|
+
def freeze_track(song, params):
|
|
288
|
+
"""Freeze a track — render all devices to audio for CPU savings.
|
|
289
|
+
|
|
290
|
+
Freeze is async in Ableton — this call initiates it and returns
|
|
291
|
+
immediately. Use get_freeze_status to poll for completion.
|
|
292
|
+
"""
|
|
293
|
+
track_index = int(params["track_index"])
|
|
294
|
+
track = get_track(song, track_index)
|
|
295
|
+
if track.is_frozen:
|
|
296
|
+
return {
|
|
297
|
+
"track_index": track_index,
|
|
298
|
+
"is_frozen": True,
|
|
299
|
+
"note": "Track is already frozen",
|
|
300
|
+
}
|
|
301
|
+
# In Live 12, freeze is a track method accessible from ControlSurface
|
|
302
|
+
try:
|
|
303
|
+
track.freeze()
|
|
304
|
+
except AttributeError:
|
|
305
|
+
raise ValueError(
|
|
306
|
+
"freeze() not available on this track type. "
|
|
307
|
+
"Only MIDI and audio tracks with devices can be frozen."
|
|
308
|
+
)
|
|
309
|
+
return {
|
|
310
|
+
"track_index": track_index,
|
|
311
|
+
"freezing": True,
|
|
312
|
+
"note": "Freeze initiated. Poll get_freeze_status to check completion.",
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@register("flatten_track")
|
|
317
|
+
def flatten_track(song, params):
|
|
318
|
+
"""Flatten a frozen track — commits rendered audio permanently.
|
|
319
|
+
|
|
320
|
+
Destructive operation. The track must already be frozen.
|
|
321
|
+
Wrapped in undo step so it can be reverted.
|
|
322
|
+
"""
|
|
323
|
+
track_index = int(params["track_index"])
|
|
324
|
+
track = get_track(song, track_index)
|
|
325
|
+
if not track.is_frozen:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
"Track %d is not frozen. Freeze it first with freeze_track."
|
|
328
|
+
% track_index
|
|
329
|
+
)
|
|
330
|
+
song.begin_undo_step()
|
|
331
|
+
try:
|
|
332
|
+
# flatten() is a method on the track, not the song
|
|
333
|
+
track.flatten()
|
|
334
|
+
finally:
|
|
335
|
+
song.end_undo_step()
|
|
336
|
+
return {
|
|
337
|
+
"track_index": track_index,
|
|
338
|
+
"flattened": True,
|
|
339
|
+
}
|