livepilot 1.14.1 → 1.16.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 +176 -1
- package/README.md +6 -6
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/device_atlas.json +91219 -7161
- package/mcp_server/atlas/tools.py +30 -2
- package/mcp_server/runtime/live_version.py +4 -2
- package/mcp_server/runtime/remote_commands.py +5 -0
- package/mcp_server/sample_engine/tools.py +692 -60
- package/mcp_server/splice_client/client.py +511 -65
- package/mcp_server/splice_client/http_bridge.py +361 -0
- package/mcp_server/splice_client/models.py +266 -2
- package/mcp_server/splice_client/quota.py +229 -0
- package/mcp_server/tools/_analyzer_engine/__init__.py +4 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +73 -0
- package/mcp_server/tools/analyzer.py +666 -6
- package/mcp_server/tools/browser.py +164 -13
- package/mcp_server/tools/devices.py +56 -11
- package/mcp_server/tools/mixing.py +64 -15
- package/mcp_server/tools/scales.py +18 -6
- package/mcp_server/tools/tracks.py +92 -4
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +2 -1
- package/remote_script/LivePilot/_clip_helpers.py +86 -0
- package/remote_script/LivePilot/_drum_helpers.py +40 -0
- package/remote_script/LivePilot/_scale_helpers.py +87 -0
- package/remote_script/LivePilot/arrangement.py +44 -15
- package/remote_script/LivePilot/clips.py +182 -2
- package/remote_script/LivePilot/devices.py +82 -2
- package/remote_script/LivePilot/notes.py +17 -2
- package/remote_script/LivePilot/scales.py +31 -16
- package/remote_script/LivePilot/simpler_sample.py +186 -0
- package/server.json +3 -3
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
LivePilot — Song-level scale handlers (Live 12.0+).
|
|
3
3
|
|
|
4
|
-
Exposes Song.root_note / scale_mode / scale_name / scale_intervals
|
|
5
|
-
|
|
4
|
+
Exposes Song.root_note / scale_mode / scale_name / scale_intervals via the
|
|
5
|
+
LivePilot TCP protocol. The scale list is resolved via a tolerant helper
|
|
6
|
+
that probes multiple attribute names because Live 12.4 moved/renamed
|
|
7
|
+
``Song.scale_names`` (see BUG-2026-04-22#2).
|
|
6
8
|
|
|
7
|
-
All
|
|
8
|
-
Gated behind the `song_scale_api` feature flag for defensive safety
|
|
9
|
-
on older versions, even though we target 12.3.6.
|
|
9
|
+
All props shipped in Live 12.0 when Scale Mode was introduced.
|
|
10
|
+
Gated behind the `song_scale_api` feature flag for defensive safety.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from .router import register
|
|
14
|
+
from ._scale_helpers import (
|
|
15
|
+
_BUILTIN_SCALES_FALLBACK,
|
|
16
|
+
_coerce_root_note,
|
|
17
|
+
_resolve_scale_names,
|
|
18
|
+
)
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
@register("get_song_scale")
|
|
@@ -23,27 +29,36 @@ def get_song_scale(song, params):
|
|
|
23
29
|
"scale_mode": bool(song.scale_mode),
|
|
24
30
|
"scale_name": str(song.scale_name),
|
|
25
31
|
"scale_intervals": list(song.scale_intervals),
|
|
26
|
-
"available_scales":
|
|
32
|
+
"available_scales": _resolve_scale_names(song),
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
|
|
30
36
|
@register("set_song_scale")
|
|
31
37
|
def set_song_scale(song, params):
|
|
32
|
-
"""Set both root_note
|
|
38
|
+
"""Set both root_note and scale_name atomically.
|
|
39
|
+
|
|
40
|
+
root_note accepts int 0-11 OR a note-name string like "C#", "F", "Bb".
|
|
41
|
+
scale_name is case-insensitive and validated against Live's scale list
|
|
42
|
+
(with a fallback to the built-in names when Live 12.4 hides scale_names).
|
|
43
|
+
"""
|
|
33
44
|
from .version_detect import has_feature
|
|
34
45
|
if not has_feature("song_scale_api"):
|
|
35
46
|
raise RuntimeError("Song scale API requires Live 12.0+.")
|
|
36
|
-
root =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
root = _coerce_root_note(params["root_note"])
|
|
48
|
+
name = str(params["scale_name"]).strip()
|
|
49
|
+
available = _resolve_scale_names(song)
|
|
50
|
+
# Case-insensitive match against Live's list (or fallback).
|
|
51
|
+
match = None
|
|
52
|
+
for candidate in available:
|
|
53
|
+
if candidate.lower() == name.lower():
|
|
54
|
+
match = candidate
|
|
55
|
+
break
|
|
56
|
+
if match is None:
|
|
42
57
|
raise ValueError(
|
|
43
58
|
"Unknown scale '%s'. Available: %s" % (name, ", ".join(available))
|
|
44
59
|
)
|
|
45
60
|
song.root_note = root
|
|
46
|
-
song.scale_name =
|
|
61
|
+
song.scale_name = match
|
|
47
62
|
return {
|
|
48
63
|
"root_note": int(song.root_note),
|
|
49
64
|
"scale_name": str(song.scale_name),
|
|
@@ -63,11 +78,11 @@ def set_song_scale_mode(song, params):
|
|
|
63
78
|
|
|
64
79
|
@register("list_available_scales")
|
|
65
80
|
def list_available_scales(song, params):
|
|
66
|
-
"""Return Live's built-in
|
|
81
|
+
"""Return Live's built-in scale names — tolerant of 12.4 API drop."""
|
|
67
82
|
from .version_detect import has_feature
|
|
68
83
|
if not has_feature("song_scale_api"):
|
|
69
84
|
raise RuntimeError("Song scale API requires Live 12.0+.")
|
|
70
|
-
return {"scales":
|
|
85
|
+
return {"scales": _resolve_scale_names(song)}
|
|
71
86
|
|
|
72
87
|
|
|
73
88
|
@register("get_tuning_system")
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# remote_script/LivePilot/simpler_sample.py
|
|
2
|
+
"""
|
|
3
|
+
LivePilot — Simpler sample replacement via the native Live 12.4 LOM API.
|
|
4
|
+
|
|
5
|
+
Exposes a ``replace_sample_native`` command that calls
|
|
6
|
+
``SimplerDevice.replace_sample(absolute_path)`` directly on the main thread.
|
|
7
|
+
Unlike the M4L-bridge path, this handler works on empty Simplers (the whole
|
|
8
|
+
reason 12.4 added the native API) and does not require a Max for Live
|
|
9
|
+
device in the Set.
|
|
10
|
+
|
|
11
|
+
Supports nested addressing into Drum Rack chains via the optional
|
|
12
|
+
``chain_index`` and ``nested_device_index`` parameters — this is how
|
|
13
|
+
BUG-#1 from docs/2026-04-22-bugs-discovered.md is unblocked. When
|
|
14
|
+
``chain_index`` is present, the device is resolved at
|
|
15
|
+
``track.devices[device_index].chains[chain_index].devices[nested_device_index]``.
|
|
16
|
+
|
|
17
|
+
Version-gated on ``replace_sample_native`` (12.4.0+). On earlier versions
|
|
18
|
+
the handler returns a STATE_ERROR; callers (MCP tools) are expected to
|
|
19
|
+
fall back to the bridge path.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .router import register
|
|
23
|
+
from .version_detect import has_feature, version_string
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_simpler_device(song, track_index, device_index, chain_index, nested_device_index):
|
|
27
|
+
"""Walk the device tree to the Simpler, supporting nested Drum Rack chains.
|
|
28
|
+
|
|
29
|
+
Returns (device, error_dict). On success error_dict is None.
|
|
30
|
+
When chain_index is None, returns the top-level device at device_index.
|
|
31
|
+
When chain_index is provided, walks into the rack's chain and returns
|
|
32
|
+
the device at nested_device_index (default 0) of that chain.
|
|
33
|
+
"""
|
|
34
|
+
tracks = list(song.tracks)
|
|
35
|
+
if track_index < 0 or track_index >= len(tracks):
|
|
36
|
+
return None, {
|
|
37
|
+
"error": "track_index " + str(track_index) + " out of range",
|
|
38
|
+
"code": "INDEX_ERROR",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
track = tracks[track_index]
|
|
42
|
+
devices = list(track.devices)
|
|
43
|
+
if device_index < 0 or device_index >= len(devices):
|
|
44
|
+
return None, {
|
|
45
|
+
"error": "device_index " + str(device_index) + " out of range",
|
|
46
|
+
"code": "INDEX_ERROR",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
top_device = devices[device_index]
|
|
50
|
+
|
|
51
|
+
# Simple top-level path
|
|
52
|
+
if chain_index is None:
|
|
53
|
+
return top_device, None
|
|
54
|
+
|
|
55
|
+
# Nested path — top device must be a rack (has `chains`)
|
|
56
|
+
if not hasattr(top_device, "chains"):
|
|
57
|
+
return None, {
|
|
58
|
+
"error": (
|
|
59
|
+
"Device at [" + str(track_index) + "][" + str(device_index) + "] "
|
|
60
|
+
"is not a rack — chain_index is only valid for racks"
|
|
61
|
+
),
|
|
62
|
+
"code": "INVALID_PARAM",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
chains = list(top_device.chains)
|
|
66
|
+
if chain_index < 0 or chain_index >= len(chains):
|
|
67
|
+
return None, {
|
|
68
|
+
"error": "chain_index " + str(chain_index) + " out of range ("
|
|
69
|
+
+ str(len(chains)) + " chains)",
|
|
70
|
+
"code": "INDEX_ERROR",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
chain_devices = list(chains[chain_index].devices)
|
|
74
|
+
nested_idx = 0 if nested_device_index is None else int(nested_device_index)
|
|
75
|
+
if nested_idx < 0 or nested_idx >= len(chain_devices):
|
|
76
|
+
return None, {
|
|
77
|
+
"error": "nested_device_index " + str(nested_idx) + " out of range ("
|
|
78
|
+
+ str(len(chain_devices)) + " devices in chain)",
|
|
79
|
+
"code": "INDEX_ERROR",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return chain_devices[nested_idx], None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@register("replace_sample_native")
|
|
86
|
+
def replace_sample_native(song, params):
|
|
87
|
+
"""Replace the sample in a Simpler device using the Live 12.4+ native API.
|
|
88
|
+
|
|
89
|
+
params dict keys:
|
|
90
|
+
track_index (int): 0-based index into song.tracks.
|
|
91
|
+
device_index (int): 0-based index into the track's devices.
|
|
92
|
+
file_path (str): absolute path to the audio file to load.
|
|
93
|
+
chain_index (int, optional): if the device_index points at a rack,
|
|
94
|
+
walk into this chain before finding the Simpler. Unlocks
|
|
95
|
+
Drum Rack pad-by-pad construction.
|
|
96
|
+
nested_device_index (int, optional): device index WITHIN the chain
|
|
97
|
+
(default 0 — first device in the chain).
|
|
98
|
+
|
|
99
|
+
Returns on success:
|
|
100
|
+
sample_loaded (bool): True.
|
|
101
|
+
track_index (int): echoed from input.
|
|
102
|
+
device_index (int): echoed from input.
|
|
103
|
+
chain_index (int|None): echoed if nested.
|
|
104
|
+
nested_device_index (int|None): echoed if nested.
|
|
105
|
+
method (str): "native_12_4".
|
|
106
|
+
live_version (str): detected Live version at call time.
|
|
107
|
+
|
|
108
|
+
Returns on error:
|
|
109
|
+
error (str): human-readable message.
|
|
110
|
+
code (str): STATE_ERROR | INDEX_ERROR | INVALID_PARAM | INTERNAL.
|
|
111
|
+
"""
|
|
112
|
+
if not has_feature("replace_sample_native"):
|
|
113
|
+
return {
|
|
114
|
+
"error": (
|
|
115
|
+
"replace_sample_native requires Live 12.4+. "
|
|
116
|
+
"Detected: " + version_string() + ". "
|
|
117
|
+
"Use the M4L-bridge replace_simpler_sample path instead."
|
|
118
|
+
),
|
|
119
|
+
"code": "STATE_ERROR",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
track_index = int(params["track_index"])
|
|
124
|
+
device_index = int(params["device_index"])
|
|
125
|
+
file_path = str(params["file_path"])
|
|
126
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
127
|
+
return {
|
|
128
|
+
"error": "Invalid params: " + str(exc),
|
|
129
|
+
"code": "INVALID_PARAM",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Optional nested addressing
|
|
133
|
+
chain_index = params.get("chain_index")
|
|
134
|
+
nested_device_index = params.get("nested_device_index")
|
|
135
|
+
if chain_index is not None:
|
|
136
|
+
try:
|
|
137
|
+
chain_index = int(chain_index)
|
|
138
|
+
except (TypeError, ValueError):
|
|
139
|
+
return {
|
|
140
|
+
"error": "chain_index must be an integer if provided",
|
|
141
|
+
"code": "INVALID_PARAM",
|
|
142
|
+
}
|
|
143
|
+
if nested_device_index is not None:
|
|
144
|
+
try:
|
|
145
|
+
nested_device_index = int(nested_device_index)
|
|
146
|
+
except (TypeError, ValueError):
|
|
147
|
+
return {
|
|
148
|
+
"error": "nested_device_index must be an integer if provided",
|
|
149
|
+
"code": "INVALID_PARAM",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
device, err = _resolve_simpler_device(
|
|
153
|
+
song, track_index, device_index, chain_index, nested_device_index,
|
|
154
|
+
)
|
|
155
|
+
if err is not None:
|
|
156
|
+
return err
|
|
157
|
+
|
|
158
|
+
class_name = getattr(device, "class_name", "")
|
|
159
|
+
# Live's LOM exposes Simpler as class_name="OriginalSimpler" (not
|
|
160
|
+
# "SimplerDevice") — see remote_script/LivePilot/devices.py:852 and
|
|
161
|
+
# mcp_server/sample_engine/tools.py:501 for the same check elsewhere.
|
|
162
|
+
if class_name != "OriginalSimpler":
|
|
163
|
+
return {
|
|
164
|
+
"error": (
|
|
165
|
+
"Device at resolved path is " + class_name + ", not Simpler"
|
|
166
|
+
),
|
|
167
|
+
"code": "INVALID_PARAM",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
device.replace_sample(file_path)
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
return {
|
|
174
|
+
"error": "SimplerDevice.replace_sample failed: " + str(exc),
|
|
175
|
+
"code": "INTERNAL",
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"sample_loaded": True,
|
|
180
|
+
"track_index": track_index,
|
|
181
|
+
"device_index": device_index,
|
|
182
|
+
"chain_index": chain_index,
|
|
183
|
+
"nested_device_index": nested_device_index,
|
|
184
|
+
"method": "native_12_4",
|
|
185
|
+
"live_version": version_string(),
|
|
186
|
+
}
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "421-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.16.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.16.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|