Files
ableton-osc/AbletonOSC/manager.py
T
valknar ac6a049516 Fix Ableton Live detection: add root __init__.py and harden Manager init
Live scans the Remote Scripts folder for __init__.py with create_instance()
at the *root* of the script folder. Our create_instance was buried inside
the AbletonOSC/ subpackage, so Live never found it.

Also:
- Inner AbletonOSC/__init__.py: guard import with try/except for pytest compat
- Manager: use show_message() (Live status bar) instead of log_message()
- Manager: wrap OSCServer.start() in try/except OSError so a port conflict
  surfaces as a readable status message instead of a silent crash
- Manager: run handler init inside component_guard() as required by the
  Ableton ControlSurface framework
- Manager stub: add show_message, schedule_message, component_guard no-ops
  so out-of-Ableton tooling/tests don't break

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 12:46:42 +02:00

145 lines
4.4 KiB
Python

"""
Main ControlSurface entry point for Ableton Live.
Manages OSC server lifecycle and all handler registrations.
"""
import logging
import os
import sys
try:
from ableton.v2.control_surface import ControlSurface
except ImportError:
from contextlib import contextmanager
class ControlSurface: # type: ignore
def __init__(self, c_instance=None):
self._c_instance = c_instance
def song(self):
return None
def application(self):
return None
def disconnect(self):
pass
def update_display(self):
pass
def log_message(self, msg):
print(msg)
def show_message(self, msg):
print(msg)
def schedule_message(self, delay, callback):
pass
@contextmanager
def component_guard(self):
yield
from .osc_server import OSCServer
from .song import SongHandler
from .track import TrackHandler
from .return_track import ReturnTrackHandler
from .clip import ClipHandler
from .clip_slot import ClipSlotHandler
from .device import DeviceHandler
from .scene import SceneHandler
from .view import ViewHandler
from .application import ApplicationHandler
from .browser import BrowserHandler
from .groove import GrooveHandler
logger = logging.getLogger(__name__)
LISTEN_PORT = int(os.environ.get("ABLETON_OSC_LISTEN_PORT", 11000))
SEND_PORT = int(os.environ.get("ABLETON_OSC_SEND_PORT", 11001))
class Manager(ControlSurface):
def __init__(self, c_instance=None):
super().__init__(c_instance)
self._setup_logging()
self._handlers = []
try:
self.osc_server = OSCServer(listen_port=LISTEN_PORT, send_port=SEND_PORT)
self.osc_server.start()
self._setup_handlers()
self.show_message("AbletonOSC: Listening on port %d" % LISTEN_PORT)
logger.info("AbletonOSC started (listen=%d, send=%d)", LISTEN_PORT, SEND_PORT)
self.osc_server.send("/live/startup", ())
except OSError as e:
self.show_message("AbletonOSC: Couldn't bind to port %d (%s)" % (LISTEN_PORT, e))
logger.error("Couldn't bind to port %d: %s", LISTEN_PORT, e)
# ------------------------------------------------------------------
def _setup_logging(self) -> None:
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
def _setup_handlers(self) -> None:
handler_classes = [
ApplicationHandler,
SongHandler,
TrackHandler,
ReturnTrackHandler,
ClipHandler,
ClipSlotHandler,
DeviceHandler,
SceneHandler,
ViewHandler,
BrowserHandler,
GrooveHandler,
]
with self.component_guard():
for cls in handler_classes:
h = cls(self)
h.init_api()
self._handlers.append(h)
# Control endpoints
self.osc_server.add_handler("/live/test", self._handle_test)
self.osc_server.add_handler("/live/api/reload", self._handle_reload)
self.osc_server.add_handler("/live/api/show_message", self._handle_show_message)
# ------------------------------------------------------------------
# ControlSurface interface
# ------------------------------------------------------------------
def update_display(self) -> None:
"""Called by Live ~100ms; drives the OSC poll loop."""
self.osc_server.process()
def disconnect(self) -> None:
for h in self._handlers:
h.clear_listeners()
self.osc_server.send("/live/shutdown", ())
self.osc_server.stop()
super().disconnect()
logger.info("AbletonOSC disconnected")
# ------------------------------------------------------------------
# Built-in handlers
# ------------------------------------------------------------------
def _handle_test(self, params: tuple):
return ("AbletonOSC active",)
def _handle_reload(self, params: tuple):
# Re-init all handlers (clears listeners and re-registers)
for h in self._handlers:
h.clear_listeners()
h.init_api()
return ("reloaded",)
def _handle_show_message(self, params: tuple):
if params:
self.log_message(str(params[0]))
return None