]>
git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - music_sampler/mapping.py
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
10 from collections
import defaultdict
12 from transitions
.extensions
import HierarchicalMachine
as Machine
14 from .music_file
import MusicFile
15 from .mixer
import Mixer
16 from .helpers
import Config
, gain
, error_print
, warn_print
17 from .action
import Action
19 class Mapping(RelativeLayout
):
30 'trigger': 'configure',
36 'source': 'configuring',
42 'source': 'configured',
57 master_volume
= NumericProperty(100)
58 ready_color
= ListProperty([1, 165/255, 0, 1])
59 state
= StringProperty("")
61 def __init__(self
, **kwargs
):
66 self
.is_leaving_application
= False
68 Machine(model
=self
, states
=self
.STATES
,
69 transitions
=self
.TRANSITIONS
, initial
='initial',
70 auto_transitions
=False, queued
=True)
71 super(Mapping
, self
).__init
__(**kwargs
)
72 self
.keyboard
= Window
.request_keyboard(self
.on_keyboard_closed
, self
)
73 self
.keyboard
.bind(on_key_down
=self
.on_keyboard_down
)
75 self
.configure(initial
=True)
77 def on_enter_configuring(self
, initial
=True):
78 if Config
.builtin_mixing
:
84 self
.key_config
, self
.open_files
= self
.parse_config()
85 except Exception as e
:
86 error_print("Error while loading configuration: {}".format(e
),
87 with_trace
=False, exit
=initial
)
91 def on_enter_loading(self
):
97 def add_widget(self
, widget
, index
=0):
98 if type(widget
).__name
__ == "Key" and widget
not in self
.keys
:
99 self
.keys
.append(widget
)
100 return super(Mapping
, self
).add_widget(widget
, index
)
102 def remove_widget(self
, widget
, index
=0):
103 if type(widget
).__name
__ == "Key" and widget
in self
.keys
:
104 self
.keys
.remove(widget
)
105 return super(Mapping
, self
).remove_widget(widget
, index
)
107 def on_keyboard_closed(self
):
108 self
.keyboard
.unbind(on_key_down
=self
.on_keyboard_down
)
111 def on_keyboard_down(self
, keyboard
, keycode
, text
, modifiers
):
112 key
= self
.find_by_key_code(keycode
)
113 if self
.allowed_modifiers(modifiers
) and key
is not None:
115 threading
.Thread(name
="MSKeyAction", target
=key
.run
,
116 args
=['-'.join(modifiers
)]).start()
117 elif 'ctrl' in modifiers
and (keycode
[0] == 113 or keycode
[0] == '99'):
118 self
.leave_application()
120 elif 'ctrl' in modifiers
and keycode
[0] == 114 and self
.is_loaded():
121 self
.reload(initial
=False)
124 def leave_application(self
):
125 self
.keyboard
.unbind(on_key_down
=self
.on_keyboard_down
)
126 self
.stop_all_running()
127 self
.is_leaving_application
= True
128 for music
in self
.open_files
.values():
130 for thread
in threading
.enumerate():
131 if thread
.getName()[0:2] == "MS":
133 elif thread
.__class
__ == threading
.Timer
:
138 def allowed_modifiers(self
, modifiers
):
140 return len([a
for a
in modifiers
if a
not in allowed
]) == 0
142 def find_by_key_code(self
, key_code
):
143 if "Key_" + str(key_code
[0]) in self
.ids
:
144 return self
.ids
["Key_" + str(key_code
[0])]
147 def all_keys_ready(self
):
149 for key
in self
.keys
:
150 if not key
.is_loaded_or_failed():
152 partial
= partial
or key
.is_failed()
160 def key_loaded_callback(self
):
161 if hasattr(self
, 'finished_loading'):
164 opacity
= int(Config
.load_all_musics
)
166 result
= self
.all_keys_ready()
167 if result
== "success":
168 self
.ready_color
= [0, 1, 0, opacity
]
169 self
.finished_loading
= True
170 elif result
== "partial":
171 self
.ready_color
= [1, 0, 0, opacity
]
172 self
.finished_loading
= True
174 self
.ready_color
= [1, 165/255, 0, opacity
]
176 ## Some global actions
177 def stop_all_running(self
, except_key
=None, key_start_time
=0):
178 running
= self
.running
179 self
.running
= [r
for r
in running\
180 if r
[0] == except_key
and r
[1] == key_start_time
]
181 for (key
, start_time
) in running
:
182 if (key
, start_time
) != (except_key
, key_start_time
):
185 # Master volume methods
187 def master_gain(self
):
188 return gain(self
.master_volume
)
190 def set_master_volume(self
, value
, delta
=False, fade
=0):
191 [db_gain
, self
.master_volume
] = gain(
192 value
+ int(delta
) * self
.master_volume
,
195 for music
in self
.open_files
.values():
196 music
.set_gain_with_effect(db_gain
, fade
=fade
)
198 # Wait handler methods
199 def add_wait(self
, action_or_wait
, wait_id
=None):
200 if wait_id
is not None:
201 self
.wait_ids
[wait_id
] = [action_or_wait
]
203 if None not in self
.wait_ids
:
204 self
.wait_ids
[None] = []
205 self
.wait_ids
[None].append(action_or_wait
)
207 def matching_wait_ids(self
, wait_id
=None):
209 matching_ids
= list(self
.wait_ids
.keys())
210 elif wait_id
in self
.wait_ids
:
211 matching_ids
= [wait_id
]
216 def interrupt_wait(self
, wait_id
=None):
217 for _wait_id
in self
.matching_wait_ids(wait_id
=wait_id
):
218 action_or_waits
= self
.wait_ids
[_wait_id
]
219 del(self
.wait_ids
[_wait_id
])
220 for action_or_wait
in action_or_waits
:
221 if isinstance(action_or_wait
, Action
):
222 action_or_wait
.interrupt()
226 def pause_wait(self
, wait_id
=None):
227 for _wait_id
in self
.matching_wait_ids(wait_id
=wait_id
):
228 action_or_waits
= self
.wait_ids
[_wait_id
]
229 for action_or_wait
in action_or_waits
:
230 if isinstance(action_or_wait
, Action
):
231 action_or_wait
.pause()
233 def unpause_wait(self
, wait_id
=None):
234 for _wait_id
in self
.matching_wait_ids(wait_id
=wait_id
):
235 action_or_waits
= self
.wait_ids
[_wait_id
]
236 for action_or_wait
in action_or_waits
:
237 if isinstance(action_or_wait
, Action
):
238 action_or_wait
.unpause()
240 def reset_wait(self
, wait_id
=None):
241 for _wait_id
in self
.matching_wait_ids(wait_id
=wait_id
):
242 action_or_waits
= self
.wait_ids
[_wait_id
]
243 for action_or_wait
in action_or_waits
:
244 if isinstance(action_or_wait
, Action
):
245 action_or_wait
.reset()
247 # Methods to control running keys
248 def start_running(self
, key
, start_time
):
249 self
.running
.append((key
, start_time
))
251 def keep_running(self
, key
, start_time
):
252 return (key
, start_time
) in self
.running
254 def finished_running(self
, key
, start_time
):
255 if (key
, start_time
) in self
.running
:
256 self
.running
.remove((key
, start_time
))
259 def parse_config(self
):
260 def update_alias(prop_hash
, aliases
, key
):
261 if isinstance(aliases
[key
], dict):
262 for alias
in aliases
[key
]:
263 prop_hash
.setdefault(alias
, aliases
[key
][alias
])
265 warn_print("Alias {} is not a hash, ignored".format(key
))
267 def include_aliases(prop_hash
, aliases
):
268 if 'include' not in prop_hash
:
271 included
= prop_hash
['include']
272 del(prop_hash
['include'])
273 if isinstance(included
, str):
274 update_alias(prop_hash
, aliases
, included
)
275 elif isinstance(included
, list):
276 for included_
in included
:
277 if isinstance(included_
, str):
278 update_alias(prop_hash
, aliases
, included_
)
280 warn_print("Unkown alias include type, ignored: "
281 "{} in {}".format(included_
, included
))
283 warn_print("Unkown alias include type, ignored: {}"
286 def check_key_property(key_property
, key
):
287 if 'description' in key_property
:
288 desc
= key_property
['description']
289 if not isinstance(desc
, list):
290 warn_print("description in key_property '{}' is not "
291 "a list, ignored".format(key
))
292 del(key_property
['description'])
293 if 'color' in key_property
:
294 color
= key_property
['color']
295 if not isinstance(color
, list)\
297 or not all(isinstance(item
, int) for item
in color
)\
298 or any(item
< 0 or item
> 255 for item
in color
):
299 warn_print("color in key_property '{}' is not "
300 "a list of 3 valid integers, ignored".format(key
))
301 del(key_property
['color'])
303 def check_key_properties(config
):
304 if 'key_properties' in config
:
305 if isinstance(config
['key_properties'], dict):
306 return config
['key_properties']
308 warn_print("key_properties config is not a hash, ignored")
313 def check_mapped_keys(config
):
315 if isinstance(config
['keys'], dict):
316 return config
['keys']
318 warn_print("keys config is not a hash, ignored")
323 def check_mapped_key(actions
, key
):
324 if not isinstance(actions
, list):
325 warn_print("key config '{}' is not an array, ignored"
331 def append_actions_to_key(mapped_key
, actions
, aliases
, seen_files
, music_properties
, key_properties
):
332 for index
, action
in enumerate(check_mapped_key(actions
, mapped_key
)):
333 if not isinstance(action
, dict) or\
334 not len(action
) == 1 or\
335 not isinstance(list(action
.values())[0] or {}, dict):
336 warn_print("action number {} of key '{}' is invalid, "
337 "ignored".format(index
+ 1, mapped_key
))
339 append_action_to_key(action
, mapped_key
, aliases
, seen_files
, music_properties
, key_properties
)
341 def append_action_to_key(action
, mapped_key
, aliases
, seen_files
, music_properties
, key_properties
):
342 action_name
= list(action
)[0]
344 if action
[action_name
] is None:
345 action
[action_name
] = {}
347 include_aliases(action
[action_name
], aliases
)
349 for argument
in action
[action_name
]:
350 if argument
== 'file':
351 filename
= str(action
[action_name
]['file'])
352 if filename
not in seen_files
:
353 music_property
= check_music_property(
354 music_properties
[filename
],
357 if filename
in self
.open_files
:
358 self
.open_files
[filename
]\
359 .reload_properties(**music_property
)
361 seen_files
[filename
] =\
362 self
.open_files
[filename
]
364 seen_files
[filename
] = MusicFile(
365 filename
, self
, **music_property
)
367 if filename
not in key_properties
[mapped_key
]['files']:
368 key_properties
[mapped_key
]['files'] \
369 .append(seen_files
[filename
])
371 action_args
['music'] = seen_files
[filename
]
373 action_args
[argument
] = action
[action_name
][argument
]
375 key_properties
[mapped_key
]['actions'] \
376 .append([action_name
, action_args
])
378 def check_music_property(music_property
, filename
):
379 if not isinstance(music_property
, dict):
380 warn_print("music_property config '{}' is not a hash, ignored"
383 if 'name' in music_property
:
384 music_property
['name'] = str(music_property
['name'])
385 if 'gain' in music_property
:
387 music_property
['gain'] = float(music_property
['gain'])
388 except ValueError as e
:
389 del(music_property
['gain'])
390 warn_print("gain for music_property '{}' is not "
391 "a float, ignored".format(filename
))
392 return music_property
394 stream
= open(Config
.yml_file
, "r")
396 config
= yaml
.safe_load(stream
)
397 except Exception as e
:
398 raise Exception("Error while loading config file: {}".format(e
)) from e
401 if not isinstance(config
, dict):
402 raise Exception("Top level config is supposed to be a hash")
404 if 'aliases' in config
and isinstance(config
['aliases'], dict):
405 aliases
= config
['aliases']
407 aliases
= defaultdict(dict)
408 if 'aliases' in config
:
409 warn_print("aliases config is not a hash, ignored")
411 music_properties
= defaultdict(dict)
412 if 'music_properties' in config
and\
413 isinstance(config
['music_properties'], dict):
414 music_properties
.update(config
['music_properties'])
415 elif 'music_properties' in config
:
416 warn_print("music_properties config is not a hash, ignored")
420 common_key_properties
= {}
421 if 'common' in config
['key_properties'] and\
422 isinstance(config
['key_properties'], dict):
423 common_key_properties
= config
['key_properties']['common']
424 include_aliases(common_key_properties
, aliases
)
425 check_key_property(common_key_properties
, 'common')
426 elif 'common' in config
['key_properties']:
427 warn_print("'common' key in key_properties is not a hash, ignored")
429 key_properties
= defaultdict(lambda: {
431 "properties": copy
.deepcopy(common_key_properties
),
435 for key
in check_key_properties(config
):
439 key_prop
= config
['key_properties'][key
]
441 if not isinstance(key_prop
, dict):
442 warn_print("key_property '{}' is not a hash, ignored"
446 include_aliases(key_prop
, aliases
)
447 check_key_property(key_prop
, key
)
449 key_properties
[key
]["properties"].update(key_prop
)
450 if 'actions' in key_prop
:
451 append_actions_to_key(key
, key_prop
['actions'], aliases
, seen_files
, music_properties
, key_properties
)
453 for mapped_key
in check_mapped_keys(config
):
454 append_actions_to_key(mapped_key
, config
['keys'][mapped_key
], aliases
, seen_files
, music_properties
, key_properties
)
456 return (key_properties
, seen_files
)