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