livepilot 1.16.1 → 1.17.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/CHANGELOG.md +269 -0
- package/README.md +16 -15
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +85 -0
- package/mcp_server/atlas/device_atlas.json +3183 -382
- package/mcp_server/atlas/device_techniques_index.json +1510 -0
- package/mcp_server/atlas/enrichments/__init__.py +1 -0
- package/mcp_server/atlas/enrichments/audio_effects/amp.yaml +112 -0
- package/mcp_server/atlas/enrichments/audio_effects/audio_effect_rack.yaml +77 -0
- package/mcp_server/atlas/enrichments/audio_effects/cabinet.yaml +81 -0
- package/mcp_server/atlas/enrichments/audio_effects/corpus.yaml +128 -0
- package/mcp_server/atlas/enrichments/audio_effects/envelope_follower.yaml +99 -0
- package/mcp_server/atlas/enrichments/audio_effects/external_audio_effect.yaml +64 -0
- package/mcp_server/atlas/enrichments/audio_effects/looper.yaml +85 -0
- package/mcp_server/atlas/enrichments/audio_effects/resonators.yaml +121 -0
- package/mcp_server/atlas/enrichments/audio_effects/snipper.yaml +17 -0
- package/mcp_server/atlas/enrichments/audio_effects/spectrum.yaml +61 -0
- package/mcp_server/atlas/enrichments/audio_effects/tuner.yaml +43 -0
- package/mcp_server/atlas/enrichments/audio_effects/utility.yaml +118 -0
- package/mcp_server/atlas/enrichments/audio_effects/vocoder.yaml +94 -0
- package/mcp_server/atlas/enrichments/instruments/analog.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bass.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/bell_tower.yaml +38 -0
- package/mcp_server/atlas/enrichments/instruments/collision.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drift.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/drum_rack.yaml +142 -0
- package/mcp_server/atlas/enrichments/instruments/electric.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/emit.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/meld.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/operator.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/poli.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/sampler.yaml +12 -0
- package/mcp_server/atlas/enrichments/instruments/simpler.yaml +15 -0
- package/mcp_server/atlas/enrichments/instruments/tension.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_fm.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/vector_grain.yaml +11 -0
- package/mcp_server/atlas/enrichments/instruments/wavetable.yaml +11 -0
- package/mcp_server/atlas/enrichments/midi_effects/filler.yaml +17 -0
- package/mcp_server/atlas/enrichments/utility/performer.yaml +15 -0
- package/mcp_server/atlas/enrichments/utility/vector_map.yaml +21 -0
- package/mcp_server/atlas/tools.py +291 -0
- package/mcp_server/m4l_bridge.py +19 -2
- package/mcp_server/sample_engine/tools.py +140 -68
- package/mcp_server/splice_client/http_bridge.py +319 -116
- package/mcp_server/tools/automation.py +168 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +216 -1
- package/server.json +3 -3
|
@@ -602,3 +602,171 @@ def analyze_for_automation(
|
|
|
602
602
|
"spectrum": spectrum,
|
|
603
603
|
"suggestions": suggestions,
|
|
604
604
|
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ── Track-level arrangement automation workaround (T5, 2026-04-22 handoff) ─
|
|
608
|
+
#
|
|
609
|
+
# The Live LOM has a well-known gap: you cannot directly write a
|
|
610
|
+
# TRACK-level automation envelope into arrangement between clips or
|
|
611
|
+
# outside any clip. You can only write CLIP-level automation (which
|
|
612
|
+
# lives inside a specific clip). The workaround is the classic
|
|
613
|
+
# session-clip + record-to-arrangement dance:
|
|
614
|
+
#
|
|
615
|
+
# 1. Build a session clip with the automation (set_clip_automation)
|
|
616
|
+
# 2. Arm the track
|
|
617
|
+
# 3. Jump to the target beat
|
|
618
|
+
# 4. Start arrangement recording
|
|
619
|
+
# 5. Fire the session clip
|
|
620
|
+
# 6. Wait for the clip to play through at tempo
|
|
621
|
+
# 7. Stop recording
|
|
622
|
+
# 8. Stop the session clip (return control to arrangement)
|
|
623
|
+
#
|
|
624
|
+
# The recorded arrangement clip has the automation rendered as a native
|
|
625
|
+
# arrangement envelope — which IS writable by Live even when the LOM
|
|
626
|
+
# disallows direct API access.
|
|
627
|
+
|
|
628
|
+
@mcp.tool()
|
|
629
|
+
async def set_arrangement_automation_via_session_record(
|
|
630
|
+
ctx: Context,
|
|
631
|
+
track_index: int,
|
|
632
|
+
parameter_type: str,
|
|
633
|
+
points: list | str,
|
|
634
|
+
target_beat: float,
|
|
635
|
+
duration_beats: float,
|
|
636
|
+
session_clip_slot: int = 0,
|
|
637
|
+
device_index: Optional[int] = None,
|
|
638
|
+
parameter_index: Optional[int] = None,
|
|
639
|
+
send_index: Optional[int] = None,
|
|
640
|
+
cleanup_session_clip: bool = True,
|
|
641
|
+
) -> dict:
|
|
642
|
+
"""Write an arrangement automation envelope at a specific beat via session record.
|
|
643
|
+
|
|
644
|
+
Workaround for the Live LOM limitation that prevents direct writing of
|
|
645
|
+
track-level arrangement automation outside of an existing arrangement
|
|
646
|
+
clip. Creates a temporary session clip with the automation, arms the
|
|
647
|
+
track, records the clip into arrangement at target_beat, then cleans
|
|
648
|
+
up. The recorded arrangement clip has the automation baked in.
|
|
649
|
+
|
|
650
|
+
**Status: LIVE** as of 2026-04-22. Uses a two-phase protocol so Live's
|
|
651
|
+
main thread never blocks: phase 1 (start) fires the session clip into
|
|
652
|
+
arrangement record; this tool then asyncio.sleeps for the expected
|
|
653
|
+
record duration (computed from the live tempo that phase 1 returns);
|
|
654
|
+
phase 2 (complete) stops record, cleans up, and returns the new
|
|
655
|
+
arrangement clip's index + start/length. Total wall time ≈
|
|
656
|
+
duration_beats × 60/tempo + 0.5s handler overhead.
|
|
657
|
+
|
|
658
|
+
parameter_type: "device" | "volume" | "panning" | "send"
|
|
659
|
+
points: [{time, value, duration?}] — time relative to
|
|
660
|
+
the session clip's start (0.0 = first beat)
|
|
661
|
+
target_beat: where the recording should begin in the
|
|
662
|
+
arrangement timeline (beats)
|
|
663
|
+
duration_beats: how long to record (usually matches the session
|
|
664
|
+
clip's natural length, but may be longer for
|
|
665
|
+
multiple repeats)
|
|
666
|
+
session_clip_slot: which session clip slot to use (default 0 — must
|
|
667
|
+
be EMPTY or its current content will be overwritten)
|
|
668
|
+
device_index,
|
|
669
|
+
parameter_index: required for parameter_type="device"
|
|
670
|
+
send_index: required for parameter_type="send" (0=A, 1=B)
|
|
671
|
+
cleanup_session_clip: delete the temp session clip after recording
|
|
672
|
+
completes (default True)
|
|
673
|
+
|
|
674
|
+
Returns a dict with the recorded arrangement clip index + verification
|
|
675
|
+
info, or an error if any step failed. Because this orchestrates a
|
|
676
|
+
real-time recording, any of the steps can fail at runtime (track
|
|
677
|
+
not armable, session clip already running, transport not cooperating).
|
|
678
|
+
Each failure is reported with the stage it happened at.
|
|
679
|
+
"""
|
|
680
|
+
if track_index < 0:
|
|
681
|
+
raise ValueError("track_index must be >= 0 (track-level automation only supports regular tracks)")
|
|
682
|
+
if parameter_type not in ("device", "volume", "panning", "send"):
|
|
683
|
+
raise ValueError("parameter_type must be 'device', 'volume', 'panning', or 'send'")
|
|
684
|
+
if parameter_type == "device":
|
|
685
|
+
if device_index is None or parameter_index is None:
|
|
686
|
+
raise ValueError("device_index and parameter_index required for parameter_type='device'")
|
|
687
|
+
if parameter_type == "send" and send_index is None:
|
|
688
|
+
raise ValueError("send_index required for parameter_type='send'")
|
|
689
|
+
points_list = _ensure_list(points)
|
|
690
|
+
if not points_list:
|
|
691
|
+
raise ValueError("points list cannot be empty")
|
|
692
|
+
if target_beat < 0:
|
|
693
|
+
raise ValueError("target_beat must be >= 0")
|
|
694
|
+
if duration_beats <= 0:
|
|
695
|
+
raise ValueError("duration_beats must be > 0")
|
|
696
|
+
|
|
697
|
+
ableton = _get_ableton(ctx)
|
|
698
|
+
|
|
699
|
+
# Two-phase protocol — the start handler can't block Live's main thread
|
|
700
|
+
# for the full recording duration (audio dropouts, UI freezes). So:
|
|
701
|
+
# 1. start — writes session clip, arms, seeks, fires, enables record
|
|
702
|
+
# 2. MCP-side asyncio.sleep for duration + buffer
|
|
703
|
+
# 3. complete — stops record, cleans up, returns arrangement clip info
|
|
704
|
+
# The session-clip route is unchanged; the orchestration moved to us.
|
|
705
|
+
start_params: dict = {
|
|
706
|
+
"track_index": track_index,
|
|
707
|
+
"parameter_type": parameter_type,
|
|
708
|
+
"points": points_list,
|
|
709
|
+
"target_beat": target_beat,
|
|
710
|
+
"duration_beats": duration_beats,
|
|
711
|
+
"session_clip_slot": session_clip_slot,
|
|
712
|
+
}
|
|
713
|
+
if device_index is not None:
|
|
714
|
+
start_params["device_index"] = device_index
|
|
715
|
+
if parameter_index is not None:
|
|
716
|
+
start_params["parameter_index"] = parameter_index
|
|
717
|
+
if send_index is not None:
|
|
718
|
+
start_params["send_index"] = send_index
|
|
719
|
+
|
|
720
|
+
start_result = ableton.send_command(
|
|
721
|
+
"arrangement_automation_via_session_record_start",
|
|
722
|
+
start_params,
|
|
723
|
+
)
|
|
724
|
+
if not isinstance(start_result, dict) or start_result.get("status") != "recording":
|
|
725
|
+
return {
|
|
726
|
+
"ok": False,
|
|
727
|
+
"phase": "start",
|
|
728
|
+
"error": "arrangement record failed to engage",
|
|
729
|
+
"details": start_result,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
# Compute sleep: duration_beats * 60/tempo seconds + 0.5s buffer for
|
|
733
|
+
# Live's record-arm latency. Tempo comes back from the start handler
|
|
734
|
+
# (fresh Song.tempo read) so we don't hardcode 120.
|
|
735
|
+
import asyncio
|
|
736
|
+
tempo = float(start_result.get("tempo") or 120.0)
|
|
737
|
+
if tempo <= 0:
|
|
738
|
+
tempo = 120.0
|
|
739
|
+
seconds_per_beat = 60.0 / tempo
|
|
740
|
+
sleep_sec = duration_beats * seconds_per_beat + 0.5
|
|
741
|
+
# Safety ceiling — if something is misconfigured we shouldn't sleep
|
|
742
|
+
# forever. 10 minutes is far beyond any realistic arrangement clip.
|
|
743
|
+
sleep_sec = min(sleep_sec, 600.0)
|
|
744
|
+
try:
|
|
745
|
+
await asyncio.sleep(sleep_sec)
|
|
746
|
+
except Exception as exc:
|
|
747
|
+
# Even if sleep fails we try to complete so we don't leave the
|
|
748
|
+
# track armed + recording.
|
|
749
|
+
logger.warning("sleep interrupted: %s", exc)
|
|
750
|
+
|
|
751
|
+
complete_params = {
|
|
752
|
+
"track_index": track_index,
|
|
753
|
+
"session_clip_slot": session_clip_slot,
|
|
754
|
+
"target_beat": target_beat,
|
|
755
|
+
"duration_beats": duration_beats,
|
|
756
|
+
"cleanup_session_clip": cleanup_session_clip,
|
|
757
|
+
}
|
|
758
|
+
complete_result = ableton.send_command(
|
|
759
|
+
"arrangement_automation_via_session_record_complete",
|
|
760
|
+
complete_params,
|
|
761
|
+
)
|
|
762
|
+
ok = (
|
|
763
|
+
isinstance(complete_result, dict)
|
|
764
|
+
and complete_result.get("status") == "completed"
|
|
765
|
+
)
|
|
766
|
+
return {
|
|
767
|
+
"ok": bool(ok),
|
|
768
|
+
"tempo": tempo,
|
|
769
|
+
"slept_sec": sleep_sec,
|
|
770
|
+
"start": start_result,
|
|
771
|
+
"complete": complete_result,
|
|
772
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 426 tools, 52 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.17.0"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
|
-
LivePilot - Arrangement domain handlers (
|
|
2
|
+
LivePilot - Arrangement domain handlers (21 commands).
|
|
3
|
+
|
|
4
|
+
As of 2026-04-22: adds the two-phase session-record workaround for
|
|
5
|
+
track-level arrangement automation (T5 handoff). Live's LOM can't
|
|
6
|
+
write track-level automation envelopes outside a clip directly —
|
|
7
|
+
this handler pair writes to a session clip, records it into
|
|
8
|
+
arrangement at a target beat, then cleans up.
|
|
3
9
|
"""
|
|
4
10
|
|
|
11
|
+
from .clip_automation import set_clip_automation as _set_clip_automation_handler
|
|
5
12
|
from .router import register
|
|
6
13
|
from .utils import get_track
|
|
7
14
|
|
|
@@ -866,3 +873,211 @@ def force_arrangement(song, params):
|
|
|
866
873
|
"is_playing": song.is_playing,
|
|
867
874
|
"loop": song.loop,
|
|
868
875
|
}
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
# ── Session-record arrangement automation (T5 workaround) ────────────
|
|
879
|
+
#
|
|
880
|
+
# Two-phase protocol so the MCP server can sleep between the record
|
|
881
|
+
# start and stop without blocking Live's main thread.
|
|
882
|
+
#
|
|
883
|
+
# Phase 1: _start — write session clip, arm, seek, record, fire
|
|
884
|
+
# Phase 2: _complete — stop record, stop transport, locate new
|
|
885
|
+
# arrangement clip, clean up session clip
|
|
886
|
+
#
|
|
887
|
+
# The MCP tool layer orchestrates: start → asyncio.sleep(duration * 60/tempo + 0.5) → complete.
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def _fire_slot(slot):
|
|
891
|
+
"""Fire a clip slot robustly — Live's API changed between versions."""
|
|
892
|
+
# Newer Live: slot.fire() is the canonical way. `slot.clip.fire()`
|
|
893
|
+
# also works when a clip is present. Try slot-level first so empty
|
|
894
|
+
# slots (we may have just created a clip) fire the row reliably.
|
|
895
|
+
try:
|
|
896
|
+
slot.fire()
|
|
897
|
+
except AttributeError:
|
|
898
|
+
slot.clip.fire()
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _ensure_session_clip_length(slot, points, min_length=1.0):
|
|
902
|
+
"""Ensure slot has a session MIDI clip covering the points' time range.
|
|
903
|
+
|
|
904
|
+
Returns (clip, was_created). If the slot is empty we call
|
|
905
|
+
create_clip(length). If points extend past the existing clip
|
|
906
|
+
we leave it alone — the user may have intentional content before.
|
|
907
|
+
"""
|
|
908
|
+
if not slot.has_clip:
|
|
909
|
+
max_t = 0.0
|
|
910
|
+
for p in points or []:
|
|
911
|
+
t = float(p.get("time", 0))
|
|
912
|
+
d = float(p.get("duration", 0.125))
|
|
913
|
+
max_t = max(max_t, t + d)
|
|
914
|
+
length = max(min_length, max_t)
|
|
915
|
+
slot.create_clip(length)
|
|
916
|
+
return slot.clip, True
|
|
917
|
+
return slot.clip, False
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
@register("arrangement_automation_via_session_record_start")
|
|
921
|
+
def arrangement_automation_via_session_record_start(song, params):
|
|
922
|
+
"""Phase 1 of the T5 workaround.
|
|
923
|
+
|
|
924
|
+
Preps the record: ensures session clip exists with automation, arms
|
|
925
|
+
the track, seeks the arrangement to target_beat, enables record, and
|
|
926
|
+
fires the session clip. Returns tempo so the MCP layer can compute
|
|
927
|
+
the sleep duration before phase 2.
|
|
928
|
+
"""
|
|
929
|
+
track_index = int(params["track_index"])
|
|
930
|
+
session_clip_slot = int(params.get("session_clip_slot", 0))
|
|
931
|
+
target_beat = float(params["target_beat"])
|
|
932
|
+
parameter_type = params["parameter_type"]
|
|
933
|
+
points = params.get("points") or []
|
|
934
|
+
if not points:
|
|
935
|
+
raise ValueError("points cannot be empty")
|
|
936
|
+
|
|
937
|
+
track = get_track(song, track_index)
|
|
938
|
+
|
|
939
|
+
# Probe 1: can we arm this track?
|
|
940
|
+
if not getattr(track, "can_be_armed", False):
|
|
941
|
+
raise ValueError(
|
|
942
|
+
"track %d cannot be armed — session-record workaround requires "
|
|
943
|
+
"an armable (MIDI or audio input) track" % track_index
|
|
944
|
+
)
|
|
945
|
+
|
|
946
|
+
slots = list(track.clip_slots)
|
|
947
|
+
if session_clip_slot < 0 or session_clip_slot >= len(slots):
|
|
948
|
+
raise IndexError(
|
|
949
|
+
"session_clip_slot %d out of range (0..%d)"
|
|
950
|
+
% (session_clip_slot, len(slots) - 1)
|
|
951
|
+
)
|
|
952
|
+
slot = slots[session_clip_slot]
|
|
953
|
+
|
|
954
|
+
# Probe 2: create a session clip if the slot is empty; otherwise reuse
|
|
955
|
+
clip, created = _ensure_session_clip_length(slot, points)
|
|
956
|
+
|
|
957
|
+
# Probe 3: write the automation envelope via the existing handler.
|
|
958
|
+
# We need to tell set_clip_automation which clip_index to target.
|
|
959
|
+
# set_clip_automation takes clip_index = the slot index. Perfect.
|
|
960
|
+
clip_auto_params = dict(params)
|
|
961
|
+
clip_auto_params["clip_index"] = session_clip_slot
|
|
962
|
+
# Drop keys that set_clip_automation doesn't expect
|
|
963
|
+
for k in ("target_beat", "duration_beats", "session_clip_slot",
|
|
964
|
+
"cleanup_session_clip"):
|
|
965
|
+
clip_auto_params.pop(k, None)
|
|
966
|
+
write_result = _set_clip_automation_handler(song, clip_auto_params)
|
|
967
|
+
|
|
968
|
+
# Probe 4: seek the arrangement to target_beat. Must be done
|
|
969
|
+
# BEFORE enabling record, or the record starts at wherever the
|
|
970
|
+
# playhead currently is.
|
|
971
|
+
song.back_to_arranger = True
|
|
972
|
+
song.current_song_time = max(0.0, target_beat)
|
|
973
|
+
|
|
974
|
+
# Probe 5: arm the track. Some tracks need `current_monitoring_state`
|
|
975
|
+
# clarification, but arming alone should route the session clip's
|
|
976
|
+
# output into arrangement record on most setups.
|
|
977
|
+
track.arm = True
|
|
978
|
+
|
|
979
|
+
# Probe 6: enable arrangement record + start transport. Live needs
|
|
980
|
+
# the transport playing for record_mode to engage.
|
|
981
|
+
if not song.is_playing:
|
|
982
|
+
song.start_playing()
|
|
983
|
+
song.record_mode = True
|
|
984
|
+
|
|
985
|
+
# Fire the session clip — its automation plays into arrangement.
|
|
986
|
+
_fire_slot(slot)
|
|
987
|
+
|
|
988
|
+
return {
|
|
989
|
+
"status": "recording",
|
|
990
|
+
"track_index": track_index,
|
|
991
|
+
"session_clip_slot": session_clip_slot,
|
|
992
|
+
"target_beat": song.current_song_time,
|
|
993
|
+
"tempo": float(song.tempo),
|
|
994
|
+
"session_clip_created": created,
|
|
995
|
+
"automation_write": write_result,
|
|
996
|
+
"record_mode": bool(song.record_mode),
|
|
997
|
+
"is_playing": bool(song.is_playing),
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
@register("arrangement_automation_via_session_record_complete")
|
|
1002
|
+
def arrangement_automation_via_session_record_complete(song, params):
|
|
1003
|
+
"""Phase 2 of the T5 workaround.
|
|
1004
|
+
|
|
1005
|
+
Stops recording, stops transport, disarms the track, locates the
|
|
1006
|
+
newly-recorded arrangement clip, and optionally cleans up the
|
|
1007
|
+
temporary session clip used to source the automation.
|
|
1008
|
+
"""
|
|
1009
|
+
track_index = int(params["track_index"])
|
|
1010
|
+
session_clip_slot = int(params.get("session_clip_slot", 0))
|
|
1011
|
+
cleanup = bool(params.get("cleanup_session_clip", True))
|
|
1012
|
+
target_beat = float(params["target_beat"])
|
|
1013
|
+
duration_beats = float(params.get("duration_beats", 0))
|
|
1014
|
+
|
|
1015
|
+
track = get_track(song, track_index)
|
|
1016
|
+
slots = list(track.clip_slots)
|
|
1017
|
+
if session_clip_slot < 0 or session_clip_slot >= len(slots):
|
|
1018
|
+
raise IndexError(
|
|
1019
|
+
"session_clip_slot %d out of range" % session_clip_slot
|
|
1020
|
+
)
|
|
1021
|
+
slot = slots[session_clip_slot]
|
|
1022
|
+
|
|
1023
|
+
# 1. Stop recording and transport
|
|
1024
|
+
song.record_mode = False
|
|
1025
|
+
was_playing = song.is_playing
|
|
1026
|
+
if was_playing:
|
|
1027
|
+
song.stop_playing()
|
|
1028
|
+
|
|
1029
|
+
# 2. Stop the session clip if still playing
|
|
1030
|
+
try:
|
|
1031
|
+
if slot.has_clip and getattr(slot, "is_playing", False):
|
|
1032
|
+
slot.clip.stop()
|
|
1033
|
+
except Exception:
|
|
1034
|
+
pass
|
|
1035
|
+
|
|
1036
|
+
# 3. Disarm the track (leave as-was if never armed)
|
|
1037
|
+
try:
|
|
1038
|
+
track.arm = False
|
|
1039
|
+
except Exception:
|
|
1040
|
+
pass
|
|
1041
|
+
|
|
1042
|
+
# 4. Find the new arrangement clip at or near target_beat.
|
|
1043
|
+
# Tolerance: half a beat — Live may nudge start by quantization.
|
|
1044
|
+
track_clips = list(track.arrangement_clips)
|
|
1045
|
+
new_clip = None
|
|
1046
|
+
new_index = None
|
|
1047
|
+
tolerance = 0.5
|
|
1048
|
+
for i, c in enumerate(track_clips):
|
|
1049
|
+
if abs(float(c.start_time) - target_beat) < tolerance:
|
|
1050
|
+
new_clip = c
|
|
1051
|
+
new_index = i
|
|
1052
|
+
break
|
|
1053
|
+
|
|
1054
|
+
# 5. Cleanup the session clip if requested
|
|
1055
|
+
session_clip_deleted = False
|
|
1056
|
+
if cleanup and slot.has_clip:
|
|
1057
|
+
try:
|
|
1058
|
+
slot.delete_clip()
|
|
1059
|
+
session_clip_deleted = True
|
|
1060
|
+
except Exception:
|
|
1061
|
+
pass
|
|
1062
|
+
|
|
1063
|
+
result = {
|
|
1064
|
+
"status": "completed",
|
|
1065
|
+
"track_index": track_index,
|
|
1066
|
+
"arrangement_clip_found": new_clip is not None,
|
|
1067
|
+
"arrangement_clip_index": new_index,
|
|
1068
|
+
"session_clip_deleted": session_clip_deleted,
|
|
1069
|
+
"record_mode": bool(song.record_mode),
|
|
1070
|
+
"was_playing": was_playing,
|
|
1071
|
+
}
|
|
1072
|
+
if new_clip is not None:
|
|
1073
|
+
result["arrangement_clip_start"] = float(new_clip.start_time)
|
|
1074
|
+
result["arrangement_clip_length"] = float(new_clip.length)
|
|
1075
|
+
try:
|
|
1076
|
+
result["arrangement_clip_name"] = str(new_clip.name)
|
|
1077
|
+
except Exception:
|
|
1078
|
+
pass
|
|
1079
|
+
elif duration_beats > 0:
|
|
1080
|
+
# Guess based on expected range — helpful debug if the lookup missed
|
|
1081
|
+
result["expected_clip_start"] = target_beat
|
|
1082
|
+
result["expected_clip_end"] = target_beat + duration_beats
|
|
1083
|
+
return result
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "426-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.17.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.17.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|