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