]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - music_sampler/mapping.py
5c61f8a5be6eb318949400bfb80398e3cd03472a
[perso/Immae/Projets/Python/MusicSampler.git] / music_sampler / mapping.py
1 from kivy.uix.relativelayout import RelativeLayout
2 from kivy.properties import NumericProperty, ListProperty, StringProperty
3 from kivy.core.window import Window
4 from kivy.clock import Clock
5
6 import threading
7 import yaml
8 import sys
9 import copy
10 from collections import defaultdict
11
12 from transitions.extensions import HierarchicalMachine as Machine
13
14 from .music_file import MusicFile
15 from .mixer import Mixer
16 from .helpers import Config, gain, error_print, warn_print
17 from .action import Action
18
19 class Mapping(RelativeLayout):
20 STATES = [
21 'initial',
22 'configuring',
23 'configured',
24 'loading',
25 'loaded'
26 ]
27
28 TRANSITIONS = [
29 {
30 'trigger': 'configure',
31 'source': 'initial',
32 'dest': 'configuring'
33 },
34 {
35 'trigger': 'success',
36 'source': 'configuring',
37 'dest': 'configured',
38 'after': 'load'
39 },
40 {
41 'trigger': 'load',
42 'source': 'configured',
43 'dest': 'loading'
44 },
45 {
46 'trigger': 'success',
47 'source': 'loading',
48 'dest': 'loaded'
49 },
50 {
51 'trigger': 'reload',
52 'source': 'loaded',
53 'dest': 'configuring'
54 }
55 ]
56
57 master_volume = NumericProperty(100)
58 ready_color = ListProperty([1, 165/255, 0, 1])
59 state = StringProperty("")
60
61 def __init__(self, **kwargs):
62 self.keys = []
63 self.running = []
64 self.wait_ids = {}
65 self.open_files = {}
66 self.is_leaving_application = False
67
68 Machine(model=self, states=self.STATES,
69 transitions=self.TRANSITIONS, initial='initial',
70 auto_transitions=False, queued=True)
71 super(Mapping, self).__init__(**kwargs)
72 self.keyboard = Window.request_keyboard(self.on_keyboard_closed, self)
73 self.keyboard.bind(on_key_down=self.on_keyboard_down)
74
75 self.configure()
76
77 def on_enter_configuring(self):
78 if Config.builtin_mixing:
79 self.mixer = Mixer()
80 else:
81 self.mixer = None
82
83 try:
84 self.key_config, self.open_files = self.parse_config()
85 except Exception as e:
86 error_print("Error while loading configuration: {}".format(e),
87 with_trace=True, exit=True)
88 else:
89 self.success()
90
91 def on_enter_loading(self):
92 for key in self.keys:
93 key.reload()
94 self.success()
95
96 # Kivy events
97 def add_widget(self, widget, index=0):
98 if type(widget).__name__ == "Key" and widget not in self.keys:
99 self.keys.append(widget)
100 return super(Mapping, self).add_widget(widget, index)
101
102 def remove_widget(self, widget, index=0):
103 if type(widget).__name__ == "Key" and widget in self.keys:
104 self.keys.remove(widget)
105 return super(Mapping, self).remove_widget(widget, index)
106
107 def on_keyboard_closed(self):
108 self.keyboard.unbind(on_key_down=self.on_keyboard_down)
109 self.keyboard = None
110
111 def on_keyboard_down(self, keyboard, keycode, text, modifiers):
112 key = self.find_by_key_code(keycode)
113 if self.allowed_modifiers(modifiers) and key is not None:
114 modifiers.sort()
115 threading.Thread(name="MSKeyAction", target=key.run,
116 args=['-'.join(modifiers)]).start()
117 elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'):
118 self.leave_application()
119 sys.exit()
120 elif 'ctrl' in modifiers and keycode[0] == 114 and self.is_loaded():
121 self.reload()
122 return True
123
124 def leave_application(self):
125 self.keyboard.unbind(on_key_down=self.on_keyboard_down)
126 self.stop_all_running()
127 self.is_leaving_application = True
128 for music in self.open_files.values():
129 music.stop()
130 for thread in threading.enumerate():
131 if thread.getName()[0:2] == "MS":
132 thread.join()
133 elif thread.__class__ == threading.Timer:
134 thread.cancel()
135 thread.join()
136
137 # Helpers
138 def allowed_modifiers(self, modifiers):
139 allowed = []
140 return len([a for a in modifiers if a not in allowed]) == 0
141
142 def find_by_key_code(self, key_code):
143 if "Key_" + str(key_code[0]) in self.ids:
144 return self.ids["Key_" + str(key_code[0])]
145 return None
146
147 def all_keys_ready(self):
148 partial = False
149 for key in self.keys:
150 if not key.is_loaded_or_failed():
151 return "not_ready"
152 partial = partial or key.is_failed()
153
154 if partial:
155 return "partial"
156 else:
157 return "success"
158
159 # Callbacks
160 def key_loaded_callback(self):
161 if hasattr(self, 'finished_loading'):
162 return
163
164 opacity = int(Config.load_all_musics)
165
166 result = self.all_keys_ready()
167 if result == "success":
168 self.ready_color = [0, 1, 0, opacity]
169 self.finished_loading = True
170 elif result == "partial":
171 self.ready_color = [1, 0, 0, opacity]
172 self.finished_loading = True
173 else:
174 self.ready_color = [1, 165/255, 0, opacity]
175
176 ## Some global actions
177 def stop_all_running(self, except_key=None, key_start_time=0):
178 running = self.running
179 self.running = [r for r in running\
180 if r[0] == except_key and r[1] == key_start_time]
181 for (key, start_time) in running:
182 if (key, start_time) != (except_key, key_start_time):
183 key.interrupt()
184
185 # Master volume methods
186 @property
187 def master_gain(self):
188 return gain(self.master_volume)
189
190 def set_master_volume(self, value, delta=False, fade=0):
191 [db_gain, self.master_volume] = gain(
192 value + int(delta) * self.master_volume,
193 self.master_volume)
194
195 for music in self.open_files.values():
196 music.set_gain_with_effect(db_gain, fade=fade)
197
198 # Wait handler methods
199 def add_wait(self, action_or_wait, wait_id=None):
200 if wait_id is not None:
201 self.wait_ids[wait_id] = [action_or_wait]
202 else:
203 if None not in self.wait_ids:
204 self.wait_ids[None] = []
205 self.wait_ids[None].append(action_or_wait)
206
207 def matching_wait_ids(self, wait_id=None):
208 if wait_id is None:
209 matching_ids = list(self.wait_ids.keys())
210 elif wait_id in self.wait_ids:
211 matching_ids = [wait_id]
212 else:
213 matching_ids = []
214 return matching_ids
215
216 def interrupt_wait(self, wait_id=None):
217 for _wait_id in self.matching_wait_ids(wait_id=wait_id):
218 action_or_waits = self.wait_ids[_wait_id]
219 del(self.wait_ids[_wait_id])
220 for action_or_wait in action_or_waits:
221 if isinstance(action_or_wait, Action):
222 action_or_wait.interrupt()
223 else:
224 action_or_wait.set()
225
226 def pause_wait(self, wait_id=None):
227 for _wait_id in self.matching_wait_ids(wait_id=wait_id):
228 action_or_waits = self.wait_ids[_wait_id]
229 for action_or_wait in action_or_waits:
230 if isinstance(action_or_wait, Action):
231 action_or_wait.pause()
232
233 def unpause_wait(self, wait_id=None):
234 for _wait_id in self.matching_wait_ids(wait_id=wait_id):
235 action_or_waits = self.wait_ids[_wait_id]
236 for action_or_wait in action_or_waits:
237 if isinstance(action_or_wait, Action):
238 action_or_wait.unpause()
239
240 def reset_wait(self, wait_id=None):
241 for _wait_id in self.matching_wait_ids(wait_id=wait_id):
242 action_or_waits = self.wait_ids[_wait_id]
243 for action_or_wait in action_or_waits:
244 if isinstance(action_or_wait, Action):
245 action_or_wait.reset()
246
247 # Methods to control running keys
248 def start_running(self, key, start_time):
249 self.running.append((key, start_time))
250
251 def keep_running(self, key, start_time):
252 return (key, start_time) in self.running
253
254 def finished_running(self, key, start_time):
255 if (key, start_time) in self.running:
256 self.running.remove((key, start_time))
257
258 # YML config parser
259 def parse_config(self):
260 def update_alias(prop_hash, aliases, key):
261 if isinstance(aliases[key], dict):
262 for alias in aliases[key]:
263 prop_hash.setdefault(alias, aliases[key][alias])
264 else:
265 warn_print("Alias {} is not a hash, ignored".format(key))
266
267 def include_aliases(prop_hash, aliases):
268 if 'include' not in prop_hash:
269 return
270
271 included = prop_hash['include']
272 del(prop_hash['include'])
273 if isinstance(included, str):
274 update_alias(prop_hash, aliases, included)
275 elif isinstance(included, list):
276 for included_ in included:
277 if isinstance(included_, str):
278 update_alias(prop_hash, aliases, included_)
279 else:
280 warn_print("Unkown alias include type, ignored: "
281 "{} in {}".format(included_, included))
282 else:
283 warn_print("Unkown alias include type, ignored: {}"
284 .format(included))
285
286 def check_key_property(key_property, key):
287 if 'description' in key_property:
288 desc = key_property['description']
289 if not isinstance(desc, list):
290 warn_print("description in key_property '{}' is not "
291 "a list, ignored".format(key))
292 del(key_property['description'])
293 if 'color' in key_property:
294 color = key_property['color']
295 if not isinstance(color, list)\
296 or len(color) != 3\
297 or not all(isinstance(item, int) for item in color)\
298 or any(item < 0 or item > 255 for item in color):
299 warn_print("color in key_property '{}' is not "
300 "a list of 3 valid integers, ignored".format(key))
301 del(key_property['color'])
302
303 def check_key_properties(config):
304 if 'key_properties' in config:
305 if isinstance(config['key_properties'], dict):
306 return config['key_properties']
307 else:
308 warn_print("key_properties config is not a hash, ignored")
309 return {}
310 else:
311 return {}
312
313 def check_mapped_keys(config):
314 if 'keys' in config:
315 if isinstance(config['keys'], dict):
316 return config['keys']
317 else:
318 warn_print("keys config is not a hash, ignored")
319 return {}
320 else:
321 return {}
322
323 def check_mapped_key(mapped_keys, key):
324 if not isinstance(mapped_keys[key], list):
325 warn_print("key config '{}' is not an array, ignored"
326 .format(key))
327 return []
328 else:
329 return mapped_keys[key]
330
331 def check_music_property(music_property, filename):
332 if not isinstance(music_property, dict):
333 warn_print("music_property config '{}' is not a hash, ignored"
334 .format(filename))
335 return {}
336 if 'name' in music_property:
337 music_property['name'] = str(music_property['name'])
338 if 'gain' in music_property:
339 try:
340 music_property['gain'] = float(music_property['gain'])
341 except ValueError as e:
342 del(music_property['gain'])
343 warn_print("gain for music_property '{}' is not "
344 "a float, ignored".format(filename))
345 return music_property
346
347 stream = open(Config.yml_file, "r")
348 try:
349 config = yaml.safe_load(stream)
350 except Exception as e:
351 error_print("Error while loading config file: {}".format(e),
352 exit=True)
353 stream.close()
354
355 if not isinstance(config, dict):
356 error_print("Top level config is supposed to be a hash",
357 exit=True)
358
359 if 'aliases' in config and isinstance(config['aliases'], dict):
360 aliases = config['aliases']
361 else:
362 aliases = defaultdict(dict)
363 if 'aliases' in config:
364 warn_print("aliases config is not a hash, ignored")
365
366 music_properties = defaultdict(dict)
367 if 'music_properties' in config and\
368 isinstance(config['music_properties'], dict):
369 music_properties.update(config['music_properties'])
370 elif 'music_properties' in config:
371 warn_print("music_properties config is not a hash, ignored")
372
373 seen_files = {}
374
375 common_key_properties = {}
376 if 'common' in config['key_properties'] and\
377 isinstance(config['key_properties'], dict):
378 common_key_properties = config['key_properties']['common']
379 include_aliases(common_key_properties, aliases)
380 check_key_property(common_key_properties, 'common')
381 elif 'common' in config['key_properties']:
382 warn_print("'common' key in key_properties is not a hash, ignored")
383
384 key_properties = defaultdict(lambda: {
385 "actions": [],
386 "properties": copy.deepcopy(common_key_properties),
387 "files": []
388 })
389
390 for key in check_key_properties(config):
391 if key == 'common':
392 continue
393
394 key_prop = config['key_properties'][key]
395
396 if not isinstance(key_prop, dict):
397 warn_print("key_property '{}' is not a hash, ignored"
398 .format(key))
399 continue
400
401 include_aliases(key_prop, aliases)
402 check_key_property(key_prop, key)
403
404 key_properties[key]["properties"].update(key_prop)
405
406 for mapped_key in check_mapped_keys(config):
407 for index, action in enumerate(check_mapped_key(
408 config['keys'], mapped_key)):
409 if not isinstance(action, dict) or\
410 not len(action) == 1 or\
411 not isinstance(list(action.values())[0] or {}, dict):
412 warn_print("action number {} of key '{}' is invalid, "
413 "ignored".format(index + 1, mapped_key))
414 continue
415
416 action_name = list(action)[0]
417 action_args = {}
418 if action[action_name] is None:
419 action[action_name] = {}
420
421 include_aliases(action[action_name], aliases)
422
423 for argument in action[action_name]:
424 if argument == 'file':
425 filename = str(action[action_name]['file'])
426 if filename not in seen_files:
427 music_property = check_music_property(
428 music_properties[filename],
429 filename)
430
431 if filename in self.open_files:
432 self.open_files[filename]\
433 .reload_properties(**music_property)
434
435 seen_files[filename] =\
436 self.open_files[filename]
437 else:
438 seen_files[filename] = MusicFile(
439 filename, self, **music_property)
440
441 if filename not in key_properties[mapped_key]['files']:
442 key_properties[mapped_key]['files'] \
443 .append(seen_files[filename])
444
445 action_args['music'] = seen_files[filename]
446 else:
447 action_args[argument] = action[action_name][argument]
448
449 key_properties[mapped_key]['actions'] \
450 .append([action_name, action_args])
451
452 return (key_properties, seen_files)
453
454