livepilot 1.10.9 → 1.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +245 -0
- package/README.md +7 -7
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/m4l_bridge.py +488 -13
- package/mcp_server/runtime/execution_router.py +7 -0
- package/mcp_server/runtime/mcp_dispatch.py +32 -0
- package/mcp_server/runtime/remote_commands.py +54 -0
- package/mcp_server/sample_engine/slice_classifier.py +169 -0
- package/mcp_server/server.py +11 -3
- package/mcp_server/tools/analyzer.py +187 -7
- package/mcp_server/tools/clips.py +65 -0
- package/mcp_server/tools/devices.py +517 -5
- package/mcp_server/tools/diagnostics.py +42 -0
- package/mcp_server/tools/follow_actions.py +202 -0
- package/mcp_server/tools/grooves.py +142 -0
- package/mcp_server/tools/miditool.py +280 -0
- package/mcp_server/tools/scales.py +126 -0
- package/mcp_server/tools/take_lanes.py +135 -0
- package/mcp_server/tools/tracks.py +46 -3
- package/mcp_server/tools/transport.py +62 -1
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +8 -4
- package/remote_script/LivePilot/clips.py +62 -0
- package/remote_script/LivePilot/devices.py +444 -0
- package/remote_script/LivePilot/diagnostics.py +52 -1
- package/remote_script/LivePilot/follow_actions.py +235 -0
- package/remote_script/LivePilot/grooves.py +185 -0
- package/remote_script/LivePilot/scales.py +138 -0
- package/remote_script/LivePilot/take_lanes.py +175 -0
- package/remote_script/LivePilot/tracks.py +59 -1
- package/remote_script/LivePilot/transport.py +90 -1
- package/remote_script/LivePilot/version_detect.py +9 -0
- package/server.json +3 -3
|
@@ -289,6 +289,52 @@ def set_device_parameter(
|
|
|
289
289
|
return _get_ableton(ctx).send_command("set_device_parameter", params)
|
|
290
290
|
|
|
291
291
|
|
|
292
|
+
def _normalize_batch_entry(entry: dict) -> dict:
|
|
293
|
+
"""Accept either the legacy 'name_or_index' shape or the aligned
|
|
294
|
+
'parameter_index' / 'parameter_name' shape used by set_device_parameter.
|
|
295
|
+
|
|
296
|
+
BUG-F4: the sibling tools had inconsistent schemas. Callers writing
|
|
297
|
+
code against set_device_parameter hit validation errors switching
|
|
298
|
+
to batch_set_parameters. Now both shapes are accepted and
|
|
299
|
+
normalized to the Remote Script's expected {name_or_index, value}.
|
|
300
|
+
"""
|
|
301
|
+
if "value" not in entry:
|
|
302
|
+
raise ValueError("Each parameter entry must include 'value'")
|
|
303
|
+
|
|
304
|
+
has_legacy = "name_or_index" in entry
|
|
305
|
+
has_index = "parameter_index" in entry
|
|
306
|
+
has_name = "parameter_name" in entry
|
|
307
|
+
|
|
308
|
+
specified = sum([has_legacy, has_index, has_name])
|
|
309
|
+
if specified == 0:
|
|
310
|
+
raise ValueError(
|
|
311
|
+
"Each parameter entry must include exactly one of: "
|
|
312
|
+
"parameter_name, parameter_index, or name_or_index (legacy)"
|
|
313
|
+
)
|
|
314
|
+
if specified > 1:
|
|
315
|
+
raise ValueError(
|
|
316
|
+
"Each parameter entry must include exactly one of "
|
|
317
|
+
"parameter_name, parameter_index, or name_or_index — not multiple"
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if has_legacy:
|
|
321
|
+
key = entry["name_or_index"]
|
|
322
|
+
elif has_index:
|
|
323
|
+
key = entry["parameter_index"]
|
|
324
|
+
else:
|
|
325
|
+
key = entry["parameter_name"]
|
|
326
|
+
|
|
327
|
+
# BUG-audit-H3: match set_device_parameter's validation so negative
|
|
328
|
+
# indices are rejected at the MCP layer rather than leaking through to
|
|
329
|
+
# the Remote Script as unstructured IndexError.
|
|
330
|
+
if isinstance(key, int) and key < 0:
|
|
331
|
+
raise ValueError(
|
|
332
|
+
"parameter_index must be >= 0 (got {})".format(key)
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
return {"name_or_index": key, "value": entry["value"]}
|
|
336
|
+
|
|
337
|
+
|
|
292
338
|
@mcp.tool()
|
|
293
339
|
def batch_set_parameters(
|
|
294
340
|
ctx: Context,
|
|
@@ -296,20 +342,24 @@ def batch_set_parameters(
|
|
|
296
342
|
device_index: int,
|
|
297
343
|
parameters: Any,
|
|
298
344
|
) -> dict:
|
|
299
|
-
"""Set multiple device parameters in one call.
|
|
345
|
+
"""Set multiple device parameters in one call.
|
|
346
|
+
|
|
347
|
+
parameters: JSON array of objects. Each entry uses exactly one of:
|
|
348
|
+
- {"parameter_index": N, "value": V} (preferred, aligned with set_device_parameter)
|
|
349
|
+
- {"parameter_name": "Dry/Wet", "value": V} (preferred)
|
|
350
|
+
- {"name_or_index": X, "value": V} (legacy, still accepted)
|
|
351
|
+
|
|
300
352
|
track_index: 0+ for regular tracks, -1/-2/... for return tracks (A/B/...), -1000 for master."""
|
|
301
353
|
_validate_track_index(track_index)
|
|
302
354
|
_validate_device_index(device_index)
|
|
303
355
|
parameters = _ensure_list(parameters)
|
|
304
356
|
if not parameters:
|
|
305
357
|
raise ValueError("parameters list cannot be empty")
|
|
306
|
-
for
|
|
307
|
-
if "name_or_index" not in entry or "value" not in entry:
|
|
308
|
-
raise ValueError("Each parameter must have 'name_or_index' and 'value'")
|
|
358
|
+
normalized = [_normalize_batch_entry(e) for e in parameters]
|
|
309
359
|
return _get_ableton(ctx).send_command("batch_set_parameters", {
|
|
310
360
|
"track_index": track_index,
|
|
311
361
|
"device_index": device_index,
|
|
312
|
-
"parameters":
|
|
362
|
+
"parameters": normalized,
|
|
313
363
|
})
|
|
314
364
|
|
|
315
365
|
|
|
@@ -719,3 +769,465 @@ async def get_plugin_presets(
|
|
|
719
769
|
_require_analyzer(cache)
|
|
720
770
|
bridge = _get_m4l(ctx)
|
|
721
771
|
return await bridge.send_command("get_plugin_presets", track_index, device_index, timeout=15.0)
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
# ── Rack Variations + Macro CRUD (Live 11+) ─────────────────────────────
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
@mcp.tool()
|
|
778
|
+
def get_rack_variations(ctx: Context, track_index: int, device_index: int) -> dict:
|
|
779
|
+
"""Get the Rack's variation count, currently selected variation index, and visible macro count (Live 11+).
|
|
780
|
+
|
|
781
|
+
Variations are macro snapshots — store a scene of macro values, recall later.
|
|
782
|
+
Returns {count, selected_index, visible_macro_count}. selected_index may be -1
|
|
783
|
+
if no variation is currently selected. Errors if the device is not a Rack
|
|
784
|
+
(Instrument/Audio Effect/Drum Rack).
|
|
785
|
+
"""
|
|
786
|
+
_validate_track_index(track_index)
|
|
787
|
+
_validate_device_index(device_index)
|
|
788
|
+
return _get_ableton(ctx).send_command("get_rack_variations", {
|
|
789
|
+
"track_index": track_index,
|
|
790
|
+
"device_index": device_index,
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@mcp.tool()
|
|
795
|
+
def store_rack_variation(ctx: Context, track_index: int, device_index: int) -> dict:
|
|
796
|
+
"""Store the Rack's current macro values as a new variation (Live 11+).
|
|
797
|
+
|
|
798
|
+
Appends a new variation at the end of the list. Returns the new total
|
|
799
|
+
{count, new_index} where new_index = count - 1.
|
|
800
|
+
"""
|
|
801
|
+
_validate_track_index(track_index)
|
|
802
|
+
_validate_device_index(device_index)
|
|
803
|
+
return _get_ableton(ctx).send_command("store_rack_variation", {
|
|
804
|
+
"track_index": track_index,
|
|
805
|
+
"device_index": device_index,
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@mcp.tool()
|
|
810
|
+
def recall_rack_variation(
|
|
811
|
+
ctx: Context,
|
|
812
|
+
track_index: int,
|
|
813
|
+
device_index: int,
|
|
814
|
+
variation_index: int,
|
|
815
|
+
) -> dict:
|
|
816
|
+
"""Select and recall a stored Rack variation by index (Live 11+).
|
|
817
|
+
|
|
818
|
+
Sets selected_variation_index then calls recall_selected_variation(),
|
|
819
|
+
immediately pushing the stored macro values to the live Rack.
|
|
820
|
+
Returns {selected_index}.
|
|
821
|
+
"""
|
|
822
|
+
_validate_track_index(track_index)
|
|
823
|
+
_validate_device_index(device_index)
|
|
824
|
+
if variation_index < 0:
|
|
825
|
+
raise ValueError("variation_index must be >= 0")
|
|
826
|
+
return _get_ableton(ctx).send_command("recall_rack_variation", {
|
|
827
|
+
"track_index": track_index,
|
|
828
|
+
"device_index": device_index,
|
|
829
|
+
"variation_index": variation_index,
|
|
830
|
+
})
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
@mcp.tool()
|
|
834
|
+
def delete_rack_variation(
|
|
835
|
+
ctx: Context,
|
|
836
|
+
track_index: int,
|
|
837
|
+
device_index: int,
|
|
838
|
+
variation_index: int,
|
|
839
|
+
) -> dict:
|
|
840
|
+
"""Delete a Rack variation by index (Live 11+).
|
|
841
|
+
|
|
842
|
+
Selects the given index first then deletes it. Returns the new {count}
|
|
843
|
+
after removal.
|
|
844
|
+
"""
|
|
845
|
+
_validate_track_index(track_index)
|
|
846
|
+
_validate_device_index(device_index)
|
|
847
|
+
if variation_index < 0:
|
|
848
|
+
raise ValueError("variation_index must be >= 0")
|
|
849
|
+
return _get_ableton(ctx).send_command("delete_rack_variation", {
|
|
850
|
+
"track_index": track_index,
|
|
851
|
+
"device_index": device_index,
|
|
852
|
+
"variation_index": variation_index,
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
@mcp.tool()
|
|
857
|
+
def randomize_rack_macros(ctx: Context, track_index: int, device_index: int) -> dict:
|
|
858
|
+
"""Randomize the Rack's macro values using Live's built-in randomize dice (Live 11+).
|
|
859
|
+
|
|
860
|
+
Does not store a variation — just scrambles the current macros. Combine
|
|
861
|
+
with store_rack_variation to snapshot the random state.
|
|
862
|
+
"""
|
|
863
|
+
_validate_track_index(track_index)
|
|
864
|
+
_validate_device_index(device_index)
|
|
865
|
+
return _get_ableton(ctx).send_command("randomize_rack_macros", {
|
|
866
|
+
"track_index": track_index,
|
|
867
|
+
"device_index": device_index,
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
@mcp.tool()
|
|
872
|
+
def add_rack_macro(ctx: Context, track_index: int, device_index: int) -> dict:
|
|
873
|
+
"""Add one macro to a Rack, raising visible_macro_count by 1 (Live 11+).
|
|
874
|
+
|
|
875
|
+
Maxes at 16 macros. Returns the new {visible_macro_count}.
|
|
876
|
+
"""
|
|
877
|
+
_validate_track_index(track_index)
|
|
878
|
+
_validate_device_index(device_index)
|
|
879
|
+
return _get_ableton(ctx).send_command("add_rack_macro", {
|
|
880
|
+
"track_index": track_index,
|
|
881
|
+
"device_index": device_index,
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@mcp.tool()
|
|
886
|
+
def remove_rack_macro(ctx: Context, track_index: int, device_index: int) -> dict:
|
|
887
|
+
"""Remove the last macro from a Rack, lowering visible_macro_count by 1 (Live 11+).
|
|
888
|
+
|
|
889
|
+
Minimum is 1 macro. Returns the new {visible_macro_count}.
|
|
890
|
+
"""
|
|
891
|
+
_validate_track_index(track_index)
|
|
892
|
+
_validate_device_index(device_index)
|
|
893
|
+
return _get_ableton(ctx).send_command("remove_rack_macro", {
|
|
894
|
+
"track_index": track_index,
|
|
895
|
+
"device_index": device_index,
|
|
896
|
+
})
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
@mcp.tool()
|
|
900
|
+
def set_rack_visible_macros(
|
|
901
|
+
ctx: Context,
|
|
902
|
+
track_index: int,
|
|
903
|
+
device_index: int,
|
|
904
|
+
count: int,
|
|
905
|
+
) -> dict:
|
|
906
|
+
"""Set the Rack's visible_macro_count directly (1-16, Live 11+).
|
|
907
|
+
|
|
908
|
+
Faster than calling add_rack_macro/remove_rack_macro repeatedly to reach
|
|
909
|
+
a target count. Returns the new {visible_macro_count}.
|
|
910
|
+
"""
|
|
911
|
+
_validate_track_index(track_index)
|
|
912
|
+
_validate_device_index(device_index)
|
|
913
|
+
if not 1 <= count <= 16:
|
|
914
|
+
raise ValueError("count must be 1-16")
|
|
915
|
+
return _get_ableton(ctx).send_command("set_rack_visible_macros", {
|
|
916
|
+
"track_index": track_index,
|
|
917
|
+
"device_index": device_index,
|
|
918
|
+
"count": count,
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
# ── Simpler Slice CRUD (Live 11+) ───────────────────────────────────────
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
@mcp.tool()
|
|
926
|
+
def insert_simpler_slice(
|
|
927
|
+
ctx: Context,
|
|
928
|
+
track_index: int,
|
|
929
|
+
device_index: int,
|
|
930
|
+
time_samples: int,
|
|
931
|
+
) -> dict:
|
|
932
|
+
"""Insert a slice at a sample-frame position on a Simpler (Live 11+).
|
|
933
|
+
|
|
934
|
+
time_samples is in raw sample frames (NOT beats, NOT seconds). Call
|
|
935
|
+
get_simpler_slices first to see existing slice positions. The Simpler
|
|
936
|
+
must be in Slice playback mode for slices to matter musically, but this
|
|
937
|
+
tool does not force that — errors only if the device is not a Simpler
|
|
938
|
+
or has no sample loaded.
|
|
939
|
+
|
|
940
|
+
Returns {slice_count} after insertion.
|
|
941
|
+
"""
|
|
942
|
+
_validate_track_index(track_index)
|
|
943
|
+
_validate_device_index(device_index)
|
|
944
|
+
if time_samples < 0:
|
|
945
|
+
raise ValueError("time_samples must be >= 0")
|
|
946
|
+
return _get_ableton(ctx).send_command("insert_simpler_slice", {
|
|
947
|
+
"track_index": track_index,
|
|
948
|
+
"device_index": device_index,
|
|
949
|
+
"time_samples": time_samples,
|
|
950
|
+
})
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
@mcp.tool()
|
|
954
|
+
def move_simpler_slice(
|
|
955
|
+
ctx: Context,
|
|
956
|
+
track_index: int,
|
|
957
|
+
device_index: int,
|
|
958
|
+
old_time_samples: int,
|
|
959
|
+
new_time_samples: int,
|
|
960
|
+
) -> dict:
|
|
961
|
+
"""Move an existing slice from one sample-frame position to another (Live 11+).
|
|
962
|
+
|
|
963
|
+
Both values are in raw sample frames. old_time_samples must match an
|
|
964
|
+
existing slice exactly — use get_simpler_slices to read current
|
|
965
|
+
positions. Returns {ok, old_time_samples, new_time_samples}.
|
|
966
|
+
"""
|
|
967
|
+
_validate_track_index(track_index)
|
|
968
|
+
_validate_device_index(device_index)
|
|
969
|
+
if old_time_samples < 0 or new_time_samples < 0:
|
|
970
|
+
raise ValueError("time values must be >= 0")
|
|
971
|
+
return _get_ableton(ctx).send_command("move_simpler_slice", {
|
|
972
|
+
"track_index": track_index,
|
|
973
|
+
"device_index": device_index,
|
|
974
|
+
"old_time_samples": old_time_samples,
|
|
975
|
+
"new_time_samples": new_time_samples,
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
@mcp.tool()
|
|
980
|
+
def remove_simpler_slice(
|
|
981
|
+
ctx: Context,
|
|
982
|
+
track_index: int,
|
|
983
|
+
device_index: int,
|
|
984
|
+
time_samples: int,
|
|
985
|
+
) -> dict:
|
|
986
|
+
"""Remove a slice at an exact sample-frame position (Live 11+).
|
|
987
|
+
|
|
988
|
+
time_samples must EXACTLY match an existing slice position. Read current
|
|
989
|
+
positions with get_simpler_slices first. Returns {slice_count} after
|
|
990
|
+
removal.
|
|
991
|
+
"""
|
|
992
|
+
_validate_track_index(track_index)
|
|
993
|
+
_validate_device_index(device_index)
|
|
994
|
+
if time_samples < 0:
|
|
995
|
+
raise ValueError("time_samples must be >= 0")
|
|
996
|
+
return _get_ableton(ctx).send_command("remove_simpler_slice", {
|
|
997
|
+
"track_index": track_index,
|
|
998
|
+
"device_index": device_index,
|
|
999
|
+
"time_samples": time_samples,
|
|
1000
|
+
})
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
@mcp.tool()
|
|
1004
|
+
def clear_simpler_slices(
|
|
1005
|
+
ctx: Context,
|
|
1006
|
+
track_index: int,
|
|
1007
|
+
device_index: int,
|
|
1008
|
+
) -> dict:
|
|
1009
|
+
"""Remove all manual slices from the Simpler (Live 11+).
|
|
1010
|
+
|
|
1011
|
+
Clears the slice list outright. Combine with reset_simpler_slices or
|
|
1012
|
+
import_slices_from_onsets to regenerate. Returns {slice_count: 0}.
|
|
1013
|
+
"""
|
|
1014
|
+
_validate_track_index(track_index)
|
|
1015
|
+
_validate_device_index(device_index)
|
|
1016
|
+
return _get_ableton(ctx).send_command("clear_simpler_slices", {
|
|
1017
|
+
"track_index": track_index,
|
|
1018
|
+
"device_index": device_index,
|
|
1019
|
+
})
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@mcp.tool()
|
|
1023
|
+
def reset_simpler_slices(
|
|
1024
|
+
ctx: Context,
|
|
1025
|
+
track_index: int,
|
|
1026
|
+
device_index: int,
|
|
1027
|
+
) -> dict:
|
|
1028
|
+
"""Reset the Simpler's slices to Live's default detection (Live 11+).
|
|
1029
|
+
|
|
1030
|
+
Re-runs detection under the CURRENT slicing_style and sensitivity. Use
|
|
1031
|
+
import_slices_from_onsets instead if you want to force Transient mode
|
|
1032
|
+
AND set sensitivity in one call. Returns the resulting {slice_count}.
|
|
1033
|
+
"""
|
|
1034
|
+
_validate_track_index(track_index)
|
|
1035
|
+
_validate_device_index(device_index)
|
|
1036
|
+
return _get_ableton(ctx).send_command("reset_simpler_slices", {
|
|
1037
|
+
"track_index": track_index,
|
|
1038
|
+
"device_index": device_index,
|
|
1039
|
+
})
|
|
1040
|
+
|
|
1041
|
+
|
|
1042
|
+
@mcp.tool()
|
|
1043
|
+
def import_slices_from_onsets(
|
|
1044
|
+
ctx: Context,
|
|
1045
|
+
track_index: int,
|
|
1046
|
+
device_index: int,
|
|
1047
|
+
sensitivity: float = 0.5,
|
|
1048
|
+
) -> dict:
|
|
1049
|
+
"""Force Transient slicing mode, set sensitivity, and re-detect (Live 11+).
|
|
1050
|
+
|
|
1051
|
+
Writes slicing_style=Transient and slicing_sensitivity, then calls
|
|
1052
|
+
reset_slices(). sensitivity must be 0.0-1.0; 0.5 is moderate, higher
|
|
1053
|
+
produces more slices. Returns {slice_count, sensitivity}.
|
|
1054
|
+
"""
|
|
1055
|
+
_validate_track_index(track_index)
|
|
1056
|
+
_validate_device_index(device_index)
|
|
1057
|
+
if not 0.0 <= sensitivity <= 1.0:
|
|
1058
|
+
raise ValueError("sensitivity must be 0.0-1.0")
|
|
1059
|
+
return _get_ableton(ctx).send_command("import_slices_from_onsets", {
|
|
1060
|
+
"track_index": track_index,
|
|
1061
|
+
"device_index": device_index,
|
|
1062
|
+
"sensitivity": sensitivity,
|
|
1063
|
+
})
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
# ── Wavetable modulation matrix (Live 11+) ──────────────────────────────
|
|
1067
|
+
|
|
1068
|
+
|
|
1069
|
+
@mcp.tool()
|
|
1070
|
+
def get_wavetable_mod_targets(
|
|
1071
|
+
ctx: Context,
|
|
1072
|
+
track_index: int,
|
|
1073
|
+
device_index: int,
|
|
1074
|
+
) -> dict:
|
|
1075
|
+
"""Enumerate visible modulation target parameter names on a Wavetable (Live 11+).
|
|
1076
|
+
|
|
1077
|
+
Returns {targets: [...]} — the list depends on the current patch
|
|
1078
|
+
configuration (e.g. which oscillators are active). Feed these strings
|
|
1079
|
+
into add_wavetable_mod_route / set_wavetable_mod_amount as the target
|
|
1080
|
+
argument.
|
|
1081
|
+
"""
|
|
1082
|
+
_validate_track_index(track_index)
|
|
1083
|
+
_validate_device_index(device_index)
|
|
1084
|
+
return _get_ableton(ctx).send_command("get_wavetable_mod_targets", {
|
|
1085
|
+
"track_index": track_index,
|
|
1086
|
+
"device_index": device_index,
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
@mcp.tool()
|
|
1091
|
+
def add_wavetable_mod_route(
|
|
1092
|
+
ctx: Context,
|
|
1093
|
+
track_index: int,
|
|
1094
|
+
device_index: int,
|
|
1095
|
+
source: str,
|
|
1096
|
+
target: str,
|
|
1097
|
+
) -> dict:
|
|
1098
|
+
"""Create a modulation routing on a Wavetable device (Live 11+).
|
|
1099
|
+
|
|
1100
|
+
source must be one of: "Env 2", "Env 3", "LFO 1", "LFO 2",
|
|
1101
|
+
"MIDI Key", "MIDI Velocity", "MIDI Aftertouch", "MIDI Pitchbend",
|
|
1102
|
+
"Macro 1".."Macro 8". target must be a name from
|
|
1103
|
+
get_wavetable_mod_targets — valid targets depend on the current patch.
|
|
1104
|
+
Returns {source, target, actual_target} where actual_target is the
|
|
1105
|
+
parameter name Live resolved the routing to.
|
|
1106
|
+
"""
|
|
1107
|
+
_validate_track_index(track_index)
|
|
1108
|
+
_validate_device_index(device_index)
|
|
1109
|
+
return _get_ableton(ctx).send_command("add_wavetable_mod_route", {
|
|
1110
|
+
"track_index": track_index,
|
|
1111
|
+
"device_index": device_index,
|
|
1112
|
+
"source": source,
|
|
1113
|
+
"target": target,
|
|
1114
|
+
})
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
@mcp.tool()
|
|
1118
|
+
def set_wavetable_mod_amount(
|
|
1119
|
+
ctx: Context,
|
|
1120
|
+
track_index: int,
|
|
1121
|
+
device_index: int,
|
|
1122
|
+
source: str,
|
|
1123
|
+
target: str,
|
|
1124
|
+
amount: float,
|
|
1125
|
+
) -> dict:
|
|
1126
|
+
"""Set the modulation amount for a Wavetable source→target routing (Live 11+).
|
|
1127
|
+
|
|
1128
|
+
amount is bipolar: -1.0 to 1.0. 0.0 effectively disables the routing.
|
|
1129
|
+
source and target use the same names documented on
|
|
1130
|
+
add_wavetable_mod_route. Returns {source, target, amount}.
|
|
1131
|
+
"""
|
|
1132
|
+
_validate_track_index(track_index)
|
|
1133
|
+
_validate_device_index(device_index)
|
|
1134
|
+
if not -1.0 <= amount <= 1.0:
|
|
1135
|
+
raise ValueError("amount must be -1.0 to 1.0")
|
|
1136
|
+
return _get_ableton(ctx).send_command("set_wavetable_mod_amount", {
|
|
1137
|
+
"track_index": track_index,
|
|
1138
|
+
"device_index": device_index,
|
|
1139
|
+
"source": source,
|
|
1140
|
+
"target": target,
|
|
1141
|
+
"amount": amount,
|
|
1142
|
+
})
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
@mcp.tool()
|
|
1146
|
+
def get_wavetable_mod_amount(
|
|
1147
|
+
ctx: Context,
|
|
1148
|
+
track_index: int,
|
|
1149
|
+
device_index: int,
|
|
1150
|
+
source: str,
|
|
1151
|
+
target: str,
|
|
1152
|
+
) -> dict:
|
|
1153
|
+
"""Read the current modulation amount for a Wavetable source→target routing (Live 11+).
|
|
1154
|
+
|
|
1155
|
+
Returns {source, target, amount, actual_target}. amount is -1.0 to 1.0.
|
|
1156
|
+
actual_target is the parameter name Live resolved the routing to — use
|
|
1157
|
+
it to confirm the routing went where you expected.
|
|
1158
|
+
"""
|
|
1159
|
+
_validate_track_index(track_index)
|
|
1160
|
+
_validate_device_index(device_index)
|
|
1161
|
+
return _get_ableton(ctx).send_command("get_wavetable_mod_amount", {
|
|
1162
|
+
"track_index": track_index,
|
|
1163
|
+
"device_index": device_index,
|
|
1164
|
+
"source": source,
|
|
1165
|
+
"target": target,
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
|
|
1169
|
+
@mcp.tool()
|
|
1170
|
+
def get_wavetable_mod_matrix(
|
|
1171
|
+
ctx: Context,
|
|
1172
|
+
track_index: int,
|
|
1173
|
+
device_index: int,
|
|
1174
|
+
) -> dict:
|
|
1175
|
+
"""Dump all non-zero modulation routings on a Wavetable device (Live 11+).
|
|
1176
|
+
|
|
1177
|
+
Iterates every source × visible target and returns any routing with a
|
|
1178
|
+
non-zero amount. O(sources × targets) but safe — useful to audit a
|
|
1179
|
+
patch or snapshot its modulation state. Returns
|
|
1180
|
+
{routings: [{source, target, amount}, ...]}.
|
|
1181
|
+
"""
|
|
1182
|
+
_validate_track_index(track_index)
|
|
1183
|
+
_validate_device_index(device_index)
|
|
1184
|
+
return _get_ableton(ctx).send_command("get_wavetable_mod_matrix", {
|
|
1185
|
+
"track_index": track_index,
|
|
1186
|
+
"device_index": device_index,
|
|
1187
|
+
})
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
# ── Device A/B compare (Live 12.3+) ─────────────────────────────────────
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
@mcp.tool()
|
|
1194
|
+
def get_device_ab_state(ctx: Context, track_index: int, device_index: int) -> dict:
|
|
1195
|
+
"""Read a device's A/B compare state (Live 12.3+).
|
|
1196
|
+
|
|
1197
|
+
Returns current_state ('A'|'B'|'unknown') and has_b (bool).
|
|
1198
|
+
If the LOM doesn't expose A/B attributes, returns 'unknown' with
|
|
1199
|
+
a 'note' field explaining the limitation.
|
|
1200
|
+
"""
|
|
1201
|
+
_validate_track_index(track_index)
|
|
1202
|
+
_validate_device_index(device_index)
|
|
1203
|
+
return _get_ableton(ctx).send_command("get_device_ab_state",
|
|
1204
|
+
{"track_index": track_index, "device_index": device_index})
|
|
1205
|
+
|
|
1206
|
+
|
|
1207
|
+
@mcp.tool()
|
|
1208
|
+
def toggle_device_ab(ctx: Context, track_index: int, device_index: int) -> dict:
|
|
1209
|
+
"""Swap a device's A/B state (Live 12.3+)."""
|
|
1210
|
+
_validate_track_index(track_index)
|
|
1211
|
+
_validate_device_index(device_index)
|
|
1212
|
+
return _get_ableton(ctx).send_command("toggle_device_ab",
|
|
1213
|
+
{"track_index": track_index, "device_index": device_index})
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
@mcp.tool()
|
|
1217
|
+
def copy_device_state(
|
|
1218
|
+
ctx: Context,
|
|
1219
|
+
track_index: int,
|
|
1220
|
+
device_index: int,
|
|
1221
|
+
direction: str,
|
|
1222
|
+
) -> dict:
|
|
1223
|
+
"""Copy one A/B state to the other (Live 12.3+).
|
|
1224
|
+
|
|
1225
|
+
direction: 'a_to_b' or 'b_to_a'.
|
|
1226
|
+
"""
|
|
1227
|
+
_validate_track_index(track_index)
|
|
1228
|
+
_validate_device_index(device_index)
|
|
1229
|
+
if direction not in ("a_to_b", "b_to_a"):
|
|
1230
|
+
raise ValueError("direction must be 'a_to_b' or 'b_to_a'")
|
|
1231
|
+
return _get_ableton(ctx).send_command("copy_device_state",
|
|
1232
|
+
{"track_index": track_index, "device_index": device_index,
|
|
1233
|
+
"direction": direction})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Diagnostic MCP tools — read-only inspection of the Live environment.
|
|
2
|
+
|
|
3
|
+
Currently exposes ControlSurface enumeration. Other session-health
|
|
4
|
+
diagnostics (``get_session_diagnostics``, ``get_recent_actions``) live in
|
|
5
|
+
``transport.py`` for historical reasons — this module is specifically for
|
|
6
|
+
inspection utilities that reach outside Song (into Application /
|
|
7
|
+
ControlSurface / Preferences / etc.).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from fastmcp import Context
|
|
13
|
+
|
|
14
|
+
from ..server import mcp
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_ableton(ctx: Context):
|
|
18
|
+
"""Extract AbletonConnection from lifespan context."""
|
|
19
|
+
return ctx.lifespan_context["ableton"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
def list_control_surfaces(ctx: Context) -> dict:
|
|
24
|
+
"""List all active ControlSurface instances (Push, APC, Launchkey, etc.).
|
|
25
|
+
|
|
26
|
+
Returns {surfaces: [{index, name, class_name}]}. Read-only diagnostic —
|
|
27
|
+
mirrors Live.Application.get_application().control_surfaces. Use the
|
|
28
|
+
index with get_control_surface_info() for per-surface detail.
|
|
29
|
+
"""
|
|
30
|
+
return _get_ableton(ctx).send_command("list_control_surfaces", {})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@mcp.tool()
|
|
34
|
+
def get_control_surface_info(ctx: Context, index: int) -> dict:
|
|
35
|
+
"""Read detailed info about a single control surface.
|
|
36
|
+
|
|
37
|
+
index: 0-based position in list_control_surfaces() results.
|
|
38
|
+
Returns {index, name, class_name, component_count}. Component count
|
|
39
|
+
falls back to 0 when the surface doesn't expose a .components iterable.
|
|
40
|
+
"""
|
|
41
|
+
return _get_ableton(ctx).send_command("get_control_surface_info",
|
|
42
|
+
{"index": index})
|