From 5e112c5fce061b82fc4ea50e91c4173ecc6d3b0d Mon Sep 17 00:00:00 2001 From: Savinay Nangalia Date: Mon, 29 Dec 2025 22:53:47 -0800 Subject: [PATCH 1/2] chain and chain device endpoints --- README.md | 69 +++++++ abletonosc/__init__.py | 1 + abletonosc/chain.py | 177 ++++++++++++++++++ manager.py | 2 + tests/test_chain.py | 400 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 649 insertions(+) create mode 100644 abletonosc/chain.py create mode 100644 tests/test_chain.py diff --git a/README.md b/README.md index 083bb8d..2d85c28 100644 --- a/README.md +++ b/README.md @@ -505,6 +505,75 @@ For devices: +--- + +### Chain properties + +Chains are collections of devices within rack devices (Drum Racks, Instrument Racks, Effect Racks). This API allows you to access and control devices nested within rack chains. + +**Navigation hierarchy**: Track → Rack Device → Chain → Chain Device → Parameters + +
+Documentation: Chain API + +#### Rack Discovery + +| Address | Query params | Response params | Description | +|:----------------------------------|:------------------------------------------------|:-------------------------------------------------------|:-----------------------------------------| +| /live/chain/get/num_chains | track_id, rack_device_id | track_id, rack_device_id, num_chains | Get number of chains in rack device | +| /live/chain/get/num_devices | track_id, rack_device_id, chain_id | track_id, rack_device_id, chain_id, num_devices | Get number of devices in chain | + +#### Chain Device Discovery + +| Address | Query params | Response params | Description | +|:----------------------------------|:------------------------------------------------|:-------------------------------------------------------|:-----------------------------------------| +| /live/chain/device/get/devices/name | track_id, rack_device_id, chain_id | track_id, rack_device_id, chain_id, [name, ...] | Get all device names in chain | +| /live/chain/device/get/devices/type | track_id, rack_device_id, chain_id | track_id, rack_device_id, chain_id, [type, ...] | Get all device types in chain | + +#### Chain Device Parameters - Bulk Operations + +| Address | Query params | Response params | Description | +|:------------------------------------------------|:--------------------------------------------------------------|:-------------------------------------------------------------------|:----------------------------------------------------------------------------------------| +| /live/chain/device/get/num_parameters | track_id, rack_device_id, chain_id, chain_device_id | track_id, rack_device_id, chain_id, chain_device_id, num_params | Get the number of parameters exposed by the chain device | +| /live/chain/device/get/parameters/name | track_id, rack_device_id, chain_id, chain_device_id | track_id, rack_device_id, chain_id, chain_device_id, [name, ...] | Get the list of parameter names exposed by the chain device | +| /live/chain/device/get/parameters/value | track_id, rack_device_id, chain_id, chain_device_id | track_id, rack_device_id, chain_id, chain_device_id, [value, ...] | Get the chain device parameter values | +| /live/chain/device/get/parameters/min | track_id, rack_device_id, chain_id, chain_device_id | track_id, rack_device_id, chain_id, chain_device_id, [value, ...] | Get the chain device parameter minimum values | +| /live/chain/device/get/parameters/max | track_id, rack_device_id, chain_id, chain_device_id | track_id, rack_device_id, chain_id, chain_device_id, [value, ...] | Get the chain device parameter maximum values | +| /live/chain/device/get/parameters/is_quantized | track_id, rack_device_id, chain_id, chain_device_id | track_id, rack_device_id, chain_id, chain_device_id, [value, ...] | Get the list of is_quantized settings (i.e., whether the parameter must be an int/bool) | +| /live/chain/device/set/parameters/value | track_id, rack_device_id, chain_id, chain_device_id, val, ... | | Set the chain device parameter values | + +#### Chain Device Parameters - Individual Operations + +| Address | Query params | Response params | Description | +|:------------------------------------------------|:--------------------------------------------------------------------|:--------------------------------------------------------------------------|:------------------------------------------------------------------------| +| /live/chain/device/get/parameter/value | track_id, rack_device_id, chain_id, chain_device_id, parameter_id | track_id, rack_device_id, chain_id, chain_device_id, parameter_id, value | Get a chain device parameter value | +| /live/chain/device/get/parameter/value_string | track_id, rack_device_id, chain_id, chain_device_id, parameter_id | track_id, rack_device_id, chain_id, chain_device_id, parameter_id, value | Get the chain device parameter value as a readable string ex: 2500 Hz | +| /live/chain/device/get/parameter/name | track_id, rack_device_id, chain_id, chain_device_id, parameter_id | track_id, rack_device_id, chain_id, chain_device_id, parameter_id, name | Get a chain device parameter name | +| /live/chain/device/set/parameter/value | track_id, rack_device_id, chain_id, chain_device_id, parameter_id, value | | Set a chain device parameter value | + +**Notes:** +- Use `/live/track/get/devices/can_have_chains` to identify which devices are racks +- Rack devices include: Drum Rack, Instrument Rack, Audio Effect Rack, MIDI Effect Rack +- Device types follow the same convention as `/live/device`: 1 = audio_effect, 2 = instrument, 4 = midi_effect +- Chain devices have the same parameter interface as top-level devices + +**Example Usage:** +```python +# Get chains in drum rack on track 0, device 0 +num_chains = query("/live/chain/get/num_chains", (0, 0)) +# Returns: (0, 0, 128) # 128 chains in drum rack + +# Get devices in first chain +num_devices = query("/live/chain/get/num_devices", (0, 0, 0)) +# Returns: (0, 0, 0, 2) # 2 devices in chain + +# Get parameter value from chain device +value = query("/live/chain/device/get/parameter/value", (0, 0, 0, 0, 5)) +# Returns: (0, 0, 0, 0, 5, 0.75) # Parameter 5 has value 0.75 +``` + +
+ --- ## MidiMap API diff --git a/abletonosc/__init__.py b/abletonosc/__init__.py index 53ba155..138505b 100644 --- a/abletonosc/__init__.py +++ b/abletonosc/__init__.py @@ -10,6 +10,7 @@ from .clip_slot import ClipSlotHandler from .track import TrackHandler from .device import DeviceHandler +from .chain import ChainHandler from .scene import SceneHandler from .view import ViewHandler from .midimap import MidiMapHandler diff --git a/abletonosc/chain.py b/abletonosc/chain.py new file mode 100644 index 0000000..2db031e --- /dev/null +++ b/abletonosc/chain.py @@ -0,0 +1,177 @@ +from typing import Tuple, Any +from .handler import AbletonOSCHandler + +class ChainHandler(AbletonOSCHandler): + """ + Handler for OSC messages related to rack chains and their devices. + Provides access to chains within rack devices and the devices within those chains. + """ + def __init__(self, manager): + super().__init__(manager) + self.class_identifier = "chain" + + def init_api(self): + #-------------------------------------------------------------------------------- + # Callback factory: Rack, Chain, Device operations + #-------------------------------------------------------------------------------- + def create_rack_callback(func, *args): + def rack_callback(params: Tuple[Any]): + track_index, rack_device_index = int(params[0]), int(params[1]) + rack_device = self.song.tracks[track_index].devices[rack_device_index] + rv = func(rack_device, *args, params[2:]) + + if rv is not None: + return (track_index, rack_device_index, *rv) + + return rack_callback + + def create_chain_callback(func, *args): + def chain_callback(params: Tuple[Any]): + track_index, rack_device_index, chain_index = int(params[0]), int(params[1]), int(params[2]) + chain = self.song.tracks[track_index].devices[rack_device_index].chains[chain_index] + rv = func(chain, *args, params[3:]) + + if rv is not None: + return (track_index, rack_device_index, chain_index, *rv) + + return chain_callback + + chain_methods = [ + ] + chain_properties_r = [ + "has_audio_input", + "has_audio_output", + "has_midi_input", + "has_midi_output", + "is_auto_colored", + "muted_via_solo", + "color_index" + ] + chain_properties_rw = [ + "name", + "color", + "mute", + "solo" + ] + + for method in chain_methods: + self.osc_server.add_handler("/live/chain/%s" % method, create_chain_callback(self._call_method, method)) + + for prop in chain_properties_r + chain_properties_rw: + self.osc_server.add_handler("/live/chain/get/%s" % prop, create_chain_callback(self._get_property, prop)) + self.osc_server.add_handler("/live/chain/start_listen/%s" % prop, create_chain_callback(self._start_listen, prop)) + self.osc_server.add_handler("/live/chain/stop_listen/%s" % prop, create_chain_callback(self._stop_listen, prop)) + for prop in chain_properties_rw: + self.osc_server.add_handler("/live/chain/set/%s" % prop, + create_chain_callback(self._set_property, prop)) + + def create_chain_device_callback(func, *args): + def chain_device_callback(params: Tuple[Any]): + track_index, rack_device_index, chain_index, chain_device_index = int(params[0]), int(params[1]), int(params[2]), int(params[3]) + chain = self.song.tracks[track_index].devices[rack_device_index].chains[chain_index] + device = chain.devices[chain_device_index] + rv = func(device, *args, params[4:]) + + if rv is not None: + return (track_index, rack_device_index, chain_index, chain_device_index, *rv) + + return chain_device_callback + + chain_device_methods = [ + ] + chain_device_properties_r = [ + "class_name", + "name", + "type" + ] + chain_device_properties_rw = [ + ] + + for method in chain_device_methods: + self.osc_server.add_handler("/live/chain/device/%s" % method, create_chain_device_callback(self._call_method, method)) + + for prop in chain_device_properties_r + chain_device_properties_rw: + self.osc_server.add_handler("/live/chain/device/get/%s" % prop, create_chain_device_callback(self._get_property, prop)) + self.osc_server.add_handler("/live/chain/device/start_listen/%s" % prop, create_chain_device_callback(self._start_listen, prop)) + self.osc_server.add_handler("/live/chain/device/stop_listen/%s" % prop, create_chain_device_callback(self._stop_listen, prop)) + for prop in chain_device_properties_rw: + self.osc_server.add_handler("/live/chain/device/set/%s" % prop, create_chain_device_callback(self._set_property, prop)) + + + #-------------------------------------------------------------------------------- + # Chain-specific functions + #-------------------------------------------------------------------------------- + def rack_device_get_num_chains(rack_device, params: Tuple[Any] = ()): + return len(rack_device.chains), + + def chain_get_num_devices(chain, params: Tuple[Any] = ()): + return len(chain.devices), + + def chain_get_device_names(chain, params: Tuple[Any] = ()): + return tuple(device.name for device in chain.devices) + + def chain_get_device_types(chain, params: Tuple[Any] = ()): + return tuple(device.type for device in chain.devices) + + self.osc_server.add_handler("/live/device/get/num_chains", create_rack_callback(rack_device_get_num_chains)) + self.osc_server.add_handler("/live/chain/get/num_devices", create_chain_callback(chain_get_num_devices)) + self.osc_server.add_handler("/live/chain/device/get/devices/name", create_chain_callback(chain_get_device_names)) + self.osc_server.add_handler("/live/chain/device/get/devices/type", create_chain_callback(chain_get_device_types)) + + #-------------------------------------------------------------------------------- + # Chain device: Get/set parameter lists + #-------------------------------------------------------------------------------- + def chain_device_get_num_parameters(device, params: Tuple[Any] = ()): + return len(device.parameters), + + def chain_device_get_parameters_name(device, params: Tuple[Any] = ()): + return tuple(parameter.name for parameter in device.parameters) + + def chain_device_get_parameters_value(device, params: Tuple[Any] = ()): + return tuple(parameter.value for parameter in device.parameters) + + def chain_device_get_parameters_min(device, params: Tuple[Any] = ()): + return tuple(parameter.min for parameter in device.parameters) + + def chain_device_get_parameters_max(device, params: Tuple[Any] = ()): + return tuple(parameter.max for parameter in device.parameters) + + def chain_device_get_parameters_is_quantized(device, params: Tuple[Any] = ()): + return tuple(parameter.is_quantized for parameter in device.parameters) + + def chain_device_set_parameters_value(device, params: Tuple[Any] = ()): + for index, value in enumerate(params): + device.parameters[index].value = value + + self.osc_server.add_handler("/live/chain/device/get/num_parameters", create_chain_device_callback(chain_device_get_num_parameters)) + self.osc_server.add_handler("/live/chain/device/get/parameters/name", create_chain_device_callback(chain_device_get_parameters_name)) + self.osc_server.add_handler("/live/chain/device/get/parameters/value", create_chain_device_callback(chain_device_get_parameters_value)) + self.osc_server.add_handler("/live/chain/device/get/parameters/min", create_chain_device_callback(chain_device_get_parameters_min)) + self.osc_server.add_handler("/live/chain/device/get/parameters/max", create_chain_device_callback(chain_device_get_parameters_max)) + self.osc_server.add_handler("/live/chain/device/get/parameters/is_quantized", create_chain_device_callback(chain_device_get_parameters_is_quantized)) + self.osc_server.add_handler("/live/chain/device/set/parameters/value", create_chain_device_callback(chain_device_set_parameters_value)) + + #-------------------------------------------------------------------------------- + # Chain device: Get/set individual parameters + #-------------------------------------------------------------------------------- + def chain_device_get_parameter_value(device, params: Tuple[Any] = ()): + param_index = int(params[0]) + return param_index, device.parameters[param_index].value + + def chain_device_get_parameter_value_string(device, params: Tuple[Any] = ()): + param_index = int(params[0]) + return param_index, device.parameters[param_index].str_for_value(device.parameters[param_index].value) + + def chain_device_set_parameter_value(device, params: Tuple[Any] = ()): + param_index, param_value = params[:2] + param_index = int(param_index) + device.parameters[param_index].value = param_value + + def chain_device_get_parameter_name(device, params: Tuple[Any] = ()): + param_index = int(params[0]) + return param_index, device.parameters[param_index].name + + self.osc_server.add_handler("/live/chain/device/get/parameter/value", create_chain_device_callback(chain_device_get_parameter_value)) + self.osc_server.add_handler("/live/chain/device/get/parameter/value_string", create_chain_device_callback(chain_device_get_parameter_value_string)) + self.osc_server.add_handler("/live/chain/device/get/parameter/name", create_chain_device_callback(chain_device_get_parameter_name)) + self.osc_server.add_handler("/live/chain/device/set/parameter/value", create_chain_device_callback(chain_device_set_parameter_value)) \ No newline at end of file diff --git a/manager.py b/manager.py index 94753c4..d13dab8 100644 --- a/manager.py +++ b/manager.py @@ -97,6 +97,7 @@ def show_message_callback(params): abletonosc.ClipSlotHandler(self), abletonosc.TrackHandler(self), abletonosc.DeviceHandler(self), + abletonosc.ChainHandler(self), abletonosc.ViewHandler(self), abletonosc.SceneHandler(self), abletonosc.MidiMapHandler(self), @@ -124,6 +125,7 @@ def reload_imports(self): importlib.reload(abletonosc.clip) importlib.reload(abletonosc.clip_slot) importlib.reload(abletonosc.device) + importlib.reload(abletonosc.chain) importlib.reload(abletonosc.handler) importlib.reload(abletonosc.osc_server) importlib.reload(abletonosc.scene) diff --git a/tests/test_chain.py b/tests/test_chain.py new file mode 100644 index 0000000..8e536f3 --- /dev/null +++ b/tests/test_chain.py @@ -0,0 +1,400 @@ +from . import client, wait_one_tick, TICK_DURATION + +#-------------------------------------------------------------------------------- +# Test chain discovery +#-------------------------------------------------------------------------------- + +def test_device_get_num_chains(client): + """ + Test getting the number of chains from a rack device. + Assumes Track 0 has a Drum Rack at device index 0. + """ + track_id = 0 + rack_device_id = 0 + + response = client.query("/live/device/get/num_chains", (track_id, rack_device_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert isinstance(response[2], int) + assert response[2] >= 1 # At least one chain should exist + + +def test_chain_get_num_devices(client): + """ + Test getting the number of devices in a chain. + Assumes Track 0, Drum Rack at device 0, has at least 1 chain with devices. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + + response = client.query("/live/chain/get/num_devices", (track_id, rack_device_id, chain_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert isinstance(response[3], int) + assert response[3] >= 1 # At least one device should exist + + +#-------------------------------------------------------------------------------- +# Test chain device discovery +#-------------------------------------------------------------------------------- + +def test_chain_device_get_devices_name(client): + """ + Test getting all device names in a chain. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + + response = client.query("/live/chain/device/get/devices/name", (track_id, rack_device_id, chain_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + + # Get number of devices to verify count + num_devices_response = client.query("/live/chain/get/num_devices", (track_id, rack_device_id, chain_id)) + num_devices = num_devices_response[3] + + # Response should have 3 prefix elements + num_devices device names + assert len(response) == 3 + num_devices + + # All device names should be strings + for i in range(3, len(response)): + assert isinstance(response[i], str) + + +def test_chain_device_get_devices_type(client): + """ + Test getting all device types in a chain. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + + response = client.query("/live/chain/device/get/devices/type", (track_id, rack_device_id, chain_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + + # Get number of devices to verify count + num_devices_response = client.query("/live/chain/get/num_devices", (track_id, rack_device_id, chain_id)) + num_devices = num_devices_response[3] + + # Response should have 3 prefix elements + num_devices device types + assert len(response) == 3 + num_devices + + # All device types should be integers + for i in range(3, len(response)): + assert isinstance(response[i], int) + + +#-------------------------------------------------------------------------------- +# Test chain device parameters +#-------------------------------------------------------------------------------- + +def test_chain_device_get_num_parameters(client): + """ + Test getting the number of parameters for a device in a chain. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + + response = client.query("/live/chain/device/get/num_parameters", (track_id, rack_device_id, chain_id, chain_device_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + assert isinstance(response[4], int) + assert response[4] >= 1 # At least one parameter should exist + + +def test_chain_device_get_parameters_name(client): + """ + Test getting all parameter names for a device in a chain. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + + response = client.query("/live/chain/device/get/parameters/name", (track_id, rack_device_id, chain_id, chain_device_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + + # Get number of parameters to verify count + num_params_response = client.query("/live/chain/device/get/num_parameters", (track_id, rack_device_id, chain_id, chain_device_id)) + num_params = num_params_response[4] + + # Response should have 4 prefix elements + num_params parameter names + assert len(response) == 4 + num_params + + # All parameter names should be strings + for i in range(4, len(response)): + assert isinstance(response[i], str) + + +def test_chain_device_get_parameter_value(client): + """ + Test getting an individual parameter value. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + param_id = 1 + + response = client.query("/live/chain/device/get/parameter/value", (track_id, rack_device_id, chain_id, chain_device_id, param_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + assert response[4] == param_id + assert isinstance(response[5], (int, float)) + + +def test_chain_device_get_parameter_value_string(client): + """ + Test getting parameter value as a formatted string. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + param_id = 1 + + response = client.query("/live/chain/device/get/parameter/value_string", (track_id, rack_device_id, chain_id, chain_device_id, param_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + assert response[4] == param_id + assert isinstance(response[5], str) + assert len(response[5]) > 0 # String should not be empty + + +#-------------------------------------------------------------------------------- +# Test chain properties +#-------------------------------------------------------------------------------- + +def test_chain_get_set_name(client): + """ + Test getting and setting chain name. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + + # Get original name + original_response = client.query("/live/chain/get/name", (track_id, rack_device_id, chain_id)) + assert original_response[0] == track_id + assert original_response[1] == rack_device_id + assert original_response[2] == chain_id + original_name = original_response[3] + + # Set new name + test_name = "TestChain" + client.send_message("/live/chain/set/name", (track_id, rack_device_id, chain_id, test_name)) + wait_one_tick() + + # Verify new name + new_response = client.query("/live/chain/get/name", (track_id, rack_device_id, chain_id)) + assert new_response[3] == test_name + + # Restore original name + client.send_message("/live/chain/set/name", (track_id, rack_device_id, chain_id, original_name)) + wait_one_tick() + + +def test_chain_get_set_mute(client): + """ + Test getting and setting chain mute state. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + + # Get original mute state + original_response = client.query("/live/chain/get/mute", (track_id, rack_device_id, chain_id)) + original_mute = original_response[3] + + # Toggle mute + new_mute = 0 if original_mute else 1 + client.send_message("/live/chain/set/mute", (track_id, rack_device_id, chain_id, new_mute)) + wait_one_tick() + + # Verify new state + new_response = client.query("/live/chain/get/mute", (track_id, rack_device_id, chain_id)) + assert new_response[3] == new_mute + + # Restore original state + client.send_message("/live/chain/set/mute", (track_id, rack_device_id, chain_id, original_mute)) + wait_one_tick() + + +def test_chain_get_has_audio_input(client): + """ + Test getting chain audio input capability. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + + response = client.query("/live/chain/get/has_audio_input", (track_id, rack_device_id, chain_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert isinstance(response[3], int) + + +def test_chain_get_color_index(client): + """ + Test getting chain color index. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + + response = client.query("/live/chain/get/color_index", (track_id, rack_device_id, chain_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert isinstance(response[3], int) + + +#-------------------------------------------------------------------------------- +# Test individual chain device properties +#-------------------------------------------------------------------------------- + +def test_chain_device_get_class_name(client): + """ + Test getting chain device class name. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + + response = client.query("/live/chain/device/get/class_name", (track_id, rack_device_id, chain_id, chain_device_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + assert isinstance(response[4], str) + assert len(response[4]) > 0 + + +def test_chain_device_get_name(client): + """ + Test getting chain device name. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + + response = client.query("/live/chain/device/get/name", (track_id, rack_device_id, chain_id, chain_device_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + assert isinstance(response[4], str) + assert len(response[4]) > 0 + + +def test_chain_device_get_type(client): + """ + Test getting chain device type. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + + response = client.query("/live/chain/device/get/type", (track_id, rack_device_id, chain_id, chain_device_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + assert isinstance(response[4], int) + + +#-------------------------------------------------------------------------------- +# Test chain device parameter metadata +#-------------------------------------------------------------------------------- + +def test_chain_device_get_parameters_min_max(client): + """ + Test getting parameter min and max values. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + + # Get min values + min_response = client.query("/live/chain/device/get/parameters/min", (track_id, rack_device_id, chain_id, chain_device_id)) + assert min_response[0] == track_id + assert min_response[1] == rack_device_id + assert min_response[2] == chain_id + assert min_response[3] == chain_device_id + + # Get max values + max_response = client.query("/live/chain/device/get/parameters/max", (track_id, rack_device_id, chain_id, chain_device_id)) + assert max_response[0] == track_id + assert max_response[1] == rack_device_id + assert max_response[2] == chain_id + assert max_response[3] == chain_device_id + + # Should have same length + assert len(min_response) == len(max_response) + + # All values should be numeric + for i in range(4, len(min_response)): + assert isinstance(min_response[i], (int, float)) + assert isinstance(max_response[i], (int, float)) + + +def test_chain_device_get_parameters_is_quantized(client): + """ + Test getting parameter quantization status. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + + response = client.query("/live/chain/device/get/parameters/is_quantized", (track_id, rack_device_id, chain_id, chain_device_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + + # All values should be integers (booleans as 0/1) + for i in range(4, len(response)): + assert isinstance(response[i], int) + + +def test_chain_device_get_parameter_name(client): + """ + Test getting individual parameter name. + """ + track_id = 0 + rack_device_id = 0 + chain_id = 0 + chain_device_id = 0 + param_id = 1 + + response = client.query("/live/chain/device/get/parameter/name", (track_id, rack_device_id, chain_id, chain_device_id, param_id)) + assert response[0] == track_id + assert response[1] == rack_device_id + assert response[2] == chain_id + assert response[3] == chain_device_id + assert response[4] == param_id + assert isinstance(response[5], str) + assert len(response[5]) > 0 From 04bd7913fa9e5fe63798ba4577079cbcc53c0d69 Mon Sep 17 00:00:00 2001 From: Savinay Nangalia Date: Thu, 1 Jan 2026 17:40:26 -0800 Subject: [PATCH 2/2] update json --- abletonosc/song.py | 62 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 14 deletions(-) diff --git a/abletonosc/song.py b/abletonosc/song.py index d9d7e01..af62509 100644 --- a/abletonosc/song.py +++ b/abletonosc/song.py @@ -163,6 +163,52 @@ def song_get_track_data(params): self.osc_server.add_handler("/live/song/get/track_data", song_get_track_data) + def serialize_device(device): + """ + Recursively serialize a device, including its parameters and chains (if it's a rack). + """ + device_data = { + "class_name": device.class_name, + "type": device.type, + "name": device.name, + "parameters": [] + } + + # Export parameters + for parameter in device.parameters: + device_data["parameters"].append({ + "name": parameter.name, + "value": parameter.value, + "min": parameter.min, + "max": parameter.max, + "is_quantized": parameter.is_quantized, + }) + + # Check if device has chains (is a rack) + try: + chains = device.chains + device_data["chains"] = [] + + for chain_index, chain in enumerate(chains): + chain_data = { + "index": chain_index, + "name": chain.name, + "devices": [] + } + + # Recursively serialize chain devices + for chain_device_index, chain_device in enumerate(chain.devices): + chain_device_data = serialize_device(chain_device) + chain_device_data["index"] = chain_device_index + chain_data["devices"].append(chain_device_data) + + device_data["chains"].append(chain_data) + except AttributeError: + # Not a rack device, skip chains + pass + + return device_data + def song_export_structure(params): tracks = [] for track_index, track in enumerate(self.song.tracks): @@ -187,20 +233,8 @@ def song_export_structure(params): track_data["clips"].append(clip_data) for device_index, device in enumerate(track.devices): - device_data = { - "class_name": device.class_name, - "type": device.type, - "name": device.name, - "parameters": [] - } - for parameter in device.parameters: - device_data["parameters"].append({ - "name": parameter.name, - "value": parameter.value, - "min": parameter.min, - "max": parameter.max, - "is_quantized": parameter.is_quantized, - }) + device_data = serialize_device(device) + device_data["index"] = device_index track_data["devices"].append(device_data) tracks.append(track_data)