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