Initial implementation: full Ableton Live 11 OSC remote script
Covers all major Live API objects with get/set/listen/method handlers: song, track, return_track, clip, clip_slot, device (incl. rack chains and drum pads), scene, view, application, browser, groove. Zero external runtime dependencies — OSC encoded/decoded in osc_server.py. Wildcard * support for track/scene indices. Listener callbacks fire to matching /get/ addresses for bidirectional state sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"""Handles /live/clip_slot/* OSC addresses."""
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from .handler import AbletonOSCHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SLOT_PROPS_RO = [
|
||||
"has_clip", "is_playing", "is_triggered", "is_recording",
|
||||
"is_group_slot", "controls_other_clips", "playing_status",
|
||||
"will_record_on_start",
|
||||
]
|
||||
|
||||
SLOT_PROPS_RW = ["has_stop_button"]
|
||||
|
||||
|
||||
class ClipSlotHandler(AbletonOSCHandler):
|
||||
def init_api(self) -> None:
|
||||
self.clear_listeners()
|
||||
|
||||
for prop in SLOT_PROPS_RO:
|
||||
self._add(f"/live/clip_slot/get/{prop}", self._make_getter(prop))
|
||||
self._add(f"/live/clip_slot/start_listen/{prop}",
|
||||
self._make_start_listen(prop))
|
||||
self._add(f"/live/clip_slot/stop_listen/{prop}",
|
||||
self._make_stop_listen(prop))
|
||||
|
||||
for prop in SLOT_PROPS_RW:
|
||||
self._add(f"/live/clip_slot/get/{prop}", self._make_getter(prop))
|
||||
self._add(f"/live/clip_slot/set/{prop}", self._make_setter(prop))
|
||||
self._add(f"/live/clip_slot/start_listen/{prop}",
|
||||
self._make_start_listen(prop))
|
||||
self._add(f"/live/clip_slot/stop_listen/{prop}",
|
||||
self._make_stop_listen(prop))
|
||||
|
||||
self._add("/live/clip_slot/fire", self._fire)
|
||||
self._add("/live/clip_slot/stop", self._stop)
|
||||
self._add("/live/clip_slot/create_clip", self._create_clip)
|
||||
self._add("/live/clip_slot/delete_clip", self._delete_clip)
|
||||
self._add("/live/clip_slot/duplicate_clip_to", self._duplicate_clip_to)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_slot(self, params: tuple) -> Optional[Any]:
|
||||
if len(params) < 2:
|
||||
return None
|
||||
try:
|
||||
track_idx = int(params[0])
|
||||
slot_idx = int(params[1])
|
||||
tracks = list(self.song.tracks)
|
||||
if 0 <= track_idx < len(tracks):
|
||||
slots = list(tracks[track_idx].clip_slots)
|
||||
if 0 <= slot_idx < len(slots):
|
||||
return slots[slot_idx]
|
||||
except Exception as e:
|
||||
logger.warning("get_slot(%s): %s", params, e)
|
||||
return None
|
||||
|
||||
def _slot_key(self, params: tuple) -> str:
|
||||
return f"clip_slot.{params[0]}.{params[1]}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _make_getter(self, prop: str):
|
||||
def handler(params: tuple) -> Optional[tuple]:
|
||||
slot = self._get_slot(params)
|
||||
if slot is None:
|
||||
return None
|
||||
val = self._get_prop(slot, prop)
|
||||
if val is None:
|
||||
return None
|
||||
return (int(params[0]), int(params[1]), val)
|
||||
return handler
|
||||
|
||||
def _make_setter(self, prop: str):
|
||||
def handler(params: tuple) -> None:
|
||||
if len(params) < 3:
|
||||
return None
|
||||
slot = self._get_slot(params)
|
||||
if slot:
|
||||
self._set_prop(slot, prop, params[2])
|
||||
return None
|
||||
return handler
|
||||
|
||||
def _make_start_listen(self, prop: str):
|
||||
def handler(params: tuple) -> None:
|
||||
slot = self._get_slot(params)
|
||||
if slot:
|
||||
key = f"{self._slot_key(params)}.{prop}"
|
||||
prefix = (int(params[0]), int(params[1]))
|
||||
self._register_listener(
|
||||
key, slot, prop, f"/live/clip_slot/get/{prop}", prefix
|
||||
)
|
||||
return handler
|
||||
|
||||
def _make_stop_listen(self, prop: str):
|
||||
def handler(params: tuple) -> None:
|
||||
if len(params) >= 2:
|
||||
self._remove_listener(f"{self._slot_key(params)}.{prop}")
|
||||
return handler
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _fire(self, params: tuple) -> None:
|
||||
slot = self._get_slot(params)
|
||||
if slot:
|
||||
try:
|
||||
slot.fire()
|
||||
except Exception as e:
|
||||
logger.warning("clip_slot.fire: %s", e)
|
||||
return None
|
||||
|
||||
def _stop(self, params: tuple) -> None:
|
||||
slot = self._get_slot(params)
|
||||
if slot:
|
||||
try:
|
||||
slot.stop()
|
||||
except Exception as e:
|
||||
logger.warning("clip_slot.stop: %s", e)
|
||||
return None
|
||||
|
||||
def _create_clip(self, params: tuple) -> None:
|
||||
"""params: track_idx, slot_idx [, length_in_beats]"""
|
||||
slot = self._get_slot(params)
|
||||
if slot:
|
||||
try:
|
||||
length = float(params[2]) if len(params) > 2 else 4.0
|
||||
slot.create_clip(length)
|
||||
except Exception as e:
|
||||
logger.warning("clip_slot.create_clip: %s", e)
|
||||
return None
|
||||
|
||||
def _delete_clip(self, params: tuple) -> None:
|
||||
slot = self._get_slot(params)
|
||||
if slot and slot.has_clip:
|
||||
try:
|
||||
slot.delete_clip()
|
||||
except Exception as e:
|
||||
logger.warning("clip_slot.delete_clip: %s", e)
|
||||
return None
|
||||
|
||||
def _duplicate_clip_to(self, params: tuple) -> None:
|
||||
"""params: src_track_idx, src_slot_idx, dst_track_idx, dst_slot_idx"""
|
||||
if len(params) < 4:
|
||||
return None
|
||||
src_slot = self._get_slot(params)
|
||||
if src_slot and src_slot.has_clip:
|
||||
try:
|
||||
dst_track_idx = int(params[2])
|
||||
dst_slot_idx = int(params[3])
|
||||
tracks = list(self.song.tracks)
|
||||
if 0 <= dst_track_idx < len(tracks):
|
||||
dst_slots = list(tracks[dst_track_idx].clip_slots)
|
||||
if 0 <= dst_slot_idx < len(dst_slots):
|
||||
src_slot.duplicate_clip_to(dst_slots[dst_slot_idx])
|
||||
except Exception as e:
|
||||
logger.warning("clip_slot.duplicate_clip_to: %s", e)
|
||||
return None
|
||||
Reference in New Issue
Block a user