-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathGenericDeviceController.py
141 lines (127 loc) · 6.22 KB
/
GenericDeviceController.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# GenericDevoceController
# - Control devices (both selected ones and the ChannelEq devices in the mixer)
#
# Part of ElectraOne
#
# Ableton Live MIDI Remote Script for the Electra One
#
# Author: Jaap-henk Hoepman ([email protected])
#
# Distributed under the MIT License, see LICENSE
#
# Ableton Live imports
import Live
# Local imports
from .config import USE_ABLETON_VALUES
from .CCInfo import CCInfo, CCMap, UNMAPPED_ID
from .ElectraOneBase import ElectraOneBase
from .UniqueParameters import make_device_parameters_unique
class GenericDeviceController(ElectraOneBase):
"""Control devices (both selected ones and the ChannelEq devices
in the mixer): build MIDI maps, refresh state
"""
def __init__(self, c_instance, device, cc_map):
"""Create a new device controller for the device and the
associated cc-map. It is assumed the preset is already
present on the E1 or will be uploaded there shortly
(with controls matching the description in cc_map)
- c_instance: Live interface object (see __init.py__)
- device: the device; Live.Device.Device
- cc_map: the preset cc-map; CCMap
"""
ElectraOneBase.__init__(self, c_instance)
self._device = device # TODO: needed to detect whether device is already deleted
self._parameters = make_device_parameters_unique(device)
self._device_name = self.get_device_name(device)
self._cc_map = cc_map
# dictionary to keep track of string value updates
self._values = { }
def build_midi_map(self, midi_map_handle):
"""Build a MIDI map for the device
- midi_map_hanlde: MIDI map handle as passed to Ableton Live, to
which MIDI mappings must be added.
"""
# device may already be deleted while this controller still exists
if not self._device:
return
assert self._cc_map != None, 'No CC map present while building MIDI map.'
self.debug(3,f'Building MIDI map for device { self._device_name }')
# TODO/FIXME: not clear how this is honoured in the Live.MidiMap.map_midi_cc call
needs_takeover = True
for p in self._parameters:
ccinfo = self._cc_map.get_cc_info(p)
if ccinfo.is_mapped():
if ccinfo.is_cc14():
map_mode = Live.MidiMap.MapMode.absolute_14_bit
else:
map_mode = Live.MidiMap.MapMode.absolute
cc_no = ccinfo.get_cc_no()
midi_channel = ccinfo.get_midi_channel()
self.debug(3,f'Mapping { p.original_name } ({p.name}) to CC { cc_no } on MIDI channel { midi_channel }')
# Ableton internally numbers MIDI channels 0..15
Live.MidiMap.map_midi_cc(midi_map_handle, p, midi_channel-1, cc_no, map_mode, not needs_takeover)
else:
self.debug(3,f'{ p.original_name } ({p.name}) not mapped.')
def _refresh_parameter(self, p, ccinfo, force):
"""Update the displayed values for a single control on the E1. If force,
MIDI CC is also updated, and possible string value update is always
sent.
(Assumes the device is visible!)
- p: parameter; Live.DeviceParameter.DeviceParameter
- ccinfo: information about the CC mapping; CCInfo
- force: whether to always send the valuestr, or only if changed.
Used to distinguish a state refresh from a value update; bool
"""
# update MIDI value on the E1 if full refresh is requested
if force:
self.send_parameter_using_ccinfo(p,ccinfo)
# update control with Ableton value string when mapped
# as such, if forced or if the value changed since last update/refresh
control_tuple = ccinfo.get_control_id()
(control_id,value_id) = control_tuple
if (control_id != UNMAPPED_ID) and USE_ABLETON_VALUES:
pstr = str(p)
# check whether sending the string value is really needed
if force or \
(control_tuple not in self._values) or \
(self._values[control_tuple] != pstr):
self._values[control_tuple] = pstr
self.debug(4,f'Value of {p.original_name} ({p.name}) (of parameter {id(p)}) updated to {pstr}.')
self.send_value_update(control_id,value_id,pstr)
def _refresh(self,full_refresh):
"""Refresh the state of the controls on the E1.
If full_refresh, send MIDI CC updates for *all* parameters
(brings MIDI info on E1 in sync with Ableton state) and update
the string values for *all* controls whose string value must be
determined by Ableton.
If not full_refresh, only update the string values for controls
whose value changed since the last _refresh
(Assumes the device is visible!)
- full_refresh: whether this is a full refresh: then MIDI CC values
need to be refreshed too, and string value updates must always
be sent; boolean
"""
# device may already be deleted while this controller still exists
if not self._device:
return
assert self._cc_map != None, 'No CC map present while refreshing device state.'
if full_refresh:
self.debug(3,f'Full state refresh for device { self._device_name }')
else:
self.debug(6,f'Partial state refresh for device { self._device_name }')
for p in self._parameters:
ccinfo = self._cc_map.get_cc_info(p)
if ccinfo.is_mapped():
self._refresh_parameter(p,ccinfo,full_refresh)
def refresh_state(self):
"""Update both the MIDI CC values and the displayed values for the
device on the E1. (Assumes the device is visible!)
"""
self._refresh(True)
def update_display(self):
"""Called every 100 ms; used to update values for controls
that want Ableton to set their value string. This way we do not
need to register value change listeners for every parameter.
(Assumes the device is visible!)
"""
self._refresh(False)