livepilot 1.9.23 → 1.10.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-plugin/marketplace.json +3 -3
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +119 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +144 -13
- 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-core/SKILL.md +21 -4
- 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 +16 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-release/SKILL.md +19 -5
- package/livepilot/skills/livepilot-sample-engine/SKILL.md +104 -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 +15 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +2 -2
- 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 +452 -0
- package/mcp_server/composer/layer_planner.py +427 -0
- package/mcp_server/composer/prompt_parser.py +329 -0
- package/mcp_server/composer/tools.py +201 -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/hook_hunter/analyzer.py +23 -0
- package/mcp_server/hook_hunter/models.py +1 -0
- package/mcp_server/hook_hunter/tools.py +4 -2
- package/mcp_server/m4l_bridge.py +1 -0
- package/mcp_server/memory/taste_graph.py +68 -1
- package/mcp_server/memory/tools.py +15 -4
- package/mcp_server/musical_intelligence/detectors.py +14 -1
- package/mcp_server/musical_intelligence/tools.py +11 -8
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -0
- package/mcp_server/preview_studio/models.py +1 -0
- package/mcp_server/preview_studio/tools.py +56 -13
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +137 -0
- package/mcp_server/runtime/execution_router.py +143 -0
- package/mcp_server/runtime/live_version.py +102 -0
- package/mcp_server/runtime/remote_commands.py +87 -0
- package/mcp_server/runtime/tools.py +18 -4
- 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/sources.py +540 -0
- package/mcp_server/sample_engine/techniques.py +908 -0
- package/mcp_server/sample_engine/tools.py +442 -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 +41 -41
- package/mcp_server/semantic_moves/performance_moves.py +13 -13
- package/mcp_server/semantic_moves/sample_compilers.py +372 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
- package/mcp_server/semantic_moves/tools.py +18 -17
- package/mcp_server/semantic_moves/transition_moves.py +16 -16
- package/mcp_server/server.py +51 -0
- package/mcp_server/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -0
- package/mcp_server/session_continuity/tracker.py +29 -1
- package/mcp_server/song_brain/builder.py +28 -1
- package/mcp_server/song_brain/models.py +4 -0
- package/mcp_server/song_brain/tools.py +20 -2
- 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/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/wonder_mode/diagnosis.py +5 -0
- package/mcp_server/wonder_mode/engine.py +85 -1
- package/mcp_server/wonder_mode/tools.py +6 -1
- package/package.json +12 -2
- 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 +236 -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
- package/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Sample Engine MCP tools — 6 intelligence-layer tools.
|
|
2
|
+
|
|
3
|
+
No new Ableton communication — these orchestrate existing tools
|
|
4
|
+
through the analyzer, critics, planner, and technique library.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from fastmcp import Context
|
|
12
|
+
|
|
13
|
+
from ..server import mcp
|
|
14
|
+
from .models import SampleProfile, SampleIntent, SampleFitReport
|
|
15
|
+
from .analyzer import build_profile_from_filename
|
|
16
|
+
from .critics import run_all_sample_critics
|
|
17
|
+
from .planner import select_technique, compile_sample_plan
|
|
18
|
+
from .techniques import find_techniques, list_techniques, get_technique
|
|
19
|
+
from .sources import BrowserSource, FilesystemSource, SpliceSource, build_search_queries
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
async def analyze_sample(
|
|
24
|
+
ctx: Context,
|
|
25
|
+
file_path: Optional[str] = None,
|
|
26
|
+
track_index: Optional[int] = None,
|
|
27
|
+
clip_index: Optional[int] = None,
|
|
28
|
+
) -> dict:
|
|
29
|
+
"""Analyze a sample and build a complete SampleProfile.
|
|
30
|
+
|
|
31
|
+
Detects material type, key, BPM, spectral character, and recommends
|
|
32
|
+
Simpler mode, slice method, and warp mode. Provide either file_path
|
|
33
|
+
OR track_index + clip_index to analyze a clip in the session.
|
|
34
|
+
|
|
35
|
+
Falls back to filename-only analysis if M4L bridge unavailable.
|
|
36
|
+
"""
|
|
37
|
+
if file_path is None and track_index is None:
|
|
38
|
+
return {"error": "Provide either file_path or track_index + clip_index"}
|
|
39
|
+
|
|
40
|
+
if track_index is not None and file_path is None:
|
|
41
|
+
try:
|
|
42
|
+
bridge = ctx.lifespan_context.get("m4l")
|
|
43
|
+
if bridge:
|
|
44
|
+
result = await bridge.send_command(
|
|
45
|
+
"get_clip_file_path", track_index, clip_index or 0
|
|
46
|
+
)
|
|
47
|
+
if not result.get("error"):
|
|
48
|
+
file_path = result.get("file_path")
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
if file_path is None:
|
|
53
|
+
return {"error": "Could not determine file path — provide file_path directly"}
|
|
54
|
+
|
|
55
|
+
source = "session_clip" if track_index is not None else "filesystem"
|
|
56
|
+
profile = build_profile_from_filename(file_path, source=source)
|
|
57
|
+
return profile.to_dict()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@mcp.tool()
|
|
61
|
+
def evaluate_sample_fit(
|
|
62
|
+
ctx: Context,
|
|
63
|
+
file_path: str,
|
|
64
|
+
intent: str = "layer",
|
|
65
|
+
philosophy: str = "auto",
|
|
66
|
+
) -> dict:
|
|
67
|
+
"""Run the 6-critic battery to evaluate how well a sample fits the current song.
|
|
68
|
+
|
|
69
|
+
Returns overall score, per-critic scores, recommendations, and
|
|
70
|
+
both surgeon (precise) and alchemist (transformative) plans.
|
|
71
|
+
|
|
72
|
+
intent: rhythm, texture, layer, melody, vocal, atmosphere, transform
|
|
73
|
+
philosophy: surgeon, alchemist, auto (context-decides)
|
|
74
|
+
"""
|
|
75
|
+
profile = build_profile_from_filename(file_path)
|
|
76
|
+
sample_intent = SampleIntent(
|
|
77
|
+
intent_type=intent, philosophy=philosophy,
|
|
78
|
+
description=f"Evaluate fitness for {intent}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Gather song context
|
|
82
|
+
song_key = None
|
|
83
|
+
session_tempo = 120.0
|
|
84
|
+
existing_roles: list[str] = []
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
88
|
+
info = ableton.send_command("get_session_info", {})
|
|
89
|
+
session_tempo = info.get("tempo", 120.0)
|
|
90
|
+
|
|
91
|
+
# Get track names as roles
|
|
92
|
+
track_count = info.get("track_count", 0)
|
|
93
|
+
for i in range(min(track_count, 16)):
|
|
94
|
+
try:
|
|
95
|
+
track_info = ableton.send_command("get_track_info", {"track_index": i})
|
|
96
|
+
name = track_info.get("name", "").lower()
|
|
97
|
+
if name:
|
|
98
|
+
existing_roles.append(name)
|
|
99
|
+
except Exception:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Detect key from MIDI tracks
|
|
103
|
+
try:
|
|
104
|
+
from ..tools._theory_engine import detect_key
|
|
105
|
+
for i in range(min(track_count, 8)):
|
|
106
|
+
try:
|
|
107
|
+
clip_info = ableton.send_command("get_clip_info", {
|
|
108
|
+
"track_index": i, "clip_index": 0,
|
|
109
|
+
})
|
|
110
|
+
if clip_info.get("is_midi"):
|
|
111
|
+
notes_result = ableton.send_command("get_notes", {
|
|
112
|
+
"track_index": i, "clip_index": 0,
|
|
113
|
+
})
|
|
114
|
+
notes = notes_result.get("notes", [])
|
|
115
|
+
if notes:
|
|
116
|
+
key_result = detect_key(notes)
|
|
117
|
+
mode = key_result.get("mode", "")
|
|
118
|
+
mode_suffix = "m" if "minor" in mode else ""
|
|
119
|
+
song_key = f"{key_result['tonic_name']}{mode_suffix}"
|
|
120
|
+
break
|
|
121
|
+
except Exception:
|
|
122
|
+
continue
|
|
123
|
+
except ImportError:
|
|
124
|
+
pass
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
critics = run_all_sample_critics(
|
|
129
|
+
profile=profile,
|
|
130
|
+
intent=sample_intent,
|
|
131
|
+
song_key=song_key,
|
|
132
|
+
session_tempo=session_tempo,
|
|
133
|
+
existing_roles=existing_roles,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Build both plans
|
|
137
|
+
surgeon_plan = compile_sample_plan(
|
|
138
|
+
profile,
|
|
139
|
+
SampleIntent(intent_type=intent, philosophy="surgeon", description=""),
|
|
140
|
+
)
|
|
141
|
+
alchemist_plan = compile_sample_plan(
|
|
142
|
+
profile,
|
|
143
|
+
SampleIntent(intent_type=intent, philosophy="alchemist", description=""),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
report = SampleFitReport(
|
|
147
|
+
sample=profile,
|
|
148
|
+
critics=critics,
|
|
149
|
+
recommended_intent=intent,
|
|
150
|
+
surgeon_plan=surgeon_plan,
|
|
151
|
+
alchemist_plan=alchemist_plan,
|
|
152
|
+
warnings=[c.recommendation for c in critics.values() if c.score < 0.5],
|
|
153
|
+
)
|
|
154
|
+
return report.to_dict()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mcp.tool()
|
|
158
|
+
def search_samples(
|
|
159
|
+
ctx: Context,
|
|
160
|
+
query: str,
|
|
161
|
+
material_type: Optional[str] = None,
|
|
162
|
+
key: Optional[str] = None,
|
|
163
|
+
bpm_range: Optional[str] = None,
|
|
164
|
+
source: Optional[str] = None,
|
|
165
|
+
max_results: int = 10,
|
|
166
|
+
) -> dict:
|
|
167
|
+
"""Search for samples across Splice library, Ableton browser, and local filesystem.
|
|
168
|
+
|
|
169
|
+
Searches all enabled sources in parallel and ranks results.
|
|
170
|
+
Splice results include rich metadata (key, BPM, genre, tags, pack info).
|
|
171
|
+
|
|
172
|
+
query: search text like "dark vocal", "breakbeat", "foley metal"
|
|
173
|
+
material_type: filter by type (vocal, drum_loop, texture, etc.)
|
|
174
|
+
key: prefer samples in this key (e.g., "Cm", "F#")
|
|
175
|
+
bpm_range: "min-max" BPM range (e.g., "120-130")
|
|
176
|
+
source: "splice", "browser", "filesystem", or None for all
|
|
177
|
+
max_results: maximum results to return (default 10)
|
|
178
|
+
"""
|
|
179
|
+
results: list[dict] = []
|
|
180
|
+
|
|
181
|
+
# Parse BPM range
|
|
182
|
+
bpm_min, bpm_max = None, None
|
|
183
|
+
if bpm_range:
|
|
184
|
+
parts = bpm_range.replace(" ", "").split("-")
|
|
185
|
+
if len(parts) == 2:
|
|
186
|
+
try:
|
|
187
|
+
bpm_min, bpm_max = float(parts[0]), float(parts[1])
|
|
188
|
+
except ValueError:
|
|
189
|
+
pass
|
|
190
|
+
|
|
191
|
+
# Splice search (richest metadata, searched first)
|
|
192
|
+
if source in (None, "splice"):
|
|
193
|
+
splice = SpliceSource()
|
|
194
|
+
if splice.enabled:
|
|
195
|
+
splice_results = splice.search(
|
|
196
|
+
query=query,
|
|
197
|
+
max_results=max_results,
|
|
198
|
+
key=key,
|
|
199
|
+
bpm_min=bpm_min,
|
|
200
|
+
bpm_max=bpm_max,
|
|
201
|
+
)
|
|
202
|
+
for candidate in splice_results:
|
|
203
|
+
d = candidate.to_dict()
|
|
204
|
+
d["source_priority"] = 1 # highest
|
|
205
|
+
results.append(d)
|
|
206
|
+
|
|
207
|
+
# Browser search
|
|
208
|
+
if source in (None, "browser"):
|
|
209
|
+
try:
|
|
210
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
211
|
+
browser = BrowserSource()
|
|
212
|
+
for category in browser.DEFAULT_CATEGORIES:
|
|
213
|
+
try:
|
|
214
|
+
search_result = ableton.send_command("search_browser", {
|
|
215
|
+
"path": category,
|
|
216
|
+
"name_filter": query,
|
|
217
|
+
"loadable_only": True,
|
|
218
|
+
"max_results": max_results,
|
|
219
|
+
})
|
|
220
|
+
raw = search_result.get("results", [])
|
|
221
|
+
parsed = browser.parse_results(raw, category)
|
|
222
|
+
for candidate in parsed:
|
|
223
|
+
d = candidate.to_dict()
|
|
224
|
+
d["source_priority"] = 2
|
|
225
|
+
results.append(d)
|
|
226
|
+
except Exception:
|
|
227
|
+
continue
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
# Filesystem search
|
|
232
|
+
if source in (None, "filesystem"):
|
|
233
|
+
fs = FilesystemSource(scan_paths=[
|
|
234
|
+
"~/Music", "~/Documents/Samples",
|
|
235
|
+
"~/Documents/LivePilot/downloads",
|
|
236
|
+
])
|
|
237
|
+
fs_results = fs.search(query, max_results=max_results)
|
|
238
|
+
for candidate in fs_results:
|
|
239
|
+
d = candidate.to_dict()
|
|
240
|
+
d["source_priority"] = 3
|
|
241
|
+
results.append(d)
|
|
242
|
+
|
|
243
|
+
# Sort by source priority (Splice first), then by relevance
|
|
244
|
+
results.sort(key=lambda r: r.get("source_priority", 9))
|
|
245
|
+
|
|
246
|
+
# Build summary
|
|
247
|
+
source_counts = {}
|
|
248
|
+
for r in results:
|
|
249
|
+
src = r.get("source", "unknown")
|
|
250
|
+
source_counts[src] = source_counts.get(src, 0) + 1
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
"query": query,
|
|
254
|
+
"result_count": len(results[:max_results]),
|
|
255
|
+
"source_counts": source_counts,
|
|
256
|
+
"results": results[:max_results],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@mcp.tool()
|
|
261
|
+
def suggest_sample_technique(
|
|
262
|
+
ctx: Context,
|
|
263
|
+
file_path: str,
|
|
264
|
+
intent: str = "rhythm",
|
|
265
|
+
philosophy: str = "auto",
|
|
266
|
+
max_suggestions: int = 3,
|
|
267
|
+
) -> dict:
|
|
268
|
+
"""Suggest sample manipulation techniques from the technique library.
|
|
269
|
+
|
|
270
|
+
Returns ranked techniques with executable step outlines for the
|
|
271
|
+
given sample + intent combination.
|
|
272
|
+
|
|
273
|
+
file_path: path to the sample
|
|
274
|
+
intent: rhythm, texture, layer, melody, vocal, atmosphere, transform, challenge
|
|
275
|
+
philosophy: surgeon, alchemist, auto
|
|
276
|
+
"""
|
|
277
|
+
profile = build_profile_from_filename(file_path)
|
|
278
|
+
sample_intent = SampleIntent(
|
|
279
|
+
intent_type=intent, philosophy=philosophy, description="",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
candidates = find_techniques(
|
|
283
|
+
material_type=profile.material_type,
|
|
284
|
+
intent=intent,
|
|
285
|
+
philosophy=philosophy if philosophy != "auto" else None,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if not candidates:
|
|
289
|
+
candidates = find_techniques(intent=intent)
|
|
290
|
+
|
|
291
|
+
suggestions = []
|
|
292
|
+
for t in candidates[:max_suggestions]:
|
|
293
|
+
steps = compile_sample_plan(profile, sample_intent, technique=t)
|
|
294
|
+
suggestions.append({
|
|
295
|
+
"technique_id": t.technique_id,
|
|
296
|
+
"name": t.name,
|
|
297
|
+
"philosophy": t.philosophy,
|
|
298
|
+
"difficulty": t.difficulty,
|
|
299
|
+
"description": t.description,
|
|
300
|
+
"inspiration": t.inspiration,
|
|
301
|
+
"step_count": len(steps),
|
|
302
|
+
"steps_preview": [s["description"] for s in steps[:5]],
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"sample": profile.name,
|
|
307
|
+
"material_type": profile.material_type,
|
|
308
|
+
"intent": intent,
|
|
309
|
+
"suggestion_count": len(suggestions),
|
|
310
|
+
"suggestions": suggestions,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
def plan_sample_workflow(
|
|
316
|
+
ctx: Context,
|
|
317
|
+
file_path: Optional[str] = None,
|
|
318
|
+
search_query: Optional[str] = None,
|
|
319
|
+
intent: str = "rhythm",
|
|
320
|
+
philosophy: str = "auto",
|
|
321
|
+
target_track: Optional[int] = None,
|
|
322
|
+
) -> dict:
|
|
323
|
+
"""Full end-to-end sample workflow: analyze, critique, select technique, compile plan.
|
|
324
|
+
|
|
325
|
+
Provide file_path for a known sample, or search_query to find one.
|
|
326
|
+
Returns a complete compiled plan ready for execution.
|
|
327
|
+
|
|
328
|
+
intent: rhythm, texture, layer, melody, vocal, atmosphere, transform
|
|
329
|
+
philosophy: surgeon, alchemist, auto
|
|
330
|
+
target_track: existing track index, or None for new track
|
|
331
|
+
"""
|
|
332
|
+
if file_path is None and search_query is None:
|
|
333
|
+
return {"error": "Provide either file_path or search_query"}
|
|
334
|
+
|
|
335
|
+
profile = None
|
|
336
|
+
if file_path:
|
|
337
|
+
profile = build_profile_from_filename(file_path)
|
|
338
|
+
|
|
339
|
+
sample_intent = SampleIntent(
|
|
340
|
+
intent_type=intent, philosophy=philosophy,
|
|
341
|
+
description=search_query or f"Process {file_path} for {intent}",
|
|
342
|
+
target_track=target_track,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
if profile is None:
|
|
346
|
+
# No file yet — return search guidance
|
|
347
|
+
queries = build_search_queries(search_query or "", material_type=None)
|
|
348
|
+
return {
|
|
349
|
+
"status": "search_needed",
|
|
350
|
+
"search_queries": queries,
|
|
351
|
+
"intent": intent,
|
|
352
|
+
"note": "Use search_samples to find a sample, then call again with file_path",
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
technique = select_technique(profile, sample_intent)
|
|
356
|
+
plan = compile_sample_plan(profile, sample_intent, target_track=target_track,
|
|
357
|
+
technique=technique)
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
"sample": profile.to_dict(),
|
|
361
|
+
"intent": intent,
|
|
362
|
+
"philosophy": philosophy,
|
|
363
|
+
"technique": technique.name if technique else "fallback",
|
|
364
|
+
"technique_id": technique.technique_id if technique else "",
|
|
365
|
+
"step_count": len(plan),
|
|
366
|
+
"compiled_plan": plan,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@mcp.tool()
|
|
371
|
+
def get_sample_opportunities(ctx: Context) -> dict:
|
|
372
|
+
"""Analyze current song and identify where samples could improve it.
|
|
373
|
+
|
|
374
|
+
Returns opportunities with suggested material types and techniques.
|
|
375
|
+
Used by Wonder Mode diagnosis for sample-aware creative rescue.
|
|
376
|
+
"""
|
|
377
|
+
opportunities: list[dict] = []
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
ableton = ctx.lifespan_context["ableton"]
|
|
381
|
+
info = ableton.send_command("get_session_info", {})
|
|
382
|
+
except Exception:
|
|
383
|
+
return {"opportunities": [], "note": "Cannot read session — Ableton not connected"}
|
|
384
|
+
|
|
385
|
+
track_count = info.get("track_count", 0)
|
|
386
|
+
track_names: list[str] = []
|
|
387
|
+
has_sampler = False
|
|
388
|
+
|
|
389
|
+
for i in range(min(track_count, 16)):
|
|
390
|
+
try:
|
|
391
|
+
track_info = ableton.send_command("get_track_info", {"track_index": i})
|
|
392
|
+
name = track_info.get("name", "").lower()
|
|
393
|
+
track_names.append(name)
|
|
394
|
+
devices = track_info.get("devices", [])
|
|
395
|
+
for d in devices:
|
|
396
|
+
if d.get("class_name") in ("OriginalSimpler", "MultiSampler"):
|
|
397
|
+
has_sampler = True
|
|
398
|
+
except Exception:
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
# No organic texture
|
|
402
|
+
has_organic = any(
|
|
403
|
+
kw in name for name in track_names
|
|
404
|
+
for kw in ("vocal", "sample", "foley", "field", "organic", "found")
|
|
405
|
+
)
|
|
406
|
+
if not has_organic and track_count >= 3:
|
|
407
|
+
opportunities.append({
|
|
408
|
+
"type": "no_organic_texture",
|
|
409
|
+
"description": "No organic/sampled textures — all tracks appear synthesized",
|
|
410
|
+
"suggested_material": ["vocal", "foley", "texture"],
|
|
411
|
+
"suggested_techniques": ["vocal_chop_rhythm", "phone_recording_texture", "tail_harvest"],
|
|
412
|
+
"confidence": 0.6,
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
# Limited drum variety
|
|
416
|
+
drum_tracks = [n for n in track_names if any(
|
|
417
|
+
kw in n for kw in ("drum", "beat", "perc", "kick", "snare")
|
|
418
|
+
)]
|
|
419
|
+
if len(drum_tracks) <= 1 and track_count >= 4:
|
|
420
|
+
opportunities.append({
|
|
421
|
+
"type": "drum_variety_needed",
|
|
422
|
+
"description": "Limited percussion variety — layer a break or add ghost notes",
|
|
423
|
+
"suggested_material": ["drum_loop"],
|
|
424
|
+
"suggested_techniques": ["break_layering", "ghost_note_texture"],
|
|
425
|
+
"confidence": 0.5,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
# No Simpler/Sampler devices
|
|
429
|
+
if not has_sampler and track_count >= 2:
|
|
430
|
+
opportunities.append({
|
|
431
|
+
"type": "no_sample_instruments",
|
|
432
|
+
"description": "No Simpler/Sampler devices — samples could add character",
|
|
433
|
+
"suggested_material": ["vocal", "instrument_loop", "one_shot"],
|
|
434
|
+
"suggested_techniques": ["syllable_instrument", "slice_and_sequence"],
|
|
435
|
+
"confidence": 0.4,
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
"opportunity_count": len(opportunities),
|
|
440
|
+
"opportunities": opportunities,
|
|
441
|
+
"track_count": track_count,
|
|
442
|
+
}
|
|
@@ -5,9 +5,12 @@ from . import mix_moves # noqa: F401
|
|
|
5
5
|
from . import transition_moves # noqa: F401
|
|
6
6
|
from . import sound_design_moves # noqa: F401
|
|
7
7
|
from . import performance_moves # noqa: F401
|
|
8
|
+
from . import device_creation_moves # noqa: F401
|
|
9
|
+
from ..sample_engine import moves as sample_moves # noqa: F401
|
|
8
10
|
|
|
9
11
|
# Import compilers to auto-register them
|
|
10
12
|
from . import mix_compilers # noqa: F401
|
|
11
13
|
from . import transition_compilers # noqa: F401
|
|
12
14
|
from . import sound_design_compilers # noqa: F401
|
|
13
15
|
from . import performance_compilers # noqa: F401
|
|
16
|
+
from . import sample_compilers # noqa: F401
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Device-creation-domain semantic moves — generate custom M4L devices.
|
|
2
|
+
|
|
3
|
+
These moves create new instruments and effects on the fly using the
|
|
4
|
+
Device Forge. Each move specifies a gen~ template and parameters.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .models import SemanticMove
|
|
8
|
+
from .registry import register
|
|
9
|
+
|
|
10
|
+
CREATE_CHAOS_MODULATOR = SemanticMove(
|
|
11
|
+
move_id="create_chaos_modulator",
|
|
12
|
+
family="device_creation",
|
|
13
|
+
intent="Generate a Lorenz attractor M4L device for chaotic modulation — "
|
|
14
|
+
"smooth but unpredictable parameter movement",
|
|
15
|
+
targets={"novelty": 0.8, "motion": 0.6, "surprise": 0.7},
|
|
16
|
+
protect={"clarity": 0.5},
|
|
17
|
+
risk_level="medium",
|
|
18
|
+
compile_plan=[
|
|
19
|
+
{
|
|
20
|
+
"tool": "generate_m4l_effect",
|
|
21
|
+
"params": {
|
|
22
|
+
"name": "Wonder Chaos Mod",
|
|
23
|
+
"device_type": "audio_effect",
|
|
24
|
+
"description": "Lorenz attractor chaotic modulation source",
|
|
25
|
+
},
|
|
26
|
+
"description": "Generate chaos modulator M4L device",
|
|
27
|
+
"backend": "mcp_tool",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"tool": "find_and_load_device",
|
|
31
|
+
"params": {"query": "Wonder Chaos Mod"},
|
|
32
|
+
"description": "Load generated device onto target track",
|
|
33
|
+
"backend": "remote_command",
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
verification_plan=[
|
|
37
|
+
{"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
CREATE_FEEDBACK_RESONATOR = SemanticMove(
|
|
42
|
+
move_id="create_feedback_resonator",
|
|
43
|
+
family="device_creation",
|
|
44
|
+
intent="Generate a tuned comb resonator M4L device — feeds harmonic resonance "
|
|
45
|
+
"at a specific frequency into the signal",
|
|
46
|
+
targets={"depth": 0.6, "texture": 0.5, "novelty": 0.4},
|
|
47
|
+
protect={"clarity": 0.6, "punch": 0.5},
|
|
48
|
+
risk_level="low",
|
|
49
|
+
compile_plan=[
|
|
50
|
+
{
|
|
51
|
+
"tool": "generate_m4l_effect",
|
|
52
|
+
"params": {
|
|
53
|
+
"name": "Wonder Resonator",
|
|
54
|
+
"device_type": "audio_effect",
|
|
55
|
+
"description": "Tuned comb filter resonator",
|
|
56
|
+
},
|
|
57
|
+
"description": "Generate resonator M4L device",
|
|
58
|
+
"backend": "mcp_tool",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"tool": "find_and_load_device",
|
|
62
|
+
"params": {"query": "Wonder Resonator"},
|
|
63
|
+
"description": "Load resonator onto target track",
|
|
64
|
+
"backend": "remote_command",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
verification_plan=[
|
|
68
|
+
{"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
|
|
69
|
+
],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
CREATE_WAVEFOLDER_EFFECT = SemanticMove(
|
|
73
|
+
move_id="create_wavefolder_effect",
|
|
74
|
+
family="device_creation",
|
|
75
|
+
intent="Generate a Buchla-style wavefolder M4L effect — rich harmonic series "
|
|
76
|
+
"from waveform folding, great for adding edge and complexity",
|
|
77
|
+
targets={"edge": 0.6, "novelty": 0.5, "energy": 0.4},
|
|
78
|
+
protect={"warmth": 0.5},
|
|
79
|
+
risk_level="medium",
|
|
80
|
+
compile_plan=[
|
|
81
|
+
{
|
|
82
|
+
"tool": "generate_m4l_effect",
|
|
83
|
+
"params": {
|
|
84
|
+
"name": "Wonder Wavefolder",
|
|
85
|
+
"device_type": "audio_effect",
|
|
86
|
+
"description": "Buchla-style harmonic wavefolder",
|
|
87
|
+
},
|
|
88
|
+
"description": "Generate wavefolder M4L device",
|
|
89
|
+
"backend": "mcp_tool",
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"tool": "find_and_load_device",
|
|
93
|
+
"params": {"query": "Wonder Wavefolder"},
|
|
94
|
+
"description": "Load wavefolder onto target track",
|
|
95
|
+
"backend": "remote_command",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
verification_plan=[
|
|
99
|
+
{"tool": "get_track_meters", "check": "track producing audio with harmonic content", "backend": "remote_command"},
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
CREATE_BITCRUSHER_EFFECT = SemanticMove(
|
|
104
|
+
move_id="create_bitcrusher_effect",
|
|
105
|
+
family="device_creation",
|
|
106
|
+
intent="Generate a bitcrusher M4L effect — sample-rate and bit-depth reduction "
|
|
107
|
+
"from subtle aliasing to full digital destruction",
|
|
108
|
+
targets={"edge": 0.5, "novelty": 0.4, "texture": 0.4},
|
|
109
|
+
protect={"clarity": 0.4},
|
|
110
|
+
risk_level="low",
|
|
111
|
+
compile_plan=[
|
|
112
|
+
{
|
|
113
|
+
"tool": "generate_m4l_effect",
|
|
114
|
+
"params": {
|
|
115
|
+
"name": "Wonder Bitcrusher",
|
|
116
|
+
"device_type": "audio_effect",
|
|
117
|
+
"description": "Sample-rate and bit-depth crusher",
|
|
118
|
+
},
|
|
119
|
+
"description": "Generate bitcrusher M4L device",
|
|
120
|
+
"backend": "mcp_tool",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"tool": "find_and_load_device",
|
|
124
|
+
"params": {"query": "Wonder Bitcrusher"},
|
|
125
|
+
"description": "Load bitcrusher onto target track",
|
|
126
|
+
"backend": "remote_command",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
verification_plan=[
|
|
130
|
+
{"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
|
|
131
|
+
],
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
CREATE_KARPLUS_STRING = SemanticMove(
|
|
135
|
+
move_id="create_karplus_string",
|
|
136
|
+
family="device_creation",
|
|
137
|
+
intent="Generate a Karplus-Strong string synth M4L instrument — plucked string "
|
|
138
|
+
"physical model with excitation input",
|
|
139
|
+
targets={"novelty": 0.5, "texture": 0.6, "depth": 0.4},
|
|
140
|
+
protect={"clarity": 0.6},
|
|
141
|
+
risk_level="low",
|
|
142
|
+
compile_plan=[
|
|
143
|
+
{
|
|
144
|
+
"tool": "generate_m4l_effect",
|
|
145
|
+
"params": {
|
|
146
|
+
"name": "Wonder String",
|
|
147
|
+
"device_type": "audio_effect",
|
|
148
|
+
"description": "Karplus-Strong plucked string physical model",
|
|
149
|
+
},
|
|
150
|
+
"description": "Generate string synth M4L device",
|
|
151
|
+
"backend": "mcp_tool",
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"tool": "find_and_load_device",
|
|
155
|
+
"params": {"query": "Wonder String"},
|
|
156
|
+
"description": "Load string synth onto target track",
|
|
157
|
+
"backend": "remote_command",
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
verification_plan=[
|
|
161
|
+
{"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
|
|
162
|
+
],
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
CREATE_STOCHASTIC_TEXTURE = SemanticMove(
|
|
166
|
+
move_id="create_stochastic_texture",
|
|
167
|
+
family="device_creation",
|
|
168
|
+
intent="Generate a stochastic resonance M4L effect — noise + threshold + feedback "
|
|
169
|
+
"creates granular textures and organic movement",
|
|
170
|
+
targets={"texture": 0.7, "novelty": 0.6, "motion": 0.5},
|
|
171
|
+
protect={"clarity": 0.4},
|
|
172
|
+
risk_level="medium",
|
|
173
|
+
compile_plan=[
|
|
174
|
+
{
|
|
175
|
+
"tool": "generate_m4l_effect",
|
|
176
|
+
"params": {
|
|
177
|
+
"name": "Wonder Stochastic",
|
|
178
|
+
"device_type": "audio_effect",
|
|
179
|
+
"description": "Stochastic resonance texture generator",
|
|
180
|
+
},
|
|
181
|
+
"description": "Generate stochastic texture M4L device",
|
|
182
|
+
"backend": "mcp_tool",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"tool": "find_and_load_device",
|
|
186
|
+
"params": {"query": "Wonder Stochastic"},
|
|
187
|
+
"description": "Load stochastic texture device onto target track",
|
|
188
|
+
"backend": "remote_command",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
verification_plan=[
|
|
192
|
+
{"tool": "get_track_meters", "check": "track producing audio", "backend": "remote_command"},
|
|
193
|
+
],
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
CREATE_FDN_REVERB = SemanticMove(
|
|
197
|
+
move_id="create_fdn_reverb",
|
|
198
|
+
family="device_creation",
|
|
199
|
+
intent="Generate a feedback delay network M4L effect — dense, diffuse reverb "
|
|
200
|
+
"with Hadamard-like cross-coupling. Tune delay times for harmonic reverb.",
|
|
201
|
+
targets={"depth": 0.7, "width": 0.5, "novelty": 0.4},
|
|
202
|
+
protect={"punch": 0.5, "clarity": 0.5},
|
|
203
|
+
risk_level="low",
|
|
204
|
+
compile_plan=[
|
|
205
|
+
{
|
|
206
|
+
"tool": "generate_m4l_effect",
|
|
207
|
+
"params": {
|
|
208
|
+
"name": "Wonder FDN Verb",
|
|
209
|
+
"device_type": "audio_effect",
|
|
210
|
+
"description": "Feedback delay network reverb",
|
|
211
|
+
},
|
|
212
|
+
"description": "Generate FDN reverb M4L device",
|
|
213
|
+
"backend": "mcp_tool",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"tool": "find_and_load_device",
|
|
217
|
+
"params": {"query": "Wonder FDN Verb"},
|
|
218
|
+
"description": "Load FDN reverb onto target track",
|
|
219
|
+
"backend": "remote_command",
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
verification_plan=[
|
|
223
|
+
{"tool": "get_track_meters", "check": "track producing audio with reverb tail", "backend": "remote_command"},
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Register all device creation moves
|
|
228
|
+
for _move in [
|
|
229
|
+
CREATE_CHAOS_MODULATOR,
|
|
230
|
+
CREATE_FEEDBACK_RESONATOR,
|
|
231
|
+
CREATE_WAVEFOLDER_EFFECT,
|
|
232
|
+
CREATE_BITCRUSHER_EFFECT,
|
|
233
|
+
CREATE_KARPLUS_STRING,
|
|
234
|
+
CREATE_STOCHASTIC_TEXTURE,
|
|
235
|
+
CREATE_FDN_REVERB,
|
|
236
|
+
]:
|
|
237
|
+
register(_move)
|