Files
ableton-osc/AbletonOSC/scene.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

112 lines
3.9 KiB
Python

"""Handles /live/scene/* OSC addresses."""
import logging
from typing import Any, Optional
from .handler import AbletonOSCHandler
logger = logging.getLogger(__name__)
SCENE_PROPS_RW = [
"name", "color", "color_index",
"tempo", "tempo_enabled",
"time_signature_numerator", "time_signature_denominator",
"time_signature_enabled",
]
SCENE_PROPS_RO = ["is_empty", "is_triggered"]
class SceneHandler(AbletonOSCHandler):
def init_api(self) -> None:
self.clear_listeners()
for prop in SCENE_PROPS_RW:
self._add(f"/live/scene/get/{prop}", self._make_getter(prop))
self._add(f"/live/scene/set/{prop}", self._make_setter(prop))
self._add(f"/live/scene/start_listen/{prop}", self._make_start_listen(prop))
self._add(f"/live/scene/stop_listen/{prop}", self._make_stop_listen(prop))
for prop in SCENE_PROPS_RO:
self._add(f"/live/scene/get/{prop}", self._make_getter(prop))
self._add(f"/live/scene/start_listen/{prop}", self._make_start_listen(prop))
self._add(f"/live/scene/stop_listen/{prop}", self._make_stop_listen(prop))
self._add("/live/scene/fire", self._fire)
self._add("/live/scene/fire_as_selected", self._fire_as_selected)
self._add("/live/scene/get/num_clips", self._get_num_clips)
self._add("/live/scene/get/clip_slots/has_clip", self._get_has_clips)
# ------------------------------------------------------------------
def _get_scene(self, params: tuple) -> Optional[Any]:
if not params:
return None
idx = int(params[0])
scenes = list(self.song.scenes)
return scenes[idx] if 0 <= idx < len(scenes) else None
def _make_getter(self, prop: str):
def handler(params: tuple) -> Optional[tuple]:
scene = self._get_scene(params)
if scene is None:
return None
val = self._get_prop(scene, prop)
return (int(params[0]), val) if val is not None else None
return handler
def _make_setter(self, prop: str):
def handler(params: tuple) -> None:
if len(params) < 2:
return None
scene = self._get_scene(params)
if scene:
self._set_prop(scene, prop, params[1])
return None
return handler
def _make_start_listen(self, prop: str):
def handler(params: tuple) -> None:
scene = self._get_scene(params)
if scene:
self._register_listener(
f"scene.{params[0]}.{prop}", scene, prop,
f"/live/scene/get/{prop}", (int(params[0]),)
)
return handler
def _make_stop_listen(self, prop: str):
def handler(params: tuple) -> None:
if params:
self._remove_listener(f"scene.{params[0]}.{prop}")
return handler
def _fire(self, params: tuple) -> None:
scene = self._get_scene(params)
if scene:
try:
scene.fire()
except Exception as e:
logger.warning("scene.fire: %s", e)
return None
def _fire_as_selected(self, params: tuple) -> None:
scene = self._get_scene(params)
if scene:
try:
scene.fire_as_selected()
except Exception as e:
logger.warning("scene.fire_as_selected: %s", e)
return None
def _get_num_clips(self, params: tuple) -> Optional[tuple]:
scene = self._get_scene(params)
if scene is None:
return None
count = sum(1 for slot in scene.clip_slots if slot.has_clip)
return (int(params[0]), count)
def _get_has_clips(self, params: tuple) -> Optional[tuple]:
scene = self._get_scene(params)
if scene is None:
return None
return (int(params[0]),) + tuple(bool(slot.has_clip) for slot in scene.clip_slots)