-
Notifications
You must be signed in to change notification settings - Fork 0
/
device_Note Handler.py
248 lines (192 loc) · 7.69 KB
/
device_Note Handler.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
# name=Note Handler
# receiveFrom=Shift Handler
# url=https://github.com/TheNathanSpace/Launchkey-Mini-FL-Studio-Scale-Mode/
import device
import midi
import ui
import constants
major_scales_dict = {}
minor_scales_dict = {}
id_to_name_dict = {}
name_to_id_dict = {}
shift_is_on = False
arrow_is_on = False
is_minor = False
current_key = "C"
def index_of(list_in, item):
"""
Returns the index of an item in a list.
:param list_in: The list to traverse.
:param item: The item to find in the list.
:return: The index of the first occurrence of the item, or -1 if not found.
"""
counter = 0
for i in list_in:
if item == i:
return counter
counter += 1
return -1
def read_scales_file(scale_string):
"""
Parses a string of scales into a dictionary.
:param scale_string: A string of scales. Each line contains a scale, starting at the root note and
ending before the next root note (so there are 7 notes). Spaces and blank
newlines are ignored.
:return: A dictionary mapping the root note to a list of notes for each scale.
"""
loc_scales_dict = {}
lines = scale_string.split("\n")
for line in lines:
# Skip comments and blank lines
if line == "\n":
continue
line = line.replace(" ", "")
split_line = line.split("\t")
root_note = split_line[0]
loc_scales_dict[root_note] = []
for note in split_line:
note = note.replace("\n", "")
loc_scales_dict[root_note].append(note)
return loc_scales_dict
def map_note_ids(all_notes):
"""
Creates a dictionary mapping note IDs from 0-127 to note names from C0 to C10
:param all_notes: A list of all possible note names, from C to B
:return: A dictionary mapping note IDs (int) --> note names including octave (str)
"""
loc_id_to_name_dict = {}
note_index = 0
octave = 0
for i in range(0, 128):
note_letter = all_notes[note_index]
loc_id_to_name_dict[i] = note_letter + str(octave)
note_index += 1
if note_index == 12:
note_index = 0
if all_notes[note_index] == "C":
octave += 1
return loc_id_to_name_dict
def get_no_octave(note_id):
"""
Returns the name of a note without the octave.
:param note_id: The ID of the note, as an int from 0-127
:return: The name of the note, from A-G, including # or b as relevant
"""
no_octave = ''.join([i for i in id_to_name_dict[note_id] if not i.isdigit()])
return no_octave
def get_c_index(note_id):
"""
Returns the index of a note in the key of C.
:param note_id: The ID of the note, as an int from 0-127
:return: The index of the note, as an int from 0-6
"""
return index_of(major_scales_dict["C"], get_no_octave(note_id))
def get_octave(note_id):
"""
Returns the octave of a note, from 0-10.
:param note_id: The ID of the note, as an int from 0-127
:return: The octave of the note, as an int from 0-10
"""
just_octave = ''.join([i for i in id_to_name_dict[note_id] if i.isdigit()])
return just_octave
def OnMidiMsg(event):
"""
Handles MIDI note events.
:param event: The FlMidiMsg
"""
event.handled = False
# print("MIDI STATUS", event.midiId, "|", "MIDI DATA1", event.data1, "|",
# "MIDI DATA2", event.data2, "|", "MIDI status", event.status, "|",
# "Channel", (event.midiChan + 1), "| Sysex", event.sysex, "|", "Handled", event.handled) # Prints MIDI data from pads, knobs and other buttons. Useful for debugging.
if event.midiId == midi.MIDI_NOTEON:
if event.pmeFlags & midi.PME_System != 0:
# print("\nBefore: " + id_to_name_dict[event.data1] + " on channel " + str(event.midiChan))
global current_key
global is_minor
# Only perform this on key down, not up
if shift_is_on and event.data2 != 0:
change_to = get_no_octave(event.data1)
# Decide if major or minor key
if arrow_is_on:
major_minor = " minor"
scales_dict = minor_scales_dict
is_minor = True
else:
major_minor = " major"
scales_dict = major_scales_dict
is_minor = False
# Parse key to change to
if change_to not in scales_dict:
# Convert sharp to flat
change_to = constants.sharps_flats_map[change_to]
if change_to not in scales_dict:
message = "Couldn't change key to " + change_to + major_minor
ui.setHintMsg(message)
print(message)
return
current_key = change_to
message = "Changed mapped key to: " + current_key + major_minor
ui.setHintMsg(message)
print(message)
event.handled = True
return
if current_key != "C" and event.midiChan == 0:
# Ignore sharps/flats while key is mapped
if "#" in id_to_name_dict[event.data1] or "b" in id_to_name_dict[event.data1]:
event.handled = True
return
# Get position of original note in scale
c_index = get_c_index(event.data1)
# Choose if major/minor
if is_minor:
scales_dict = minor_scales_dict
else:
scales_dict = major_scales_dict
# Get translated note from new key
translated_key = scales_dict[current_key][c_index]
octave = get_octave(event.data1)
# Fix octave if note is late enough
for note in scales_dict[current_key][:c_index + 1]:
if "C" in note:
octave = int(octave) + 1
octave = str(octave)
break
# Get note ID
translated_key_with_octave = translated_key + octave
if translated_key_with_octave in name_to_id_dict:
translated_id = name_to_id_dict[translated_key_with_octave]
else:
# Convert flat to sharp if necessary
translated_id = name_to_id_dict[constants.flats_sharps_map[translated_key] + octave]
# Modify event note
event.data1 = translated_id
# print("After: " + id_to_name_dict[event.data1] + " on channel " + str(event.midiChan))
def OnSysEx(event):
"""
Handles SysEx events. The only expected events are shift on/off
and arrow on/off, sent by the Shift handler.
:param event: The SysexMidiMsg
"""
received_message = int.from_bytes(bytes = event.sysex, byteorder = 'big')
global shift_is_on
global arrow_is_on
if received_message == 18:
shift_is_on = True
elif received_message == 17:
shift_is_on = False
elif received_message == 20:
arrow_is_on = True
elif received_message == 19:
arrow_is_on = False
def OnInit():
# Create scale dicts
global major_scales_dict
global minor_scales_dict
major_scales_dict = read_scales_file(constants.major_scales)
minor_scales_dict = read_scales_file(constants.minor_scales)
# Create id/name maps (both directions)
global id_to_name_dict
global name_to_id_dict
id_to_name_dict = map_note_ids(constants.all_notes)
name_to_id_dict = {v: k for k, v in id_to_name_dict.items()}
print("Initialized note handler\n")