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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
LivePilot - Browser domain handlers (
|
|
2
|
+
LivePilot - Browser domain handlers (6 commands).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import Live
|
|
@@ -386,6 +386,61 @@ def load_browser_item(song, params):
|
|
|
386
386
|
)
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
_SCAN_MAX_ITERATIONS = 100000
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _scan_recursive(item, results, depth, max_depth, max_per_category,
|
|
393
|
+
_counter=None):
|
|
394
|
+
"""Recursively collect loadable browser items with iteration cap."""
|
|
395
|
+
if _counter is None:
|
|
396
|
+
_counter = [0]
|
|
397
|
+
if depth > max_depth or len(results) >= max_per_category:
|
|
398
|
+
return
|
|
399
|
+
for child in item.children:
|
|
400
|
+
_counter[0] += 1
|
|
401
|
+
if _counter[0] > _SCAN_MAX_ITERATIONS or len(results) >= max_per_category:
|
|
402
|
+
return
|
|
403
|
+
if child.is_loadable:
|
|
404
|
+
entry = {"name": child.name, "is_loadable": True}
|
|
405
|
+
try:
|
|
406
|
+
entry["uri"] = child.uri
|
|
407
|
+
except AttributeError:
|
|
408
|
+
entry["uri"] = None
|
|
409
|
+
results.append(entry)
|
|
410
|
+
if child.is_folder:
|
|
411
|
+
_scan_recursive(
|
|
412
|
+
child, results, depth + 1, max_depth, max_per_category,
|
|
413
|
+
_counter
|
|
414
|
+
)
|
|
415
|
+
if len(results) >= max_per_category:
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@register("scan_browser_deep")
|
|
420
|
+
def scan_browser_deep(song, params):
|
|
421
|
+
"""Walk the entire browser tree and return all loadable items by category.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
max_per_category : int, optional
|
|
426
|
+
Maximum items to collect per top-level category (default 1000).
|
|
427
|
+
max_depth : int, optional
|
|
428
|
+
Maximum recursion depth into the browser tree (default 4).
|
|
429
|
+
"""
|
|
430
|
+
max_per_category = int(params.get("max_per_category", 1000))
|
|
431
|
+
max_depth = int(params.get("max_depth", 4))
|
|
432
|
+
browser = _get_browser()
|
|
433
|
+
categories = _get_categories(browser)
|
|
434
|
+
|
|
435
|
+
result = {}
|
|
436
|
+
for cat_name, cat_item in categories.items():
|
|
437
|
+
items = []
|
|
438
|
+
_scan_recursive(cat_item, items, 0, max_depth, max_per_category)
|
|
439
|
+
result[cat_name] = items
|
|
440
|
+
|
|
441
|
+
return {"categories": result}
|
|
442
|
+
|
|
443
|
+
|
|
389
444
|
@register("get_device_presets")
|
|
390
445
|
def get_device_presets(song, params):
|
|
391
446
|
"""List available presets for a device type by searching the browser.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
LivePilot - Device domain handlers (
|
|
2
|
+
LivePilot - Device domain handlers (12 commands).
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
import Live
|
|
@@ -46,7 +46,7 @@ def get_device_parameters(song, params):
|
|
|
46
46
|
|
|
47
47
|
parameters = []
|
|
48
48
|
for i, param in enumerate(device.parameters):
|
|
49
|
-
|
|
49
|
+
info = {
|
|
50
50
|
"index": i,
|
|
51
51
|
"name": param.name,
|
|
52
52
|
"value": param.value,
|
|
@@ -54,7 +54,13 @@ def get_device_parameters(song, params):
|
|
|
54
54
|
"max": param.max,
|
|
55
55
|
"is_quantized": param.is_quantized,
|
|
56
56
|
"value_string": param.str_for_value(param.value),
|
|
57
|
-
}
|
|
57
|
+
}
|
|
58
|
+
# 12.2+ feature: native display_value
|
|
59
|
+
try:
|
|
60
|
+
info["display_value"] = param.display_value
|
|
61
|
+
except AttributeError:
|
|
62
|
+
pass
|
|
63
|
+
parameters.append(info)
|
|
58
64
|
return {"parameters": parameters}
|
|
59
65
|
|
|
60
66
|
|
|
@@ -104,13 +110,19 @@ def set_device_parameter(song, params):
|
|
|
104
110
|
raise ValueError("Must provide parameter_name or parameter_index")
|
|
105
111
|
|
|
106
112
|
param.value = value
|
|
107
|
-
|
|
113
|
+
result = {
|
|
108
114
|
"name": param.name,
|
|
109
115
|
"value": param.value,
|
|
110
116
|
"value_string": param.str_for_value(param.value),
|
|
111
117
|
"min": param.min,
|
|
112
118
|
"max": param.max,
|
|
113
119
|
}
|
|
120
|
+
# 12.2+: include display_value
|
|
121
|
+
try:
|
|
122
|
+
result["display_value"] = param.display_value
|
|
123
|
+
except AttributeError:
|
|
124
|
+
pass
|
|
125
|
+
return result
|
|
114
126
|
|
|
115
127
|
|
|
116
128
|
@register("batch_set_parameters")
|
|
@@ -163,11 +175,17 @@ def batch_set_parameters(song, params):
|
|
|
163
175
|
)
|
|
164
176
|
|
|
165
177
|
param.value = value
|
|
166
|
-
|
|
178
|
+
result_entry = {
|
|
167
179
|
"name": param.name,
|
|
168
180
|
"value": param.value,
|
|
169
181
|
"value_string": param.str_for_value(param.value),
|
|
170
|
-
}
|
|
182
|
+
}
|
|
183
|
+
# 12.2+: include display_value
|
|
184
|
+
try:
|
|
185
|
+
result_entry["display_value"] = param.display_value
|
|
186
|
+
except AttributeError:
|
|
187
|
+
pass
|
|
188
|
+
results.append(result_entry)
|
|
171
189
|
|
|
172
190
|
return {"parameters": results}
|
|
173
191
|
|
|
@@ -408,6 +426,198 @@ def load_device_by_uri(song, params):
|
|
|
408
426
|
)
|
|
409
427
|
|
|
410
428
|
|
|
429
|
+
# ── Device name registry for insert_device (12.3+) ──────────────────────
|
|
430
|
+
|
|
431
|
+
NATIVE_DEVICE_NAMES = frozenset({
|
|
432
|
+
# Instruments
|
|
433
|
+
"Analog", "Collision", "Drift", "Electric", "Drum Rack",
|
|
434
|
+
"Instrument Rack", "Meld", "Operator", "Sampler", "Simpler",
|
|
435
|
+
"Tension", "Wavetable",
|
|
436
|
+
# Audio Effects
|
|
437
|
+
"Align Delay", "Amp", "Audio Effect Rack", "Auto Filter",
|
|
438
|
+
"Auto Pan-Tremolo", "Auto Shift", "Beat Repeat", "Cabinet",
|
|
439
|
+
"Channel EQ", "Chorus-Ensemble", "Color Limiter", "Compressor",
|
|
440
|
+
"Convolution Reverb", "Corpus", "Delay", "Drum Buss",
|
|
441
|
+
"Dynamic Tube", "Echo", "EQ Eight", "EQ Three", "Erosion",
|
|
442
|
+
"External Audio Effect", "Flanger", "Frequency Shifter", "Gate",
|
|
443
|
+
"Glue Compressor", "Grain Delay", "Hybrid Reverb", "Limiter",
|
|
444
|
+
"Looper", "Multiband Dynamics", "Overdrive", "Pedal",
|
|
445
|
+
"Phaser-Flanger", "Pitch Hack", "Redux", "Re-Enveloper",
|
|
446
|
+
"Resonators", "Reverb", "Roar", "Saturator", "Shifter",
|
|
447
|
+
"Spectral Blur", "Spectral Resonator", "Spectral Time", "Tuner",
|
|
448
|
+
"Utility", "Vinyl Distortion", "Vocoder",
|
|
449
|
+
# MIDI Effects
|
|
450
|
+
"Arpeggiator", "Chord", "Expression Control", "MIDI Effect Rack",
|
|
451
|
+
"Note Echo", "Note Length", "Pitch", "Random", "Scale", "Strum",
|
|
452
|
+
"Velocity",
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
# Case-insensitive lookup for user convenience
|
|
456
|
+
_DEVICE_NAME_LOOKUP = {name.lower(): name for name in NATIVE_DEVICE_NAMES}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@register("insert_device")
|
|
460
|
+
def insert_device(song, params):
|
|
461
|
+
"""Insert a native Live device by name (12.3+ API).
|
|
462
|
+
|
|
463
|
+
Much faster than browser search — a single call with no state dependency.
|
|
464
|
+
Only works for native devices (not plugins or M4L).
|
|
465
|
+
|
|
466
|
+
Required: track_index, device_name
|
|
467
|
+
Optional: position (-1 = end of chain, default), chain_index + device_index (for rack chains)
|
|
468
|
+
"""
|
|
469
|
+
from .version_detect import has_feature
|
|
470
|
+
|
|
471
|
+
if not has_feature("insert_device"):
|
|
472
|
+
raise RuntimeError(
|
|
473
|
+
"insert_device requires Live 12.3+. "
|
|
474
|
+
"Use find_and_load_device (browser search) instead."
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
track_index = int(params["track_index"])
|
|
478
|
+
device_name = str(params["device_name"])
|
|
479
|
+
position = int(params.get("position", -1))
|
|
480
|
+
chain_index = params.get("chain_index")
|
|
481
|
+
|
|
482
|
+
# Resolve canonical name (case-insensitive)
|
|
483
|
+
canonical = _DEVICE_NAME_LOOKUP.get(device_name.lower())
|
|
484
|
+
if canonical is None:
|
|
485
|
+
raise ValueError(
|
|
486
|
+
"Device '%s' is not a native Live device. "
|
|
487
|
+
"insert_device only supports native devices (not plugins or M4L). "
|
|
488
|
+
"Use find_and_load_device for plugins."
|
|
489
|
+
% device_name
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
track = get_track(song, track_index)
|
|
493
|
+
|
|
494
|
+
song.begin_undo_step()
|
|
495
|
+
try:
|
|
496
|
+
if chain_index is not None:
|
|
497
|
+
# 12.3+ Chain.insert_device — insert into a rack chain
|
|
498
|
+
chain_index = int(chain_index)
|
|
499
|
+
device_on_track = get_device(track, int(params.get("device_index", 0)))
|
|
500
|
+
chains = list(device_on_track.chains)
|
|
501
|
+
if chain_index < 0 or chain_index >= len(chains):
|
|
502
|
+
raise IndexError(
|
|
503
|
+
"Chain index %d out of range (0..%d)"
|
|
504
|
+
% (chain_index, len(chains) - 1)
|
|
505
|
+
)
|
|
506
|
+
chain = chains[chain_index]
|
|
507
|
+
if position >= 0:
|
|
508
|
+
device = chain.insert_device(canonical, position)
|
|
509
|
+
else:
|
|
510
|
+
device = chain.insert_device(canonical)
|
|
511
|
+
else:
|
|
512
|
+
# Track-level insertion
|
|
513
|
+
if position >= 0:
|
|
514
|
+
device = track.insert_device(canonical, position)
|
|
515
|
+
else:
|
|
516
|
+
device = track.insert_device(canonical)
|
|
517
|
+
finally:
|
|
518
|
+
song.end_undo_step()
|
|
519
|
+
|
|
520
|
+
# Read back the device info — use "loaded" key to match
|
|
521
|
+
# the convention expected by _postflight_loaded_device on MCP side
|
|
522
|
+
result = {
|
|
523
|
+
"loaded": device.name,
|
|
524
|
+
"class_name": device.class_name,
|
|
525
|
+
"track_index": track_index,
|
|
526
|
+
"parameter_count": len(list(device.parameters)),
|
|
527
|
+
}
|
|
528
|
+
if position >= 0:
|
|
529
|
+
result["position"] = position
|
|
530
|
+
return result
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
@register("insert_rack_chain")
|
|
534
|
+
def insert_rack_chain(song, params):
|
|
535
|
+
"""Insert a new chain into an Instrument Rack, Audio Effect Rack, or Drum Rack (12.3+).
|
|
536
|
+
|
|
537
|
+
Required: track_index, device_index
|
|
538
|
+
Optional: position (-1 = end)
|
|
539
|
+
"""
|
|
540
|
+
from .version_detect import has_feature
|
|
541
|
+
|
|
542
|
+
if not has_feature("insert_chain"):
|
|
543
|
+
raise RuntimeError(
|
|
544
|
+
"insert_rack_chain requires Live 12.3+."
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
track_index = int(params["track_index"])
|
|
548
|
+
device_index = int(params["device_index"])
|
|
549
|
+
position = int(params.get("position", -1))
|
|
550
|
+
|
|
551
|
+
track = get_track(song, track_index)
|
|
552
|
+
device = get_device(track, device_index)
|
|
553
|
+
|
|
554
|
+
if not device.can_have_chains:
|
|
555
|
+
raise ValueError(
|
|
556
|
+
"Device '%s' is not a rack — cannot insert chains"
|
|
557
|
+
% device.name
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
song.begin_undo_step()
|
|
561
|
+
try:
|
|
562
|
+
if position >= 0:
|
|
563
|
+
device.insert_chain(position)
|
|
564
|
+
else:
|
|
565
|
+
device.insert_chain()
|
|
566
|
+
finally:
|
|
567
|
+
song.end_undo_step()
|
|
568
|
+
|
|
569
|
+
chain_count = len(list(device.chains))
|
|
570
|
+
return {
|
|
571
|
+
"inserted": True,
|
|
572
|
+
"track_index": track_index,
|
|
573
|
+
"device_index": device_index,
|
|
574
|
+
"chain_count": chain_count,
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@register("set_drum_chain_note")
|
|
579
|
+
def set_drum_chain_note(song, params):
|
|
580
|
+
"""Set which MIDI note triggers a drum chain (12.3+).
|
|
581
|
+
|
|
582
|
+
Required: track_index, device_index, chain_index, note
|
|
583
|
+
note: MIDI note number (0-127), or -1 for 'All Notes'
|
|
584
|
+
"""
|
|
585
|
+
from .version_detect import has_feature
|
|
586
|
+
|
|
587
|
+
if not has_feature("drum_chain_in_note"):
|
|
588
|
+
raise RuntimeError(
|
|
589
|
+
"set_drum_chain_note requires Live 12.3+."
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
track_index = int(params["track_index"])
|
|
593
|
+
device_index = int(params["device_index"])
|
|
594
|
+
chain_index = int(params["chain_index"])
|
|
595
|
+
note = int(params["note"])
|
|
596
|
+
|
|
597
|
+
if note < -1 or note > 127:
|
|
598
|
+
raise ValueError("note must be -1 (All Notes) or 0-127")
|
|
599
|
+
|
|
600
|
+
track = get_track(song, track_index)
|
|
601
|
+
device = get_device(track, device_index)
|
|
602
|
+
|
|
603
|
+
chains = list(device.chains)
|
|
604
|
+
if chain_index < 0 or chain_index >= len(chains):
|
|
605
|
+
raise IndexError(
|
|
606
|
+
"Chain index %d out of range (0..%d)"
|
|
607
|
+
% (chain_index, len(chains) - 1)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
chain = chains[chain_index]
|
|
611
|
+
chain.in_note = note
|
|
612
|
+
|
|
613
|
+
return {
|
|
614
|
+
"track_index": track_index,
|
|
615
|
+
"device_index": device_index,
|
|
616
|
+
"chain_index": chain_index,
|
|
617
|
+
"in_note": note,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
|
|
411
621
|
@register("find_and_load_device")
|
|
412
622
|
def find_and_load_device(song, params):
|
|
413
623
|
"""Find a device by name in the browser and load it onto a track.
|
|
@@ -420,6 +630,26 @@ def find_and_load_device(song, params):
|
|
|
420
630
|
track = get_track(song, track_index)
|
|
421
631
|
browser = _get_browser()
|
|
422
632
|
|
|
633
|
+
# 12.3+ fast path: try insert_device for native devices
|
|
634
|
+
from .version_detect import has_feature
|
|
635
|
+
if has_feature("insert_device"):
|
|
636
|
+
canonical = _DEVICE_NAME_LOOKUP.get(device_name)
|
|
637
|
+
if canonical is not None:
|
|
638
|
+
try:
|
|
639
|
+
song.begin_undo_step()
|
|
640
|
+
try:
|
|
641
|
+
device = track.insert_device(canonical)
|
|
642
|
+
finally:
|
|
643
|
+
song.end_undo_step()
|
|
644
|
+
return {
|
|
645
|
+
"loaded": device.name,
|
|
646
|
+
"class_name": device.class_name,
|
|
647
|
+
"track_index": track_index,
|
|
648
|
+
"parameter_count": len(list(device.parameters)),
|
|
649
|
+
}
|
|
650
|
+
except Exception:
|
|
651
|
+
pass # Fall through to browser search
|
|
652
|
+
|
|
423
653
|
MAX_ITERATIONS = 50000
|
|
424
654
|
iterations = 0
|
|
425
655
|
|
|
@@ -133,14 +133,15 @@ def get_track_meters(song, params):
|
|
|
133
133
|
"index": idx,
|
|
134
134
|
"name": track.name,
|
|
135
135
|
}
|
|
136
|
-
|
|
136
|
+
muted = bool(getattr(track, "mute", False))
|
|
137
|
+
if track.has_audio_output and not muted:
|
|
137
138
|
entry["level"] = track.output_meter_level
|
|
138
139
|
if include_stereo:
|
|
139
140
|
entry["left"] = track.output_meter_left
|
|
140
141
|
entry["right"] = track.output_meter_right
|
|
141
142
|
else:
|
|
142
143
|
entry["level"] = 0.0
|
|
143
|
-
entry["has_audio_output"] = False
|
|
144
|
+
entry["has_audio_output"] = bool(getattr(track, "has_audio_output", False))
|
|
144
145
|
if include_stereo:
|
|
145
146
|
entry["left"] = 0.0
|
|
146
147
|
entry["right"] = 0.0
|
|
@@ -177,7 +178,11 @@ def get_mix_snapshot(song, params):
|
|
|
177
178
|
tracks.append({
|
|
178
179
|
"index": i,
|
|
179
180
|
"name": track.name,
|
|
180
|
-
"meter_level":
|
|
181
|
+
"meter_level": (
|
|
182
|
+
track.output_meter_level
|
|
183
|
+
if track.has_audio_output and not bool(getattr(track, "mute", False))
|
|
184
|
+
else 0.0
|
|
185
|
+
),
|
|
181
186
|
"volume": track.mixer_device.volume.value,
|
|
182
187
|
"pan": track.mixer_device.panning.value,
|
|
183
188
|
"mute": track.mute,
|
|
@@ -255,7 +255,11 @@ class LivePilotServer(object):
|
|
|
255
255
|
except AssertionError:
|
|
256
256
|
# ControlSurface is disconnecting — return error instead of
|
|
257
257
|
# running LOM calls on the TCP thread (which would be unsafe)
|
|
258
|
-
|
|
258
|
+
try:
|
|
259
|
+
self._command_queue.get_nowait()
|
|
260
|
+
except queue.Empty:
|
|
261
|
+
pass
|
|
262
|
+
self._send(client, {
|
|
259
263
|
"id": request_id,
|
|
260
264
|
"ok": False,
|
|
261
265
|
"error": {"code": "STATE_ERROR", "message": "Script is disconnecting"},
|
|
@@ -3,6 +3,7 @@ LivePilot - Transport domain handlers (10 commands).
|
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from .router import register
|
|
6
|
+
from .version_detect import version_string, get_api_features
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
@register("get_session_info")
|
|
@@ -59,6 +60,8 @@ def get_session_info(song, params):
|
|
|
59
60
|
"tracks": tracks_info,
|
|
60
61
|
"return_tracks": return_tracks_info,
|
|
61
62
|
"scenes": scenes_info,
|
|
63
|
+
"live_version": version_string(),
|
|
64
|
+
"api_features": get_api_features(),
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LivePilot - Ableton Live version detection and feature flags.
|
|
3
|
+
|
|
4
|
+
Detects the running Live version and provides feature availability checks.
|
|
5
|
+
Used by handlers to conditionally use new APIs (12.1.10+, 12.2+, 12.3+).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import Live
|
|
9
|
+
|
|
10
|
+
# ── Feature version requirements ────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
FEATURES = {
|
|
13
|
+
"create_midi_clip_arrangement": (12, 1, 10),
|
|
14
|
+
"looper_export": (12, 1, 0),
|
|
15
|
+
"tuning_system": (12, 1, 0),
|
|
16
|
+
"display_value": (12, 2, 0),
|
|
17
|
+
"clip_start_time_observable": (12, 2, 0),
|
|
18
|
+
"take_lanes_api": (12, 2, 0),
|
|
19
|
+
"insert_device": (12, 3, 0),
|
|
20
|
+
"insert_chain": (12, 3, 0),
|
|
21
|
+
"drum_chain_in_note": (12, 3, 0),
|
|
22
|
+
"stem_separation": (12, 3, 0),
|
|
23
|
+
"replace_sample_native": (12, 4, 0),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── Cached version ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
_cached_version = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_live_version():
|
|
33
|
+
"""Return (major, minor, patch) of the running Live instance.
|
|
34
|
+
|
|
35
|
+
Uses Live.Application.get_application() to read version info.
|
|
36
|
+
Falls back to (12, 0, 0) if detection fails.
|
|
37
|
+
"""
|
|
38
|
+
global _cached_version
|
|
39
|
+
if _cached_version is not None:
|
|
40
|
+
return _cached_version
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
app = Live.Application.get_application()
|
|
44
|
+
major = app.get_major_version()
|
|
45
|
+
minor = app.get_minor_version()
|
|
46
|
+
# get_bugfix_version() was added later; fall back to 0
|
|
47
|
+
try:
|
|
48
|
+
patch = app.get_bugfix_version()
|
|
49
|
+
except AttributeError:
|
|
50
|
+
patch = 0
|
|
51
|
+
_cached_version = (int(major), int(minor), int(patch))
|
|
52
|
+
except Exception:
|
|
53
|
+
_cached_version = (12, 0, 0)
|
|
54
|
+
|
|
55
|
+
return _cached_version
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def has_feature(feature_name):
|
|
59
|
+
"""Check if a feature is available in the running Live version.
|
|
60
|
+
|
|
61
|
+
Returns True if the detected version >= the feature's required version.
|
|
62
|
+
Returns False for unknown feature names (safe default).
|
|
63
|
+
"""
|
|
64
|
+
required = FEATURES.get(feature_name)
|
|
65
|
+
if required is None:
|
|
66
|
+
return False
|
|
67
|
+
return get_live_version() >= required
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_api_features():
|
|
71
|
+
"""Return a dict of all feature flags for the current version."""
|
|
72
|
+
return {name: has_feature(name) for name in FEATURES}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def version_string():
|
|
76
|
+
"""Return version as a dot-separated string, e.g. '12.3.6'."""
|
|
77
|
+
v = get_live_version()
|
|
78
|
+
return "%d.%d.%d" % v
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Metadata sync — single source of truth for version and tool count.
|
|
3
|
+
|
|
4
|
+
Reads version from package.json, tool count from test_tools_contract.py,
|
|
5
|
+
and verifies all known locations are in sync.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python scripts/sync_metadata.py --check # verify, exit 1 if stale
|
|
9
|
+
python scripts/sync_metadata.py --fix # auto-fix stale references
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_version() -> str:
|
|
21
|
+
"""Read version from package.json (source of truth)."""
|
|
22
|
+
pkg = json.loads((ROOT / "package.json").read_text())
|
|
23
|
+
return pkg["version"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_tool_count() -> int:
|
|
27
|
+
"""Read tool count from test_tools_contract.py assertion."""
|
|
28
|
+
src = (ROOT / "tests" / "test_tools_contract.py").read_text()
|
|
29
|
+
match = re.search(r"assert len\(tools\) == (\d+)", src)
|
|
30
|
+
if match:
|
|
31
|
+
return int(match.group(1))
|
|
32
|
+
raise ValueError("Could not find tool count assertion in test_tools_contract.py")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Files that must contain the version string
|
|
36
|
+
VERSION_FILES = [
|
|
37
|
+
"package.json",
|
|
38
|
+
"server.json",
|
|
39
|
+
"manifest.json",
|
|
40
|
+
"livepilot/.claude-plugin/plugin.json",
|
|
41
|
+
"livepilot/.Codex-plugin/plugin.json",
|
|
42
|
+
".claude-plugin/marketplace.json",
|
|
43
|
+
"mcp_server/__init__.py",
|
|
44
|
+
"remote_script/LivePilot/__init__.py",
|
|
45
|
+
"CLAUDE.md",
|
|
46
|
+
"AGENTS.md",
|
|
47
|
+
"livepilot/skills/livepilot-core/references/overview.md",
|
|
48
|
+
"docs/M4L_BRIDGE.md",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Files that must contain the tool count
|
|
52
|
+
TOOL_COUNT_FILES = [
|
|
53
|
+
"README.md",
|
|
54
|
+
"package.json",
|
|
55
|
+
"server.json",
|
|
56
|
+
"CLAUDE.md",
|
|
57
|
+
"AGENTS.md",
|
|
58
|
+
"CONTRIBUTING.md",
|
|
59
|
+
"livepilot/.claude-plugin/plugin.json",
|
|
60
|
+
"livepilot/.Codex-plugin/plugin.json",
|
|
61
|
+
"livepilot/skills/livepilot-core/SKILL.md",
|
|
62
|
+
"livepilot/skills/livepilot-core/references/overview.md",
|
|
63
|
+
"docs/manual/index.md",
|
|
64
|
+
"docs/manual/tool-reference.md",
|
|
65
|
+
"docs/manual/tool-catalog.md",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_version(version: str) -> list[str]:
|
|
70
|
+
"""Check all version files for staleness."""
|
|
71
|
+
issues = []
|
|
72
|
+
for rel_path in VERSION_FILES:
|
|
73
|
+
path = ROOT / rel_path
|
|
74
|
+
if not path.exists():
|
|
75
|
+
continue
|
|
76
|
+
content = path.read_text()
|
|
77
|
+
if version not in content:
|
|
78
|
+
# Find what version IS there
|
|
79
|
+
old = re.search(r"1\.\d+\.\d+", content)
|
|
80
|
+
old_ver = old.group(0) if old else "???"
|
|
81
|
+
if old_ver != version:
|
|
82
|
+
issues.append(f" {rel_path}: has {old_ver}, expected {version}")
|
|
83
|
+
return issues
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_tool_count(count: int) -> list[str]:
|
|
87
|
+
"""Check all tool count files for staleness."""
|
|
88
|
+
issues = []
|
|
89
|
+
count_str = str(count)
|
|
90
|
+
for rel_path in TOOL_COUNT_FILES:
|
|
91
|
+
path = ROOT / rel_path
|
|
92
|
+
if not path.exists():
|
|
93
|
+
continue
|
|
94
|
+
content = path.read_text()
|
|
95
|
+
# Look for "N tools" pattern
|
|
96
|
+
matches = re.findall(r"(\d+)\s+tools", content)
|
|
97
|
+
for m in matches:
|
|
98
|
+
if m != count_str and int(m) > 250: # ignore subset counts like "210 tools"
|
|
99
|
+
issues.append(f" {rel_path}: has '{m} tools', expected '{count_str} tools'")
|
|
100
|
+
break
|
|
101
|
+
return issues
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main():
|
|
105
|
+
mode = sys.argv[1] if len(sys.argv) > 1 else "--check"
|
|
106
|
+
|
|
107
|
+
version = get_version()
|
|
108
|
+
tool_count = get_tool_count()
|
|
109
|
+
|
|
110
|
+
print(f"Source of truth: version={version}, tools={tool_count}")
|
|
111
|
+
|
|
112
|
+
version_issues = check_version(version)
|
|
113
|
+
count_issues = check_tool_count(tool_count)
|
|
114
|
+
|
|
115
|
+
all_issues = version_issues + count_issues
|
|
116
|
+
|
|
117
|
+
if all_issues:
|
|
118
|
+
print(f"\nFound {len(all_issues)} stale reference(s):")
|
|
119
|
+
for issue in all_issues:
|
|
120
|
+
print(issue)
|
|
121
|
+
if mode == "--check":
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
elif mode == "--fix":
|
|
124
|
+
print("\n--fix mode not yet implemented. Fix manually.")
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
else:
|
|
127
|
+
print("All metadata in sync.")
|
|
128
|
+
sys.exit(0)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
main()
|