]> git.immae.eu Git - perso/Immae/Projets/Python/MusicSampler.git/blob - helpers/__init__.py
Move running keeper to Mapping
[perso/Immae/Projets/Python/MusicSampler.git] / helpers / __init__.py
1 # -*- coding: utf-8 -*-
2 from pygame import *
3 import pydub
4 import sys
5 import time
6 import threading
7
8 draw_lock = threading.RLock()
9
10 class Action:
11 action_types = [
12 'command',
13 'pause',
14 'play',
15 'stop',
16 'stop_all_actions',
17 'volume',
18 'wait',
19 ]
20
21 def __init__(self, action, key, **kwargs):
22 if action in self.action_types:
23 self.action = action
24 else:
25 raise Exception("Unknown action {}".format(action))
26
27 self.key = key
28 self.arguments = kwargs
29
30 def ready(self):
31 if 'music' in self.arguments:
32 return self.arguments['music'].loaded
33 else:
34 return True
35
36 def run(self):
37 print(getattr(self, self.action + "_print")(**self.arguments))
38 return getattr(self, self.action)(**self.arguments)
39
40 def command(self, command = "", **kwargs):
41 pass
42
43 def pause(self, music = None, **kwargs):
44 if music is not None:
45 music.pause()
46 else:
47 mixer.pause()
48
49 def play(self, music = None, fade_in = 0, start_at = 0,
50 restart_if_running = False, volume = 100, **kwargs):
51 if music is not None:
52 music.play()
53 else:
54 mixer.unpause()
55
56 def stop(self, music = None, fade_out = 0, **kwargs):
57 if music is not None:
58 music.stop()
59 else:
60 mixer.stop()
61
62 def stop_all_actions(self, **kwargs):
63 self.key.mapping.stop_all_running()
64
65 def volume(self, music = None, value = 100, **kwargs):
66 pass
67
68 def wait(self, duration = 0, **kwargs):
69 time.sleep(duration)
70
71 def command_print(self, command = "", **kwargs):
72 return "running command {}".format(command)
73
74 def pause_print(self, music = None, **kwargs):
75 if music is not None:
76 return "pausing {}".format(music.filename)
77 else:
78 return "pausing all musics"
79
80 def play_print(self, music = None, fade_in = 0, start_at = 0,
81 restart_if_running = False, volume = 100, **kwargs):
82 message = "starting "
83 if music is not None:
84 message += music.filename
85 else:
86 message += "music"
87
88 if start_at != 0:
89 message += " at {}s".format(start_at)
90
91 if fade_in != 0:
92 message += " with {}s fade_in".format(fade_in)
93
94 message += " at volume {}%".format(volume)
95
96 if restart_if_running:
97 message += " (restarting if already running)"
98
99 return message
100
101 def stop_print(self, music = None, fade_out = 0, **kwargs):
102 if music is not None:
103 if fade_out == 0:
104 return "stopping music {}".format(music.filename)
105 else:
106 return "stopping music {} with {}s fadeout".format(music.filename, fade_out)
107 else:
108 if fade_out == 0:
109 return "stopping all musics"
110 else:
111 return "stopping all musics with {}s fadeout".format(fade_out)
112
113 def stop_all_actions_print(self):
114 return "stopping all actions"
115
116 def volume_print(self, music = None, value = 100, *kwargs):
117 if music is not None:
118 return "setting volume of {} to {}%".format(music.filename, value)
119 else:
120 return "setting volume to {}%".format(value)
121
122 def wait_print(self, duration, **kwargs):
123 return "waiting {}s".format(duration)
124
125 class Key:
126 row_positions = {
127 'first': 0,
128 'second': 50,
129 'third': 100,
130 'fourth': 150,
131 'fifth': 200,
132 'sixth': 250,
133 }
134
135 default_outer_color = (120, 120, 120)
136 lighter_outer_color = (200, 200, 200)
137 default_inner_color = (255, 255, 255)
138 mapped_inner_color = ( 0, 255, 0)
139 mapped_unready_inner_color = (255, 165, 0)
140
141 def __init__(self, mapping, key_name, key_sym, top, left, width = 48, height = 48, disabled = False):
142 self.mapping = mapping
143 self.key_name = key_name
144 self.key_sym = key_sym
145
146 if isinstance(top, str):
147 self.top = self.row_positions[top]
148 else:
149 self.top = top
150
151 self.left = left
152 self.width = width
153 self.height = height
154
155 self.bottom = self.top + self.height
156 self.right = self.left + self.width
157
158 self.rect = (self.left, self.top, self.right, self.bottom)
159 self.position = (self.left, self.top)
160
161 if disabled:
162 self.outer_color = self.lighter_outer_color
163 self.linewidth = 1
164 else:
165 self.outer_color = self.default_outer_color
166 self.linewidth = 3
167
168 self.inner_color = self.default_inner_color
169 self.actions = []
170
171 def square(self, all_actions_ready):
172 if self.has_actions():
173 if all_actions_ready:
174 self.inner_color = self.mapped_inner_color
175 else:
176 self.inner_color = self.mapped_unready_inner_color
177
178 return RoundedRect((0, 0, self.width, self.height),
179 self.outer_color, self.inner_color, self.linewidth)
180
181 def collidepoint(self, position):
182 return self.surface.get_rect().collidepoint(
183 position[0] - self.position[0],
184 position[1] - self.position[1]
185 )
186
187 def draw(self, background_surface):
188 draw_lock.acquire()
189 all_actions_ready = self.all_actions_ready()
190
191 self.surface = self.square(all_actions_ready).surface()
192
193 if getattr(sys, 'frozen', False):
194 police = font.Font(sys._MEIPASS + "/Ubuntu-Regular.ttf", 14)
195 else:
196 police = font.Font("Ubuntu-Regular.ttf", 14)
197
198 text = police.render(self.key_sym, True, (0,0,0))
199 self.surface.blit(text, (5,5))
200 background_surface.blit(self.surface, self.position)
201 draw_lock.release()
202
203 return not all_actions_ready
204
205 def poll_redraw(self, background):
206 while True:
207 time.sleep(1)
208 if self.all_actions_ready():
209 self.draw(background)
210 self.mapping.blit()
211 break
212
213 def has_actions(self):
214 return len(self.actions) > 0
215
216 def all_actions_ready(self):
217 return all(action.ready() for action in self.actions)
218
219 def add_action(self, action_name, **arguments):
220 self.actions.append(Action(action_name, self, **arguments))
221
222 def do_actions(self):
223 print("running actions for {}".format(self.key_sym))
224 start_time = time.time()
225 self.mapping.start_running(self, start_time)
226 for action in self.actions:
227 if self.mapping.keep_running(self, start_time):
228 action.run()
229
230 self.mapping.finished_running(self, start_time)
231
232 def list_actions(self, surface):
233 # FIXME: Todo
234 print("bouh", self.key_sym)
235 surface.fill((255, 0, 0))
236
237
238 class Mapping:
239 WIDTH = 903
240 HEIGHT = 298
241 SIZE = WIDTH, HEIGHT
242
243 KEYS = [
244 (K_ESCAPE, 'ESC', 'first', 0, {}),
245
246 (K_F1, 'F1', 'first', 100, {}),
247 (K_F2, 'F2', 'first', 150, {}),
248 (K_F3, 'F3', 'first', 200, {}),
249 (K_F4, 'F4', 'first', 250, {}),
250
251 (K_F5, 'F5', 'first', 325, {}),
252 (K_F6, 'F6', 'first', 375, {}),
253 (K_F7, 'F7', 'first', 425, {}),
254 (K_F8, 'F8', 'first', 475, {}),
255
256 (K_F9, 'F9', 'first', 550, {}),
257 (K_F10, 'F10', 'first', 600, {}),
258 (K_F11, 'F11', 'first', 650, {}),
259 (K_F12, 'F12', 'first', 700, {}),
260
261
262 (178, '²', 'second', 0, {}),
263 (K_AMPERSAND, '&', 'second', 50, {}),
264 (233, 'é', 'second', 100, {}),
265 (K_QUOTEDBL, '"', 'second', 150, {}),
266 (K_QUOTE, "'", 'second', 200, {}),
267 (K_LEFTPAREN, '(', 'second', 250, {}),
268 (K_MINUS, '-', 'second', 300, {}),
269 (232, 'è', 'second', 350, {}),
270 (K_UNDERSCORE, '_', 'second', 400, {}),
271 (231, 'ç', 'second', 450, {}),
272 (224, 'à', 'second', 500, {}),
273 (K_RIGHTPAREN, ')', 'second', 550, {}),
274 (K_EQUALS, '=', 'second', 600, {}),
275
276 (K_BACKSPACE, '<-', 'second', 650, { 'width': 98 }),
277
278
279 (K_TAB, 'tab', 'third', 0, { 'width' : 73 }),
280 (K_a, 'a', 'third', 75, {}),
281 (K_z, 'z', 'third', 125, {}),
282 (K_e, 'e', 'third', 175, {}),
283 (K_r, 'r', 'third', 225, {}),
284 (K_t, 't', 'third', 275, {}),
285 (K_y, 'y', 'third', 325, {}),
286 (K_u, 'u', 'third', 375, {}),
287 (K_i, 'i', 'third', 425, {}),
288 (K_o, 'o', 'third', 475, {}),
289 (K_p, 'p', 'third', 525, {}),
290 (K_CARET, '^', 'third', 575, {}),
291 (K_DOLLAR, '$', 'third', 625, {}),
292
293 (K_RETURN, 'Enter', 'third', 692, { 'width': 56, 'height': 98 }),
294
295 (K_CAPSLOCK, 'CAPS', 'fourth', 0, { 'width': 88, 'disabled': True }),
296
297 (K_q, 'q', 'fourth', 90, {}),
298 (K_s, 's', 'fourth', 140, {}),
299 (K_d, 'd', 'fourth', 190, {}),
300 (K_f, 'f', 'fourth', 240, {}),
301 (K_g, 'g', 'fourth', 290, {}),
302 (K_h, 'h', 'fourth', 340, {}),
303 (K_j, 'j', 'fourth', 390, {}),
304 (K_k, 'k', 'fourth', 440, {}),
305 (K_l, 'l', 'fourth', 490, {}),
306 (K_m, 'm', 'fourth', 540, {}),
307 (249, 'ù', 'fourth', 590, {}),
308 (K_ASTERISK, '*', 'fourth', 640, {}),
309
310
311 (K_LSHIFT, 'LShift', 'fifth', 0, { 'width': 63, 'disabled': True }),
312
313 (K_LESS, '<', 'fifth', 65, {}),
314 (K_w, 'w', 'fifth', 115, {}),
315 (K_x, 'x', 'fifth', 165, {}),
316 (K_c, 'c', 'fifth', 215, {}),
317 (K_v, 'v', 'fifth', 265, {}),
318 (K_b, 'b', 'fifth', 315, {}),
319 (K_n, 'n', 'fifth', 365, {}),
320 (K_COMMA, ',', 'fifth', 415, {}),
321 (K_SEMICOLON, ';', 'fifth', 465, {}),
322 (K_COLON, ':', 'fifth', 515, {}),
323 (K_EXCLAIM, '!', 'fifth', 565, {}),
324
325 (K_RSHIFT, 'RShift', 'fifth', 615, { 'width': 133, 'disabled': True }),
326
327 (K_LCTRL, 'LCtrl', 'sixth', 0, { 'width': 63, 'disabled': True }),
328 (K_LSUPER, 'LSuper', 'sixth', 115, { 'disabled': True }),
329 (K_LALT, 'LAlt', 'sixth', 165, { 'disabled': True }),
330 (K_SPACE, 'Espace', 'sixth', 215, { 'width': 248 }),
331 (K_MODE, 'AltGr', 'sixth', 465, { 'disabled': True }),
332 (314, 'Compose', 'sixth', 515, { 'disabled': True }),
333 (K_RCTRL, 'RCtrl', 'sixth', 565, { 'width': 63, 'disabled': True }),
334
335
336 (K_INSERT, 'ins', 'second', 755, {}),
337 (K_HOME, 'home', 'second', 805, {}),
338 (K_PAGEUP, 'pg_u', 'second', 855, {}),
339 (K_DELETE, 'del', 'third', 755, {}),
340 (K_END, 'end', 'third', 805, {}),
341 (K_PAGEDOWN, 'pg_d', 'third', 855, {}),
342
343
344 (K_UP, 'up', 'fifth', 805, {}),
345 (K_DOWN, 'down', 'sixth', 805, {}),
346 (K_LEFT, 'left', 'sixth', 755, {}),
347 (K_RIGHT, 'right', 'sixth', 855, {}),
348 ]
349
350 def __init__(self, screen):
351 self.screen = screen
352 self.background = Surface(self.SIZE).convert()
353 self.background.fill((250, 250, 250))
354 self.keys = {}
355 self.running = []
356 for key in self.KEYS:
357 self.keys[key[0]] = Key(self, *key[0:4], **key[4])
358
359 def draw(self):
360 for key_name in self.keys:
361 key = self.keys[key_name]
362 should_redraw_key = key.draw(self.background)
363
364 if should_redraw_key:
365 threading.Thread(target = key.poll_redraw, args = [self.background]).start()
366 self.blit()
367
368 def blit(self):
369 draw_lock.acquire()
370 self.screen.blit(self.background, (5, 5))
371 display.flip()
372 draw_lock.release()
373
374 def find_by_key_num(self, key_num):
375 if key_num in self.keys:
376 return self.keys[key_num]
377 return None
378
379 def find_by_collidepoint(self, position):
380 for key in self.keys:
381 if self.keys[key].collidepoint(position):
382 return self.keys[key]
383 return None
384
385 def find_by_unicode(self, key_sym):
386 for key in self.keys:
387 if self.keys[key].key_sym == key_sym:
388 return self.keys[key]
389 return None
390
391 def stop_all_running(self):
392 self.running = []
393
394 def start_running(self, key, start_time):
395 self.running.append((key, start_time))
396
397 def keep_running(self, key, start_time):
398 return (key, start_time) in self.running
399
400 def finished_running(self, key, start_time):
401 if (key, start_time) in self.running:
402 self.running.remove((key, start_time))
403
404
405 class MusicFile:
406 def __init__(self, filename, lock):
407 self.filename = filename
408 self.channel = None
409 self.raw_data = None
410 self.sound = None
411
412 self.loaded = False
413 threading.Thread(target = self.load_sound, args = [lock]).start()
414
415 def load_sound(self, lock):
416 lock.acquire()
417 print("Loading {}".format(self.filename))
418 self.raw_data = pydub.AudioSegment.from_file(self.filename).raw_data
419 self.sound = mixer.Sound(self.raw_data)
420 print("Loaded {}".format(self.filename))
421 self.loaded = True
422 lock.release()
423
424 def play(self):
425 self.channel = self.sound.play()
426
427 def pause(self):
428 if self.channel is not None:
429 self.channel.pause()
430
431 def stop(self):
432 self.channel = None
433 self.sound.stop()
434
435
436 class RoundedRect:
437 def __init__(self, rect, outer_color, inner_color, linewidth = 2, radius = 0.4):
438 self.rect = Rect(rect)
439 self.outer_color = Color(*outer_color)
440 self.inner_color = Color(*inner_color)
441 self.linewidth = linewidth
442 self.radius = radius
443
444 def surface(self):
445 rectangle = self.filledRoundedRect(self.rect, self.outer_color, self.radius)
446
447 inner_rect = Rect((
448 self.rect.left + 2 * self.linewidth,
449 self.rect.top + 2 * self.linewidth,
450 self.rect.right - 2 * self.linewidth,
451 self.rect.bottom - 2 * self.linewidth
452 ))
453
454 inner_rectangle = self.filledRoundedRect(inner_rect, self.inner_color, self.radius)
455
456 rectangle.blit(inner_rectangle, (self.linewidth, self.linewidth))
457
458 return rectangle
459
460 def filledRoundedRect(self, rect, color, radius=0.4):
461 """
462 filledRoundedRect(rect,color,radius=0.4)
463
464 rect : rectangle
465 color : rgb or rgba
466 radius : 0 <= radius <= 1
467 """
468
469 alpha = color.a
470 color.a = 0
471 pos = rect.topleft
472 rect.topleft = 0,0
473 rectangle = Surface(rect.size,SRCALPHA)
474
475 circle = Surface([min(rect.size)*3]*2,SRCALPHA)
476 draw.ellipse(circle,(0,0,0),circle.get_rect(),0)
477 circle = transform.smoothscale(circle,[int(min(rect.size)*radius)]*2)
478
479 radius = rectangle.blit(circle,(0,0))
480 radius.bottomright = rect.bottomright
481 rectangle.blit(circle,radius)
482 radius.topright = rect.topright
483 rectangle.blit(circle,radius)
484 radius.bottomleft = rect.bottomleft
485 rectangle.blit(circle,radius)
486
487 rectangle.fill((0,0,0),rect.inflate(-radius.w,0))
488 rectangle.fill((0,0,0),rect.inflate(0,-radius.h))
489
490 rectangle.fill(color,special_flags=BLEND_RGBA_MAX)
491 rectangle.fill((255,255,255,alpha),special_flags=BLEND_RGBA_MIN)
492
493 return rectangle
494
495
496 def parse_config(mapping):
497 import yaml
498 stream = open("config.yml", "r")
499 config = yaml.load(stream)
500 stream.close()
501
502 aliases = config['aliases']
503 seen_files = {}
504
505 file_lock = threading.RLock()
506
507 for mapped_key in config['keys']:
508 key = mapping.find_by_unicode(mapped_key)
509 if key is None:
510 continue
511
512 for action in config['keys'][mapped_key]:
513 action_name = list(action)[0]
514 action_args = {}
515 if action[action_name] is None:
516 action[action_name] = []
517
518 if 'include' in action[action_name]:
519 included = action[action_name]['include']
520 del(action[action_name]['include'])
521
522 if isinstance(included, str):
523 action[action_name].update(aliases[included], **action[action_name])
524 else:
525 for included_ in included:
526 action[action_name].update(aliases[included_], **action[action_name])
527
528 for argument in action[action_name]:
529 if argument == 'file':
530 filename = action[action_name]['file']
531 if filename not in seen_files:
532 seen_files[filename] = MusicFile(filename, file_lock)
533
534 action_args['music'] = seen_files[filename]
535
536 else:
537 action_args[argument] = action[action_name][argument]
538
539 key.add_action(action_name, **action_args)