Files
ableton-osc/AbletonOSC/browser.py
T

150 lines
5.0 KiB
Python
Raw Normal View History

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