livepilot 1.1.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/.claude/settings.local.json +10 -0
- package/.mcpregistry_github_token +1 -0
- package/.mcpregistry_registry_token +1 -0
- package/.playwright-mcp/console-2026-03-17T15-47-29-021Z.log +10 -0
- package/.playwright-mcp/console-2026-03-17T15-51-09-247Z.log +10 -0
- package/.playwright-mcp/console-2026-03-17T15-52-22-831Z.log +12 -0
- package/.playwright-mcp/console-2026-03-17T15-52-29-709Z.log +10 -0
- package/.playwright-mcp/console-2026-03-17T15-53-20-147Z.log +1 -0
- package/.playwright-mcp/glama-snapshot.md +2140 -0
- package/.playwright-mcp/page-2026-03-17T15-49-02-625Z.png +0 -0
- package/.playwright-mcp/page-2026-03-17T15-52-15-149Z.png +0 -0
- package/.playwright-mcp/page-2026-03-17T15-52-57-333Z.png +0 -0
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/bin/livepilot.js +376 -0
- package/installer/install.js +95 -0
- package/installer/paths.js +79 -0
- package/mcp_server/__init__.py +2 -0
- package/mcp_server/__main__.py +5 -0
- package/mcp_server/connection.py +207 -0
- package/mcp_server/server.py +40 -0
- package/mcp_server/tools/__init__.py +1 -0
- package/mcp_server/tools/arrangement.py +399 -0
- package/mcp_server/tools/browser.py +78 -0
- package/mcp_server/tools/clips.py +187 -0
- package/mcp_server/tools/devices.py +238 -0
- package/mcp_server/tools/mixing.py +113 -0
- package/mcp_server/tools/notes.py +266 -0
- package/mcp_server/tools/scenes.py +63 -0
- package/mcp_server/tools/tracks.py +148 -0
- package/mcp_server/tools/transport.py +113 -0
- package/package.json +38 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/agents/livepilot-producer/AGENT.md +61 -0
- package/plugin/commands/beat.md +18 -0
- package/plugin/commands/mix.md +15 -0
- package/plugin/commands/session.md +13 -0
- package/plugin/commands/sounddesign.md +16 -0
- package/plugin/plugin.json +18 -0
- package/plugin/skills/livepilot-core/SKILL.md +160 -0
- package/plugin/skills/livepilot-core/references/ableton-workflow-patterns.md +831 -0
- package/plugin/skills/livepilot-core/references/m4l-devices.md +352 -0
- package/plugin/skills/livepilot-core/references/midi-recipes.md +402 -0
- package/plugin/skills/livepilot-core/references/mixing-patterns.md +578 -0
- package/plugin/skills/livepilot-core/references/overview.md +191 -0
- package/plugin/skills/livepilot-core/references/sound-design.md +392 -0
- package/remote_script/LivePilot/__init__.py +42 -0
- package/remote_script/LivePilot/arrangement.py +678 -0
- package/remote_script/LivePilot/browser.py +325 -0
- package/remote_script/LivePilot/clips.py +172 -0
- package/remote_script/LivePilot/devices.py +466 -0
- package/remote_script/LivePilot/diagnostics.py +198 -0
- package/remote_script/LivePilot/mixing.py +194 -0
- package/remote_script/LivePilot/notes.py +339 -0
- package/remote_script/LivePilot/router.py +74 -0
- package/remote_script/LivePilot/scenes.py +75 -0
- package/remote_script/LivePilot/server.py +286 -0
- package/remote_script/LivePilot/tracks.py +229 -0
- package/remote_script/LivePilot/transport.py +147 -0
- package/remote_script/LivePilot/utils.py +112 -0
- package/requirements.txt +2 -0
- package/server.json +20 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LivePilot - Browser domain handlers (5 commands).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import Live
|
|
6
|
+
|
|
7
|
+
from .router import register
|
|
8
|
+
from .utils import get_track
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_browser():
|
|
12
|
+
"""Get the browser from the Application object (not Song)."""
|
|
13
|
+
return Live.Application.get_application().browser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_categories(browser):
|
|
17
|
+
"""Return a dict of browser category name -> browser item."""
|
|
18
|
+
return {
|
|
19
|
+
"instruments": browser.instruments,
|
|
20
|
+
"audio_effects": browser.audio_effects,
|
|
21
|
+
"midi_effects": browser.midi_effects,
|
|
22
|
+
"sounds": browser.sounds,
|
|
23
|
+
"drums": browser.drums,
|
|
24
|
+
"samples": browser.samples,
|
|
25
|
+
"packs": browser.packs,
|
|
26
|
+
"user_library": browser.user_library,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _navigate_path(browser, path):
|
|
31
|
+
"""Walk the browser tree by slash-separated path, return the item."""
|
|
32
|
+
categories = _get_categories(browser)
|
|
33
|
+
parts = [p.strip() for p in path.strip("/").split("/") if p.strip()]
|
|
34
|
+
if not parts:
|
|
35
|
+
raise ValueError("Path cannot be empty")
|
|
36
|
+
|
|
37
|
+
# First part must be a category name
|
|
38
|
+
first = parts[0].lower()
|
|
39
|
+
if first not in categories:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"Unknown category '%s'. Available: %s"
|
|
42
|
+
% (first, ", ".join(sorted(categories.keys())))
|
|
43
|
+
)
|
|
44
|
+
current = categories[first]
|
|
45
|
+
|
|
46
|
+
# Walk remaining parts by child name
|
|
47
|
+
for part in parts[1:]:
|
|
48
|
+
children = list(current.children)
|
|
49
|
+
matched = None
|
|
50
|
+
for child in children:
|
|
51
|
+
if child.name == part:
|
|
52
|
+
matched = child
|
|
53
|
+
break
|
|
54
|
+
if matched is None:
|
|
55
|
+
child_names = [c.name for c in children[:20]]
|
|
56
|
+
raise ValueError(
|
|
57
|
+
"Item '%s' not found in '%s'. Available: %s"
|
|
58
|
+
% (part, current.name, ", ".join(child_names))
|
|
59
|
+
)
|
|
60
|
+
current = matched
|
|
61
|
+
|
|
62
|
+
return current
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _search_recursive(item, name_filter, loadable_only, results, depth, max_depth,
|
|
66
|
+
max_results=100):
|
|
67
|
+
"""Recursively search browser children."""
|
|
68
|
+
if depth > max_depth or len(results) >= max_results:
|
|
69
|
+
return
|
|
70
|
+
for child in item.children:
|
|
71
|
+
if len(results) >= max_results:
|
|
72
|
+
return
|
|
73
|
+
match = True
|
|
74
|
+
if name_filter and name_filter.lower() not in child.name.lower():
|
|
75
|
+
match = False
|
|
76
|
+
if loadable_only and not child.is_loadable:
|
|
77
|
+
match = False
|
|
78
|
+
if match:
|
|
79
|
+
entry = {
|
|
80
|
+
"name": child.name,
|
|
81
|
+
"is_loadable": child.is_loadable,
|
|
82
|
+
}
|
|
83
|
+
try:
|
|
84
|
+
entry["uri"] = child.uri
|
|
85
|
+
except Exception:
|
|
86
|
+
entry["uri"] = None
|
|
87
|
+
results.append(entry)
|
|
88
|
+
if child.is_folder:
|
|
89
|
+
_search_recursive(
|
|
90
|
+
child, name_filter, loadable_only, results, depth + 1, max_depth,
|
|
91
|
+
max_results
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@register("get_browser_tree")
|
|
96
|
+
def get_browser_tree(song, params):
|
|
97
|
+
"""Return an overview of the browser categories."""
|
|
98
|
+
category_type = str(params.get("category_type", "all")).lower()
|
|
99
|
+
browser = _get_browser()
|
|
100
|
+
categories = _get_categories(browser)
|
|
101
|
+
|
|
102
|
+
if category_type != "all":
|
|
103
|
+
if category_type not in categories:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
"Unknown category '%s'. Available: %s"
|
|
106
|
+
% (category_type, ", ".join(sorted(categories.keys())))
|
|
107
|
+
)
|
|
108
|
+
categories = {category_type: categories[category_type]}
|
|
109
|
+
|
|
110
|
+
result = []
|
|
111
|
+
for name, item in categories.items():
|
|
112
|
+
children = list(item.children)
|
|
113
|
+
child_names = [c.name for c in children[:20]]
|
|
114
|
+
result.append({
|
|
115
|
+
"name": name,
|
|
116
|
+
"children_count": len(children),
|
|
117
|
+
"children_preview": child_names,
|
|
118
|
+
})
|
|
119
|
+
return {"categories": result}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@register("get_browser_items")
|
|
123
|
+
def get_browser_items(song, params):
|
|
124
|
+
"""List items at a browser path."""
|
|
125
|
+
path = str(params["path"])
|
|
126
|
+
browser = _get_browser()
|
|
127
|
+
item = _navigate_path(browser, path)
|
|
128
|
+
|
|
129
|
+
result = []
|
|
130
|
+
for child in item.children:
|
|
131
|
+
entry = {
|
|
132
|
+
"name": child.name,
|
|
133
|
+
"is_loadable": child.is_loadable,
|
|
134
|
+
"is_folder": child.is_folder,
|
|
135
|
+
}
|
|
136
|
+
if child.is_loadable:
|
|
137
|
+
try:
|
|
138
|
+
entry["uri"] = child.uri
|
|
139
|
+
except Exception:
|
|
140
|
+
entry["uri"] = None
|
|
141
|
+
result.append(entry)
|
|
142
|
+
return {"path": path, "items": result}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@register("search_browser")
|
|
146
|
+
def search_browser(song, params):
|
|
147
|
+
"""Search the browser tree by name filter."""
|
|
148
|
+
path = str(params["path"])
|
|
149
|
+
name_filter = params.get("name_filter", None)
|
|
150
|
+
loadable_only = bool(params.get("loadable_only", False))
|
|
151
|
+
max_depth = int(params.get("max_depth", 8))
|
|
152
|
+
max_results = int(params.get("max_results", 100))
|
|
153
|
+
browser = _get_browser()
|
|
154
|
+
item = _navigate_path(browser, path)
|
|
155
|
+
|
|
156
|
+
results = []
|
|
157
|
+
_search_recursive(item, name_filter, loadable_only, results, 0, max_depth,
|
|
158
|
+
max_results)
|
|
159
|
+
truncated = len(results) >= max_results
|
|
160
|
+
result = {"path": path, "results": results, "count": len(results)}
|
|
161
|
+
if truncated:
|
|
162
|
+
result["truncated"] = True
|
|
163
|
+
result["max_results"] = max_results
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@register("load_browser_item")
|
|
168
|
+
def load_browser_item(song, params):
|
|
169
|
+
"""Load a browser item onto a track by URI.
|
|
170
|
+
|
|
171
|
+
First tries URI-based matching (exact child.uri comparison).
|
|
172
|
+
Falls back to name extraction from the URI's last path segment.
|
|
173
|
+
Searches all browser categories including user_library and samples.
|
|
174
|
+
"""
|
|
175
|
+
track_index = int(params["track_index"])
|
|
176
|
+
uri = str(params["uri"])
|
|
177
|
+
track = get_track(song, track_index)
|
|
178
|
+
browser = _get_browser()
|
|
179
|
+
|
|
180
|
+
# All categories to search
|
|
181
|
+
category_attrs = (
|
|
182
|
+
"user_library", "samples", "instruments", "audio_effects",
|
|
183
|
+
"midi_effects", "packs", "sounds", "drums",
|
|
184
|
+
)
|
|
185
|
+
categories = []
|
|
186
|
+
for attr in category_attrs:
|
|
187
|
+
try:
|
|
188
|
+
categories.append(getattr(browser, attr))
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
_iterations = [0]
|
|
193
|
+
MAX_ITERATIONS = 10000
|
|
194
|
+
|
|
195
|
+
# ── Strategy 1: match by URI directly ────────────────────────────
|
|
196
|
+
def find_by_uri(parent, target_uri, depth=0):
|
|
197
|
+
if depth > 8 or _iterations[0] > MAX_ITERATIONS:
|
|
198
|
+
return None
|
|
199
|
+
try:
|
|
200
|
+
children = list(parent.children)
|
|
201
|
+
except Exception:
|
|
202
|
+
return None
|
|
203
|
+
for child in children:
|
|
204
|
+
_iterations[0] += 1
|
|
205
|
+
if _iterations[0] > MAX_ITERATIONS:
|
|
206
|
+
return None
|
|
207
|
+
try:
|
|
208
|
+
if child.uri == target_uri and child.is_loadable:
|
|
209
|
+
return child
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
result = find_by_uri(child, target_uri, depth + 1)
|
|
213
|
+
if result is not None:
|
|
214
|
+
return result
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
for category in categories:
|
|
218
|
+
_iterations[0] = 0
|
|
219
|
+
found = find_by_uri(category, uri)
|
|
220
|
+
if found is not None:
|
|
221
|
+
song.view.selected_track = track
|
|
222
|
+
browser.load_item(found)
|
|
223
|
+
device_count = len(list(track.devices))
|
|
224
|
+
return {
|
|
225
|
+
"track_index": track_index,
|
|
226
|
+
"loaded": True,
|
|
227
|
+
"name": found.name,
|
|
228
|
+
"device_count": device_count,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
# ── Strategy 2: extract name from URI, search by name ────────────
|
|
232
|
+
device_name = uri
|
|
233
|
+
if "#" in uri:
|
|
234
|
+
device_name = uri.split("#", 1)[1]
|
|
235
|
+
for sep in (":", "/"):
|
|
236
|
+
if sep in device_name:
|
|
237
|
+
device_name = device_name.rsplit(sep, 1)[1]
|
|
238
|
+
# URL-decode
|
|
239
|
+
try:
|
|
240
|
+
from urllib.parse import unquote
|
|
241
|
+
device_name = unquote(device_name)
|
|
242
|
+
except ImportError:
|
|
243
|
+
device_name = device_name.replace("%20", " ")
|
|
244
|
+
# Strip file extensions
|
|
245
|
+
for ext in (".amxd", ".adv", ".adg", ".aupreset", ".als", ".wav", ".aif", ".aiff", ".mp3"):
|
|
246
|
+
if device_name.lower().endswith(ext):
|
|
247
|
+
device_name = device_name[:-len(ext)]
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
target = device_name.lower()
|
|
251
|
+
|
|
252
|
+
def find_by_name(parent, depth=0):
|
|
253
|
+
if depth > 8 or _iterations[0] > MAX_ITERATIONS:
|
|
254
|
+
return None
|
|
255
|
+
try:
|
|
256
|
+
children = list(parent.children)
|
|
257
|
+
except Exception:
|
|
258
|
+
return None
|
|
259
|
+
for child in children:
|
|
260
|
+
_iterations[0] += 1
|
|
261
|
+
if _iterations[0] > MAX_ITERATIONS:
|
|
262
|
+
return None
|
|
263
|
+
child_lower = child.name.lower()
|
|
264
|
+
if (child_lower == target or target in child_lower) and child.is_loadable:
|
|
265
|
+
return child
|
|
266
|
+
result = find_by_name(child, depth + 1)
|
|
267
|
+
if result is not None:
|
|
268
|
+
return result
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
for category in categories:
|
|
272
|
+
_iterations[0] = 0
|
|
273
|
+
found = find_by_name(category)
|
|
274
|
+
if found is not None:
|
|
275
|
+
song.view.selected_track = track
|
|
276
|
+
browser.load_item(found)
|
|
277
|
+
device_count = len(list(track.devices))
|
|
278
|
+
return {
|
|
279
|
+
"track_index": track_index,
|
|
280
|
+
"loaded": True,
|
|
281
|
+
"name": found.name,
|
|
282
|
+
"device_count": device_count,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
raise ValueError(
|
|
286
|
+
"Item '%s' not found in browser" % device_name
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@register("get_device_presets")
|
|
291
|
+
def get_device_presets(song, params):
|
|
292
|
+
"""List available presets for a device type by searching the browser."""
|
|
293
|
+
device_name = str(params["device_name"])
|
|
294
|
+
browser = _get_browser()
|
|
295
|
+
|
|
296
|
+
categories = {
|
|
297
|
+
"audio_effects": browser.audio_effects,
|
|
298
|
+
"instruments": browser.instruments,
|
|
299
|
+
"midi_effects": browser.midi_effects,
|
|
300
|
+
}
|
|
301
|
+
results = []
|
|
302
|
+
found_category = None
|
|
303
|
+
for cat_name, cat_item in categories.items():
|
|
304
|
+
for item in cat_item.children:
|
|
305
|
+
if item.name.lower() == device_name.lower():
|
|
306
|
+
found_category = cat_name
|
|
307
|
+
for preset in item.children:
|
|
308
|
+
if preset.is_loadable:
|
|
309
|
+
entry = {
|
|
310
|
+
"name": preset.name,
|
|
311
|
+
"is_folder": preset.is_folder,
|
|
312
|
+
}
|
|
313
|
+
try:
|
|
314
|
+
entry["uri"] = preset.uri
|
|
315
|
+
except Exception:
|
|
316
|
+
entry["uri"] = None
|
|
317
|
+
results.append(entry)
|
|
318
|
+
break
|
|
319
|
+
if found_category:
|
|
320
|
+
break
|
|
321
|
+
return {
|
|
322
|
+
"device_name": device_name,
|
|
323
|
+
"category": found_category,
|
|
324
|
+
"presets": results,
|
|
325
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LivePilot - Clip domain handlers (10 commands).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .router import register
|
|
6
|
+
from .utils import get_clip, get_clip_slot
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register("get_clip_info")
|
|
10
|
+
def get_clip_info(song, params):
|
|
11
|
+
"""Return detailed info for a single clip."""
|
|
12
|
+
track_index = int(params["track_index"])
|
|
13
|
+
clip_index = int(params["clip_index"])
|
|
14
|
+
clip = get_clip(song, track_index, clip_index)
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
"track_index": track_index,
|
|
18
|
+
"clip_index": clip_index,
|
|
19
|
+
"name": clip.name,
|
|
20
|
+
"color_index": clip.color_index,
|
|
21
|
+
"length": clip.length,
|
|
22
|
+
"is_playing": clip.is_playing,
|
|
23
|
+
"is_recording": clip.is_recording,
|
|
24
|
+
"looping": clip.looping,
|
|
25
|
+
"loop_start": clip.loop_start,
|
|
26
|
+
"loop_end": clip.loop_end,
|
|
27
|
+
"start_marker": clip.start_marker,
|
|
28
|
+
"end_marker": clip.end_marker,
|
|
29
|
+
"launch_mode": clip.launch_mode,
|
|
30
|
+
"launch_quantization": clip.launch_quantization,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@register("create_clip")
|
|
35
|
+
def create_clip(song, params):
|
|
36
|
+
"""Create an empty MIDI clip in the given clip slot."""
|
|
37
|
+
track_index = int(params["track_index"])
|
|
38
|
+
clip_index = int(params["clip_index"])
|
|
39
|
+
length = float(params["length"])
|
|
40
|
+
if length <= 0:
|
|
41
|
+
raise ValueError("Clip length must be > 0")
|
|
42
|
+
|
|
43
|
+
clip_slot = get_clip_slot(song, track_index, clip_index)
|
|
44
|
+
clip_slot.create_clip(length)
|
|
45
|
+
clip = clip_slot.clip
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"track_index": track_index,
|
|
49
|
+
"clip_index": clip_index,
|
|
50
|
+
"name": clip.name,
|
|
51
|
+
"length": clip.length,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@register("delete_clip")
|
|
56
|
+
def delete_clip(song, params):
|
|
57
|
+
"""Delete the clip in the given clip slot."""
|
|
58
|
+
track_index = int(params["track_index"])
|
|
59
|
+
clip_index = int(params["clip_index"])
|
|
60
|
+
clip_slot = get_clip_slot(song, track_index, clip_index)
|
|
61
|
+
clip_slot.delete_clip()
|
|
62
|
+
return {"track_index": track_index, "clip_index": clip_index, "deleted": True}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@register("duplicate_clip")
|
|
66
|
+
def duplicate_clip(song, params):
|
|
67
|
+
"""Duplicate a clip from one slot to another."""
|
|
68
|
+
track_index = int(params["track_index"])
|
|
69
|
+
clip_index = int(params["clip_index"])
|
|
70
|
+
target_track = int(params["target_track"])
|
|
71
|
+
target_clip = int(params["target_clip"])
|
|
72
|
+
|
|
73
|
+
source_slot = get_clip_slot(song, track_index, clip_index)
|
|
74
|
+
target_slot = get_clip_slot(song, target_track, target_clip)
|
|
75
|
+
source_slot.duplicate_clip_to(target_slot)
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"source_track": track_index,
|
|
79
|
+
"source_clip": clip_index,
|
|
80
|
+
"target_track": target_track,
|
|
81
|
+
"target_clip": target_clip,
|
|
82
|
+
"duplicated": True,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@register("fire_clip")
|
|
87
|
+
def fire_clip(song, params):
|
|
88
|
+
"""Launch/fire a clip slot."""
|
|
89
|
+
track_index = int(params["track_index"])
|
|
90
|
+
clip_index = int(params["clip_index"])
|
|
91
|
+
clip_slot = get_clip_slot(song, track_index, clip_index)
|
|
92
|
+
clip_slot.fire()
|
|
93
|
+
return {"track_index": track_index, "clip_index": clip_index, "fired": True}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@register("stop_clip")
|
|
97
|
+
def stop_clip(song, params):
|
|
98
|
+
"""Stop a clip slot."""
|
|
99
|
+
track_index = int(params["track_index"])
|
|
100
|
+
clip_index = int(params["clip_index"])
|
|
101
|
+
clip_slot = get_clip_slot(song, track_index, clip_index)
|
|
102
|
+
clip_slot.stop()
|
|
103
|
+
return {"track_index": track_index, "clip_index": clip_index, "stopped": True}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@register("set_clip_name")
|
|
107
|
+
def set_clip_name(song, params):
|
|
108
|
+
"""Rename a clip."""
|
|
109
|
+
track_index = int(params["track_index"])
|
|
110
|
+
clip_index = int(params["clip_index"])
|
|
111
|
+
clip = get_clip(song, track_index, clip_index)
|
|
112
|
+
clip.name = str(params["name"])
|
|
113
|
+
return {
|
|
114
|
+
"track_index": track_index,
|
|
115
|
+
"clip_index": clip_index,
|
|
116
|
+
"name": clip.name,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@register("set_clip_color")
|
|
121
|
+
def set_clip_color(song, params):
|
|
122
|
+
"""Set a clip's color."""
|
|
123
|
+
track_index = int(params["track_index"])
|
|
124
|
+
clip_index = int(params["clip_index"])
|
|
125
|
+
clip = get_clip(song, track_index, clip_index)
|
|
126
|
+
clip.color_index = int(params["color_index"])
|
|
127
|
+
return {
|
|
128
|
+
"track_index": track_index,
|
|
129
|
+
"clip_index": clip_index,
|
|
130
|
+
"color_index": clip.color_index,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@register("set_clip_loop")
|
|
135
|
+
def set_clip_loop(song, params):
|
|
136
|
+
"""Enable/disable clip looping and optionally set loop start/end."""
|
|
137
|
+
track_index = int(params["track_index"])
|
|
138
|
+
clip_index = int(params["clip_index"])
|
|
139
|
+
clip = get_clip(song, track_index, clip_index)
|
|
140
|
+
|
|
141
|
+
clip.looping = bool(params["enabled"])
|
|
142
|
+
if "start" in params:
|
|
143
|
+
clip.loop_start = float(params["start"])
|
|
144
|
+
if "end" in params:
|
|
145
|
+
clip.loop_end = float(params["end"])
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"track_index": track_index,
|
|
149
|
+
"clip_index": clip_index,
|
|
150
|
+
"looping": clip.looping,
|
|
151
|
+
"loop_start": clip.loop_start,
|
|
152
|
+
"loop_end": clip.loop_end,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@register("set_clip_launch")
|
|
157
|
+
def set_clip_launch(song, params):
|
|
158
|
+
"""Set clip launch mode and optional quantization."""
|
|
159
|
+
track_index = int(params["track_index"])
|
|
160
|
+
clip_index = int(params["clip_index"])
|
|
161
|
+
clip = get_clip(song, track_index, clip_index)
|
|
162
|
+
|
|
163
|
+
clip.launch_mode = int(params["mode"])
|
|
164
|
+
if "quantization" in params:
|
|
165
|
+
clip.launch_quantization = int(params["quantization"])
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"track_index": track_index,
|
|
169
|
+
"clip_index": clip_index,
|
|
170
|
+
"launch_mode": clip.launch_mode,
|
|
171
|
+
"launch_quantization": clip.launch_quantization,
|
|
172
|
+
}
|