livepilot 1.4.5 → 1.6.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/CHANGELOG.md +187 -144
- 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/curves.py +741 -0
- package/mcp_server/m4l_bridge.py +285 -0
- package/mcp_server/server.py +29 -3
- package/mcp_server/tools/analyzer.py +508 -0
- package/mcp_server/tools/automation.py +431 -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/agents/livepilot-producer/AGENT.md +32 -2
- package/plugin/plugin.json +2 -2
- package/plugin/skills/livepilot-core/SKILL.md +76 -11
- package/plugin/skills/livepilot-core/references/automation-atlas.md +272 -0
- package/plugin/skills/livepilot-core/references/overview.md +68 -5
- package/plugin/skills/livepilot-release/SKILL.md +101 -0
- package/remote_script/LivePilot/__init__.py +3 -2
- package/remote_script/LivePilot/clip_automation.py +220 -0
- package/remote_script/LivePilot/mixing.py +90 -1
- package/remote_script/LivePilot/server.py +3 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""Analyzer MCP tools — real-time spectral analysis and deep LOM access.
|
|
2
|
+
|
|
3
|
+
20 tools requiring the LivePilot Analyzer M4L device on the master track.
|
|
4
|
+
These tools are optional — all 107 core tools work without the device.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from fastmcp import Context
|
|
10
|
+
|
|
11
|
+
from ..server import mcp
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_spectral(ctx: Context):
|
|
15
|
+
"""Get SpectralCache from lifespan context."""
|
|
16
|
+
cache = ctx.lifespan_context.get("spectral")
|
|
17
|
+
if not cache:
|
|
18
|
+
raise RuntimeError("Spectral cache not initialized")
|
|
19
|
+
return cache
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_m4l(ctx: Context):
|
|
23
|
+
"""Get M4LBridge from lifespan context."""
|
|
24
|
+
bridge = ctx.lifespan_context.get("m4l")
|
|
25
|
+
if not bridge:
|
|
26
|
+
raise RuntimeError("M4L bridge not initialized")
|
|
27
|
+
return bridge
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _require_analyzer(cache) -> None:
|
|
31
|
+
"""Raise a helpful error if the analyzer is not connected."""
|
|
32
|
+
if not cache.is_connected:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"LivePilot Analyzer not detected. "
|
|
35
|
+
"Drag 'LivePilot Analyzer' onto the master track from "
|
|
36
|
+
"Audio Effects > Max Audio Effect."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@mcp.tool()
|
|
41
|
+
def get_master_spectrum(ctx: Context) -> dict:
|
|
42
|
+
"""Get 8-band frequency analysis of the master bus.
|
|
43
|
+
|
|
44
|
+
Returns band energies: sub (20-60Hz), low (60-200Hz), low_mid (200-500Hz),
|
|
45
|
+
mid (500-2kHz), high_mid (2-4kHz), high (4-8kHz), presence (8-12kHz),
|
|
46
|
+
air (12-20kHz). Values 0.0-1.0.
|
|
47
|
+
|
|
48
|
+
Also returns detected key/scale if enough audio has been analyzed.
|
|
49
|
+
Requires LivePilot Analyzer on master track.
|
|
50
|
+
"""
|
|
51
|
+
cache = _get_spectral(ctx)
|
|
52
|
+
_require_analyzer(cache)
|
|
53
|
+
|
|
54
|
+
result = {}
|
|
55
|
+
spectrum = cache.get("spectrum")
|
|
56
|
+
if spectrum:
|
|
57
|
+
result["bands"] = spectrum["value"]
|
|
58
|
+
result["age_ms"] = spectrum["age_ms"]
|
|
59
|
+
|
|
60
|
+
key_data = cache.get("key")
|
|
61
|
+
if key_data:
|
|
62
|
+
result["detected_key"] = key_data["value"]
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@mcp.tool()
|
|
68
|
+
def get_master_rms(ctx: Context) -> dict:
|
|
69
|
+
"""Get real-time RMS and peak levels from the master bus.
|
|
70
|
+
|
|
71
|
+
More accurate than LOM meters — includes true RMS (not just peak hold).
|
|
72
|
+
Requires LivePilot Analyzer on master track.
|
|
73
|
+
"""
|
|
74
|
+
cache = _get_spectral(ctx)
|
|
75
|
+
_require_analyzer(cache)
|
|
76
|
+
|
|
77
|
+
result = {}
|
|
78
|
+
rms = cache.get("rms")
|
|
79
|
+
if rms:
|
|
80
|
+
result["rms"] = rms["value"]
|
|
81
|
+
result["age_ms"] = rms["age_ms"]
|
|
82
|
+
|
|
83
|
+
peak = cache.get("peak")
|
|
84
|
+
if peak:
|
|
85
|
+
result["peak"] = peak["value"]
|
|
86
|
+
|
|
87
|
+
pitch = cache.get("pitch")
|
|
88
|
+
if pitch:
|
|
89
|
+
result["pitch"] = pitch["value"]
|
|
90
|
+
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@mcp.tool()
|
|
95
|
+
async def get_detected_key(ctx: Context) -> dict:
|
|
96
|
+
"""Get the detected musical key and scale of the current session.
|
|
97
|
+
|
|
98
|
+
Uses the Krumhansl-Schmuckler key-finding algorithm on accumulated
|
|
99
|
+
pitch data from the master bus. Needs 4-8 bars of audio to be reliable.
|
|
100
|
+
Returns key (C, C#, D, etc.), scale (major/minor), and confidence
|
|
101
|
+
(number of pitch samples collected).
|
|
102
|
+
Requires LivePilot Analyzer on master track.
|
|
103
|
+
"""
|
|
104
|
+
cache = _get_spectral(ctx)
|
|
105
|
+
_require_analyzer(cache)
|
|
106
|
+
|
|
107
|
+
# First check the streaming cache for a recent key detection
|
|
108
|
+
key_data = cache.get("key")
|
|
109
|
+
if key_data:
|
|
110
|
+
return key_data["value"]
|
|
111
|
+
|
|
112
|
+
# Fall back to querying the bridge directly (key detection runs in JS
|
|
113
|
+
# and may not be forwarded via OSC streaming)
|
|
114
|
+
bridge = _get_m4l(ctx)
|
|
115
|
+
result = await bridge.send_command("get_key")
|
|
116
|
+
if "error" in result:
|
|
117
|
+
return result
|
|
118
|
+
if not result.get("key"):
|
|
119
|
+
return {"error": "Not enough audio analyzed yet. Play 4-8 bars for key detection."}
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@mcp.tool()
|
|
124
|
+
async def get_hidden_parameters(
|
|
125
|
+
ctx: Context,
|
|
126
|
+
track_index: int,
|
|
127
|
+
device_index: int,
|
|
128
|
+
) -> dict:
|
|
129
|
+
"""Get ALL parameters for a device, including hidden ones not accessible
|
|
130
|
+
via the standard ControlSurface API.
|
|
131
|
+
|
|
132
|
+
Returns parameter name, value, min, max, default, automation state,
|
|
133
|
+
and value string for every parameter — even non-automatable ones.
|
|
134
|
+
Requires LivePilot Analyzer on master track.
|
|
135
|
+
"""
|
|
136
|
+
cache = _get_spectral(ctx)
|
|
137
|
+
_require_analyzer(cache)
|
|
138
|
+
bridge = _get_m4l(ctx)
|
|
139
|
+
return await bridge.send_command("get_hidden_params", track_index, device_index)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@mcp.tool()
|
|
143
|
+
async def get_automation_state(
|
|
144
|
+
ctx: Context,
|
|
145
|
+
track_index: int,
|
|
146
|
+
device_index: int,
|
|
147
|
+
) -> dict:
|
|
148
|
+
"""Get automation state for all parameters on a device.
|
|
149
|
+
|
|
150
|
+
Returns only parameters that HAVE automation:
|
|
151
|
+
- state 1 = automation active (envelope is playing)
|
|
152
|
+
- state 2 = automation overridden (user moved knob manually)
|
|
153
|
+
|
|
154
|
+
Use this before writing automation to avoid overwriting existing curves.
|
|
155
|
+
Requires LivePilot Analyzer on master track.
|
|
156
|
+
"""
|
|
157
|
+
cache = _get_spectral(ctx)
|
|
158
|
+
_require_analyzer(cache)
|
|
159
|
+
bridge = _get_m4l(ctx)
|
|
160
|
+
return await bridge.send_command("get_auto_state", track_index, device_index)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@mcp.tool()
|
|
164
|
+
async def walk_device_tree(
|
|
165
|
+
ctx: Context,
|
|
166
|
+
track_index: int,
|
|
167
|
+
device_index: int = 0,
|
|
168
|
+
) -> dict:
|
|
169
|
+
"""Walk the full device chain tree including nested racks, drum pads,
|
|
170
|
+
and grouped devices. Returns the complete hierarchy up to 6 levels deep.
|
|
171
|
+
|
|
172
|
+
Use this to see inside Instrument Racks, Effect Racks, and Drum Racks
|
|
173
|
+
that the standard get_device_info can't penetrate.
|
|
174
|
+
Requires LivePilot Analyzer on master track.
|
|
175
|
+
"""
|
|
176
|
+
cache = _get_spectral(ctx)
|
|
177
|
+
_require_analyzer(cache)
|
|
178
|
+
bridge = _get_m4l(ctx)
|
|
179
|
+
return await bridge.send_command("walk_rack", track_index, device_index)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ── Phase 2: Sample Operations ─────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@mcp.tool()
|
|
186
|
+
async def get_clip_file_path(
|
|
187
|
+
ctx: Context,
|
|
188
|
+
track_index: int,
|
|
189
|
+
clip_index: int,
|
|
190
|
+
) -> dict:
|
|
191
|
+
"""Get the audio file path of a clip on disk.
|
|
192
|
+
|
|
193
|
+
Returns the absolute path to the audio file, clip name, and length.
|
|
194
|
+
Only works on audio clips — MIDI clips have no file path.
|
|
195
|
+
Use this to get a path for replace_simpler_sample.
|
|
196
|
+
Requires LivePilot Analyzer on master track.
|
|
197
|
+
"""
|
|
198
|
+
cache = _get_spectral(ctx)
|
|
199
|
+
_require_analyzer(cache)
|
|
200
|
+
bridge = _get_m4l(ctx)
|
|
201
|
+
return await bridge.send_command("get_clip_file_path", track_index, clip_index)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@mcp.tool()
|
|
205
|
+
async def replace_simpler_sample(
|
|
206
|
+
ctx: Context,
|
|
207
|
+
track_index: int,
|
|
208
|
+
device_index: int,
|
|
209
|
+
file_path: str,
|
|
210
|
+
) -> dict:
|
|
211
|
+
"""Load an audio file into a Simpler device by absolute file path.
|
|
212
|
+
|
|
213
|
+
Replaces the currently loaded sample. The Simpler must already have
|
|
214
|
+
a sample loaded — this replaces it, it cannot load into an empty Simpler.
|
|
215
|
+
If the Simpler is empty (freshly created with no sample), load a sample
|
|
216
|
+
manually first or use find_and_load_device to load a preset that already
|
|
217
|
+
contains a sample.
|
|
218
|
+
|
|
219
|
+
Use get_clip_file_path to get the path of a resampled clip, then pass
|
|
220
|
+
it here to load it into Simpler for slicing.
|
|
221
|
+
Requires LivePilot Analyzer on master track.
|
|
222
|
+
"""
|
|
223
|
+
cache = _get_spectral(ctx)
|
|
224
|
+
_require_analyzer(cache)
|
|
225
|
+
bridge = _get_m4l(ctx)
|
|
226
|
+
result = await bridge.send_command(
|
|
227
|
+
"replace_simpler_sample", track_index, device_index, file_path
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Validate the response — the bridge may report success even when the
|
|
231
|
+
# sample silently failed to load (e.g., empty Simpler, bad path)
|
|
232
|
+
if "error" in result:
|
|
233
|
+
return result
|
|
234
|
+
if not result.get("sample_loaded"):
|
|
235
|
+
return {
|
|
236
|
+
"error": "Sample may not have loaded. Ensure the Simpler already "
|
|
237
|
+
"has a sample loaded — replace_sample silently fails on empty Simplers."
|
|
238
|
+
}
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@mcp.tool()
|
|
243
|
+
async def load_sample_to_simpler(
|
|
244
|
+
ctx: Context,
|
|
245
|
+
track_index: int,
|
|
246
|
+
file_path: str,
|
|
247
|
+
device_index: int = 0,
|
|
248
|
+
) -> dict:
|
|
249
|
+
"""Load an audio file into a NEW Simpler device on a track.
|
|
250
|
+
|
|
251
|
+
This is the full workflow for programmatic sample loading:
|
|
252
|
+
1. Loads a dummy sample via the browser (creates Simpler with a sample)
|
|
253
|
+
2. Replaces the dummy with your audio file
|
|
254
|
+
3. Returns the Simpler ready for slicing/warping
|
|
255
|
+
|
|
256
|
+
Use this instead of replace_simpler_sample when the track has no Simpler
|
|
257
|
+
or the Simpler is empty. Works with any audio file path.
|
|
258
|
+
Requires LivePilot Analyzer on master track.
|
|
259
|
+
"""
|
|
260
|
+
cache = _get_spectral(ctx)
|
|
261
|
+
_require_analyzer(cache)
|
|
262
|
+
bridge = _get_m4l(ctx)
|
|
263
|
+
|
|
264
|
+
# Step 1: Load a sample from the browser to create Simpler with content
|
|
265
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
266
|
+
search = ableton.send_command("search_browser", {
|
|
267
|
+
"path": "samples",
|
|
268
|
+
"name_filter": "kick",
|
|
269
|
+
"loadable_only": True,
|
|
270
|
+
"max_results": 1,
|
|
271
|
+
})
|
|
272
|
+
results = search.get("results", [])
|
|
273
|
+
if not results:
|
|
274
|
+
return {"error": "No samples found in browser to bootstrap Simpler"}
|
|
275
|
+
|
|
276
|
+
# Load the dummy sample — Ableton auto-creates Simpler
|
|
277
|
+
uri = results[0]["uri"]
|
|
278
|
+
ableton.send_command("load_browser_item", {
|
|
279
|
+
"track_index": track_index,
|
|
280
|
+
"uri": uri,
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
# Step 2: Replace with the desired sample via M4L bridge
|
|
284
|
+
result = await bridge.send_command(
|
|
285
|
+
"replace_simpler_sample", track_index, device_index, file_path
|
|
286
|
+
)
|
|
287
|
+
if "error" in result:
|
|
288
|
+
return result
|
|
289
|
+
if not result.get("sample_loaded"):
|
|
290
|
+
return {"error": "Sample replacement failed after bootstrap"}
|
|
291
|
+
|
|
292
|
+
result["method"] = "bootstrap_and_replace"
|
|
293
|
+
return result
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@mcp.tool()
|
|
297
|
+
async def get_simpler_slices(
|
|
298
|
+
ctx: Context,
|
|
299
|
+
track_index: int,
|
|
300
|
+
device_index: int = 0,
|
|
301
|
+
) -> dict:
|
|
302
|
+
"""Get slice point positions from a Simpler device.
|
|
303
|
+
|
|
304
|
+
Returns each slice's position in frames and seconds, plus sample metadata
|
|
305
|
+
(sample rate, length, playback mode). Use this to understand the rhythmic
|
|
306
|
+
structure of a sliced sample and program MIDI patterns targeting slices.
|
|
307
|
+
Requires LivePilot Analyzer on master track.
|
|
308
|
+
"""
|
|
309
|
+
cache = _get_spectral(ctx)
|
|
310
|
+
_require_analyzer(cache)
|
|
311
|
+
bridge = _get_m4l(ctx)
|
|
312
|
+
return await bridge.send_command("get_simpler_slices", track_index, device_index)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@mcp.tool()
|
|
316
|
+
async def crop_simpler(
|
|
317
|
+
ctx: Context,
|
|
318
|
+
track_index: int,
|
|
319
|
+
device_index: int = 0,
|
|
320
|
+
) -> dict:
|
|
321
|
+
"""Crop a Simpler's sample to the currently active region.
|
|
322
|
+
|
|
323
|
+
Destructive — removes audio outside the region. Use undo to revert.
|
|
324
|
+
Requires LivePilot Analyzer on master track.
|
|
325
|
+
"""
|
|
326
|
+
cache = _get_spectral(ctx)
|
|
327
|
+
_require_analyzer(cache)
|
|
328
|
+
bridge = _get_m4l(ctx)
|
|
329
|
+
return await bridge.send_command("crop_simpler", track_index, device_index)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
@mcp.tool()
|
|
333
|
+
async def reverse_simpler(
|
|
334
|
+
ctx: Context,
|
|
335
|
+
track_index: int,
|
|
336
|
+
device_index: int = 0,
|
|
337
|
+
) -> dict:
|
|
338
|
+
"""Reverse the sample loaded in a Simpler device.
|
|
339
|
+
|
|
340
|
+
Can be called again to un-reverse.
|
|
341
|
+
Requires LivePilot Analyzer on master track.
|
|
342
|
+
"""
|
|
343
|
+
cache = _get_spectral(ctx)
|
|
344
|
+
_require_analyzer(cache)
|
|
345
|
+
bridge = _get_m4l(ctx)
|
|
346
|
+
return await bridge.send_command("reverse_simpler", track_index, device_index)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@mcp.tool()
|
|
350
|
+
async def warp_simpler(
|
|
351
|
+
ctx: Context,
|
|
352
|
+
track_index: int,
|
|
353
|
+
device_index: int = 0,
|
|
354
|
+
beats: int = 4,
|
|
355
|
+
) -> dict:
|
|
356
|
+
"""Warp a Simpler's sample to fit the specified number of beats.
|
|
357
|
+
|
|
358
|
+
The sample will time-stretch to match the project tempo at the given
|
|
359
|
+
beat count. E.g., beats=4 makes it exactly 1 bar at current tempo.
|
|
360
|
+
Requires LivePilot Analyzer on master track.
|
|
361
|
+
"""
|
|
362
|
+
cache = _get_spectral(ctx)
|
|
363
|
+
_require_analyzer(cache)
|
|
364
|
+
bridge = _get_m4l(ctx)
|
|
365
|
+
return await bridge.send_command("warp_simpler", track_index, device_index, beats)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ── Phase 2: Warp Markers ──────────────────────────────────────────────
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@mcp.tool()
|
|
372
|
+
async def get_warp_markers(
|
|
373
|
+
ctx: Context,
|
|
374
|
+
track_index: int,
|
|
375
|
+
clip_index: int,
|
|
376
|
+
) -> dict:
|
|
377
|
+
"""Get all warp markers from an audio clip.
|
|
378
|
+
|
|
379
|
+
Returns each marker's beat_time (position in arrangement) and
|
|
380
|
+
sample_time (position in the original audio file). Use this to
|
|
381
|
+
understand timing, extract groove templates, or prepare for manipulation.
|
|
382
|
+
Only works on audio clips. Requires LivePilot Analyzer on master track.
|
|
383
|
+
"""
|
|
384
|
+
cache = _get_spectral(ctx)
|
|
385
|
+
_require_analyzer(cache)
|
|
386
|
+
bridge = _get_m4l(ctx)
|
|
387
|
+
return await bridge.send_command("get_warp_markers", track_index, clip_index)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@mcp.tool()
|
|
391
|
+
async def add_warp_marker(
|
|
392
|
+
ctx: Context,
|
|
393
|
+
track_index: int,
|
|
394
|
+
clip_index: int,
|
|
395
|
+
beat_time: float,
|
|
396
|
+
) -> dict:
|
|
397
|
+
"""Add a warp marker to an audio clip at the specified beat position.
|
|
398
|
+
|
|
399
|
+
Warp markers pin audio to beats, enabling time-stretching of surrounding
|
|
400
|
+
regions. Add at downbeats to lock timing, then move them for tempo changes.
|
|
401
|
+
Only works on audio clips. Requires LivePilot Analyzer on master track.
|
|
402
|
+
"""
|
|
403
|
+
cache = _get_spectral(ctx)
|
|
404
|
+
_require_analyzer(cache)
|
|
405
|
+
bridge = _get_m4l(ctx)
|
|
406
|
+
return await bridge.send_command(
|
|
407
|
+
"add_warp_marker", track_index, clip_index, beat_time
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@mcp.tool()
|
|
412
|
+
async def move_warp_marker(
|
|
413
|
+
ctx: Context,
|
|
414
|
+
track_index: int,
|
|
415
|
+
clip_index: int,
|
|
416
|
+
old_beat_time: float,
|
|
417
|
+
new_beat_time: float,
|
|
418
|
+
) -> dict:
|
|
419
|
+
"""Move a warp marker from one beat position to another.
|
|
420
|
+
|
|
421
|
+
Changes the tempo of the audio segment between this marker and its
|
|
422
|
+
neighbors. Moving later = slower, moving earlier = faster. Use for
|
|
423
|
+
tape-stop effects, tempo ramps, and groove manipulation.
|
|
424
|
+
Only works on audio clips. Requires LivePilot Analyzer on master track.
|
|
425
|
+
"""
|
|
426
|
+
cache = _get_spectral(ctx)
|
|
427
|
+
_require_analyzer(cache)
|
|
428
|
+
bridge = _get_m4l(ctx)
|
|
429
|
+
return await bridge.send_command(
|
|
430
|
+
"move_warp_marker", track_index, clip_index, old_beat_time, new_beat_time
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@mcp.tool()
|
|
435
|
+
async def remove_warp_marker(
|
|
436
|
+
ctx: Context,
|
|
437
|
+
track_index: int,
|
|
438
|
+
clip_index: int,
|
|
439
|
+
beat_time: float,
|
|
440
|
+
) -> dict:
|
|
441
|
+
"""Remove a warp marker from an audio clip at the specified beat.
|
|
442
|
+
|
|
443
|
+
Only works on audio clips. Requires LivePilot Analyzer on master track.
|
|
444
|
+
"""
|
|
445
|
+
cache = _get_spectral(ctx)
|
|
446
|
+
_require_analyzer(cache)
|
|
447
|
+
bridge = _get_m4l(ctx)
|
|
448
|
+
return await bridge.send_command(
|
|
449
|
+
"remove_warp_marker", track_index, clip_index, beat_time
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ── Phase 2: Clip & Display ────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@mcp.tool()
|
|
457
|
+
async def scrub_clip(
|
|
458
|
+
ctx: Context,
|
|
459
|
+
track_index: int,
|
|
460
|
+
clip_index: int,
|
|
461
|
+
beat_time: float,
|
|
462
|
+
) -> dict:
|
|
463
|
+
"""Scrub/preview a clip at a specific beat position.
|
|
464
|
+
|
|
465
|
+
Plays audio from that position until stop_scrub is called. Use to
|
|
466
|
+
audition sections, preview slices, or find the right warp marker spot.
|
|
467
|
+
Requires LivePilot Analyzer on master track.
|
|
468
|
+
"""
|
|
469
|
+
cache = _get_spectral(ctx)
|
|
470
|
+
_require_analyzer(cache)
|
|
471
|
+
bridge = _get_m4l(ctx)
|
|
472
|
+
return await bridge.send_command(
|
|
473
|
+
"scrub_clip", track_index, clip_index, beat_time
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@mcp.tool()
|
|
478
|
+
async def stop_scrub(
|
|
479
|
+
ctx: Context,
|
|
480
|
+
track_index: int,
|
|
481
|
+
clip_index: int,
|
|
482
|
+
) -> dict:
|
|
483
|
+
"""Stop scrubbing a clip. Call after scrub_clip to stop preview.
|
|
484
|
+
|
|
485
|
+
Requires LivePilot Analyzer on master track.
|
|
486
|
+
"""
|
|
487
|
+
cache = _get_spectral(ctx)
|
|
488
|
+
_require_analyzer(cache)
|
|
489
|
+
bridge = _get_m4l(ctx)
|
|
490
|
+
return await bridge.send_command("stop_scrub", track_index, clip_index)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
@mcp.tool()
|
|
494
|
+
async def get_display_values(
|
|
495
|
+
ctx: Context,
|
|
496
|
+
track_index: int,
|
|
497
|
+
device_index: int,
|
|
498
|
+
) -> dict:
|
|
499
|
+
"""Get human-readable display values for all device parameters.
|
|
500
|
+
|
|
501
|
+
Returns the value as shown in Live's UI (e.g., '440 Hz', '-6.0 dB',
|
|
502
|
+
'50 %') instead of raw normalized floats. Skips irrelevant parameters.
|
|
503
|
+
Requires LivePilot Analyzer on master track.
|
|
504
|
+
"""
|
|
505
|
+
cache = _get_spectral(ctx)
|
|
506
|
+
_require_analyzer(cache)
|
|
507
|
+
bridge = _get_m4l(ctx)
|
|
508
|
+
return await bridge.send_command("get_display_values", track_index, device_index)
|