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
|
@@ -121,7 +121,11 @@ def _build_triptych(
|
|
|
121
121
|
compiled_plan = None
|
|
122
122
|
if moves and i < len(moves):
|
|
123
123
|
move_id = moves[i].get("move_id", "")
|
|
124
|
-
|
|
124
|
+
# Compile through the semantic compiler — single source of truth
|
|
125
|
+
from ..wonder_mode.engine import _compile_variant_plan
|
|
126
|
+
kernel = {"session_info": {"tempo": 120, "tracks": []}, "mode": "improve"}
|
|
127
|
+
compiled_plan = _compile_variant_plan(moves[i], kernel)
|
|
128
|
+
# No fallback to plan_template — uncompilable moves stay analytical
|
|
125
129
|
|
|
126
130
|
variants.append(PreviewVariant(
|
|
127
131
|
variant_id=f"{set_id}_{profile['label']}",
|
|
@@ -264,7 +268,10 @@ def _compute_set_id(request_text: str, kernel_id: str) -> str:
|
|
|
264
268
|
|
|
265
269
|
def _estimate_taste_fit(novelty: float, taste_graph: dict) -> float:
|
|
266
270
|
"""Estimate how well a novelty level fits user taste."""
|
|
267
|
-
|
|
271
|
+
# Routes through the canonical accessor so dimension_weights.transition_boldness
|
|
272
|
+
# is honored. Previously read the top-level key directly and always got 0.5.
|
|
273
|
+
from ..memory.taste_accessors import get_dimension_pref
|
|
274
|
+
boldness = get_dimension_pref(taste_graph, "transition_boldness", default=0.5)
|
|
268
275
|
# Users who like boldness prefer higher novelty
|
|
269
276
|
fit = 1.0 - abs(novelty - boldness) * 0.5
|
|
270
277
|
return round(max(0.0, min(1.0, fit)), 3)
|
|
@@ -23,7 +23,15 @@ def _get_ableton(ctx: Context):
|
|
|
23
23
|
|
|
24
24
|
def _should_refuse_analytical(compiled_plan, wonder_linked: bool) -> bool:
|
|
25
25
|
"""Check if an analytical variant should be refused in Wonder context."""
|
|
26
|
-
|
|
26
|
+
if not wonder_linked:
|
|
27
|
+
return False
|
|
28
|
+
if compiled_plan is None:
|
|
29
|
+
return True
|
|
30
|
+
if isinstance(compiled_plan, dict):
|
|
31
|
+
return len(compiled_plan.get("steps", [])) == 0
|
|
32
|
+
if isinstance(compiled_plan, list):
|
|
33
|
+
return len(compiled_plan) == 0
|
|
34
|
+
return True
|
|
27
35
|
|
|
28
36
|
|
|
29
37
|
def _find_wonder_session_by_preview(set_id: str):
|
|
@@ -308,7 +316,7 @@ def commit_preview_variant(
|
|
|
308
316
|
|
|
309
317
|
|
|
310
318
|
@mcp.tool()
|
|
311
|
-
def render_preview_variant(
|
|
319
|
+
async def render_preview_variant(
|
|
312
320
|
ctx: Context,
|
|
313
321
|
set_id: str = "",
|
|
314
322
|
variant_id: str = "",
|
|
@@ -352,7 +360,7 @@ def render_preview_variant(
|
|
|
352
360
|
"analytical_only": True,
|
|
353
361
|
}
|
|
354
362
|
|
|
355
|
-
# If the variant has a compiled plan,
|
|
363
|
+
# If the variant has a compiled plan, apply -> capture audible -> undo.
|
|
356
364
|
# Without a compiled plan, return the variant's analytical preview.
|
|
357
365
|
if variant.compiled_plan:
|
|
358
366
|
ableton = _get_ableton(ctx)
|
|
@@ -360,52 +368,87 @@ def render_preview_variant(
|
|
|
360
368
|
plan = variant.compiled_plan
|
|
361
369
|
steps = plan if isinstance(plan, list) else plan.get("steps", [])
|
|
362
370
|
|
|
363
|
-
from ..runtime.execution_router import
|
|
371
|
+
from ..runtime.execution_router import execute_plan_steps_async
|
|
364
372
|
|
|
365
373
|
applied_count = 0
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
374
|
+
playback_started = False
|
|
375
|
+
preview_mode = "metadata_only_preview"
|
|
376
|
+
spectral_before: Optional[dict] = None
|
|
377
|
+
spectral_after: Optional[dict] = None
|
|
378
|
+
before_info: dict = {}
|
|
379
|
+
after_info: dict = {}
|
|
380
|
+
|
|
381
|
+
bridge = ctx.lifespan_context.get("m4l")
|
|
382
|
+
mcp_registry = ctx.lifespan_context.get("mcp_dispatch", {})
|
|
369
383
|
|
|
370
|
-
|
|
371
|
-
|
|
384
|
+
try:
|
|
385
|
+
# ── 1. Capture BEFORE metadata ──
|
|
386
|
+
before_info = ableton.send_command("get_session_info", {}) or {}
|
|
387
|
+
|
|
388
|
+
# ── 2. Apply the variant ──
|
|
389
|
+
exec_results = await execute_plan_steps_async(
|
|
390
|
+
steps,
|
|
391
|
+
ableton=ableton,
|
|
392
|
+
bridge=bridge,
|
|
393
|
+
mcp_registry=mcp_registry,
|
|
394
|
+
ctx=ctx,
|
|
395
|
+
)
|
|
372
396
|
applied_count = sum(1 for r in exec_results if r.ok)
|
|
397
|
+
if applied_count == 0 and steps:
|
|
398
|
+
return {
|
|
399
|
+
"error": "Variant failed to apply any steps",
|
|
400
|
+
"variant_id": variant_id,
|
|
401
|
+
"step_errors": [r.error for r in exec_results if not r.ok],
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# ── 3. Capture AFTER metadata (variant is live) ──
|
|
405
|
+
after_info = ableton.send_command("get_session_info", {}) or {}
|
|
406
|
+
|
|
407
|
+
# ── 4. Audible capture WHILE variant is still applied ──
|
|
408
|
+
# This is the critical ordering fix: previously this block ran AFTER
|
|
409
|
+
# the finally's undo loop, so "audible_preview" captured pre-variant
|
|
410
|
+
# audio and lied about it. Now playback + spectrum sampling happens
|
|
411
|
+
# while the variant is actually in effect, then the finally undoes it.
|
|
412
|
+
try:
|
|
413
|
+
from ..m4l_bridge import SpectralCache
|
|
414
|
+
cache = ctx.lifespan_context.get("spectral")
|
|
415
|
+
if cache and isinstance(cache, SpectralCache) and cache.is_connected:
|
|
416
|
+
spectral_before = cache.get_all()
|
|
417
|
+
|
|
418
|
+
tempo = before_info.get("tempo", 120) or 120
|
|
419
|
+
play_seconds = min(bars * (60.0 / tempo) * 4, 8.0)
|
|
420
|
+
|
|
421
|
+
ableton.send_command("start_playback", {})
|
|
422
|
+
playback_started = True
|
|
423
|
+
|
|
424
|
+
import time as _time
|
|
425
|
+
_time.sleep(play_seconds)
|
|
426
|
+
|
|
427
|
+
spectral_after = cache.get_all()
|
|
428
|
+
|
|
429
|
+
ableton.send_command("stop_playback", {})
|
|
430
|
+
playback_started = False
|
|
431
|
+
|
|
432
|
+
preview_mode = "audible_preview"
|
|
433
|
+
except Exception:
|
|
434
|
+
# Spectral capture is best-effort; keep preview_mode as metadata_only
|
|
435
|
+
pass
|
|
373
436
|
|
|
374
|
-
# Capture after state
|
|
375
|
-
after_info = ableton.send_command("get_session_info", {})
|
|
376
437
|
except Exception as e:
|
|
377
438
|
return {"error": f"Render failed: {e}", "variant_id": variant_id}
|
|
378
439
|
finally:
|
|
379
|
-
#
|
|
440
|
+
# ── 5. Cleanup: stop playback if still running, then undo everything ──
|
|
441
|
+
if playback_started:
|
|
442
|
+
try:
|
|
443
|
+
ableton.send_command("stop_playback", {})
|
|
444
|
+
except Exception:
|
|
445
|
+
pass
|
|
380
446
|
for _ in range(applied_count):
|
|
381
447
|
try:
|
|
382
448
|
ableton.send_command("undo")
|
|
383
449
|
except Exception:
|
|
384
450
|
break
|
|
385
451
|
|
|
386
|
-
# Determine preview mode: audible (M4L available) or metadata-only
|
|
387
|
-
preview_mode = "metadata_only_preview"
|
|
388
|
-
spectral_before = None
|
|
389
|
-
spectral_after = None
|
|
390
|
-
|
|
391
|
-
# Try audible preview — capture spectrum via M4L spectral cache
|
|
392
|
-
try:
|
|
393
|
-
from ..m4l_bridge import SpectralCache
|
|
394
|
-
cache = ctx.lifespan_context.get("spectral_cache")
|
|
395
|
-
if cache and isinstance(cache, SpectralCache) and cache.has_data():
|
|
396
|
-
spectral_before = cache.get_snapshot()
|
|
397
|
-
# Play for the requested bar count
|
|
398
|
-
tempo = before_info.get("tempo", 120)
|
|
399
|
-
play_seconds = bars * (60.0 / tempo) * 4 # bars * beat_duration * 4 beats
|
|
400
|
-
ableton.send_command("start_playback", {})
|
|
401
|
-
import time as _time
|
|
402
|
-
_time.sleep(min(play_seconds, 8.0)) # cap at 8 seconds
|
|
403
|
-
spectral_after = cache.get_snapshot()
|
|
404
|
-
ableton.send_command("stop_playback", {})
|
|
405
|
-
preview_mode = "audible_preview"
|
|
406
|
-
except Exception:
|
|
407
|
-
pass # fall back to metadata_only
|
|
408
|
-
|
|
409
452
|
variant.status = "rendered"
|
|
410
453
|
variant.preview_mode = preview_mode
|
|
411
454
|
variant.render_ref = f"render_{variant_id}_{bars}bars"
|
|
@@ -78,6 +78,39 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
78
78
|
except Exception:
|
|
79
79
|
pass
|
|
80
80
|
|
|
81
|
+
# 5b. Build notes_map for role inference.
|
|
82
|
+
# Shape: {section_id: {track_index: [notes]}}. Without this, role_graph
|
|
83
|
+
# falls back to "assume all tracks active in every section" which destroys
|
|
84
|
+
# section-scoped role confidence.
|
|
85
|
+
notes_map: dict[str, dict[int, list[dict]]] = {}
|
|
86
|
+
try:
|
|
87
|
+
for scene_idx, scene in enumerate(scenes or []):
|
|
88
|
+
section_id = str(
|
|
89
|
+
scene.get("section_id")
|
|
90
|
+
or scene.get("name")
|
|
91
|
+
or f"scene_{scene_idx}"
|
|
92
|
+
)
|
|
93
|
+
per_track: dict[int, list[dict]] = {}
|
|
94
|
+
for track in tracks:
|
|
95
|
+
t_idx = track.get("index", 0)
|
|
96
|
+
try:
|
|
97
|
+
notes_resp = ableton.send_command("get_notes", {
|
|
98
|
+
"track_index": t_idx,
|
|
99
|
+
"clip_index": scene_idx,
|
|
100
|
+
})
|
|
101
|
+
if isinstance(notes_resp, dict):
|
|
102
|
+
notes = notes_resp.get("notes", [])
|
|
103
|
+
if notes:
|
|
104
|
+
per_track[t_idx] = notes
|
|
105
|
+
except Exception:
|
|
106
|
+
# Individual note fetch failing is fine — continue with others
|
|
107
|
+
continue
|
|
108
|
+
if per_track:
|
|
109
|
+
notes_map[section_id] = per_track
|
|
110
|
+
except Exception:
|
|
111
|
+
# Overall failure: empty map, degrade to "all tracks active" fallback
|
|
112
|
+
notes_map = {}
|
|
113
|
+
|
|
81
114
|
# 6. Probe capabilities (direct SpectralCache access, not TCP)
|
|
82
115
|
analyzer_ok = False
|
|
83
116
|
analyzer_fresh = False
|
|
@@ -103,6 +136,7 @@ def build_project_brain(ctx: Context) -> dict:
|
|
|
103
136
|
scenes=scenes if scenes and clip_matrix else None,
|
|
104
137
|
clip_matrix=clip_matrix if clip_matrix else None,
|
|
105
138
|
track_infos=track_infos if track_infos else None,
|
|
139
|
+
notes_map=notes_map if notes_map else None,
|
|
106
140
|
arrangement_clips=arrangement_clips if arrangement_clips else None,
|
|
107
141
|
analyzer_ok=analyzer_ok,
|
|
108
142
|
flucoma_ok=flucoma_ok,
|
|
@@ -34,6 +34,23 @@ def probe_capabilities(
|
|
|
34
34
|
"detail": "TCP 9878 connection active" if ableton_ok else "Not connected",
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
# 1b. Live version capabilities
|
|
38
|
+
live_version_str = "12.0.0"
|
|
39
|
+
if ableton_ok:
|
|
40
|
+
try:
|
|
41
|
+
info = ableton.send_command("get_session_info")
|
|
42
|
+
live_version_str = info.get("live_version", "12.0.0")
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
from .live_version import LiveVersionCapabilities
|
|
46
|
+
version_caps = LiveVersionCapabilities.from_version_string(live_version_str)
|
|
47
|
+
report["live_version"] = {
|
|
48
|
+
"status": "ok",
|
|
49
|
+
"version": live_version_str,
|
|
50
|
+
"capability_tier": version_caps.capability_tier,
|
|
51
|
+
"features": version_caps.to_dict(),
|
|
52
|
+
}
|
|
53
|
+
|
|
37
54
|
# 2. Remote Script parity
|
|
38
55
|
from .remote_commands import REMOTE_COMMANDS
|
|
39
56
|
report["remote_script"] = {
|
|
@@ -45,8 +62,10 @@ def probe_capabilities(
|
|
|
45
62
|
# 3. M4L bridge
|
|
46
63
|
bridge_ok = False
|
|
47
64
|
if ctx is not None:
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
lifespan_context = getattr(ctx, "lifespan_context", {}) if hasattr(ctx, "lifespan_context") else {}
|
|
66
|
+
bridge = lifespan_context.get("m4l")
|
|
67
|
+
spectral = lifespan_context.get("spectral")
|
|
68
|
+
bridge_ok = bridge is not None and spectral is not None and getattr(spectral, "is_connected", False)
|
|
50
69
|
report["m4l_bridge"] = {
|
|
51
70
|
"status": "ok" if bridge_ok else "unavailable",
|
|
52
71
|
"detail": "UDP 9880 / OSC 9881 active" if bridge_ok else "Not connected — 30 analyzer tools unavailable",
|
|
@@ -1,34 +1,52 @@
|
|
|
1
|
-
"""Unified execution router for compiled plan steps.
|
|
1
|
+
"""Unified async execution router for compiled plan steps.
|
|
2
2
|
|
|
3
3
|
Classifies each step by backend (remote_command, mcp_tool, bridge_command)
|
|
4
|
-
and dispatches
|
|
5
|
-
|
|
4
|
+
and dispatches through the correct transport. Async-only — there is no
|
|
5
|
+
sync path. Callers that need to execute plans live inside an async tool
|
|
6
|
+
and await execute_plan_steps_async.
|
|
6
7
|
|
|
7
8
|
Step backends:
|
|
8
|
-
remote_command — valid Remote Script handler, goes through TCP
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
remote_command — valid Remote Script handler, goes through the sync TCP
|
|
10
|
+
client (ableton.send_command)
|
|
11
|
+
bridge_command — M4L bridge handler, goes through the async UDP M4L bridge
|
|
12
|
+
client (bridge.send_command), NOT through ableton
|
|
13
|
+
mcp_tool — in-process Python function, dispatched via an mcp_registry
|
|
14
|
+
dict supplied by the server lifespan
|
|
15
|
+
unknown — not a valid target anywhere; returns a clear error
|
|
16
|
+
|
|
17
|
+
Step-result binding:
|
|
18
|
+
Any step may carry an optional step_id. Later steps may reference an
|
|
19
|
+
earlier result by setting a param to {"$from_step": "<id>", "path": "a.b"}.
|
|
20
|
+
Resolved recursively BEFORE dispatch.
|
|
12
21
|
"""
|
|
13
22
|
|
|
14
23
|
from __future__ import annotations
|
|
15
24
|
|
|
25
|
+
import inspect
|
|
16
26
|
from dataclasses import dataclass
|
|
17
|
-
from typing import Any, Optional
|
|
27
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
18
28
|
|
|
19
29
|
from .remote_commands import BRIDGE_COMMANDS, REMOTE_COMMANDS
|
|
20
30
|
|
|
21
31
|
|
|
22
32
|
# MCP-only tools that exist as Python functions but NOT as TCP handlers.
|
|
23
33
|
# These must be called through direct import, not ableton.send_command().
|
|
34
|
+
# NOTE: capture_audio is a BRIDGE command (livepilot_bridge.js:146), not MCP.
|
|
35
|
+
# It used to be duplicated here; removed to keep classification unambiguous.
|
|
24
36
|
MCP_TOOLS: frozenset[str] = frozenset({
|
|
25
37
|
"apply_automation_shape",
|
|
26
38
|
"apply_gesture_template",
|
|
27
39
|
"analyze_mix",
|
|
28
40
|
"get_master_spectrum",
|
|
29
41
|
"get_emotional_arc",
|
|
30
|
-
"capture_audio",
|
|
31
42
|
"get_motif_graph",
|
|
43
|
+
# Sample-engine workflow tools — async Python that orchestrates multiple
|
|
44
|
+
# sub-commands (search_browser + load_browser_item + bridge.replace_simpler_sample).
|
|
45
|
+
"load_sample_to_simpler",
|
|
46
|
+
# Device Forge tools (MCP-only, no TCP handler)
|
|
47
|
+
"generate_m4l_effect",
|
|
48
|
+
"install_m4l_device",
|
|
49
|
+
"list_genexpr_templates",
|
|
32
50
|
})
|
|
33
51
|
|
|
34
52
|
|
|
@@ -62,17 +80,75 @@ def classify_step(tool: str) -> str:
|
|
|
62
80
|
return "unknown"
|
|
63
81
|
|
|
64
82
|
|
|
65
|
-
|
|
83
|
+
# ── Step-result binding ─────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def _resolve_binding(binding: dict, step_results: dict) -> Any:
|
|
86
|
+
"""Resolve a {"$from_step": step_id, "path": "a.b.c"} binding.
|
|
87
|
+
|
|
88
|
+
Raises ValueError with a clear message on missing step_id or missing key.
|
|
89
|
+
"""
|
|
90
|
+
step_id = binding["$from_step"]
|
|
91
|
+
path = binding.get("path", "")
|
|
92
|
+
|
|
93
|
+
if step_id not in step_results:
|
|
94
|
+
available = sorted(step_results.keys())
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Step binding failed: step_id '{step_id}' not found. "
|
|
97
|
+
f"Available: {available or '(no earlier results)'}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
current = step_results[step_id]
|
|
101
|
+
if not isinstance(current, dict):
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Step binding failed: result of '{step_id}' is "
|
|
104
|
+
f"{type(current).__name__}, not a dict"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if not path:
|
|
108
|
+
return current
|
|
109
|
+
|
|
110
|
+
for segment in path.split("."):
|
|
111
|
+
if not isinstance(current, dict) or segment not in current:
|
|
112
|
+
keys = list(current.keys()) if isinstance(current, dict) else type(current).__name__
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"Step binding failed: path '{path}' not found in result of "
|
|
115
|
+
f"'{step_id}'. Available at this level: {keys}"
|
|
116
|
+
)
|
|
117
|
+
current = current[segment]
|
|
118
|
+
|
|
119
|
+
return current
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _resolve_params(params: Any, step_results: dict) -> Any:
|
|
123
|
+
"""Recursively walk params and resolve any $from_step bindings."""
|
|
124
|
+
if isinstance(params, dict):
|
|
125
|
+
if "$from_step" in params:
|
|
126
|
+
return _resolve_binding(params, step_results)
|
|
127
|
+
return {k: _resolve_params(v, step_results) for k, v in params.items()}
|
|
128
|
+
if isinstance(params, list):
|
|
129
|
+
return [_resolve_params(v, step_results) for v in params]
|
|
130
|
+
return params
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Async execution path ────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
async def _execute_step_async(
|
|
66
136
|
tool: str,
|
|
67
137
|
params: dict,
|
|
68
|
-
ableton: Any
|
|
69
|
-
|
|
70
|
-
|
|
138
|
+
ableton: Any,
|
|
139
|
+
bridge: Any,
|
|
140
|
+
mcp_registry: dict[str, Callable],
|
|
141
|
+
ctx: Any,
|
|
142
|
+
declared_backend: Optional[str] = None,
|
|
71
143
|
) -> ExecutionResult:
|
|
72
|
-
"""
|
|
73
|
-
backend =
|
|
144
|
+
"""Dispatch a single step through the correct transport, async-aware."""
|
|
145
|
+
backend = (
|
|
146
|
+
declared_backend
|
|
147
|
+
if declared_backend in ("remote_command", "bridge_command", "mcp_tool")
|
|
148
|
+
else classify_step(tool)
|
|
149
|
+
)
|
|
74
150
|
|
|
75
|
-
if backend
|
|
151
|
+
if backend == "remote_command":
|
|
76
152
|
if ableton is None:
|
|
77
153
|
return ExecutionResult(
|
|
78
154
|
ok=False, backend=backend, tool=tool,
|
|
@@ -80,45 +156,95 @@ def execute_step(
|
|
|
80
156
|
)
|
|
81
157
|
try:
|
|
82
158
|
result = ableton.send_command(tool, params)
|
|
159
|
+
if isinstance(result, dict) and "error" in result:
|
|
160
|
+
return ExecutionResult(ok=False, backend=backend, tool=tool, error=result["error"])
|
|
83
161
|
return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
|
|
84
162
|
except Exception as e:
|
|
85
163
|
return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
|
|
86
164
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
165
|
+
if backend == "bridge_command":
|
|
166
|
+
if bridge is None:
|
|
167
|
+
return ExecutionResult(
|
|
168
|
+
ok=False, backend=backend, tool=tool,
|
|
169
|
+
error="M4L bridge unavailable — cannot dispatch bridge command",
|
|
170
|
+
)
|
|
171
|
+
try:
|
|
172
|
+
# M4LBridge.send_command accepts (command, *args) and OSC-encodes
|
|
173
|
+
# each arg positionally. Plan authors construct params dicts in
|
|
174
|
+
# the order the bridge command expects; we unpack by insertion
|
|
175
|
+
# order (Python 3.7+ guarantees this). This keeps plans readable
|
|
176
|
+
# while matching the real bridge's positional wire format.
|
|
177
|
+
positional = list(params.values()) if params else []
|
|
178
|
+
call = bridge.send_command(tool, *positional)
|
|
179
|
+
result = await call if inspect.isawaitable(call) else call
|
|
180
|
+
if isinstance(result, dict) and "error" in result:
|
|
181
|
+
return ExecutionResult(ok=False, backend=backend, tool=tool, error=result["error"])
|
|
182
|
+
return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
|
|
96
185
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
186
|
+
if backend == "mcp_tool":
|
|
187
|
+
fn = mcp_registry.get(tool) if mcp_registry else None
|
|
188
|
+
if fn is None:
|
|
189
|
+
return ExecutionResult(
|
|
190
|
+
ok=False, backend=backend, tool=tool,
|
|
191
|
+
error=(
|
|
192
|
+
f"MCP tool '{tool}' not registered in async router dispatch map. "
|
|
193
|
+
f"Add it to mcp_server.runtime.mcp_dispatch.build_mcp_dispatch_registry()."
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
try:
|
|
197
|
+
sig = inspect.signature(fn)
|
|
198
|
+
kwargs = {"ctx": ctx} if "ctx" in sig.parameters else {}
|
|
199
|
+
call = fn(params, **kwargs)
|
|
200
|
+
result = await call if inspect.isawaitable(call) else call
|
|
201
|
+
if isinstance(result, dict) and "error" in result:
|
|
202
|
+
return ExecutionResult(ok=False, backend=backend, tool=tool, error=result["error"])
|
|
203
|
+
return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
|
|
206
|
+
|
|
207
|
+
return ExecutionResult(
|
|
208
|
+
ok=False, backend="unknown", tool=tool,
|
|
209
|
+
error=(
|
|
210
|
+
f"Unknown tool '{tool}' — not a Remote Script command, "
|
|
211
|
+
f"bridge command, or registered MCP tool"
|
|
212
|
+
),
|
|
213
|
+
)
|
|
103
214
|
|
|
104
215
|
|
|
105
|
-
def
|
|
216
|
+
async def execute_plan_steps_async(
|
|
106
217
|
steps: list[dict],
|
|
107
218
|
ableton: Any = None,
|
|
219
|
+
bridge: Any = None,
|
|
220
|
+
mcp_registry: Optional[dict[str, Callable]] = None,
|
|
108
221
|
ctx: Any = None,
|
|
109
222
|
stop_on_failure: bool = True,
|
|
110
223
|
) -> list[ExecutionResult]:
|
|
111
|
-
"""
|
|
224
|
+
"""Async plan executor with step-result binding and correct bridge transport.
|
|
112
225
|
|
|
113
|
-
|
|
114
|
-
|
|
226
|
+
Supports three backends:
|
|
227
|
+
- remote_command via ableton.send_command (sync TCP client)
|
|
228
|
+
- bridge_command via bridge.send_command (async UDP M4L bridge client)
|
|
229
|
+
- mcp_tool via mcp_registry[tool](params, ctx=ctx)
|
|
230
|
+
|
|
231
|
+
Step-result binding:
|
|
232
|
+
Any step may carry an optional "step_id". Later steps may reference an
|
|
233
|
+
earlier result by setting a param to {"$from_step": "<id>", "path": "a.b"}.
|
|
234
|
+
The router walks params recursively and resolves bindings before dispatch.
|
|
235
|
+
Missing ids or missing paths fail that step with a clear error.
|
|
236
|
+
|
|
237
|
+
stop_on_failure: Stop the plan on the first failing step (default). Set to
|
|
238
|
+
False for best-effort execution (each result still recorded).
|
|
115
239
|
"""
|
|
116
240
|
results: list[ExecutionResult] = []
|
|
241
|
+
step_results: dict[str, Any] = {}
|
|
242
|
+
mcp_registry = mcp_registry or {}
|
|
117
243
|
|
|
118
244
|
for step in steps:
|
|
119
245
|
tool = step.get("tool") or step.get("command", "")
|
|
120
|
-
|
|
121
|
-
|
|
246
|
+
raw_params = step.get("params") or step.get("args", {}) or {}
|
|
247
|
+
step_id = step.get("step_id")
|
|
122
248
|
declared_backend = step.get("backend")
|
|
123
249
|
|
|
124
250
|
if not tool:
|
|
@@ -130,9 +256,29 @@ def execute_plan_steps(
|
|
|
130
256
|
break
|
|
131
257
|
continue
|
|
132
258
|
|
|
133
|
-
|
|
259
|
+
# Resolve any $from_step bindings in params BEFORE dispatch.
|
|
260
|
+
try:
|
|
261
|
+
params = _resolve_params(raw_params, step_results)
|
|
262
|
+
except ValueError as e:
|
|
263
|
+
results.append(ExecutionResult(
|
|
264
|
+
ok=False, backend="binding", tool=tool, error=str(e),
|
|
265
|
+
))
|
|
266
|
+
if stop_on_failure:
|
|
267
|
+
break
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
result = await _execute_step_async(
|
|
271
|
+
tool, params,
|
|
272
|
+
ableton=ableton, bridge=bridge,
|
|
273
|
+
mcp_registry=mcp_registry, ctx=ctx,
|
|
274
|
+
declared_backend=declared_backend,
|
|
275
|
+
)
|
|
134
276
|
results.append(result)
|
|
135
277
|
|
|
278
|
+
# Record successful step result for future bindings
|
|
279
|
+
if result.ok and step_id and isinstance(result.result, dict):
|
|
280
|
+
step_results[step_id] = result.result
|
|
281
|
+
|
|
136
282
|
if not result.ok and stop_on_failure:
|
|
137
283
|
break
|
|
138
284
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""MCP-side Live version capabilities model.
|
|
2
|
+
|
|
3
|
+
Pure data model — no I/O. Parses version info from get_session_info
|
|
4
|
+
responses and exposes feature flags for tool routing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class LiveVersionCapabilities:
|
|
14
|
+
"""Feature availability based on detected Live version."""
|
|
15
|
+
|
|
16
|
+
major: int = 12
|
|
17
|
+
minor: int = 0
|
|
18
|
+
patch: int = 0
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_version_string(cls, version_str: str) -> LiveVersionCapabilities:
|
|
22
|
+
"""Parse '12.3.6' into a capabilities instance."""
|
|
23
|
+
parts = version_str.split(".")
|
|
24
|
+
major = int(parts[0]) if len(parts) > 0 else 12
|
|
25
|
+
minor = int(parts[1]) if len(parts) > 1 else 0
|
|
26
|
+
patch = int(parts[2]) if len(parts) > 2 else 0
|
|
27
|
+
return cls(major=major, minor=minor, patch=patch)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_session_info(cls, session_info: dict) -> LiveVersionCapabilities:
|
|
31
|
+
"""Extract version from get_session_info response.
|
|
32
|
+
|
|
33
|
+
Looks for 'live_version' field. Falls back to 12.0.0 if absent
|
|
34
|
+
(pre-upgrade Remote Script).
|
|
35
|
+
"""
|
|
36
|
+
version_str = session_info.get("live_version", "12.0.0")
|
|
37
|
+
return cls.from_version_string(version_str)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def _version_tuple(self) -> tuple[int, int, int]:
|
|
41
|
+
return (self.major, self.minor, self.patch)
|
|
42
|
+
|
|
43
|
+
# ── Feature flags ──────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def has_native_arrangement_clips(self) -> bool:
|
|
47
|
+
"""Track.create_midi_clip(start, length) — 12.1.10+"""
|
|
48
|
+
return self._version_tuple >= (12, 1, 10)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def has_display_value(self) -> bool:
|
|
52
|
+
"""DeviceParameter.display_value — 12.2+"""
|
|
53
|
+
return self._version_tuple >= (12, 2, 0)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def has_insert_device(self) -> bool:
|
|
57
|
+
"""Track.insert_device(name, index?) — 12.3+"""
|
|
58
|
+
return self._version_tuple >= (12, 3, 0)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def has_drum_rack_construction(self) -> bool:
|
|
62
|
+
"""insert_chain + DrumChain.in_note — 12.3+"""
|
|
63
|
+
return self._version_tuple >= (12, 3, 0)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def has_take_lanes(self) -> bool:
|
|
67
|
+
"""Take Lanes API — 12.2+"""
|
|
68
|
+
return self._version_tuple >= (12, 2, 0)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def has_stem_separation(self) -> bool:
|
|
72
|
+
"""Stem separation via MFL — 12.3+"""
|
|
73
|
+
return self._version_tuple >= (12, 3, 0)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def has_replace_sample_native(self) -> bool:
|
|
77
|
+
"""SimplerDevice.replace_sample(path) — 12.4+"""
|
|
78
|
+
return self._version_tuple >= (12, 4, 0)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def capability_tier(self) -> str:
|
|
82
|
+
"""Human-readable tier: core | enhanced_arrangement | full_intelligence."""
|
|
83
|
+
if self._version_tuple >= (12, 3, 0):
|
|
84
|
+
return "full_intelligence"
|
|
85
|
+
elif self._version_tuple >= (12, 1, 10):
|
|
86
|
+
return "enhanced_arrangement"
|
|
87
|
+
else:
|
|
88
|
+
return "core"
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict:
|
|
91
|
+
"""Serialize for API responses and capability probes."""
|
|
92
|
+
return {
|
|
93
|
+
"version": f"{self.major}.{self.minor}.{self.patch}",
|
|
94
|
+
"capability_tier": self.capability_tier,
|
|
95
|
+
"native_arrangement_clips": self.has_native_arrangement_clips,
|
|
96
|
+
"display_value": self.has_display_value,
|
|
97
|
+
"insert_device": self.has_insert_device,
|
|
98
|
+
"drum_rack_construction": self.has_drum_rack_construction,
|
|
99
|
+
"take_lanes": self.has_take_lanes,
|
|
100
|
+
"stem_separation": self.has_stem_separation,
|
|
101
|
+
"replace_sample_native": self.has_replace_sample_native,
|
|
102
|
+
}
|