diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-27 21:33:09 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2016-07-27 21:33:09 +0200 |
commit | 63ba5a8dc2aa4ec3e6f203b0ba4db249ecf0b00e (patch) | |
tree | f043ab215425aca3434bf178029dad9f9e62dbe9 /helpers/mapping.py | |
parent | 35bde798b6cda13579337b0ec5a803fdd5eab19a (diff) | |
download | MusicSampler-63ba5a8dc2aa4ec3e6f203b0ba4db249ecf0b00e.tar.gz MusicSampler-63ba5a8dc2aa4ec3e6f203b0ba4db249ecf0b00e.tar.zst MusicSampler-63ba5a8dc2aa4ec3e6f203b0ba4db249ecf0b00e.zip |
Rename helpers to music_sampler
Diffstat (limited to 'helpers/mapping.py')
-rw-r--r-- | helpers/mapping.py | 399 |
1 files changed, 0 insertions, 399 deletions
diff --git a/helpers/mapping.py b/helpers/mapping.py deleted file mode 100644 index bb20e67..0000000 --- a/helpers/mapping.py +++ /dev/null | |||
@@ -1,399 +0,0 @@ | |||
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 | 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 | 'trigger': 'reload', | ||
62 | 'source': 'loaded', | ||
63 | 'dest': 'configuring' | ||
64 | } | ||
65 | ] | ||
66 | |||
67 | master_volume = NumericProperty(100) | ||
68 | ready_color = ListProperty([1, 165/255, 0, 1]) | ||
69 | state = StringProperty("") | ||
70 | |||
71 | def __init__(self, **kwargs): | ||
72 | self.keys = [] | ||
73 | self.running = [] | ||
74 | self.wait_ids = {} | ||
75 | self.open_files = {} | ||
76 | |||
77 | Machine(model=self, states=self.STATES, | ||
78 | transitions=self.TRANSITIONS, initial='initial', | ||
79 | ignore_invalid_triggers=True, queued=True) | ||
80 | super(Mapping, self).__init__(**kwargs) | ||
81 | self.keyboard = Window.request_keyboard(self.on_keyboard_closed, self) | ||
82 | self.keyboard.bind(on_key_down=self.on_keyboard_down) | ||
83 | |||
84 | self.configure() | ||
85 | |||
86 | def on_enter_configuring(self): | ||
87 | if Config.builtin_mixing: | ||
88 | self.mixer = Mixer() | ||
89 | else: | ||
90 | self.mixer = None | ||
91 | |||
92 | try: | ||
93 | self.key_config, self.open_files = self.parse_config() | ||
94 | except Exception as e: | ||
95 | error_print("Error while loading configuration: {}".format(e), | ||
96 | with_trace=True) | ||
97 | sys.exit() | ||
98 | else: | ||
99 | self.success() | ||
100 | |||
101 | def on_enter_loading(self): | ||
102 | for key in self.keys: | ||
103 | key.reload() | ||
104 | self.success() | ||
105 | |||
106 | # Kivy events | ||
107 | def add_widget(self, widget, index=0): | ||
108 | if type(widget).__name__ == "Key" and widget not in self.keys: | ||
109 | self.keys.append(widget) | ||
110 | return super(Mapping, self).add_widget(widget, index) | ||
111 | |||
112 | def remove_widget(self, widget, index=0): | ||
113 | if type(widget).__name__ == "Key" and widget in self.keys: | ||
114 | self.keys.remove(widget) | ||
115 | return super(Mapping, self).remove_widget(widget, index) | ||
116 | |||
117 | def on_keyboard_closed(self): | ||
118 | self.keyboard.unbind(on_key_down=self.on_keyboard_down) | ||
119 | self.keyboard = None | ||
120 | |||
121 | def on_keyboard_down(self, keyboard, keycode, text, modifiers): | ||
122 | key = self.find_by_key_code(keycode) | ||
123 | if self.allowed_modifiers(modifiers) and key is not None: | ||
124 | modifiers.sort() | ||
125 | threading.Thread(name="MSKeyAction", target=key.run, | ||
126 | args=['-'.join(modifiers)]).start() | ||
127 | elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'): | ||
128 | self.stop_all_running() | ||
129 | for thread in threading.enumerate(): | ||
130 | if thread.getName()[0:2] != "MS": | ||
131 | continue | ||
132 | thread.join() | ||
133 | |||
134 | sys.exit() | ||
135 | elif 'ctrl' in modifiers and keycode[0] == 114: | ||
136 | threading.Thread(name="MSReload", target=self.reload).start() | ||
137 | return True | ||
138 | |||
139 | # Helpers | ||
140 | def allowed_modifiers(self, modifiers): | ||
141 | allowed = [] | ||
142 | return len([a for a in modifiers if a not in allowed]) == 0 | ||
143 | |||
144 | def find_by_key_code(self, key_code): | ||
145 | if "Key_" + str(key_code[0]) in self.ids: | ||
146 | return self.ids["Key_" + str(key_code[0])] | ||
147 | return None | ||
148 | |||
149 | def all_keys_ready(self): | ||
150 | partial = False | ||
151 | for key in self.keys: | ||
152 | if not key.is_loaded_or_failed(): | ||
153 | return "not_ready" | ||
154 | partial = partial or key.is_failed() | ||
155 | |||
156 | if partial: | ||
157 | return "partial" | ||
158 | else: | ||
159 | return "success" | ||
160 | |||
161 | # Callbacks | ||
162 | def key_loaded_callback(self): | ||
163 | result = self.all_keys_ready() | ||
164 | if result == "success": | ||
165 | self.ready_color = [0, 1, 0, 1] | ||
166 | elif result == "partial": | ||
167 | self.ready_color = [1, 0, 0, 1] | ||
168 | else: | ||
169 | self.ready_color = [1, 165/255, 0, 1] | ||
170 | |||
171 | ## Some global actions | ||
172 | def stop_all_running(self, except_key=None, key_start_time=0): | ||
173 | running = self.running | ||
174 | self.running = [r for r in running\ | ||
175 | if r[0] == except_key and r[1] == key_start_time] | ||
176 | for (key, start_time) in running: | ||
177 | if (key, start_time) != (except_key, key_start_time): | ||
178 | key.interrupt() | ||
179 | |||
180 | # Master volume methods | ||
181 | @property | ||
182 | def master_gain(self): | ||
183 | return gain(self.master_volume) | ||
184 | |||
185 | def set_master_volume(self, value, delta=False, fade=0): | ||
186 | [db_gain, self.master_volume] = gain( | ||
187 | value + int(delta) * self.master_volume, | ||
188 | self.master_volume) | ||
189 | |||
190 | for music in self.open_files.values(): | ||
191 | music.set_gain_with_effect(db_gain, fade=fade) | ||
192 | |||
193 | # Wait handler methods | ||
194 | def add_wait_id(self, wait_id, action_or_wait): | ||
195 | self.wait_ids[wait_id] = action_or_wait | ||
196 | |||
197 | def interrupt_wait(self, wait_id): | ||
198 | if wait_id in self.wait_ids: | ||
199 | action_or_wait = self.wait_ids[wait_id] | ||
200 | del(self.wait_ids[wait_id]) | ||
201 | if isinstance(action_or_wait, Action): | ||
202 | action_or_wait.interrupt() | ||
203 | else: | ||
204 | action_or_wait.set() | ||
205 | |||
206 | # Methods to control running keys | ||
207 | def start_running(self, key, start_time): | ||
208 | self.running.append((key, start_time)) | ||
209 | |||
210 | def keep_running(self, key, start_time): | ||
211 | return (key, start_time) in self.running | ||
212 | |||
213 | def finished_running(self, key, start_time): | ||
214 | if (key, start_time) in self.running: | ||
215 | self.running.remove((key, start_time)) | ||
216 | |||
217 | # YML config parser | ||
218 | def parse_config(self): | ||
219 | def update_alias(prop_hash, aliases, key): | ||
220 | if isinstance(aliases[key], dict): | ||
221 | prop_hash.update(aliases[key], **prop_hash) | ||
222 | else: | ||
223 | warn_print("Alias {} is not a hash, ignored".format(key)) | ||
224 | |||
225 | def include_aliases(prop_hash, aliases): | ||
226 | if 'include' not in prop_hash: | ||
227 | return | ||
228 | |||
229 | included = prop_hash['include'] | ||
230 | del(prop_hash['include']) | ||
231 | if isinstance(included, str): | ||
232 | update_alias(prop_hash, aliases, included) | ||
233 | elif isinstance(included, list): | ||
234 | for included_ in included: | ||
235 | if isinstance(included_, str): | ||
236 | update_alias(prop_hash, aliases, included_) | ||
237 | else: | ||
238 | warn_print("Unkown alias include type, ignored: " | ||
239 | "{} in {}".format(included_, included)) | ||
240 | else: | ||
241 | warn_print("Unkown alias include type, ignored: {}" | ||
242 | .format(included)) | ||
243 | |||
244 | def check_key_property(key_property, key): | ||
245 | if 'description' in key_property: | ||
246 | desc = key_property['description'] | ||
247 | if not isinstance(desc, list): | ||
248 | warn_print("description in key_property '{}' is not " | ||
249 | "a list, ignored".format(key)) | ||
250 | del(key_property['description']) | ||
251 | if 'color' in key_property: | ||
252 | color = key_property['color'] | ||
253 | if not isinstance(color, list)\ | ||
254 | or len(color) != 3\ | ||
255 | or not all(isinstance(item, int) for item in color)\ | ||
256 | or any(item < 0 or item > 255 for item in color): | ||
257 | warn_print("color in key_property '{}' is not " | ||
258 | "a list of 3 valid integers, ignored".format(key)) | ||
259 | del(key_property['color']) | ||
260 | |||
261 | def check_key_properties(config): | ||
262 | if 'key_properties' in config: | ||
263 | if isinstance(config['key_properties'], dict): | ||
264 | return config['key_properties'] | ||
265 | else: | ||
266 | warn_print("key_properties config is not a hash, ignored") | ||
267 | return {} | ||
268 | else: | ||
269 | return {} | ||
270 | |||
271 | def check_mapped_keys(config): | ||
272 | if 'keys' in config: | ||
273 | if isinstance(config['keys'], dict): | ||
274 | return config['keys'] | ||
275 | else: | ||
276 | warn_print("keys config is not a hash, ignored") | ||
277 | return {} | ||
278 | else: | ||
279 | return {} | ||
280 | |||
281 | def check_mapped_key(mapped_keys, key): | ||
282 | if not isinstance(mapped_keys[key], list): | ||
283 | warn_print("key config '{}' is not an array, ignored" | ||
284 | .format(key)) | ||
285 | return [] | ||
286 | else: | ||
287 | return mapped_keys[key] | ||
288 | |||
289 | def check_music_property(music_property, filename): | ||
290 | if not isinstance(music_property, dict): | ||
291 | warn_print("music_property config '{}' is not a hash, ignored" | ||
292 | .format(filename)) | ||
293 | return {} | ||
294 | if 'name' in music_property: | ||
295 | music_property['name'] = str(music_property['name']) | ||
296 | if 'gain' in music_property: | ||
297 | try: | ||
298 | music_property['gain'] = float(music_property['gain']) | ||
299 | except ValueError as e: | ||
300 | del(music_property['gain']) | ||
301 | warn_print("gain for music_property '{}' is not " | ||
302 | "a float, ignored".format(filename)) | ||
303 | return music_property | ||
304 | |||
305 | stream = open(Config.yml_file, "r") | ||
306 | try: | ||
307 | config = yaml.safe_load(stream) | ||
308 | except Exception as e: | ||
309 | error_print("Error while loading config file: {}".format(e)) | ||
310 | sys.exit() | ||
311 | stream.close() | ||
312 | |||
313 | if not isinstance(config, dict): | ||
314 | raise Exception("Top level config is supposed to be a hash") | ||
315 | |||
316 | if 'aliases' in config and isinstance(config['aliases'], dict): | ||
317 | aliases = config['aliases'] | ||
318 | else: | ||
319 | aliases = defaultdict(dict) | ||
320 | if 'aliases' in config: | ||
321 | warn_print("aliases config is not a hash, ignored") | ||
322 | |||
323 | music_properties = defaultdict(dict) | ||
324 | if 'music_properties' in config and\ | ||
325 | isinstance(config['music_properties'], dict): | ||
326 | music_properties.update(config['music_properties']) | ||
327 | elif 'music_properties' in config: | ||
328 | warn_print("music_properties config is not a hash, ignored") | ||
329 | |||
330 | seen_files = {} | ||
331 | |||
332 | key_properties = defaultdict(lambda: { | ||
333 | "actions": [], | ||
334 | "properties": {}, | ||
335 | "files": [] | ||
336 | }) | ||
337 | |||
338 | for key in check_key_properties(config): | ||
339 | key_prop = config['key_properties'][key] | ||
340 | |||
341 | if not isinstance(key_prop, dict): | ||
342 | warn_print("key_property '{}' is not a hash, ignored" | ||
343 | .format(key)) | ||
344 | continue | ||
345 | |||
346 | include_aliases(key_prop, aliases) | ||
347 | check_key_property(key_prop, key) | ||
348 | |||
349 | key_properties[key]["properties"] = key_prop | ||
350 | |||
351 | for mapped_key in check_mapped_keys(config): | ||
352 | for index, action in enumerate(check_mapped_key( | ||
353 | config['keys'], mapped_key)): | ||
354 | if not isinstance(action, dict) or\ | ||
355 | not len(action) == 1 or\ | ||
356 | not isinstance(list(action.values())[0] or {}, dict): | ||
357 | warn_print("action number {} of key '{}' is invalid, " | ||
358 | "ignored".format(index + 1, mapped_key)) | ||
359 | continue | ||
360 | |||
361 | action_name = list(action)[0] | ||
362 | action_args = {} | ||
363 | if action[action_name] is None: | ||
364 | action[action_name] = {} | ||
365 | |||
366 | include_aliases(action[action_name], aliases) | ||
367 | |||
368 | for argument in action[action_name]: | ||
369 | if argument == 'file': | ||
370 | filename = str(action[action_name]['file']) | ||
371 | if filename not in seen_files: | ||
372 | music_property = check_music_property( | ||
373 | music_properties[filename], | ||
374 | filename) | ||
375 | |||
376 | if filename in self.open_files: | ||
377 | self.open_files[filename]\ | ||
378 | .reload_properties(**music_property) | ||
379 | |||
380 | seen_files[filename] =\ | ||
381 | self.open_files[filename] | ||
382 | else: | ||
383 | seen_files[filename] = MusicFile( | ||
384 | filename, self, **music_property) | ||
385 | |||
386 | if filename not in key_properties[mapped_key]['files']: | ||
387 | key_properties[mapped_key]['files'] \ | ||
388 | .append(seen_files[filename]) | ||
389 | |||
390 | action_args['music'] = seen_files[filename] | ||
391 | else: | ||
392 | action_args[argument] = action[action_name][argument] | ||
393 | |||
394 | key_properties[mapped_key]['actions'] \ | ||
395 | .append([action_name, action_args]) | ||
396 | |||
397 | return (key_properties, seen_files) | ||
398 | |||
399 | |||