]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - helpers/mapping.py
6e3b29153e4d817205d015cf8a441f56da654189
[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 .music_file import MusicFile
12 from .mixer import Mixer
13 from . import Config, gain, error_print, warn_print
14 from .action import Action
15
16 class Mapping(RelativeLayout):
17 expected_keys = NumericProperty(0)
18 master_volume = NumericProperty(100)
19 ready_color = ListProperty([1, 165/255, 0, 1])
20
21 def __init__(self, **kwargs):
22 if Config.builtin_mixing:
23 self.mixer = Mixer()
24 else:
25 self.mixer = None
26
27 try:
28 self.key_config, self.open_files = self.parse_config()
29 except Exception as e:
30 error_print("Error while loading configuration: {}".format(e),
31 with_trace=True)
32 sys.exit()
33
34 super(Mapping, self).__init__(**kwargs)
35 self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
36 self._keyboard.bind(on_key_down=self._on_keyboard_down)
37 self.running = []
38 self.wait_ids = {}
39 Clock.schedule_interval(self.not_all_keys_ready, 1)
40
41 @property
42 def master_gain(self):
43 return gain(self.master_volume)
44
45 def set_master_volume(self, value, delta=False, fade=0):
46 [db_gain, self.master_volume] = gain(
47 value + int(delta) * self.master_volume,
48 self.master_volume)
49
50 for music in self.open_files.values():
51 music.set_gain_with_effect(db_gain, fade=fade)
52
53 def add_wait_id(self, wait_id, action_or_wait):
54 self.wait_ids[wait_id] = action_or_wait
55
56 def interrupt_wait(self, wait_id):
57 if wait_id in self.wait_ids:
58 action_or_wait = self.wait_ids[wait_id]
59 del(self.wait_ids[wait_id])
60 if isinstance(action_or_wait, Action):
61 action_or_wait.interrupt()
62 else:
63 action_or_wait.set()
64
65 def _keyboard_closed(self):
66 self._keyboard.unbind(on_key_down=self._on_keyboard_down)
67 self._keyboard = None
68
69 def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
70 key = self.find_by_key_code(keycode)
71 if len(modifiers) == 0 and key is not None:
72 threading.Thread(name="MSKeyAction", target=key.run).start()
73 elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'):
74 self.stop_all_running()
75 for thread in threading.enumerate():
76 if thread.getName()[0:2] != "MS":
77 continue
78 thread.join()
79
80 sys.exit()
81 return True
82
83 def find_by_key_code(self, key_code):
84 if "Key_" + str(key_code[0]) in self.ids:
85 return self.ids["Key_" + str(key_code[0])]
86 return None
87
88 def not_all_keys_ready(self, dt):
89 for key in self.children:
90 if not type(key).__name__ == "Key":
91 continue
92 if not key.is_loaded_or_failed():
93 return True
94 self.ready_color = [0, 1, 0, 1]
95 return False
96
97 def stop_all_running(self):
98 running = self.running
99 self.running = []
100 for (key, start_time) in running:
101 key.interrupt()
102
103 def start_running(self, key, start_time):
104 self.running.append((key, start_time))
105
106 def keep_running(self, key, start_time):
107 return (key, start_time) in self.running
108
109 def finished_running(self, key, start_time):
110 if (key, start_time) in self.running:
111 self.running.remove((key, start_time))
112
113 def parse_config(self):
114 def update_alias(prop_hash, aliases, key):
115 if isinstance(aliases[key], dict):
116 prop_hash.update(aliases[key], **prop_hash)
117 else:
118 warn_print("Alias {} is not a hash, ignored".format(key))
119
120 def include_aliases(prop_hash, aliases):
121 if 'include' not in prop_hash:
122 return
123
124 included = prop_hash['include']
125 del(prop_hash['include'])
126 if isinstance(included, str):
127 update_alias(prop_hash, aliases, included)
128 elif isinstance(included, list):
129 for included_ in included:
130 if isinstance(included_, str):
131 update_alias(prop_hash, aliases, included_)
132 else:
133 warn_print("Unkown alias include type, ignored: "
134 "{} in {}".format(included_, included))
135 else:
136 warn_print("Unkown alias include type, ignored: {}"
137 .format(included))
138
139 def check_key_property(key_property, key):
140 if 'description' in key_property:
141 desc = key_property['description']
142 if not isinstance(desc, list):
143 warn_print("description in key_property '{}' is not "
144 "a list, ignored".format(key))
145 del(key_property['description'])
146 if 'color' in key_property:
147 color = key_property['color']
148 if not isinstance(color, list)\
149 or len(color) != 3\
150 or not all(isinstance(item, int) for item in color)\
151 or any(item < 0 or item > 255 for item in color):
152 warn_print("color in key_property '{}' is not "
153 "a list of 3 valid integers, ignored".format(key))
154 del(key_property['color'])
155
156 def check_key_properties(config):
157 if 'key_properties' in config:
158 if isinstance(config['key_properties'], dict):
159 return config['key_properties']
160 else:
161 warn_print("key_properties config is not a hash, ignored")
162 return {}
163 else:
164 return {}
165
166 def check_mapped_keys(config):
167 if 'keys' in config:
168 if isinstance(config['keys'], dict):
169 return config['keys']
170 else:
171 warn_print("keys config is not a hash, ignored")
172 return {}
173 else:
174 return {}
175
176 def check_mapped_key(mapped_keys, key):
177 if not isinstance(mapped_keys[key], list):
178 warn_print("key config '{}' is not an array, ignored"
179 .format(key))
180 return []
181 else:
182 return mapped_keys[key]
183
184 def check_music_property(music_property, filename):
185 if not isinstance(music_property, dict):
186 warn_print("music_property config '{}' is not a hash, ignored"
187 .format(filename))
188 return {}
189 if 'name' in music_property:
190 music_property['name'] = str(music_property['name'])
191 if 'gain' in music_property:
192 try:
193 music_property['gain'] = float(music_property['gain'])
194 except ValueError as e:
195 del(music_property['gain'])
196 warn_print("gain for music_property '{}' is not "
197 "a float, ignored".format(filename))
198 return music_property
199
200 stream = open(Config.yml_file, "r")
201 try:
202 config = yaml.safe_load(stream)
203 except Exception as e:
204 error_print("Error while loading config file: {}".format(e))
205 sys.exit()
206 stream.close()
207
208 if not isinstance(config, dict):
209 raise Exception("Top level config is supposed to be a hash")
210
211 if 'aliases' in config and isinstance(config['aliases'], dict):
212 aliases = config['aliases']
213 else:
214 aliases = defaultdict(dict)
215 if 'aliases' in config:
216 warn_print("aliases config is not a hash, ignored")
217
218 music_properties = defaultdict(dict)
219 if 'music_properties' in config and\
220 isinstance(config['music_properties'], dict):
221 music_properties.update(config['music_properties'])
222 elif 'music_properties' in config:
223 warn_print("music_properties config is not a hash, ignored")
224
225 seen_files = {}
226
227 key_properties = defaultdict(lambda: {
228 "actions": [],
229 "properties": {},
230 "files": []
231 })
232
233 for key in check_key_properties(config):
234 key_prop = config['key_properties'][key]
235
236 if not isinstance(key_prop, dict):
237 warn_print("key_property '{}' is not a hash, ignored"
238 .format(key))
239 continue
240
241 include_aliases(key_prop, aliases)
242 check_key_property(key_prop, key)
243
244 key_properties[key]["properties"] = key_prop
245
246 for mapped_key in check_mapped_keys(config):
247 for index, action in enumerate(check_mapped_key(
248 config['keys'], mapped_key)):
249 if not isinstance(action, dict) or\
250 not len(action) == 1 or\
251 not isinstance(list(action.values())[0] or {}, dict):
252 warn_print("action number {} of key '{}' is invalid, "
253 "ignored".format(index + 1, mapped_key))
254 continue
255
256 action_name = list(action)[0]
257 action_args = {}
258 if action[action_name] is None:
259 action[action_name] = {}
260
261 include_aliases(action[action_name], aliases)
262
263 for argument in action[action_name]:
264 if argument == 'file':
265 filename = str(action[action_name]['file'])
266 if filename not in seen_files:
267 music_property = check_music_property(
268 music_properties[filename],
269 filename)
270
271 seen_files[filename] = MusicFile(
272 filename, self, **music_property)
273
274 if filename not in key_properties[mapped_key]['files']:
275 key_properties[mapped_key]['files'] \
276 .append(seen_files[filename])
277
278 action_args['music'] = seen_files[filename]
279 else:
280 action_args[argument] = action[action_name][argument]
281
282 key_properties[mapped_key]['actions'] \
283 .append([action_name, action_args])
284
285 return (key_properties, seen_files)
286
287