Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,75 @@ For devices:
</details>


---

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

<details>
<summary><b>Documentation</b>: Chain API</summary>

#### 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
```

</details>

---

## MidiMap API
Expand Down
1 change: 1 addition & 0 deletions abletonosc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
177 changes: 177 additions & 0 deletions abletonosc/chain.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have chain callbacks (/live/chain/) which refer to the properties of the chain itself and chain device callbacks (/live/chain/device/`) which refer to properties of devices within the chain.

Chain devices are different than top level devices because I think we want to access them inside the chain. This allows us to ask for and modify devices inside a chain.

is it okay that they are all in the same file? or would you like the chain device methods to be separated from the chain methods?

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"
]
Comment on lines +39 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: this could all be in constants because it doesn't change from invocation to invocation. Negligible performance impact. Minor memory impact on startup if you have a lot of device chains.


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))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

put this /live/device/get/num_chains endpoint in here since it is related to chains, but every other endpoint in here is /live/chain/....

Should I move this to live with the other device endpoints?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chains are part of devices so it almost feels like you want to do /live/device/chain/... as your namespace to better represent the hierarchy.

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))
Loading