]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - helpers/mapping.py
1f63459adbf76ca206788ba405cc2e448a0198a3
[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 for thread in threading.enumerate():
75 if thread.getName()[0:2] != "MS":
76 continue
77 thread.join()
78
79 sys.exit()
80 return True
81
82 def find_by_key_code(self, key_code):
83 if "Key_" + str(key_code[0]) in self.ids:
84 return self.ids["Key_" + str(key_code[0])]
85 return None
86
87 def not_all_keys_ready(self, dt):
88 for key in self.children:
89 if not type(key).__name__ == "Key":
90 continue
91 if not key.is_loaded_or_failed():
92 return True
93 self.ready_color = [0, 1, 0, 1]
94 return False
95
96 def stop_all_running(self):
97 running = self.running
98 self.running = []
99 for (key, start_time) in running:
100 key.interrupt()
101
102 def start_running(self, key, start_time):
103 self.running.append((key, start_time))
104
105 def keep_running(self, key, start_time):
106 return (key, start_time) in self.running
107
108 def finished_running(self, key, start_time):
109 if (key, start_time) in self.running:
110 self.running.remove((key, start_time))
111
112 def parse_config(self):
113 def update_alias(prop_hash, aliases, key):
114 if isinstance(aliases[key], dict):
115 prop_hash.update(aliases[key], **prop_hash)
116 else:
117 warn_print("Alias {} is not a hash, ignored".format(key))
118
119 def include_aliases(prop_hash, aliases):
120 if 'include' not in prop_hash:
121 return
122
123 included = prop_hash['include']
124 del(prop_hash['include'])
125 if isinstance(included, str):
126 update_alias(prop_hash, aliases, included)
127 elif isinstance(included, list):
128 for included_ in included:
129 if isinstance(included_, str):
130 update_alias(prop_hash, aliases, included_)
131 else:
132 warn_print("Unkown alias include type, ignored: "
133 "{} in {}".format(included_, included))
134 else:
135 warn_print("Unkown alias include type, ignored: {}"
136 .format(included))
137
138 def check_key_property(key_property, key):
139 if 'description' in key_property:
140 desc = key_property['description']
141 if not isinstance(desc, list):
142 warn_print("description in key_property '{}' is not "
143 "a list, ignored".format(key))
144 del(key_property['description'])
145 if 'color' in key_property:
146 color = key_property['color']
147 if not isinstance(color, list)\
148 or len(color) != 3\
149 or not all(isinstance(item, int) for item in color)\
150 or any(item < 0 or item > 255 for item in color):
151 warn_print("color in key_property '{}' is not "
152 "a list of 3 valid integers, ignored".format(key))
153 del(key_property['color'])
154
155 def check_key_properties(config):
156 if 'key_properties' in config:
157 if isinstance(config['key_properties'], dict):
158 return config['key_properties']
159 else:
160 warn_print("key_properties config is not a hash, ignored")
161 return {}
162 else:
163 return {}
164
165 def check_mapped_keys(config):
166 if 'keys' in config:
167 if isinstance(config['keys'], dict):
168 return config['keys']
169 else:
170 warn_print("keys config is not a hash, ignored")
171 return {}
172 else:
173 return {}
174
175 def check_mapped_key(mapped_keys, key):
176 if not isinstance(mapped_keys[key], list):
177 warn_print("key config '{}' is not an array, ignored"
178 .format(key))
179 return []
180 else:
181 return mapped_keys[key]
182
183 def check_music_property(music_property, filename):
184 if not isinstance(music_property, dict):
185 warn_print("music_property config '{}' is not a hash, ignored"
186 .format(filename))
187 return {}
188 if 'name' in music_property:
189 music_property['name'] = str(music_property['name'])
190 if 'gain' in music_property:
191 try:
192 music_property['gain'] = float(music_property['gain'])
193 except ValueError as e:
194 del(music_property['gain'])
195 warn_print("gain for music_property '{}' is not "
196 "a float, ignored".format(filename))
197 return music_property
198
199 stream = open(Config.yml_file, "r")
200 try:
201 config = yaml.safe_load(stream)
202 except Exception as e:
203 error_print("Error while loading config file: {}".format(e))
204 sys.exit()
205 stream.close()
206
207 if not isinstance(config, dict):
208 raise Exception("Top level config is supposed to be a hash")
209
210 if 'aliases' in config and isinstance(config['aliases'], dict):
211 aliases = config['aliases']
212 else:
213 aliases = defaultdict(dict)
214 if 'aliases' in config:
215 warn_print("aliases config is not a hash, ignored")
216
217 music_properties = defaultdict(dict)
218 if 'music_properties' in config and\
219 isinstance(config['music_properties'], dict):
220 music_properties.update(config['music_properties'])
221 elif 'music_properties' in config:
222 warn_print("music_properties config is not a hash, ignored")
223
224 seen_files = {}
225
226 key_properties = defaultdict(lambda: {
227 "actions": [],
228 "properties": {},
229 "files": []
230 })
231
232 for key in check_key_properties(config):
233 key_prop = config['key_properties'][key]
234
235 if not isinstance(key_prop, dict):
236 warn_print("key_property '{}' is not a hash, ignored"
237 .format(key))
238 continue
239
240 include_aliases(key_prop, aliases)
241 check_key_property(key_prop, key)
242
243 key_properties[key]["properties"] = key_prop
244
245 for mapped_key in check_mapped_keys(config):
246 for index, action in enumerate(check_mapped_key(
247 config['keys'], mapped_key)):
248 if not isinstance(action, dict) or\
249 not len(action) == 1 or\
250 not isinstance(list(action.values())[0] or {}, dict):
251 warn_print("action number {} of key '{}' is invalid, "
252 "ignored".format(index + 1, mapped_key))
253 continue
254
255 action_name = list(action)[0]
256 action_args = {}
257 if action[action_name] is None:
258 action[action_name] = {}
259
260 include_aliases(action[action_name], aliases)
261
262 for argument in action[action_name]:
263 if argument == 'file':
264 filename = str(action[action_name]['file'])
265 if filename not in seen_files:
266 music_property = check_music_property(
267 music_properties[filename],
268 filename)
269
270 seen_files[filename] = MusicFile(
271 filename, self, **music_property)
272
273 if filename not in key_properties[mapped_key]['files']:
274 key_properties[mapped_key]['files'] \
275 .append(seen_files[filename])
276
277 action_args['music'] = seen_files[filename]
278 else:
279 action_args[argument] = action[action_name][argument]
280
281 key_properties[mapped_key]['actions'] \
282 .append([action_name, action_args])
283
284 return (key_properties, seen_files)
285
286