-
Notifications
You must be signed in to change notification settings - Fork 3
/
mixer.py
402 lines (318 loc) · 17.1 KB
/
mixer.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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
# MIT License
# Copyright (c) 2024 Pablo Peral
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
Submodule of flmidi-nihia for mixer manipulation of Komplete Kontrol keyboards.
"""
import nihia
import device
import math
###########################################################################################################################################
# Dictionaries and constants
###########################################################################################################################################
# Knob to knob ID matrix
# Each knob has two possible DATA1 values, one for volume and another for pan adjustment
# The rows specify the knob mode (volume adjustment and shifted/pan adjustment knobs)
# The columns specify the knob
# Example:
# knobs[0][0] --> First knob without shift. It is meant to adjust volume.
# knobs[0][1] --> First knob shifted (SHIFT button is being held down while using the knob). It is meant to adjust panning.
knobs = [
[80, 81, 82, 83, 84, 85, 86, 87],
[88, 89, 90, 91, 92, 93, 94, 95]
]
# The DATA2 value of a knob turning message specifies the speed the user is turning the knob at
# On S-Series keyboards, the knobs are speed-sensitive and send different DATA2 values to specify the turning speed
# - If turned clockwise, the speed will go from 0 to 63 (slowest to fastest)
# - If turned counterclockwise, the speed will go from 127 to 65 (slowest to fastest, they are inverted)
# On A/M-Series, the knobs aren't speed-sensitive and they will always report the max value for each direction
# - If turned clockwise, the DATA2 value will be 63
# - If turned counterclockwise, the DATA2 value will be 65
KNOB_INCREASE_MIN_SPEED = 0 # DATA2 byte sent by the keyboard on clockwise knob turning messages at minimum speed
KNOB_INCREASE_MAX_SPEED = 63 # DATA2 byte sent by the keyboard on clockwise knob turning messages at maximum speed
KNOB_DECREASE_MIN_SPEED = 127 # DATA2 byte sent by the keyboard on counterclockwise knob turning messages at minimum speed
KNOB_DECREASE_MAX_SPEED = 65 # DATA2 byte sent by the keyboard on counterclockwise knob turning messages at maximum speed
# Dictionary that goes between the different kinds of information that can be sent to the device to specify information about the mixer tracks
# and their corresponding identification bytes
mixerinfo_types = {
"VOLUME": 70,
"PAN": 71,
"IS_MUTE": 67,
"IS_SOLO": 68,
"NAME": 72,
"IS_ARMED": 69,
"MUTED_BY_SOLO": 74,
"MUTE_SELECTED": 102,
"SOLO_SELECTED": 103,
# This one makes more sense on DAWs that create more tracks as the user requests it, as there might be projects (for example) on Ableton Live
# with only two tracks
# However, since FL Studio has all playlist and mixer tracks created, it has no use at all (maybe on the channel rack) and all tracks should have
# their existence reported as 1 (which means the track exists) in order to light on the Mute and Solo buttons on the device
"EXIST": 64,
"SELECTED": 66,
"SELECTED_AVAILABLE": 104,
"SELECTED_MUTE_BY_SOLO": 105,
# This one only will make an effect on devices with full feature support, like the S-Series MK2 and it's used to send the peak meter information
"PEAK": 73,
# Serves to tell the device if there's a Komplete Kontrol instance added in a certain track or not
# In case there's one, we would use mixerSendInfo("KOMPLETE_INSTANCE", trackID, info="NIKBxx")
# In case there's none, we would use mixerSendInfo("KOMPLETE_INSTANCE", trackID, info="")
# NIKBxx is the name of the first automation parameter of the Komplete Kontrol plugin
"KOMPLETE_INSTANCE": 65,
# The S-Series keyboard have two arrows that graphically show the position of the volume fader and the pan on the screen
# These definitions have the MIDI values that have to be set as the data1 value of a simple MIDI message to tell the device where the volume arrow
# or the pan arrow should be for the first track
# For the rest of the tracks, you sum incrementally
# Example:
# ----------------------------------------------------
# BF 50 00 // Moves the volume fader of the first track down to the bottom
# BF 50 40 // Moves the volume fader of the first track to the middle
#
# BF 51 00 // Moves the volume fader of the second track down to the bottom
# BF 51 40 // Moves the volume fader of the second track to the middle
#
# BF 58 00 // Moves the pan fader of the first track down to the bottom
# BF 58 40 // Moves the pan fader of the first track to the middle
#
# BF 59 00 // Moves the pan fader of the second track down to the bottom
# BF 59 40 // Moves the pan fader of the second track to the middle
"VOLUME_GRAPH": 80,
"PAN_GRAPH": 88,
}
# Track types dictionary
# Used when reporting existence of tracks
track_types = {
"EMPTY": 0,
"GENERIC": 1,
"MIDI": 2,
"AUDIO": 3,
"GROUP": 4,
"RETURN_BUS": 5,
"MASTER": 6
}
# Methods for reporting information about the mixer tracks, which is done through SysEx
def setTrackExist(trackID: int, value: int or str):
""" Method to report existence of a track and update its type as well.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (int or str): Track type from `track_types`.
"""
if value == str:
value = track_types.get(track_types)
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("EXIST"), value, trackID] + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackName(trackID: int, name: str):
""" Method to update the name of a track being displayed on the device.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- name (str): Name of the track.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("NAME"), 0, trackID] + Str2Bytes(name) + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackPan(trackID: int, value: str):
""" Method to update the pan string of a track being displayed on the device.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (str): String to show on the display as the pan of the track.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("PAN"), 0, trackID] + Str2Bytes(value) + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackVol(trackID: int, value: str):
""" Method to update the volume string of a track being displayed on the device.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (str): String to show on the display as the volume of the track.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("VOLUME"), 0, trackID] + Str2Bytes(value) + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackArm(trackID: int, value: bool):
""" Method to report the arm for recording state state of a track.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (Bool): Selection status.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("IS_ARMED"), value, trackID] + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackSel(trackID: int, value: bool):
""" Method to report selection state of a track.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (bool): Selection status.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("SELECTED"), value, trackID] + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackSolo(trackID:int, value: bool):
""" Method to report solo state of a track.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (bool): Solo status.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("IS_SOLO"), value, trackID] + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackMute(trackID:int, value: bool):
""" Method to report mute state of a track.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (bool): Mute status.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("IS_MUTE"), value, trackID] + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setTrackMutedBySolo(trackID:int, value: bool):
""" Method to report mute by solo state of a track.
### Arguments
- trackID (int): From 0 to 7, the number of the track being represented on the display.
- value (bool): Mute by solo status.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("MUTED_BY_SOLO"), value, trackID] + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
def setKompleteInstance(instanceID: str):
""" Method to report the currently selected Komplete Kontrol instance.
### Arguments
- instanceID (str): The `NIKBxx` string retrieved from the name of the first automation parameter of the Komplete Kontrol instance.
If it's left to nothing (`""`), the Komplete Kontrol integration will be disabled for that track.
"""
msg = nihia.SYSEX_HEADER + [mixerinfo_types.get("KOMPLETE_INSTANCE"), 0, 0] + Str2Bytes(instanceID) + [247]
device.midiOutSysex(bytes(msg))
def sendPeakMeterData(peakValues: list):
""" Send peak meter data to be displayed on the device.
### Arguments
- peakValues (list): A list of 16 integer values representing peak values for each track and stereo channel like ``[peakL_0, peakR_0, peakL_1, peakR_1 ...]``
in a range of 0 to 127. To convert values coming from FL Studio's ``mixer.getTrackPeaks()`` function, the recommended range is 0 to 1.1 so that any value
greater than 1.1 should be set to 1.1 anyway.
"""
# Conforms the kind of message midiOutSysex is waiting for
msg = [240, 0, 33, 9, 0, 0, 68, 67, 1, 0, mixerinfo_types.get("PEAK"), 2, 0] + peakValues + [247]
# Warps the data and sends it to the device
device.midiOutSysex(bytes(msg))
# Methods for changing the locations of the pan and volume arrows on the screen of S-Series devices to graphically show where the pan and volume faders are
def setTrackVolGraph(trackID: int, location: float):
""" Method for changing the location of the volume arrow of a track on the screen of S-Series MK2 devices to graphically show where the volume fader is.
### Arguments
- trackID: From 0 to 7, the track whose the graph you want to update belongs to.
- location: Can be filled using `mixer.getTrackVolume()`, expecting a `0 <= x <= 1` range.
"""
# Gets the right data1 value to update the volume graph
graphValue = mixerinfo_types.get("VOLUME_GRAPH")
# Translates the 0-1 range given by FL Studio to 0-127 range
location = location * 127
# Truncates the possible decimals and declares the number as an integer to avoid errors in the translation of the data
location = int(math.trunc(location))
# Reports the change of the desired graph to the device
nihia.dataOut(graphValue + trackID, location)
def setTrackPanGraph(trackID: int, location: float):
""" Method for changing the location of the pan arrow of a track on the screen of S-Series MK2 devices to graphically show where the pan fader is.
### Arguments
- trackID: From 0 to 7, the track whose the graph you want to update belongs to.
- location: Can be filled using `mixer.getTrackPan()`, expecting a `-1 <= x <= 1` range.
"""
# Gets the right data1 value to update the pan graph
graphValue = mixerinfo_types.get("PAN_GRAPH")
# Translates the -1 to 1 range from FL Studio to 0-127 range
if location < 0: # If the pan is negative, for hence is set to the left
location = abs(location) # Gets the absolute value of the location
location = 64 - location * 64
elif location == 0: # If the pan is 0, for hence is set to the center
location = 64
elif location > 0: # If the pan is positive, for hence is set to the right
location = 64 + location * 63
# Truncates the possible decimals and declares the number as an integer to avoid errors in the translation of the data
location = int(math.trunc(location))
# Reports the change of the desired graph to the device
nihia.dataOut(graphValue + trackID, location)
# Deprecated due to not having enough knowledge about how this actually works TODO
# -----------------------
# def setSelTrack(info_type: str, info: str):
# """ Send selection state of a mixer track.
# ### Parameters
# - info_type: The data you are going to tell about the selected track.
# - SELECTED: If there's a track selected on the mixer or not.
# - MUTE_BY_SOLO: To tell if it's muted by solo.
# - info: The value of the info you are telling.
# - `info_type = SELECTED`: The track type as defined on `track_types`.
# - `info_type = MUTE_BY_SOLO`: Yes or no.
# """
# if info_type == "SELECTED":
# info_type = mixerinfo_types.get("SELECTED_AVAILABLE")
# info = track_types.get(info)
# # Not implemented yet in FL Studio
# elif info_type == "MUTE_BY_SOLO":
# info_type = mixerinfo_types.get("SELECTED_MUTE_BY_SOLO")
# # Sends the message
# dataOut(info_type, info)
def setCurrentTrackAvailable(track_type: int or str):
""" Unknown functionality TODO
### Arguments
- track_type (int or str): Track types as specified on `track_types`.
"""
if track_type == str:
track_type = track_types.get(track_type)
# Sends the message
nihia.dataOut(mixerinfo_types.get("SELECTED_AVAILABLE"), track_type)
def setCurrentTrackMuted(value: bool):
""" Sets the mute status of the currently selected track. Used just to highlight change the light of the MUTE button
between dimmed and highlighted. For this method to take an effect, track existence must have been declared beforehand.
### Arguments
- value (bool): Mute status.
"""
# Sends the message
nihia.dataOut(mixerinfo_types.get("MUTE_SELECTED"), value)
def setCurrentTrackSolo(value: bool):
""" Sets the solo status of the currently selected track. Used just to highlight change the light of the MUTE button
between dimmed and highlighted. For this method to take an effect, track existence must have been declared beforehand.
### Arguments
- value (bool): Mute status.
"""
# Sends the message
nihia.dataOut(mixerinfo_types.get("SOLO_SELECTED"), value)
def setCurrentTrackMuteBySolo(value: str):
"""
Unknown functionality TODO
### Arguments
- value (bool):
"""
# Sends the message
nihia.dataOut(mixerinfo_types.get("SELECTED_MUTE_BY_SOLO"), value)
def Str2Bytes(string: str) -> list:
""" Utility function that encodes a given string to a Python list of its corresponding values in the UTF-8 standard.
### Args
- str (str): String to encode
### Returns
- list: List of the corresponding bytes to the string specified on function call in the UTF-8 standard.
"""
# Tells Python that the additional_info argument is in UTF-8
string = string.encode("UTF-8")
# Converts the text string to a list of Unicode values
string = list(bytes(string))
return string