6fc0a84dfe
ControlSurface exposes song and application as properties, not callable methods. Replace manager.song() and manager.application() with the correct attribute access throughout handler, application, browser, and view modules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
150 lines
5.0 KiB
Python
150 lines
5.0 KiB
Python
"""Handles /live/browser/* OSC addresses."""
|
|
import logging
|
|
from typing import Optional
|
|
from .handler import AbletonOSCHandler
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _iter_items(node, path_parts, depth=0):
|
|
"""Recursively find a browser item by path."""
|
|
if not path_parts:
|
|
return node
|
|
name = path_parts[0]
|
|
try:
|
|
children = list(node.children)
|
|
except Exception:
|
|
return None
|
|
for child in children:
|
|
if child.name == name:
|
|
return _iter_items(child, path_parts[1:], depth + 1)
|
|
return None
|
|
|
|
|
|
class BrowserHandler(AbletonOSCHandler):
|
|
def init_api(self) -> None:
|
|
self.clear_listeners()
|
|
|
|
# Category listings (returns flat list of names)
|
|
for category in [
|
|
"audio_effects", "instruments", "midi_effects",
|
|
"samples", "sounds", "clips", "packs", "plugins",
|
|
]:
|
|
self._add(f"/live/browser/get/{category}",
|
|
self._make_category_lister(category))
|
|
|
|
# Load by path (e.g., "Instruments/Wavetable")
|
|
self._add("/live/browser/load_item", self._load_item)
|
|
self._add("/live/browser/preview_item", self._preview_item)
|
|
self._add("/live/browser/stop_preview", self._stop_preview)
|
|
|
|
# Hotswap
|
|
self._add("/live/browser/get/hotswap_target", self._get_hotswap_target)
|
|
self._add("/live/browser/begin_hotswap", self._begin_hotswap)
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def _browser(self):
|
|
try:
|
|
return self.manager.application.browser
|
|
except Exception:
|
|
return None
|
|
|
|
def _make_category_lister(self, category: str):
|
|
def handler(params: tuple) -> Optional[tuple]:
|
|
browser = self._browser()
|
|
if browser is None:
|
|
return None
|
|
try:
|
|
root = getattr(browser, category)
|
|
names = [item.name for item in root.children]
|
|
return tuple(names)
|
|
except Exception as e:
|
|
logger.warning("browser.%s: %s", category, e)
|
|
return None
|
|
return handler
|
|
|
|
def _load_item(self, params: tuple) -> None:
|
|
"""params: path_part1 [, path_part2, ...] -- e.g. 'Instruments' 'Wavetable'"""
|
|
if not params:
|
|
return None
|
|
browser = self._browser()
|
|
if browser is None:
|
|
return None
|
|
path = [str(p) for p in params]
|
|
# Try to find within all top-level categories
|
|
for category in [
|
|
"audio_effects", "instruments", "midi_effects",
|
|
"samples", "sounds", "clips", "packs", "plugins",
|
|
]:
|
|
try:
|
|
root = getattr(browser, category)
|
|
item = _iter_items(root, path)
|
|
if item is not None:
|
|
browser.load_item(item)
|
|
return None
|
|
except Exception:
|
|
continue
|
|
logger.warning("browser.load_item: item not found: %s", path)
|
|
return None
|
|
|
|
def _preview_item(self, params: tuple) -> None:
|
|
if not params:
|
|
return None
|
|
browser = self._browser()
|
|
if browser is None:
|
|
return None
|
|
path = [str(p) for p in params]
|
|
for category in [
|
|
"audio_effects", "instruments", "midi_effects",
|
|
"samples", "sounds", "clips",
|
|
]:
|
|
try:
|
|
root = getattr(browser, category)
|
|
item = _iter_items(root, path)
|
|
if item is not None:
|
|
browser.preview_item(item)
|
|
return None
|
|
except Exception:
|
|
continue
|
|
return None
|
|
|
|
def _stop_preview(self, params: tuple) -> None:
|
|
browser = self._browser()
|
|
if browser:
|
|
try:
|
|
browser.stop_preview()
|
|
except Exception as e:
|
|
logger.warning("stop_preview: %s", e)
|
|
return None
|
|
|
|
def _get_hotswap_target(self, params: tuple) -> Optional[tuple]:
|
|
browser = self._browser()
|
|
if browser:
|
|
try:
|
|
target = browser.hotswap_target
|
|
if target is not None:
|
|
return (target.name,)
|
|
return ("",)
|
|
except Exception as e:
|
|
logger.warning("hotswap_target: %s", e)
|
|
return None
|
|
|
|
def _begin_hotswap(self, params: tuple) -> None:
|
|
"""params: track_idx, device_idx"""
|
|
if len(params) < 2:
|
|
return None
|
|
try:
|
|
track_idx = int(params[0])
|
|
device_idx = int(params[1])
|
|
tracks = list(self.song.tracks)
|
|
if 0 <= track_idx < len(tracks):
|
|
devices = list(tracks[track_idx].devices)
|
|
if 0 <= device_idx < len(devices):
|
|
browser = self._browser()
|
|
if browser:
|
|
browser.hotswap_target = devices[device_idx]
|
|
except Exception as e:
|
|
logger.warning("begin_hotswap: %s", e)
|
|
return None
|