]>
git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - music_sampler/mapping.py
50b68a9d903df95b0236cf76b4c39245628e9403
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
9 from collections
import defaultdict
11 from transitions
.extensions
import HierarchicalMachine
as Machine
13 from .music_file
import MusicFile
14 from .mixer
import Mixer
15 from .helpers
import Config
, gain
, error_print
, warn_print
16 from .action
import Action
18 class Mapping(RelativeLayout
):
30 'trigger': 'configure',
36 'source': 'configuring',
41 'source': 'configuring',
47 'source': 'configured',
67 master_volume
= NumericProperty(100)
68 ready_color
= ListProperty([1, 165/255, 0, 1])
69 state
= StringProperty("")
71 def __init__(self
, **kwargs
):
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
)
86 def on_enter_configuring(self
):
87 if Config
.builtin_mixing
:
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, exit
=True)
100 def on_enter_loading(self
):
101 for key
in self
.keys
:
106 def add_widget(self
, widget
, index
=0):
107 if type(widget
).__name
__ == "Key" and widget
not in self
.keys
:
108 self
.keys
.append(widget
)
109 return super(Mapping
, self
).add_widget(widget
, index
)
111 def remove_widget(self
, widget
, index
=0):
112 if type(widget
).__name
__ == "Key" and widget
in self
.keys
:
113 self
.keys
.remove(widget
)
114 return super(Mapping
, self
).remove_widget(widget
, index
)
116 def on_keyboard_closed(self
):
117 self
.keyboard
.unbind(on_key_down
=self
.on_keyboard_down
)
120 def on_keyboard_down(self
, keyboard
, keycode
, text
, modifiers
):
121 key
= self
.find_by_key_code(keycode
)
122 if self
.allowed_modifiers(modifiers
) and key
is not None:
124 threading
.Thread(name
="MSKeyAction", target
=key
.run
,
125 args
=['-'.join(modifiers
)]).start()
126 elif 'ctrl' in modifiers
and (keycode
[0] == 113 or keycode
[0] == '99'):
127 self
.leave_application()
129 elif 'ctrl' in modifiers
and keycode
[0] == 114:
130 threading
.Thread(name
="MSReload", target
=self
.reload).start()
133 def leave_application(self
):
134 self
.keyboard
.unbind(on_key_down
=self
.on_keyboard_down
)
135 self
.stop_all_running()
136 for music
in self
.open_files
.values():
138 for thread
in threading
.enumerate():
139 if thread
.getName()[0:2] == "MS":
141 elif thread
.__class
__ == threading
.Timer
:
146 def allowed_modifiers(self
, modifiers
):
148 return len([a
for a
in modifiers
if a
not in allowed
]) == 0
150 def find_by_key_code(self
, key_code
):
151 if "Key_" + str(key_code
[0]) in self
.ids
:
152 return self
.ids
["Key_" + str(key_code
[0])]
155 def all_keys_ready(self
):
157 for key
in self
.keys
:
158 if not key
.is_loaded_or_failed():
160 partial
= partial
or key
.is_failed()
168 def key_loaded_callback(self
):
169 result
= self
.all_keys_ready()
170 if result
== "success":
171 self
.ready_color
= [0, 1, 0, 1]
172 elif result
== "partial":
173 self
.ready_color
= [1, 0, 0, 1]
175 self
.ready_color
= [1, 165/255, 0, 1]
177 ## Some global actions
178 def stop_all_running(self
, except_key
=None, key_start_time
=0):
179 running
= self
.running
180 self
.running
= [r
for r
in running\
181 if r
[0] == except_key
and r
[1] == key_start_time
]
182 for (key
, start_time
) in running
:
183 if (key
, start_time
) != (except_key
, key_start_time
):
186 # Master volume methods
188 def master_gain(self
):
189 return gain(self
.master_volume
)
191 def set_master_volume(self
, value
, delta
=False, fade
=0):
192 [db_gain
, self
.master_volume
] = gain(
193 value
+ int(delta
) * self
.master_volume
,
196 for music
in self
.open_files
.values():
197 music
.set_gain_with_effect(db_gain
, fade
=fade
)
199 # Wait handler methods
200 def add_wait_id(self
, wait_id
, action_or_wait
):
201 self
.wait_ids
[wait_id
] = action_or_wait
203 def interrupt_wait(self
, wait_id
):
204 if wait_id
in self
.wait_ids
:
205 action_or_wait
= self
.wait_ids
[wait_id
]
206 del(self
.wait_ids
[wait_id
])
207 if isinstance(action_or_wait
, Action
):
208 action_or_wait
.interrupt()
212 # Methods to control running keys
213 def start_running(self
, key
, start_time
):
214 self
.running
.append((key
, start_time
))
216 def keep_running(self
, key
, start_time
):
217 return (key
, start_time
) in self
.running
219 def finished_running(self
, key
, start_time
):
220 if (key
, start_time
) in self
.running
:
221 self
.running
.remove((key
, start_time
))
224 def parse_config(self
):
225 def update_alias(prop_hash
, aliases
, key
):
226 if isinstance(aliases
[key
], dict):
227 for alias
in aliases
[key
]:
228 prop_hash
.setdefault(alias
, aliases
[key
][alias
])
230 warn_print("Alias {} is not a hash, ignored".format(key
))
232 def include_aliases(prop_hash
, aliases
):
233 if 'include' not in prop_hash
:
236 included
= prop_hash
['include']
237 del(prop_hash
['include'])
238 if isinstance(included
, str):
239 update_alias(prop_hash
, aliases
, included
)
240 elif isinstance(included
, list):
241 for included_
in included
:
242 if isinstance(included_
, str):
243 update_alias(prop_hash
, aliases
, included_
)
245 warn_print("Unkown alias include type, ignored: "
246 "{} in {}".format(included_
, included
))
248 warn_print("Unkown alias include type, ignored: {}"
251 def check_key_property(key_property
, key
):
252 if 'description' in key_property
:
253 desc
= key_property
['description']
254 if not isinstance(desc
, list):
255 warn_print("description in key_property '{}' is not "
256 "a list, ignored".format(key
))
257 del(key_property
['description'])
258 if 'color' in key_property
:
259 color
= key_property
['color']
260 if not isinstance(color
, list)\
262 or not all(isinstance(item
, int) for item
in color
)\
263 or any(item
< 0 or item
> 255 for item
in color
):
264 warn_print("color in key_property '{}' is not "
265 "a list of 3 valid integers, ignored".format(key
))
266 del(key_property
['color'])
268 def check_key_properties(config
):
269 if 'key_properties' in config
:
270 if isinstance(config
['key_properties'], dict):
271 return config
['key_properties']
273 warn_print("key_properties config is not a hash, ignored")
278 def check_mapped_keys(config
):
280 if isinstance(config
['keys'], dict):
281 return config
['keys']
283 warn_print("keys config is not a hash, ignored")
288 def check_mapped_key(mapped_keys
, key
):
289 if not isinstance(mapped_keys
[key
], list):
290 warn_print("key config '{}' is not an array, ignored"
294 return mapped_keys
[key
]
296 def check_music_property(music_property
, filename
):
297 if not isinstance(music_property
, dict):
298 warn_print("music_property config '{}' is not a hash, ignored"
301 if 'name' in music_property
:
302 music_property
['name'] = str(music_property
['name'])
303 if 'gain' in music_property
:
305 music_property
['gain'] = float(music_property
['gain'])
306 except ValueError as e
:
307 del(music_property
['gain'])
308 warn_print("gain for music_property '{}' is not "
309 "a float, ignored".format(filename
))
310 return music_property
312 stream
= open(Config
.yml_file
, "r")
314 config
= yaml
.safe_load(stream
)
315 except Exception as e
:
316 error_print("Error while loading config file: {}".format(e
),
320 if not isinstance(config
, dict):
321 error_print("Top level config is supposed to be a hash",
324 if 'aliases' in config
and isinstance(config
['aliases'], dict):
325 aliases
= config
['aliases']
327 aliases
= defaultdict(dict)
328 if 'aliases' in config
:
329 warn_print("aliases config is not a hash, ignored")
331 music_properties
= defaultdict(dict)
332 if 'music_properties' in config
and\
333 isinstance(config
['music_properties'], dict):
334 music_properties
.update(config
['music_properties'])
335 elif 'music_properties' in config
:
336 warn_print("music_properties config is not a hash, ignored")
340 common_key_properties
= {}
341 if 'common' in config
['key_properties'] and\
342 isinstance(config
['key_properties'], dict):
343 common_key_properties
= config
['key_properties']['common']
344 include_aliases(common_key_properties
, aliases
)
345 elif 'common' in config
['key_properties']:
346 warn_print("'common' key in key_properties is not a hash, ignored")
348 key_properties
= defaultdict(lambda: {
354 for key
in check_key_properties(config
):
358 key_prop
= config
['key_properties'][key
]
360 if not isinstance(key_prop
, dict):
361 warn_print("key_property '{}' is not a hash, ignored"
365 include_aliases(key_prop
, aliases
)
366 for _key
in common_key_properties
:
367 key_prop
.setdefault(_key
, common_key_properties
[_key
])
369 check_key_property(key_prop
, key
)
371 key_properties
[key
]["properties"] = key_prop
373 for mapped_key
in check_mapped_keys(config
):
374 for index
, action
in enumerate(check_mapped_key(
375 config
['keys'], mapped_key
)):
376 if not isinstance(action
, dict) or\
377 not len(action
) == 1 or\
378 not isinstance(list(action
.values())[0] or {}, dict):
379 warn_print("action number {} of key '{}' is invalid, "
380 "ignored".format(index
+ 1, mapped_key
))
383 action_name
= list(action
)[0]
385 if action
[action_name
] is None:
386 action
[action_name
] = {}
388 include_aliases(action
[action_name
], aliases
)
390 for argument
in action
[action_name
]:
391 if argument
== 'file':
392 filename
= str(action
[action_name
]['file'])
393 if filename
not in seen_files
:
394 music_property
= check_music_property(
395 music_properties
[filename
],
398 if filename
in self
.open_files
:
399 self
.open_files
[filename
]\
400 .reload_properties(**music_property
)
402 seen_files
[filename
] =\
403 self
.open_files
[filename
]
405 seen_files
[filename
] = MusicFile(
406 filename
, self
, **music_property
)
408 if filename
not in key_properties
[mapped_key
]['files']:
409 key_properties
[mapped_key
]['files'] \
410 .append(seen_files
[filename
])
412 action_args
['music'] = seen_files
[filename
]
414 action_args
[argument
] = action
[action_name
][argument
]
416 key_properties
[mapped_key
]['actions'] \
417 .append([action_name
, action_args
])
419 return (key_properties
, seen_files
)