This repository has been archived by the owner on Mar 3, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 9
/
melodic_mode.py
332 lines (284 loc) · 15.3 KB
/
melodic_mode.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
import definitions
import mido
import push2_python.constants
import time
class MelodicMode(definitions.PyshaMode):
xor_group = 'pads'
notes_being_played = []
root_midi_note = 0 # default redefined in initialize
scale_pattern = [True, False, True, False, True, True, False, True, False, True, False, True]
fixed_velocity_mode = False
use_poly_at = False # default redefined in initialize
channel_at_range_start = 401 # default redefined in initialize
channel_at_range_end = 800 # default redefined in initialize
poly_at_max_range = 40 # default redefined in initialize
poly_at_curve_bending = 50 # default redefined in initialize
latest_channel_at_value = (0, 0)
latest_poly_at_value = (0, 0)
latest_velocity_value = (0, 0)
last_time_at_params_edited = None
modulation_wheel_mode = False
lumi_midi_out = None
last_time_tried_initialize_lumi = 0
def init_lumi_midi_out(self):
print('Configuring LUMI notes MIDI out...')
self.last_time_tried_initialize_lumi = time.time()
device_name = "LUMI Keys BLOCK"
try:
full_name = [name for name in mido.get_output_names() if device_name.lower() in name.lower()][0]
except IndexError:
full_name = None
if full_name is not None:
try:
self.lumi_midi_out = mido.open_output(full_name)
print('Sending notes MIDI in to "{0}"'.format(full_name))
except IOError:
print('Could not connect to notes MIDI output port "{0}"\nAvailable device names:'.format(full_name))
else:
print('No available device name found for {}'.format(device_name))
def set_lumi_pressure_mode(self):
if self.lumi_midi_out is not None:
if not self.use_poly_at:
msg = mido.Message('sysex', data=[0x00, 0x21, 0x10, 0x77, 0x27, 0x10, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64])
else:
msg = mido.Message('sysex', data=[0x00, 0x21, 0x10, 0x77, 0x27, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04])
self.lumi_midi_out.send(msg)
def initialize(self, settings=None):
if settings is not None:
self.use_poly_at = settings.get('use_poly_at', True)
self.set_root_midi_note(settings.get('root_midi_note', 64))
self.channel_at_range_start = settings.get('channel_at_range_start', 401)
self.channel_at_range_end = settings.get('channel_at_range_end', 800)
self.poly_at_max_range = settings.get('poly_at_max_range', 40)
self.poly_at_curve_bending = settings.get('poly_at_curve_bending', 50)
self.init_lumi_midi_out()
def get_settings_to_save(self):
return {
'use_poly_at': self.use_poly_at,
'root_midi_note': self.root_midi_note,
'channel_at_range_start': self.channel_at_range_start,
'channel_at_range_end': self.channel_at_range_end,
'poly_at_max_range': self.poly_at_max_range,
'poly_at_curve_bending': self.poly_at_curve_bending,
}
def set_channel_at_range_start(self, value):
# Parameter in range [401, channel_at_range_end - 1]
if value < 401:
value = 401
elif value >= self.channel_at_range_end:
value = self.channel_at_range_end - 1
self.channel_at_range_start = value
self.last_time_at_params_edited = time.time()
def set_channel_at_range_end(self, value):
# Parameter in range [channel_at_range_start + 1, 2000]
if value <= self.channel_at_range_start:
value = self.channel_at_range_start + 1
elif value > 2000:
value = 2000
self.channel_at_range_end = value
self.last_time_at_params_edited = time.time()
def set_poly_at_max_range(self, value):
# Parameter in range [0, 127]
if value < 0:
value = 0
elif value > 127:
value = 127
self.poly_at_max_range = value
self.last_time_at_params_edited = time.time()
def set_poly_at_curve_bending(self, value):
# Parameter in range [0, 100]
if value < 0:
value = 0
elif value > 100:
value = 100
self.poly_at_curve_bending = value
self.last_time_at_params_edited = time.time()
def get_poly_at_curve(self):
pow_curve = [pow(e, 3*self.poly_at_curve_bending/100) for e in [i/self.poly_at_max_range for i in range(0, self.poly_at_max_range)]]
return [int(127 * pow_curve[i]) if i < self.poly_at_max_range else 127 for i in range(0, 128)]
def add_note_being_played(self, midi_note, source):
self.notes_being_played.append({'note': midi_note, 'source': source})
def remove_note_being_played(self, midi_note, source):
self.notes_being_played = [note for note in self.notes_being_played if note['note'] != midi_note or note['source'] != source]
def remove_all_notes_being_played(self):
self.notes_being_played = []
def pad_ij_to_midi_note(self, pad_ij):
return self.root_midi_note + ((7 - pad_ij[0]) * 5 + pad_ij[1])
def is_midi_note_root_octave(self, midi_note):
relative_midi_note = (midi_note - self.root_midi_note) % 12
return relative_midi_note == 0
def is_black_key_midi_note(self, midi_note):
relative_midi_note = (midi_note - self.root_midi_note) % 12
return not self.scale_pattern[relative_midi_note]
def is_midi_note_being_played(self, midi_note):
for note in self.notes_being_played:
if note['note'] == midi_note:
return True
return False
def note_number_to_name(self, note_number):
semis = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
note_number = int(round(note_number))
return semis[note_number % 12] + str(note_number//12 - 2)
def set_root_midi_note(self, note_number):
self.root_midi_note = note_number
if self.root_midi_note < 0:
self.root_midi_note = 0
elif self.root_midi_note > 127:
self.root_midi_note = 127
def activate(self):
# Configure polyAT and AT
if self.use_poly_at:
self.push.pads.set_polyphonic_aftertouch()
else:
self.push.pads.set_channel_aftertouch()
self.push.pads.set_channel_aftertouch_range(range_start=self.channel_at_range_start, range_end=self.channel_at_range_end)
self.push.pads.set_velocity_curve(velocities=self.get_poly_at_curve())
self.set_lumi_pressure_mode()
# Configure touchstrip behaviour
if self.modulation_wheel_mode:
self.push.touchstrip.set_modulation_wheel_mode()
else:
self.push.touchstrip.set_pitch_bend_mode()
# Update buttons and pads
self.update_buttons()
self.update_pads()
def deactivate(self):
self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_DOWN, definitions.BLACK)
self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_UP, definitions.BLACK)
self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.BLACK)
self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.BLACK)
def check_for_delayed_actions(self):
if self.last_time_at_params_edited is not None and time.time() - self.last_time_at_params_edited > definitions.DELAYED_ACTIONS_APPLY_TIME:
# Update channel and poly AT parameters
self.push.pads.set_channel_aftertouch_range(range_start=self.channel_at_range_start, range_end=self.channel_at_range_end)
self.push.pads.set_velocity_curve(velocities=self.get_poly_at_curve())
self.last_time_at_params_edited = None
def on_midi_in(self, msg, source=None):
# Update the list of notes being currently played so push2 pads can be updated accordingly
if msg.type == "note_on":
if msg.velocity == 0:
self.remove_note_being_played(msg.note, source)
else:
self.add_note_being_played(msg.note, source)
elif msg.type == "note_off":
self.remove_note_being_played(msg.note, source)
self.app.pads_need_update = True
def update_octave_buttons(self):
self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_DOWN, definitions.WHITE)
self.push.buttons.set_button_color(push2_python.constants.BUTTON_OCTAVE_UP, definitions.WHITE)
def update_accent_button(self):
if self.fixed_velocity_mode:
self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.BLACK)
self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION)
else:
self.push.buttons.set_button_color(push2_python.constants.BUTTON_ACCENT, definitions.OFF_BTN_COLOR)
def update_modulation_wheel_mode_button(self):
if self.modulation_wheel_mode:
self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.BLACK)
self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.WHITE, animation=definitions.DEFAULT_ANIMATION)
else:
self.push.buttons.set_button_color(push2_python.constants.BUTTON_SHIFT, definitions.OFF_BTN_COLOR)
def update_buttons(self):
self.update_octave_buttons()
self.update_modulation_wheel_mode_button()
self.update_accent_button()
def update_pads(self):
color_matrix = []
for i in range(0, 8):
row_colors = []
for j in range(0, 8):
corresponding_midi_note = self.pad_ij_to_midi_note([i, j])
cell_color = definitions.WHITE
if self.is_black_key_midi_note(corresponding_midi_note):
cell_color = definitions.BLACK
if self.is_midi_note_root_octave(corresponding_midi_note):
try:
cell_color = self.app.track_selection_mode.get_current_track_color()
except AttributeError:
cell_color = definitions.YELLOW
if self.is_midi_note_being_played(corresponding_midi_note):
cell_color = definitions.NOTE_ON_COLOR
row_colors.append(cell_color)
color_matrix.append(row_colors)
self.push.pads.set_pads_color(color_matrix)
def on_pad_pressed(self, pad_n, pad_ij, velocity):
midi_note = self.pad_ij_to_midi_note(pad_ij)
if midi_note is not None:
self.latest_velocity_value = (time.time(), velocity)
if self.app.track_selection_mode.get_current_track_info().get('illuminate_local_notes', True) or self.app.notes_midi_in is None:
# illuminate_local_notes is used to decide wether a pad/key should be lighted when pressing it. This will probably be the default behaviour,
# but in synth definitions this can be disabled because we will be receiving back note events at the "notes_midi_in" device and in this
# case we don't want to light the pad "twice" (or if the note pressed gets processed and another note is actually played we don't want to
# light the currently presed pad). However, if "notes_midi_in" input is not configured, we do want to liht the pad as we won't have
# notes info comming from any other source
self.add_note_being_played(midi_note, 'push')
msg = mido.Message('note_on', note=midi_note, velocity=velocity if not self.fixed_velocity_mode else 127)
self.app.send_midi(msg)
self.update_pads() # Directly calling update pads method because we want user to feel feedback as quick as possible
return True
def on_pad_released(self, pad_n, pad_ij, velocity):
midi_note = self.pad_ij_to_midi_note(pad_ij)
if midi_note is not None:
if self.app.track_selection_mode.get_current_track_info().get('illuminate_local_notes', True) or self.app.notes_midi_in is None:
# see comment in "on_pad_pressed" above
self.remove_note_being_played(midi_note, 'push')
msg = mido.Message('note_off', note=midi_note, velocity=velocity)
self.app.send_midi(msg)
self.update_pads() # Directly calling update pads method because we want user to feel feedback as quick as possible
return True
def on_pad_aftertouch(self, pad_n, pad_ij, velocity):
if pad_n is not None:
# polyAT mode
self.latest_poly_at_value = (time.time(), velocity)
midi_note = self.pad_ij_to_midi_note(pad_ij)
if midi_note is not None:
msg = mido.Message('polytouch', note=midi_note, value=velocity)
else:
# channel AT mode
self.latest_channel_at_value = (time.time(), velocity)
msg = mido.Message('aftertouch', value=velocity)
self.app.send_midi(msg)
return True
def on_touchstrip(self, value):
if self.modulation_wheel_mode:
msg = mido.Message('control_change', control=1, value=value)
else:
msg = mido.Message('pitchwheel', pitch=value)
self.app.send_midi(msg)
return True
def on_sustain_pedal(self, sustain_on):
msg = mido.Message('control_change', control=64, value=127 if sustain_on else 0)
self.app.send_midi(msg)
return True
def on_button_pressed(self, button_name):
if button_name == push2_python.constants.BUTTON_OCTAVE_UP:
self.set_root_midi_note(self.root_midi_note + 12)
self.app.pads_need_update = True
self.app.add_display_notification("Octave up: from {0} to {1}".format(
self.note_number_to_name(self.pad_ij_to_midi_note((7, 0))),
self.note_number_to_name(self.pad_ij_to_midi_note((0, 7))),
))
return True
elif button_name == push2_python.constants.BUTTON_OCTAVE_DOWN:
self.set_root_midi_note(self.root_midi_note - 12)
self.app.pads_need_update = True
self.app.add_display_notification("Octave down: from {0} to {1}".format(
self.note_number_to_name(self.pad_ij_to_midi_note((7, 0))),
self.note_number_to_name(self.pad_ij_to_midi_note((0, 7))),
))
return True
elif button_name == push2_python.constants.BUTTON_ACCENT:
self.fixed_velocity_mode = not self.fixed_velocity_mode
self.app.buttons_need_update = True
self.app.pads_need_update = True
self.app.add_display_notification("Fixed velocity: {0}".format('On' if self.fixed_velocity_mode else 'Off'))
return True
elif button_name == push2_python.constants.BUTTON_SHIFT:
self.modulation_wheel_mode = not self.modulation_wheel_mode
if self.modulation_wheel_mode:
self.push.touchstrip.set_modulation_wheel_mode()
else:
self.push.touchstrip.set_pitch_bend_mode()
self.app.buttons_need_update = True
self.app.add_display_notification("Touchstrip mode: {0}".format('Modulation wheel' if self.modulation_wheel_mode else 'Pitch bend'))
return True