-
Notifications
You must be signed in to change notification settings - Fork 0
/
configuration.py
330 lines (280 loc) · 11.5 KB
/
configuration.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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
from __future__ import annotations
import json
from logging import getLogger
from typing import Dict, Iterable, List, NamedTuple, Optional, Tuple, Union
from .mappings import (
ACTION_MAPPINGS,
NAVIGATION_TARGET_MAPPINGS,
get_element,
get_key_position,
)
from .types import (
Action,
ClipSlotAction,
KeyNumber,
KeySafetyStrategy,
MainMode,
NavigationTarget,
Quantization,
TrackControl,
)
logger = getLogger(__name__)
Numeric = Union[int, float]
# Song-specific config via clip names. Prefixes are followed by JSON strings. To replace
# your entire config, add a clip with a name like:
#
# ms={"initial_mode": "device_parameters_pressure"}
#
# To keep your overall config but replace some fields, add a clip with a name like:
#
# ms<{"wide_clip_launch": true}
#
SONG_CONFIGURATION_REPLACE_PREFIX = "ms="
SONG_CONFIGURATION_MERGE_PREFIX = "ms<"
# Global control surface configuration. To customize, create a file called `user.py` in
# the repository root directory, and export a `configuration` object, for example:
#
# # user.py
# from .control_surface.configuration import Configuration
#
# configuration = Configuration(
# # ...
# )
#
# See `types.py` for lists of possible values for `MainMode`, `KeySafetyStrategy`, etc.
class Configuration(NamedTuple):
# Startup mode.
initial_mode: MainMode = "transport"
# Initial "previous mode", i.e. the mode when the mode button is long-pressed.
initial_last_mode: MainMode = "mode_select"
# Whether to auto-arm/"pink arm" selected tracks. Can be toggled from utility
# mode. If you have other control surfaces (e.g. Push) which enable auto-arm, they
# may override this behavior.
auto_arm: bool = False
# Backlight on/off state (or `None` to leave it unmanaged) to be set at
# startup.
backlight: Optional[bool] = None
# Backlight on/off state (or `None` to leave it unmanaged) to be set at
# exit.
disconnect_backlight: Optional[bool] = None
# Add a behavior when long pressing a clip (currently just "stop_track_clips" is available).
clip_long_press_action: Optional[ClipSlotAction] = None
# If true, use a 1x8 box for the clip launch grid, instead of 2x4.
wide_clip_launch: bool = False
# Quantization settings.
quantize_to: Quantization = "sixtenth" # [sic]
quantize_amount: float = 1.0 # 1.0 is full quantization.
# Whether to scroll the session ring along with scenes/tracks.
link_session_ring_to_scene_selection: bool = False
link_session_ring_to_track_selection: bool = False
# The behavior when multiple keys are pressed at the same time.
key_safety_strategy: KeySafetyStrategy = "all_keys"
# The CC value at which keys should be considered fully pressed. Lower values ==
# more sensitive.
full_pressure: int = 37
# The range of CC value change per second for incremental controls. The first number
# is the change per second when the control is lightly pressed, the second
# is the change per second when the control is fully pressed.
incremental_steps_per_second: Tuple[Numeric, Numeric] = (10, 127)
# A tuple with the (completely_off, completely_on) CC values for your expression
# pedal.
expression_pedal_range: Tuple[int, int] = (0, 127)
# The delta in CC value that constitutes intentional movement of the expression
# pedal. Any smaller movements will be ignored. Use this if your expression pedal
# sends CC noise when it's not being touched.
expression_pedal_movement_threshold: int = 2
# Initial device parameter index (from 0-7) to be controlled by the expression
# pedal.
initial_expression_parameter: Optional[int] = None
# Program change message (0-indexed) to be sent before switching the SoftStep out of
# standalone mode and into hosted mode. Program changes 0 to 15 will load presets
# 1-16 in your setlist.
#
# Use this to prevent LED states for toggle buttons from getting overwritten in your
# other standalone presets while using modeStep.
#
# To avoid interference with the display when using the nav pad, make sure the nav's
# display mode is set to None in this preset.
background_program: Optional[int] = None
# Program change message (0-indexed) to be sent when exiting Live, after the
# SoftStep has been placed back into standalone mode.
disconnect_program: Optional[int] = None
# Customize the track controls on mode select keys 1-5. For example:
#
# override_track_controls = {1: (
# # Top control.
# "solo",
# # Bottom control.
# "arm",
# # Key 5 action.
# "stop_all_clips"
# )}
#
override_track_controls: Dict[
KeyNumber, Optional[Tuple[TrackControl, TrackControl, Action]]
] = {}
# Override the key safety strategy for specific modes, for example:
#
# key_safety_strategy = "all_keys"
# override_key_safety_strategies = {"device_parameters_xy": "adjacent_lockout"}
#
override_key_safety_strategies: Dict[MainMode, KeySafetyStrategy] = {}
# Customize keys on the mode select screen. For example, to load your own standalone
# programs on key 5 short/long press:
#
# override_modes = {5: ("standalone_1", "standalone_2")}
#
override_modes: Dict[KeyNumber, Optional[Tuple[MainMode, Optional[MainMode]]]] = {}
# Override specific control elements in any mode. You can use the helpers in this
# file to override keys with known actions and nav controls:
#
# override_elements = {"transport": [
# # Replace the Tap Tempo button with Capture MIDI.
# override_key_with_action(9, "capture_midi"),
#
# # Replace the Metronome button with device nav controls.
# override_key_with_nav(4, horizontal="selected_device", vertical="device_bank"),
#
# # Override the main nav controls.
# override_nav(horizontal="session_ring_tracks", vertical="session_ring_scenes")
# ]}
#
# If you're adventurous and/or know your way around Live v3 control surfaces, you
# can add more complex overrides for any (element, component, attribute):
#
# override_elements = {"transport": [
# ("grid_left_pressure_sliders", "Device", "parameter_controls")
# ]}
#
override_elements: Dict[
MainMode,
Iterable[
Union[ElementOverride, List[ElementOverride], Tuple[ElementOverride, ...]]
],
] = {}
def get_configuration(song) -> Configuration:
# Load a local configuration if possible, or fall back to the default.
local_configuration: Optional[Configuration] = None
try:
from ..user import ( # type: ignore
configuration as local_configuration, # type: ignore
)
logger.info("loaded local configuration")
except (ImportError, ModuleNotFoundError):
logger.info("loaded default configuration")
configuration = local_configuration or Configuration()
# Try to load song-specific config.
try:
assert song
for track in song.tracks:
for clip_slot in track.clip_slots:
clip = clip_slot.clip
if clip is not None:
name: Optional[str] = clip.name
if name is not None:
json_str: Optional[str] = None
replace: bool = False
for prefix, replaces in (
(
SONG_CONFIGURATION_REPLACE_PREFIX,
True,
),
(SONG_CONFIGURATION_MERGE_PREFIX, False),
):
if name.startswith(prefix):
json_str = name[len(prefix) :]
replace = replaces
if json_str is not None:
new_configuration_attrs = (
{} if replace else configuration._asdict()
)
new_configuration_attrs.update(json.loads(json_str))
# This technically isn't correctly typed, since the parsed
# object contains arrays instead of tuples. Hopefully tests
# would catch any related breakage.
configuration = Configuration(**new_configuration_attrs)
logger.info("loaded song configuration")
except Exception as e:
logger.warning(f"error reading song config: {e}")
logger.info(f"using configuration: {configuration._asdict()}")
return configuration
# Element name, component name, attribute name, e.g. ("grid_pressure_sliders", "Device",
# "parameter_controls").
ElementOverride = Tuple[str, str, str]
##
# Helpers for overriding elements, see above for usage examples. Keys are the
# physical key numbers on the SoftStep.
# Override a key with an action.
def override_key_with_action(key: int, action: Action) -> ElementOverride:
row, col = get_key_position(key)
action_mapping = ACTION_MAPPINGS[action]
return (
get_element("buttons", row=row, col=col),
action_mapping[0],
action_mapping[1],
)
# Set a key's directional sensors to nav controls.
def override_key_with_nav(
key: int,
horizontal: Optional[NavigationTarget] = None,
vertical: Optional[NavigationTarget] = None,
) -> List[ElementOverride]:
row, col = get_key_position(key)
left, right, down, up = [
get_element(f"{dir}_buttons", row, col)
for dir in ("left", "right", "down", "up")
]
return _override_elements_with_nav(
left=left,
right=right,
down=down,
up=up,
horizontal=horizontal,
vertical=vertical,
)
# Override the main nav pad assignments.
def override_nav(
horizontal: Optional[NavigationTarget] = None,
vertical: Optional[NavigationTarget] = None,
) -> List[ElementOverride]:
return _override_elements_with_nav(
left="nav_left_button",
right="nav_right_button",
down="nav_down_button",
up="nav_up_button",
horizontal=horizontal,
vertical=vertical,
)
def _override_elements_with_nav(
# Names of elements representing each direction.
left: str,
right: str,
up: str,
down: str,
# Nav to apply in the horizontal/vertical directions, or None if no nav should be specified.
horizontal: Optional[NavigationTarget],
vertical: Optional[NavigationTarget],
) -> List[ElementOverride]:
results: List[ElementOverride] = []
elements_and_targets: Iterable[Tuple[str, str, Optional[NavigationTarget]]] = (
(left, right, horizontal),
# The down button increases values - think selected scene.
(up, down, vertical),
)
for down_element, up_element, target in elements_and_targets:
# Get a unique control name based on an element name.
def background_control_name(element: str):
return element.replace("[", "_").replace("]", "_")
component, down_button, up_button = (
# If no target specified, disable the elements.
(
"Background",
background_control_name(left),
background_control_name(right),
)
if target is None
else NAVIGATION_TARGET_MAPPINGS[target]
)
results.append((down_element, component, down_button))
results.append((up_element, component, up_button))
return results