livepilot 1.9.24 → 1.10.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 +223 -0
- package/CONTRIBUTING.md +2 -2
- package/LICENSE +62 -21
- package/README.md +291 -276
- package/bin/livepilot.js +87 -0
- package/installer/codex.js +147 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/skills/livepilot-arrangement/SKILL.md +18 -1
- package/livepilot/skills/livepilot-core/SKILL.md +22 -5
- package/livepilot/skills/livepilot-core/references/device-knowledge/00-index.md +34 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/automation-as-music.md +204 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/chains-genre.md +173 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/creative-thinking.md +211 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-distortion.md +188 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-space.md +162 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/effects-spectral.md +229 -0
- package/livepilot/skills/livepilot-core/references/device-knowledge/instruments-synths.md +243 -0
- package/livepilot/skills/livepilot-core/references/overview.md +13 -9
- package/livepilot/skills/livepilot-core/references/sample-manipulation.md +724 -0
- package/livepilot/skills/livepilot-core/references/sound-design-deep.md +140 -0
- package/livepilot/skills/livepilot-devices/SKILL.md +39 -4
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +23 -19
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +105 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-critics.md +87 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-philosophy.md +51 -0
- package/livepilot/skills/livepilot-sample-engine/references/sample-techniques.md +131 -0
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +45 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +17 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +4 -4
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +357 -0
- package/mcp_server/atlas/device_atlas.json +44067 -0
- package/mcp_server/atlas/enrichments/__init__.py +111 -0
- package/mcp_server/atlas/enrichments/audio_effects/auto_filter.yaml +162 -0
- package/mcp_server/atlas/enrichments/audio_effects/beat_repeat.yaml +183 -0
- package/mcp_server/atlas/enrichments/audio_effects/channel_eq.yaml +126 -0
- package/mcp_server/atlas/enrichments/audio_effects/chorus_ensemble.yaml +149 -0
- package/mcp_server/atlas/enrichments/audio_effects/color_limiter.yaml +109 -0
- package/mcp_server/atlas/enrichments/audio_effects/compressor.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb.yaml +143 -0
- package/mcp_server/atlas/enrichments/audio_effects/convolution_reverb_pro.yaml +178 -0
- package/mcp_server/atlas/enrichments/audio_effects/delay.yaml +151 -0
- package/mcp_server/atlas/enrichments/audio_effects/drum_buss.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/dynamic_tube.yaml +147 -0
- package/mcp_server/atlas/enrichments/audio_effects/echo.yaml +167 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_eight.yaml +148 -0
- package/mcp_server/atlas/enrichments/audio_effects/eq_three.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/erosion.yaml +103 -0
- package/mcp_server/atlas/enrichments/audio_effects/filter_delay.yaml +173 -0
- package/mcp_server/atlas/enrichments/audio_effects/gate.yaml +130 -0
- package/mcp_server/atlas/enrichments/audio_effects/gated_delay.yaml +133 -0
- package/mcp_server/atlas/enrichments/audio_effects/glue_compressor.yaml +142 -0
- package/mcp_server/atlas/enrichments/audio_effects/grain_delay.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/hybrid_reverb.yaml +160 -0
- package/mcp_server/atlas/enrichments/audio_effects/limiter.yaml +97 -0
- package/mcp_server/atlas/enrichments/audio_effects/multiband_dynamics.yaml +174 -0
- package/mcp_server/atlas/enrichments/audio_effects/overdrive.yaml +119 -0
- package/mcp_server/atlas/enrichments/audio_effects/pedal.yaml +145 -0
- package/mcp_server/atlas/enrichments/audio_effects/phaser_flanger.yaml +161 -0
- package/mcp_server/atlas/enrichments/audio_effects/redux.yaml +114 -0
- package/mcp_server/atlas/enrichments/audio_effects/reverb.yaml +190 -0
- package/mcp_server/atlas/enrichments/audio_effects/roar.yaml +159 -0
- package/mcp_server/atlas/enrichments/audio_effects/saturator.yaml +146 -0
- package/mcp_server/atlas/enrichments/audio_effects/shifter.yaml +154 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_resonator.yaml +141 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectral_time.yaml +164 -0
- package/mcp_server/atlas/enrichments/audio_effects/vector_delay.yaml +140 -0
- package/mcp_server/atlas/enrichments/audio_effects/vinyl_distortion.yaml +141 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +222 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +202 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +150 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +167 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +137 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +163 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +164 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +197 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +192 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +218 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +217 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +156 -0
- package/mcp_server/atlas/enrichments/instruments/tree_tone.yaml +162 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +165 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +166 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +162 -0
- package/mcp_server/atlas/enrichments/midi_effects/arpeggiator.yaml +156 -0
- package/mcp_server/atlas/enrichments/midi_effects/bouncy_notes.yaml +93 -0
- package/mcp_server/atlas/enrichments/midi_effects/chord.yaml +147 -0
- package/mcp_server/atlas/enrichments/midi_effects/melodic_steps.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_echo.yaml +108 -0
- package/mcp_server/atlas/enrichments/midi_effects/note_length.yaml +97 -0
- package/mcp_server/atlas/enrichments/midi_effects/pitch.yaml +76 -0
- package/mcp_server/atlas/enrichments/midi_effects/random.yaml +117 -0
- package/mcp_server/atlas/enrichments/midi_effects/rhythmic_steps.yaml +103 -0
- package/mcp_server/atlas/enrichments/midi_effects/scale.yaml +83 -0
- package/mcp_server/atlas/enrichments/midi_effects/step_arp.yaml +112 -0
- package/mcp_server/atlas/enrichments/midi_effects/velocity.yaml +119 -0
- package/mcp_server/atlas/enrichments/utility/amp.yaml +159 -0
- package/mcp_server/atlas/enrichments/utility/cabinet.yaml +109 -0
- package/mcp_server/atlas/enrichments/utility/corpus.yaml +150 -0
- package/mcp_server/atlas/enrichments/utility/resonators.yaml +131 -0
- package/mcp_server/atlas/enrichments/utility/spectrum.yaml +63 -0
- package/mcp_server/atlas/enrichments/utility/tuner.yaml +51 -0
- package/mcp_server/atlas/enrichments/utility/utility.yaml +136 -0
- package/mcp_server/atlas/enrichments/utility/vocoder.yaml +160 -0
- package/mcp_server/atlas/scanner.py +236 -0
- package/mcp_server/atlas/tools.py +224 -0
- package/mcp_server/composer/__init__.py +1 -0
- package/mcp_server/composer/engine.py +532 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/sample_resolver.py +153 -0
- package/mcp_server/composer/tools.py +211 -0
- package/mcp_server/connection.py +53 -8
- package/mcp_server/corpus/__init__.py +377 -0
- package/mcp_server/device_forge/__init__.py +1 -0
- package/mcp_server/device_forge/builder.py +377 -0
- package/mcp_server/device_forge/models.py +142 -0
- package/mcp_server/device_forge/templates.py +483 -0
- package/mcp_server/device_forge/tools.py +162 -0
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_accessors.py +47 -0
- package/mcp_server/preview_studio/engine.py +9 -2
- package/mcp_server/preview_studio/tools.py +78 -35
- package/mcp_server/project_brain/tools.py +34 -0
- package/mcp_server/runtime/capability_probe.py +21 -2
- package/mcp_server/runtime/execution_router.py +184 -38
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/mcp_dispatch.py +46 -0
- package/mcp_server/runtime/remote_commands.py +13 -5
- package/mcp_server/runtime/tools.py +66 -29
- package/mcp_server/sample_engine/__init__.py +1 -0
- package/mcp_server/sample_engine/analyzer.py +216 -0
- package/mcp_server/sample_engine/critics.py +390 -0
- package/mcp_server/sample_engine/models.py +193 -0
- package/mcp_server/sample_engine/moves.py +127 -0
- package/mcp_server/sample_engine/planner.py +186 -0
- package/mcp_server/sample_engine/slice_workflow.py +190 -0
- package/mcp_server/sample_engine/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +545 -0
- package/mcp_server/semantic_moves/__init__.py +3 -0
- package/mcp_server/semantic_moves/device_creation_moves.py +237 -0
- package/mcp_server/semantic_moves/mix_moves.py +8 -8
- package/mcp_server/semantic_moves/models.py +7 -7
- package/mcp_server/semantic_moves/performance_moves.py +4 -4
- package/mcp_server/semantic_moves/sample_compilers.py +377 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +4 -4
- package/mcp_server/semantic_moves/tools.py +63 -10
- package/mcp_server/semantic_moves/transition_moves.py +4 -4
- package/mcp_server/server.py +71 -1
- package/mcp_server/session_continuity/tracker.py +4 -1
- package/mcp_server/sound_design/critics.py +89 -1
- package/mcp_server/splice_client/__init__.py +1 -0
- package/mcp_server/splice_client/client.py +347 -0
- package/mcp_server/splice_client/models.py +96 -0
- package/mcp_server/splice_client/protos/__init__.py +1 -0
- package/mcp_server/splice_client/protos/app_pb2.py +319 -0
- package/mcp_server/splice_client/protos/app_pb2.pyi +1153 -0
- package/mcp_server/splice_client/protos/app_pb2_grpc.py +1946 -0
- package/mcp_server/tools/_conductor.py +16 -0
- package/mcp_server/tools/_planner_engine.py +24 -0
- package/mcp_server/tools/analyzer.py +2 -0
- package/mcp_server/tools/arrangement.py +69 -0
- package/mcp_server/tools/automation.py +15 -2
- package/mcp_server/tools/devices.py +117 -6
- package/mcp_server/tools/notes.py +37 -4
- package/mcp_server/tools/planner.py +3 -0
- package/mcp_server/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +144 -14
- package/mcp_server/wonder_mode/tools.py +33 -1
- package/package.json +14 -4
- package/remote_script/LivePilot/__init__.py +8 -1
- package/remote_script/LivePilot/arrangement.py +114 -0
- package/remote_script/LivePilot/browser.py +56 -1
- package/remote_script/LivePilot/devices.py +246 -6
- package/remote_script/LivePilot/mixing.py +8 -3
- package/remote_script/LivePilot/server.py +5 -1
- package/remote_script/LivePilot/transport.py +3 -0
- package/remote_script/LivePilot/version_detect.py +78 -0
|
@@ -110,6 +110,12 @@ _ROUTING_PATTERNS: list[tuple[str, str, str, str, list[str]]] = [
|
|
|
110
110
|
# Research requests
|
|
111
111
|
(r"research|how.?to|technique|tutorial|learn", "research", "research", "research_technique", []),
|
|
112
112
|
(r"style.?tactic|production.?style|genre.?approach", "research", "research", "get_style_tactics", []),
|
|
113
|
+
|
|
114
|
+
# Sample requests
|
|
115
|
+
(r"sample|splice|loop|chop|flip|break(?:beat)?|one.?shot", "sample_engine", "sample", "search_samples", ["analyze_sample", "plan_sample_workflow"]),
|
|
116
|
+
(r"slice|transient.?hit|slice.?mode", "sample_engine", "sample", "plan_slice_workflow", ["search_samples"]),
|
|
117
|
+
(r"vocal.?sample|foley|field.?record|found.?sound", "sample_engine", "sample", "search_samples", ["analyze_sample"]),
|
|
118
|
+
(r"texture.?sample|ambient.?sample|atmo.?sample", "sample_engine", "sample", "search_samples", ["suggest_sample_technique"]),
|
|
113
119
|
]
|
|
114
120
|
|
|
115
121
|
|
|
@@ -164,6 +170,16 @@ def _infer_workflow_mode(request_lower: str) -> str:
|
|
|
164
170
|
if re.search(r"fix|quick|just|only|undo|revert|simple", request_lower):
|
|
165
171
|
return "quick_fix"
|
|
166
172
|
|
|
173
|
+
# Slice workflow
|
|
174
|
+
if re.search(r"slice|chop|transient.?hit", request_lower):
|
|
175
|
+
return "slice_workflow"
|
|
176
|
+
|
|
177
|
+
# Sample workflows
|
|
178
|
+
if re.search(r"sample|splice|foley|found.?sound|one.?shot|break(?:beat)?|flip|loop", request_lower):
|
|
179
|
+
if re.search(r"arrange|section|verse|chorus|drop|bridge|hook", request_lower):
|
|
180
|
+
return "sample_plus_arrangement"
|
|
181
|
+
return "sample_discovery"
|
|
182
|
+
|
|
167
183
|
# Agentic loop keywords (full autonomous)
|
|
168
184
|
if re.search(r"autonomous|auto|full|everything|deep|polish|finish", request_lower):
|
|
169
185
|
return "agentic_loop"
|
|
@@ -164,6 +164,8 @@ class SectionPlan:
|
|
|
164
164
|
tracks_entering: list[int] # new elements introduced in this section
|
|
165
165
|
tracks_exiting: list[int] # elements removed in this section
|
|
166
166
|
|
|
167
|
+
sample_hints: list[str] = field(default_factory=list)
|
|
168
|
+
|
|
167
169
|
def length_bars(self) -> int:
|
|
168
170
|
return self.end_bar - self.start_bar
|
|
169
171
|
|
|
@@ -196,6 +198,28 @@ class ArrangementPlan:
|
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
|
|
201
|
+
# ── Section Sample Hints ─────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
_SECTION_SAMPLE_DEFAULTS: dict[str, list[str]] = {
|
|
204
|
+
"intro": ["texture_bed", "fill_one_shot"],
|
|
205
|
+
"verse": ["texture_bed", "fill_one_shot"],
|
|
206
|
+
"pre_chorus": ["transition_fx", "texture_bed"],
|
|
207
|
+
"chorus": ["hook_sample", "break_layer", "fill_one_shot"],
|
|
208
|
+
"drop": ["hook_sample", "break_layer", "fill_one_shot"],
|
|
209
|
+
"build": ["transition_fx", "texture_bed"],
|
|
210
|
+
"bridge": ["texture_bed", "transition_fx"],
|
|
211
|
+
"breakdown": ["texture_bed"],
|
|
212
|
+
"outro": ["texture_bed", "fill_one_shot"],
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def add_sample_hints(plan: "ArrangementPlan") -> None:
|
|
217
|
+
"""Populate sample_hints on each section based on section type."""
|
|
218
|
+
for section in plan.sections:
|
|
219
|
+
section_key = section.section_type.value.lower()
|
|
220
|
+
section.sample_hints = _SECTION_SAMPLE_DEFAULTS.get(section_key, ["texture_bed"])
|
|
221
|
+
|
|
222
|
+
|
|
199
223
|
# ── Core Planner ─────────────────────────────────────────────────────
|
|
200
224
|
|
|
201
225
|
def plan_arrangement_from_loop(
|
|
@@ -381,6 +381,8 @@ async def load_sample_to_simpler(
|
|
|
381
381
|
return {"error": "Sample replacement failed after bootstrap"}
|
|
382
382
|
|
|
383
383
|
result["method"] = "bootstrap_and_replace"
|
|
384
|
+
result["device_index"] = actual_device_index # additive — for step-result binding
|
|
385
|
+
result["track_index"] = track_index
|
|
384
386
|
return result
|
|
385
387
|
|
|
386
388
|
|
|
@@ -143,6 +143,48 @@ def create_arrangement_clip(
|
|
|
143
143
|
return _get_ableton(ctx).send_command("create_arrangement_clip", params)
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
@mcp.tool()
|
|
147
|
+
def create_native_arrangement_clip(
|
|
148
|
+
ctx: Context,
|
|
149
|
+
track_index: int,
|
|
150
|
+
start_time: float,
|
|
151
|
+
length: float,
|
|
152
|
+
name: str = "",
|
|
153
|
+
color_index: Optional[int] = None,
|
|
154
|
+
) -> dict:
|
|
155
|
+
"""Create an empty MIDI clip directly in Arrangement View (Live 12.1.10+).
|
|
156
|
+
|
|
157
|
+
Unlike create_arrangement_clip (which duplicates a session clip), this creates
|
|
158
|
+
a native arrangement clip with full automation envelope support — volume rides,
|
|
159
|
+
filter sweeps, send automation all work natively.
|
|
160
|
+
|
|
161
|
+
Requires Live 12.1.10+. Falls back with a clear error on older versions.
|
|
162
|
+
|
|
163
|
+
track_index: 0+ for regular MIDI tracks
|
|
164
|
+
start_time: beat position (0.0 = song start, 4.0 = bar 2 in 4/4)
|
|
165
|
+
length: clip length in beats
|
|
166
|
+
name: optional clip display name
|
|
167
|
+
color_index: optional 0-69 Ableton color
|
|
168
|
+
"""
|
|
169
|
+
_validate_track_index(track_index)
|
|
170
|
+
if start_time < 0:
|
|
171
|
+
raise ValueError("start_time must be >= 0")
|
|
172
|
+
if length <= 0:
|
|
173
|
+
raise ValueError("length must be > 0")
|
|
174
|
+
|
|
175
|
+
params = {
|
|
176
|
+
"track_index": track_index,
|
|
177
|
+
"start_time": start_time,
|
|
178
|
+
"length": length,
|
|
179
|
+
}
|
|
180
|
+
if name:
|
|
181
|
+
params["name"] = name
|
|
182
|
+
if color_index is not None:
|
|
183
|
+
params["color_index"] = color_index
|
|
184
|
+
|
|
185
|
+
return _get_ableton(ctx).send_command("create_native_arrangement_clip", params)
|
|
186
|
+
|
|
187
|
+
|
|
146
188
|
@mcp.tool()
|
|
147
189
|
def add_arrangement_notes(
|
|
148
190
|
ctx: Context,
|
|
@@ -288,6 +330,33 @@ def back_to_arranger(ctx: Context) -> dict:
|
|
|
288
330
|
return _get_ableton(ctx).send_command("back_to_arranger")
|
|
289
331
|
|
|
290
332
|
|
|
333
|
+
@mcp.tool()
|
|
334
|
+
def force_arrangement(
|
|
335
|
+
ctx: Context,
|
|
336
|
+
beat_time: float = 0,
|
|
337
|
+
loop_start: float = 0,
|
|
338
|
+
loop_length: float = 0,
|
|
339
|
+
play: bool = True,
|
|
340
|
+
) -> dict:
|
|
341
|
+
"""Force ALL tracks to follow the arrangement and start playback.
|
|
342
|
+
|
|
343
|
+
Atomically: stops all session clips, releases every track from
|
|
344
|
+
session override, sets back-to-arranger, jumps to position, and
|
|
345
|
+
starts playing. This is the "play my arrangement from the top"
|
|
346
|
+
command.
|
|
347
|
+
|
|
348
|
+
beat_time: position to start from (default 0 = beginning)
|
|
349
|
+
loop_start: loop region start in beats (default 0)
|
|
350
|
+
loop_length: loop region length in beats (0 = no loop change)
|
|
351
|
+
play: whether to start playback (default True)
|
|
352
|
+
"""
|
|
353
|
+
params: dict = {"beat_time": beat_time, "play": play}
|
|
354
|
+
if loop_length > 0:
|
|
355
|
+
params["loop_start"] = loop_start
|
|
356
|
+
params["loop_length"] = loop_length
|
|
357
|
+
return _get_ableton(ctx).send_command("force_arrangement", params)
|
|
358
|
+
|
|
359
|
+
|
|
291
360
|
@mcp.tool()
|
|
292
361
|
def get_arrangement_notes(
|
|
293
362
|
ctx: Context,
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
from typing import Any, Optional
|
|
11
11
|
|
|
12
12
|
from fastmcp import Context
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
13
14
|
|
|
14
15
|
from ..curves import generate_curve, generate_from_recipe, list_recipes
|
|
15
16
|
from ..server import mcp
|
|
@@ -27,10 +28,22 @@ def _ensure_list(v: Any) -> list:
|
|
|
27
28
|
except json.JSONDecodeError as exc:
|
|
28
29
|
raise ValueError(f"Invalid JSON in parameter: {exc}") from exc
|
|
29
30
|
if isinstance(v, list):
|
|
30
|
-
|
|
31
|
+
normalized = []
|
|
32
|
+
for item in v:
|
|
33
|
+
if isinstance(item, BaseModel):
|
|
34
|
+
normalized.append(item.model_dump(exclude_none=True))
|
|
35
|
+
else:
|
|
36
|
+
normalized.append(item)
|
|
37
|
+
return normalized
|
|
31
38
|
return [v]
|
|
32
39
|
|
|
33
40
|
|
|
41
|
+
class AutomationPoint(BaseModel):
|
|
42
|
+
time: float
|
|
43
|
+
value: float
|
|
44
|
+
duration: Optional[float] = Field(default=None, ge=0.0)
|
|
45
|
+
|
|
46
|
+
|
|
34
47
|
@mcp.tool()
|
|
35
48
|
def get_clip_automation(
|
|
36
49
|
ctx: Context,
|
|
@@ -62,7 +75,7 @@ def set_clip_automation(
|
|
|
62
75
|
track_index: int,
|
|
63
76
|
clip_index: int,
|
|
64
77
|
parameter_type: str,
|
|
65
|
-
points:
|
|
78
|
+
points: list[AutomationPoint] | str,
|
|
66
79
|
device_index: Optional[int] = None,
|
|
67
80
|
parameter_index: Optional[int] = None,
|
|
68
81
|
send_index: Optional[int] = None,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Device MCP tools — parameters, racks, browser loading, plugin deep control.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
16 tools matching the Remote Script devices domain + M4L bridge.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -359,14 +359,15 @@ def find_and_load_device(ctx: Context, track_index: int, device_name: str) -> di
|
|
|
359
359
|
if not device_name.strip():
|
|
360
360
|
raise ValueError("device_name cannot be empty")
|
|
361
361
|
|
|
362
|
-
# Guardrail: bare Drum Rack produces silence
|
|
362
|
+
# Guardrail: bare Drum Rack produces silence unless building programmatically (12.3+)
|
|
363
363
|
if device_name.strip().lower() == "drum rack":
|
|
364
364
|
raise ValueError(
|
|
365
365
|
"Loading a bare 'Drum Rack' creates an empty rack that produces silence. "
|
|
366
|
-
"
|
|
367
|
-
"(e.g., '808 Core Kit'), then load
|
|
368
|
-
"
|
|
369
|
-
"
|
|
366
|
+
"Options: (1) search_browser(path='drums') to find a kit preset "
|
|
367
|
+
"(e.g., '808 Core Kit'), then load with load_browser_item(). "
|
|
368
|
+
"(2) On Live 12.3+: use insert_device('Drum Rack') + insert_rack_chain "
|
|
369
|
+
"+ set_drum_chain_note to build a kit programmatically. "
|
|
370
|
+
"(3) DS drum synths (DS Kick, DS Snare, DS HH) are self-contained."
|
|
370
371
|
)
|
|
371
372
|
|
|
372
373
|
result = _get_ableton(ctx).send_command("find_and_load_device", {
|
|
@@ -376,6 +377,116 @@ def find_and_load_device(ctx: Context, track_index: int, device_name: str) -> di
|
|
|
376
377
|
return _postflight_loaded_device(ctx, result)
|
|
377
378
|
|
|
378
379
|
|
|
380
|
+
@mcp.tool()
|
|
381
|
+
def insert_device(
|
|
382
|
+
ctx: Context,
|
|
383
|
+
track_index: int,
|
|
384
|
+
device_name: str,
|
|
385
|
+
position: int = -1,
|
|
386
|
+
device_index: Optional[int] = None,
|
|
387
|
+
chain_index: Optional[int] = None,
|
|
388
|
+
) -> dict:
|
|
389
|
+
"""Insert a native Live device by name — 10x faster than browser search (Live 12.3+).
|
|
390
|
+
|
|
391
|
+
Only works for native devices (Reverb, Compressor, EQ Eight, Drift, etc.).
|
|
392
|
+
For plugins, M4L devices, or presets, use find_and_load_device or load_browser_item.
|
|
393
|
+
|
|
394
|
+
track_index: 0+ for regular tracks, -1/-2 for returns, -1000 for master
|
|
395
|
+
device_name: exact device name (e.g. 'Reverb', 'Auto Filter', 'Wavetable')
|
|
396
|
+
position: device chain position (0 = first, -1 = end of chain)
|
|
397
|
+
device_index: required when inserting into a rack chain (identifies the rack)
|
|
398
|
+
chain_index: insert into this chain of a rack device (for building drum kits)
|
|
399
|
+
|
|
400
|
+
Drum Rack construction workflow (12.3+):
|
|
401
|
+
1. insert_device(track_index, 'Drum Rack') — create empty rack
|
|
402
|
+
2. insert_rack_chain(track_index, device_index) — add chains
|
|
403
|
+
3. set_drum_chain_note(chain_index, note=36) — assign C1 (kick)
|
|
404
|
+
4. insert_device(track_index, 'Simpler', — add instrument
|
|
405
|
+
device_index=rack_idx, chain_index=0) into chain
|
|
406
|
+
|
|
407
|
+
On Live < 12.3: returns an error suggesting find_and_load_device instead.
|
|
408
|
+
"""
|
|
409
|
+
_validate_track_index(track_index)
|
|
410
|
+
if not device_name.strip():
|
|
411
|
+
raise ValueError("device_name cannot be empty")
|
|
412
|
+
|
|
413
|
+
params = {
|
|
414
|
+
"track_index": track_index,
|
|
415
|
+
"device_name": device_name,
|
|
416
|
+
"position": position,
|
|
417
|
+
}
|
|
418
|
+
if device_index is not None:
|
|
419
|
+
params["device_index"] = device_index
|
|
420
|
+
if chain_index is not None:
|
|
421
|
+
if device_index is None:
|
|
422
|
+
raise ValueError("device_index is required when chain_index is provided")
|
|
423
|
+
_validate_device_index(device_index)
|
|
424
|
+
_validate_chain_index(chain_index)
|
|
425
|
+
params["chain_index"] = chain_index
|
|
426
|
+
|
|
427
|
+
result = _get_ableton(ctx).send_command("insert_device", params)
|
|
428
|
+
return _postflight_loaded_device(ctx, result)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
@mcp.tool()
|
|
432
|
+
def insert_rack_chain(
|
|
433
|
+
ctx: Context,
|
|
434
|
+
track_index: int,
|
|
435
|
+
device_index: int,
|
|
436
|
+
position: int = -1,
|
|
437
|
+
) -> dict:
|
|
438
|
+
"""Insert a new chain into a Rack device — Instrument Rack, Audio Effect Rack, or Drum Rack (Live 12.3+).
|
|
439
|
+
|
|
440
|
+
Use with insert_device + set_drum_chain_note to build Drum Racks from scratch:
|
|
441
|
+
1. insert_device(track, 'Drum Rack') to create the rack
|
|
442
|
+
2. insert_rack_chain(track, rack_device_index) to add chains
|
|
443
|
+
3. set_drum_chain_note(chain_index=0, note=36) to assign C1 (kick)
|
|
444
|
+
4. insert_device(track, 'Simpler', device_index=rack, chain_index=0) into chain
|
|
445
|
+
|
|
446
|
+
track_index: track containing the rack
|
|
447
|
+
device_index: rack device index on the track
|
|
448
|
+
position: chain position (-1 = append to end)
|
|
449
|
+
"""
|
|
450
|
+
_validate_track_index(track_index)
|
|
451
|
+
_validate_device_index(device_index)
|
|
452
|
+
|
|
453
|
+
return _get_ableton(ctx).send_command("insert_rack_chain", {
|
|
454
|
+
"track_index": track_index,
|
|
455
|
+
"device_index": device_index,
|
|
456
|
+
"position": position,
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
@mcp.tool()
|
|
461
|
+
def set_drum_chain_note(
|
|
462
|
+
ctx: Context,
|
|
463
|
+
track_index: int,
|
|
464
|
+
device_index: int,
|
|
465
|
+
chain_index: int,
|
|
466
|
+
note: int,
|
|
467
|
+
) -> dict:
|
|
468
|
+
"""Set which MIDI note triggers a Drum Rack chain (Live 12.3+).
|
|
469
|
+
|
|
470
|
+
Standard drum mapping:
|
|
471
|
+
C1 (36) = Kick, D1 (38) = Snare, F#1 (42) = Closed HH,
|
|
472
|
+
A#1 (46) = Open HH, C#2 (49) = Crash, D#2 (51) = Ride
|
|
473
|
+
|
|
474
|
+
note: MIDI note 0-127, or -1 for 'All Notes'
|
|
475
|
+
"""
|
|
476
|
+
_validate_track_index(track_index)
|
|
477
|
+
_validate_device_index(device_index)
|
|
478
|
+
_validate_chain_index(chain_index)
|
|
479
|
+
if note < -1 or note > 127:
|
|
480
|
+
raise ValueError("note must be -1 (All Notes) or 0-127")
|
|
481
|
+
|
|
482
|
+
return _get_ableton(ctx).send_command("set_drum_chain_note", {
|
|
483
|
+
"track_index": track_index,
|
|
484
|
+
"device_index": device_index,
|
|
485
|
+
"chain_index": chain_index,
|
|
486
|
+
"note": note,
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
|
|
379
490
|
@mcp.tool()
|
|
380
491
|
def set_simpler_playback_mode(
|
|
381
492
|
ctx: Context,
|
|
@@ -9,6 +9,7 @@ import json
|
|
|
9
9
|
from typing import Any, Optional
|
|
10
10
|
|
|
11
11
|
from fastmcp import Context
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
12
13
|
|
|
13
14
|
from ..server import mcp
|
|
14
15
|
|
|
@@ -25,9 +26,36 @@ def _ensure_list(value: Any) -> list:
|
|
|
25
26
|
return json.loads(value)
|
|
26
27
|
except json.JSONDecodeError as exc:
|
|
27
28
|
raise ValueError(f"Invalid JSON in parameter: {exc}") from exc
|
|
29
|
+
if isinstance(value, list):
|
|
30
|
+
normalized = []
|
|
31
|
+
for item in value:
|
|
32
|
+
if isinstance(item, BaseModel):
|
|
33
|
+
normalized.append(item.model_dump(exclude_none=True))
|
|
34
|
+
else:
|
|
35
|
+
normalized.append(item)
|
|
36
|
+
return normalized
|
|
28
37
|
return value
|
|
29
38
|
|
|
30
39
|
|
|
40
|
+
class NoteSpec(BaseModel):
|
|
41
|
+
pitch: int = Field(ge=0, le=127)
|
|
42
|
+
start_time: float
|
|
43
|
+
duration: float = Field(gt=0)
|
|
44
|
+
velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
|
|
45
|
+
probability: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
46
|
+
velocity_deviation: Optional[float] = Field(default=None, ge=-127.0, le=127.0)
|
|
47
|
+
release_velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NoteModification(BaseModel):
|
|
51
|
+
note_id: int
|
|
52
|
+
pitch: Optional[int] = Field(default=None, ge=0, le=127)
|
|
53
|
+
start_time: Optional[float] = None
|
|
54
|
+
duration: Optional[float] = Field(default=None, gt=0)
|
|
55
|
+
velocity: Optional[float] = Field(default=None, ge=0.0, le=127.0)
|
|
56
|
+
probability: Optional[float] = Field(default=None, ge=0.0, le=1.0)
|
|
57
|
+
|
|
58
|
+
|
|
31
59
|
def _validate_track_index(track_index: int):
|
|
32
60
|
"""Validate track index. Must be >= 0 for regular tracks."""
|
|
33
61
|
if track_index < 0:
|
|
@@ -78,7 +106,7 @@ def _validate_note(note: dict):
|
|
|
78
106
|
|
|
79
107
|
|
|
80
108
|
@mcp.tool()
|
|
81
|
-
def add_notes(ctx: Context, track_index: int, clip_index: int, notes:
|
|
109
|
+
def add_notes(ctx: Context, track_index: int, clip_index: int, notes: list[NoteSpec] | str) -> dict:
|
|
82
110
|
"""Add MIDI notes to a clip. notes is a JSON array: [{pitch, start_time, duration, velocity?, probability?, velocity_deviation?, release_velocity?}]."""
|
|
83
111
|
_validate_track_index(track_index)
|
|
84
112
|
_validate_clip_index(clip_index)
|
|
@@ -157,7 +185,7 @@ def remove_notes(
|
|
|
157
185
|
|
|
158
186
|
|
|
159
187
|
@mcp.tool()
|
|
160
|
-
def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids:
|
|
188
|
+
def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids: list[int] | str) -> dict:
|
|
161
189
|
"""Remove specific MIDI notes by their IDs (JSON array of ints). Use undo to revert."""
|
|
162
190
|
_validate_track_index(track_index)
|
|
163
191
|
_validate_clip_index(clip_index)
|
|
@@ -172,7 +200,12 @@ def remove_notes_by_id(ctx: Context, track_index: int, clip_index: int, note_ids
|
|
|
172
200
|
|
|
173
201
|
|
|
174
202
|
@mcp.tool()
|
|
175
|
-
def modify_notes(
|
|
203
|
+
def modify_notes(
|
|
204
|
+
ctx: Context,
|
|
205
|
+
track_index: int,
|
|
206
|
+
clip_index: int,
|
|
207
|
+
modifications: list[NoteModification] | str,
|
|
208
|
+
) -> dict:
|
|
176
209
|
"""Modify existing MIDI notes by ID. modifications is a JSON array: [{note_id, pitch?, start_time?, duration?, velocity?, probability?}]."""
|
|
177
210
|
_validate_track_index(track_index)
|
|
178
211
|
_validate_clip_index(clip_index)
|
|
@@ -202,7 +235,7 @@ def duplicate_notes(
|
|
|
202
235
|
ctx: Context,
|
|
203
236
|
track_index: int,
|
|
204
237
|
clip_index: int,
|
|
205
|
-
note_ids:
|
|
238
|
+
note_ids: list[int] | str,
|
|
206
239
|
time_offset: float = 0.0,
|
|
207
240
|
) -> dict:
|
|
208
241
|
"""Duplicate specific notes by ID (JSON array of ints), with optional time offset (in beats)."""
|
|
@@ -88,6 +88,9 @@ def plan_arrangement(
|
|
|
88
88
|
style=style,
|
|
89
89
|
)
|
|
90
90
|
|
|
91
|
+
# Add section-level sample role hints
|
|
92
|
+
planner_engine.add_sample_hints(plan)
|
|
93
|
+
|
|
91
94
|
result = plan.to_dict()
|
|
92
95
|
result["loop_identity"] = loop_identity.to_dict()
|
|
93
96
|
result["available_styles"] = sorted(planner_engine.VALID_STYLES)
|
|
@@ -23,6 +23,11 @@ _DOMAIN_MAP: dict[str, list[str]] = {
|
|
|
23
23
|
"too_safe_to_progress": ["sound_design", "transition"],
|
|
24
24
|
"section_missing": ["arrangement", "transition"],
|
|
25
25
|
"transition_not_earned": ["transition", "arrangement"],
|
|
26
|
+
# Sample-domain patterns (session-state analysis, not action-history)
|
|
27
|
+
"no_organic_texture": ["sample", "sound_design"],
|
|
28
|
+
"stale_drums": ["sample", "arrangement"],
|
|
29
|
+
"vocal_processing_monotony": ["sample", "sound_design"],
|
|
30
|
+
"dense_but_static": ["sample", "mix"],
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
_STUCKNESS_THRESHOLD = 0.2 # Below this, treat as user_request
|