Files
ableton-osc/AbletonOSC/clip_slot.py
T
valknar aad042650e 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>
2026-06-01 12:27:47 +02:00

159 lines
5.7 KiB
Python

"""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