aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2016-06-25 23:37:49 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2016-06-25 23:40:51 +0200
commit4b2d79ca27dcbb85465829595ad81cec5fc63983 (patch)
tree683b3a9b19aa01de53b0dccc3ac143d876dcf088
parent8612c5f8bb0fc9529bc489a6719654d4474db6d5 (diff)
downloadMusicSampler-4b2d79ca27dcbb85465829595ad81cec5fc63983.tar.gz
MusicSampler-4b2d79ca27dcbb85465829595ad81cec5fc63983.tar.zst
MusicSampler-4b2d79ca27dcbb85465829595ad81cec5fc63983.zip
Migrate to kivy
-rw-r--r--helpers/__init__.py149
-rw-r--r--helpers/font.py10
-rw-r--r--helpers/key.py202
-rw-r--r--helpers/mapping.py281
-rw-r--r--helpers/rounded_rect.py62
-rw-r--r--keyboard.py222
-rw-r--r--music_sampler.py123
-rw-r--r--musicsampler.kv7
8 files changed, 243 insertions, 813 deletions
diff --git a/helpers/__init__.py b/helpers/__init__.py
index eb948f2..40a96af 100644
--- a/helpers/__init__.py
+++ b/helpers/__init__.py
@@ -1,150 +1 @@
1# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
2from .music_file import *
3from .mapping import *
4from .lock import *
5from .font import *
6import yaml
7
8def parse_config2():
9 stream = open("config.yml", "r")
10 config = yaml.load(stream)
11 stream.close()
12
13 aliases = config['aliases']
14 seen_files = {}
15
16 file_lock = Lock("file")
17
18 channel_id = 0
19
20 key_properties = {}
21
22 for key in config['key_properties']:
23 if key not in key_properties:
24 key_properties[key] = {
25 "actions": [],
26 "properties": config['key_properties'][key],
27 "files": []
28 }
29
30 for mapped_key in config['keys']:
31 if mapped_key not in key_properties:
32 key_properties[mapped_key] = {
33 "actions": [],
34 "properties": {},
35 "files": []
36 }
37 for action in config['keys'][mapped_key]:
38 action_name = list(action)[0]
39 action_args = {}
40 if action[action_name] is None:
41 action[action_name] = []
42
43 if 'include' in action[action_name]:
44 included = action[action_name]['include']
45 del(action[action_name]['include'])
46
47 if isinstance(included, str):
48 action[action_name].update(aliases[included], **action[action_name])
49 else:
50 for included_ in included:
51 action[action_name].update(aliases[included_], **action[action_name])
52
53 for argument in action[action_name]:
54 if argument == 'file':
55 filename = action[action_name]['file']
56 if filename not in seen_files:
57 if filename in config['music_properties']:
58 seen_files[filename] = MusicFile(
59 filename,
60 file_lock,
61 channel_id,
62 **config['music_properties'][filename])
63 else:
64 seen_files[filename] = MusicFile(
65 filename,
66 file_lock,
67 channel_id)
68 channel_id = channel_id + 1
69
70 if filename not in key_properties[mapped_key]['files']:
71 key_properties[mapped_key]['files'].append(seen_files[filename])
72
73 action_args['music'] = seen_files[filename]
74
75 else:
76 action_args[argument] = action[action_name][argument]
77
78 key_properties[mapped_key]['actions'].append([action_name, action_args])
79
80 return (key_properties, channel_id + 1, seen_files)
81
82def parse_config(mapping):
83 stream = open("config.yml", "r")
84 config = yaml.load(stream)
85 stream.close()
86
87 aliases = config['aliases']
88 seen_files = {}
89
90 file_lock = Lock("file")
91
92 channel_id = 0
93
94 for mapped_key in config['keys']:
95 key = mapping.find_by_unicode(mapped_key)
96 if key is None:
97 continue
98
99 for action in config['keys'][mapped_key]:
100 action_name = list(action)[0]
101 action_args = {}
102 if action[action_name] is None:
103 action[action_name] = []
104
105 if 'include' in action[action_name]:
106 included = action[action_name]['include']
107 del(action[action_name]['include'])
108
109 if isinstance(included, str):
110 action[action_name].update(aliases[included], **action[action_name])
111 else:
112 for included_ in included:
113 action[action_name].update(aliases[included_], **action[action_name])
114
115 for argument in action[action_name]:
116 if argument == 'file':
117 filename = action[action_name]['file']
118 if filename not in seen_files:
119 if filename in config['music_properties']:
120 seen_files[filename] = MusicFile(
121 filename,
122 file_lock,
123 channel_id,
124 **config['music_properties'][filename])
125 else:
126 seen_files[filename] = MusicFile(
127 filename,
128 file_lock,
129 channel_id)
130 channel_id = channel_id + 1
131
132 action_args['music'] = seen_files[filename]
133
134 else:
135 action_args[argument] = action[action_name][argument]
136
137 key.add_action(action_name, **action_args)
138
139 for key_property in config['key_properties']:
140 key = mapping.find_by_unicode(key_property)
141 if key is None:
142 continue
143
144 if 'description' in config['key_properties'][key_property]:
145 key.set_description(config['key_properties'][key_property]['description'])
146 if 'color' in config['key_properties'][key_property]:
147 key.set_color(config['key_properties'][key_property]['color'])
148
149 # Return the number of channels reserved
150 return (channel_id + 1, seen_files)
diff --git a/helpers/font.py b/helpers/font.py
deleted file mode 100644
index 83999ef..0000000
--- a/helpers/font.py
+++ /dev/null
@@ -1,10 +0,0 @@
1import os
2import pygame
3import sys
4
5def font(size, font = "Ubuntu-Regular"):
6 if getattr(sys, 'frozen', False):
7 return pygame.font.Font(sys._MEIPASS + "/fonts/" + font + ".ttf", size)
8 else:
9 path = os.path.dirname(os.path.realpath(__file__))
10 return pygame.font.Font(path + "/../fonts/" + font + ".ttf", size)
diff --git a/helpers/key.py b/helpers/key.py
index d923b1d..658c17f 100644
--- a/helpers/key.py
+++ b/helpers/key.py
@@ -1,169 +1,101 @@
1from .rounded_rect import * 1from kivy.uix.widget import Widget
2from kivy.properties import AliasProperty, BooleanProperty, ListProperty, StringProperty
3from kivy.clock import Clock
4from kivy.uix.behaviors import ButtonBehavior
5
2from .action import * 6from .action import *
3from .font import font
4import time 7import time
5import sys 8
6import pygame 9class Key(ButtonBehavior, Widget):
7 10 key_sym = StringProperty(None)
8class Key: 11 custom_color = ListProperty([0, 1, 0, 1])
9 default_outer_color = (120, 120, 120) 12 custom_unready_color = ListProperty([0, 1, 0, 100/255])
10 lighter_outer_color = (200, 200, 200) 13 description_title = StringProperty("")
11 default_inner_color = (255, 255, 255) 14 description = ListProperty([])
12 mapped_inner_color = ( 0, 255, 0) 15 is_key_ready = BooleanProperty(True)
13 mapped_unready_inner_color = ( 0, 255, 0, 100) 16
14 17 def get_color(self):
15 def __init__(self, mapping, draw_lock, key_name, key_sym, top, left, width = 48, height = 48, disabled = False): 18 if not self.has_actions:
16 self.draw_lock = draw_lock 19 return [1, 1, 1, 1]
17 self.mapping = mapping 20 elif self.all_actions_ready:
18 self.key_name = key_name 21 return self.custom_color
19 self.key_sym = key_sym
20
21 self.top = top
22 self.left = left
23 self.width = width
24 self.height = height
25
26 self.bottom = self.top + self.height
27 self.right = self.left + self.width
28
29 self.rect = (self.left, self.top, self.right, self.bottom)
30 self.position = (self.left, self.top)
31 self.disabled = disabled
32
33 if disabled:
34 self.outer_color = self.lighter_outer_color
35 self.linewidth = 1
36 else: 22 else:
37 self.outer_color = self.default_outer_color 23 return self.custom_unready_color
38 self.linewidth = 3 24 def set_color(self):
25 pass
26
27 color = AliasProperty(get_color, set_color, bind=['is_key_ready'])
39 28
40 self.inner_color = self.default_inner_color 29 def __init__(self, **kwargs):
30 super(Key, self).__init__(**kwargs)
41 self.actions = [] 31 self.actions = []
42 self.description = []
43 self.custom_color = self.mapped_inner_color
44 self.custom_unready_color = self.mapped_unready_inner_color
45
46 def square(self, all_actions_ready):
47 if self.has_actions():
48 if all_actions_ready:
49 self.inner_color = self.custom_color
50 else:
51 self.inner_color = self.custom_unready_color
52 32
53 return RoundedRect((0, 0, self.width, self.height), 33 def on_key_sym(self, key, key_sym):
54 self.outer_color, self.inner_color, self.linewidth) 34 if key_sym in self.parent.key_config:
35 self.is_key_ready = False
36
37 self.config = self.parent.key_config[key_sym]
55 38
56 def collidepoint(self, position): 39 self.actions = []
57 return self.surface.get_rect().collidepoint( 40 for key_action in self.config['actions']:
58 position[0] - self.position[0], 41 self.add_action(key_action[0], **key_action[1])
59 position[1] - self.position[1] 42
60 ) 43 if 'description' in self.config['properties']:
44 key.set_description(self.config['properties']['description'])
45 if 'color' in self.config['properties']:
46 key.set_color(self.config['properties']['color'])
47
48 Clock.schedule_interval(self.check_all_active, 1)
49
50 def check_all_active(self, dt):
51 if self.all_actions_ready:
52 self.is_key_ready = True
53 return False
61 54
62 def set_description(self, description): 55 def set_description(self, description):
63 for desc in description: 56 if description[0] is not None:
57 self.description_title = str(description[0])
58 for desc in description[1:]:
64 if desc is None: 59 if desc is None:
65 self.description.append("") 60 self.description.append("")
66 else: 61 else:
67 self.description.append(str(desc)) 62 self.description.append(str(desc).replace(" ", " "))
68 63
69 def set_color(self, color): 64 def set_color(self, color):
70 self.custom_color = tuple(color) 65 color = [x / 255 for x in color]
71 color.append(100) 66 color.append(1)
67 self.custom_color = color
68 color[3] = 100 / 255
72 self.custom_unready_color = tuple(color) 69 self.custom_unready_color = tuple(color)
73 70
74 def draw(self, background_surface): 71 @property
75 self.draw_lock.acquire()
76 all_actions_ready = self.all_actions_ready()
77
78 self.surface = self.square(all_actions_ready).surface()
79
80 police = font(14)
81 text_police = font(10)
82
83 police.set_bold(True)
84 text = police.render(self.key_sym, True, (0,0,0))
85 self.surface.blit(text, (5,5))
86
87 is_first_line = True
88 offset = 11 + text_police.get_linesize() - 4
89 first_line_offset = 18
90 for description in self.description:
91 text = text_police.render(description, True, (0,0,0))
92 if is_first_line:
93 self.surface.blit(text, (first_line_offset, 9))
94 is_first_line = False
95 else:
96 self.surface.blit(text, (3, offset))
97 offset += text_police.get_linesize() - 4
98
99 background_surface.blit(self.surface, self.position)
100 self.draw_lock.release()
101
102 return not all_actions_ready
103
104 def poll_redraw(self, background):
105 while True:
106 time.sleep(1)
107 if self.all_actions_ready():
108 self.draw(background)
109 self.mapping.blit()
110 break
111
112 def has_actions(self): 72 def has_actions(self):
113 return len(self.actions) > 0 73 return len(self.actions) > 0
114 74
75 @property
115 def all_actions_ready(self): 76 def all_actions_ready(self):
116 return all(action.ready() for action in self.actions) 77 return all(action.ready() for action in self.actions)
117 78
118 def add_action(self, action_name, **arguments): 79 def add_action(self, action_name, **arguments):
119 self.actions.append(Action(action_name, self, **arguments)) 80 self.actions.append(Action(action_name, self, **arguments))
120 81
121 def do_actions(self, screen): 82 def do_actions(self):
122 print("running actions for {}".format(self.key_sym)) 83 print("running actions for {}".format(self.key_sym))
123 start_time = time.time() 84 start_time = time.time()
124 self.mapping.start_running(self, start_time) 85 self.parent.start_running(self, start_time)
125 action_number = 0 86 action_number = 0
126 for action in self.actions: 87 for action in self.actions:
127 if self.mapping.keep_running(self, start_time): 88 if self.parent.keep_running(self, start_time):
128 self.list_actions(screen, action_number = action_number + 0.5) 89 self.list_actions(action_number = action_number + 0.5)
129 action.run() 90 action.run()
130 action_number += 1 91 action_number += 1
131 self.list_actions(screen, action_number = action_number) 92 self.list_actions(action_number = action_number)
132
133 self.mapping.finished_running(self, start_time)
134
135 def list_actions(self, screen, action_number = 0):
136 action_descriptions = [action.description() for action in self.actions]
137 #print("actions linked to key {}:".format(self.key_sym))
138 #print("\t" + "\n\t".join(action_descriptions))
139 self.draw_lock.acquire()
140 surface = pygame.Surface((690, 250)).convert()
141 surface.fill((250, 250, 250))
142 police = font(14)
143
144 offset = 0
145 police.set_bold(True)
146 text = police.render("actions linked to key {}:".format(self.key_sym), True, (0,0,0))
147 surface.blit(text, (0, offset))
148 offset += police.get_linesize()
149
150 police.set_bold(False)
151 icon_police = font(14, font = "Symbola")
152 for index, description in enumerate(action_descriptions):
153 if index < int(action_number):
154 icon = icon_police.render("✓", True, (0,0,0))
155 elif index + 0.5 == action_number:
156 icon = icon_police.render("✅", True, (0,0,0))
157 else:
158 icon = icon_police.render(" ", True, (0,0,0))
159
160 text = police.render(description, True, (0,0,0))
161 surface.blit(icon, (0, offset))
162 surface.blit(text, (10, offset))
163 offset += police.get_linesize()
164 93
165 screen.blit(surface, (5, 308)) 94 self.parent.finished_running(self, start_time)
166 pygame.display.flip()
167 self.draw_lock.release()
168 95
96 def list_actions(self, action_number = 0):
97 self.parent.parent.ids['ActionList'].update_list(self, action_number)
169 98
99 def on_press(self):
100 self.list_actions()
101 pass
diff --git a/helpers/mapping.py b/helpers/mapping.py
index 58aaf3d..4a1cd98 100644
--- a/helpers/mapping.py
+++ b/helpers/mapping.py
@@ -1,175 +1,51 @@
1from kivy.uix.relativelayout import RelativeLayout
2from kivy.properties import NumericProperty
3from kivy.core.window import Window
4
1import threading 5import threading
2import pygame 6import pygame
3from .key import * 7import yaml
4 8
5class Mapping: 9from .lock import *
6 WIDTH = 903 10from .music_file import *
7 HEIGHT = 298 11
8 SIZE = WIDTH, HEIGHT 12class Mapping(RelativeLayout):
9 13 expected_keys = NumericProperty(0)
10 ROW_POSITIONS = { 14
11 'first': 0, 15 def __init__(self, **kwargs):
12 'second': 50, 16 self.key_config, self.channel_number, self.open_files = self.parse_config()
13 'third': 100, 17 super(Mapping, self).__init__(**kwargs)
14 'fourth': 150, 18 self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
15 'fifth': 200, 19 self._keyboard.bind(on_key_down=self._on_keyboard_down)
16 'sixth': 250,
17 }
18
19 KEYS = [
20 (pygame.K_ESCAPE, 'ESC', 'first', 0, {}),
21
22 (pygame.K_F1, 'F1', 'first', 100, {}),
23 (pygame.K_F2, 'F2', 'first', 150, {}),
24 (pygame.K_F3, 'F3', 'first', 200, {}),
25 (pygame.K_F4, 'F4', 'first', 250, {}),
26
27 (pygame.K_F5, 'F5', 'first', 325, {}),
28 (pygame.K_F6, 'F6', 'first', 375, {}),
29 (pygame.K_F7, 'F7', 'first', 425, {}),
30 (pygame.K_F8, 'F8', 'first', 475, {}),
31
32 (pygame.K_F9, 'F9', 'first', 550, {}),
33 (pygame.K_F10, 'F10', 'first', 600, {}),
34 (pygame.K_F11, 'F11', 'first', 650, {}),
35 (pygame.K_F12, 'F12', 'first', 700, {}),
36
37
38 (178, '²', 'second', 0, {}),
39 (pygame.K_AMPERSAND, '&', 'second', 50, {}),
40 (233, 'é', 'second', 100, {}),
41 (pygame.K_QUOTEDBL, '"', 'second', 150, {}),
42 (pygame.K_QUOTE, "'", 'second', 200, {}),
43 (pygame.K_LEFTPAREN, '(', 'second', 250, {}),
44 (pygame.K_MINUS, '-', 'second', 300, {}),
45 (232, 'è', 'second', 350, {}),
46 (pygame.K_UNDERSCORE, '_', 'second', 400, {}),
47 (231, 'ç', 'second', 450, {}),
48 (224, 'à', 'second', 500, {}),
49 (pygame.K_RIGHTPAREN, ')', 'second', 550, {}),
50 (pygame.K_EQUALS, '=', 'second', 600, {}),
51
52 (pygame.K_BACKSPACE, '<-', 'second', 650, { 'width': 98 }),
53
54
55 (pygame.K_TAB, 'tab', 'third', 0, { 'width' : 73 }),
56 (pygame.K_a, 'a', 'third', 75, {}),
57 (pygame.K_z, 'z', 'third', 125, {}),
58 (pygame.K_e, 'e', 'third', 175, {}),
59 (pygame.K_r, 'r', 'third', 225, {}),
60 (pygame.K_t, 't', 'third', 275, {}),
61 (pygame.K_y, 'y', 'third', 325, {}),
62 (pygame.K_u, 'u', 'third', 375, {}),
63 (pygame.K_i, 'i', 'third', 425, {}),
64 (pygame.K_o, 'o', 'third', 475, {}),
65 (pygame.K_p, 'p', 'third', 525, {}),
66 (pygame.K_CARET, '^', 'third', 575, {}),
67 (pygame.K_DOLLAR, '$', 'third', 625, {}),
68
69 (pygame.K_RETURN, 'Enter', 'third', 692, { 'width': 56, 'height': 98 }),
70
71 (pygame.K_CAPSLOCK, 'CAPS', 'fourth', 0, { 'width': 88, 'disabled': True }),
72
73 (pygame.K_q, 'q', 'fourth', 90, {}),
74 (pygame.K_s, 's', 'fourth', 140, {}),
75 (pygame.K_d, 'd', 'fourth', 190, {}),
76 (pygame.K_f, 'f', 'fourth', 240, {}),
77 (pygame.K_g, 'g', 'fourth', 290, {}),
78 (pygame.K_h, 'h', 'fourth', 340, {}),
79 (pygame.K_j, 'j', 'fourth', 390, {}),
80 (pygame.K_k, 'k', 'fourth', 440, {}),
81 (pygame.K_l, 'l', 'fourth', 490, {}),
82 (pygame.K_m, 'm', 'fourth', 540, {}),
83 (249, 'ù', 'fourth', 590, {}),
84 (pygame.K_ASTERISK, '*', 'fourth', 640, {}),
85
86
87 (pygame.K_LSHIFT, 'LShift', 'fifth', 0, { 'width': 63, 'disabled': True }),
88
89 (pygame.K_LESS, '<', 'fifth', 65, {}),
90 (pygame.K_w, 'w', 'fifth', 115, {}),
91 (pygame.K_x, 'x', 'fifth', 165, {}),
92 (pygame.K_c, 'c', 'fifth', 215, {}),
93 (pygame.K_v, 'v', 'fifth', 265, {}),
94 (pygame.K_b, 'b', 'fifth', 315, {}),
95 (pygame.K_n, 'n', 'fifth', 365, {}),
96 (pygame.K_COMMA, ',', 'fifth', 415, {}),
97 (pygame.K_SEMICOLON, ';', 'fifth', 465, {}),
98 (pygame.K_COLON, ':', 'fifth', 515, {}),
99 (pygame.K_EXCLAIM, '!', 'fifth', 565, {}),
100
101 (pygame.K_RSHIFT, 'RShift', 'fifth', 615, { 'width': 133, 'disabled': True }),
102
103 (pygame.K_LCTRL, 'LCtrl', 'sixth', 0, { 'width': 63, 'disabled': True }),
104 (pygame.K_LSUPER, 'LSuper', 'sixth', 115, { 'disabled': True }),
105 (pygame.K_LALT, 'LAlt', 'sixth', 165, { 'disabled': True }),
106 (pygame.K_SPACE, 'Espace', 'sixth', 215, { 'width': 248 }),
107 (pygame.K_MODE, 'AltGr', 'sixth', 465, { 'disabled': True }),
108 (314, 'Compose', 'sixth', 515, { 'disabled': True }),
109 (pygame.K_RCTRL, 'RCtrl', 'sixth', 565, { 'width': 63, 'disabled': True }),
110
111
112 (pygame.K_INSERT, 'ins', 'second', 755, {}),
113 (pygame.K_HOME, 'home', 'second', 805, {}),
114 (pygame.K_PAGEUP, 'pg_u', 'second', 855, {}),
115 (pygame.K_DELETE, 'del', 'third', 755, {}),
116 (pygame.K_END, 'end', 'third', 805, {}),
117 (pygame.K_PAGEDOWN, 'pg_d', 'third', 855, {}),
118
119
120 (pygame.K_UP, 'up', 'fifth', 805, {}),
121 (pygame.K_DOWN, 'down', 'sixth', 805, {}),
122 (pygame.K_LEFT, 'left', 'sixth', 755, {}),
123 (pygame.K_RIGHT, 'right', 'sixth', 855, {}),
124 ]
125
126 def __init__(self, screen, draw_lock):
127 self.draw_lock = draw_lock
128 self.screen = screen
129 self.background = pygame.Surface(self.SIZE).convert()
130 self.background.fill((250, 250, 250))
131 self.keys = {}
132 self.running = [] 20 self.running = []
133 for key in self.KEYS:
134 if key[2] in self.ROW_POSITIONS:
135 position = self.ROW_POSITIONS[key[2]]
136 else:
137 position = key[2]
138 self.keys[key[0]] = Key(self,
139 self.draw_lock,
140 key[0], key[1], position, key[3],
141 **key[4])
142
143 def draw(self):
144 for key_name in self.keys:
145 key = self.keys[key_name]
146 should_redraw_key = key.draw(self.background)
147
148 if should_redraw_key:
149 threading.Thread(name = "MSPollRedraw", target = key.poll_redraw, args = [self.background]).start()
150 self.blit()
151
152 def blit(self):
153 self.draw_lock.acquire()
154 self.screen.blit(self.background, (5, 5))
155 pygame.display.flip()
156 self.draw_lock.release()
157
158 def find_by_key_num(self, key_num):
159 if key_num in self.keys:
160 return self.keys[key_num]
161 return None
162 21
163 def find_by_collidepoint(self, position): 22
164 for key in self.keys: 23 pygame.mixer.init(frequency = 44100)
165 if self.keys[key].collidepoint(position): 24 pygame.mixer.set_num_channels(self.channel_number)
166 return self.keys[key] 25
26 def _keyboard_closed(self):
27 self._keyboard.unbind(on_key_down=self._on_keyboard_down)
28 self._keyboard = None
29
30 def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
31 key = self.find_by_key_code(keycode)
32 if key is not None:
33 threading.Thread(name = "MSKeyAction", target=key.do_actions).start()
34 return True
35
36 def find_by_key_code(self, key_code):
37 if "Key_" + str(key_code[0]) in self.ids:
38 return self.ids["Key_" + str(key_code[0])]
167 return None 39 return None
168 40
169 def find_by_unicode(self, key_sym): 41 def find_by_unicode(self, key_sym):
170 for key in self.keys: 42 for key in self.children:
171 if self.keys[key].key_sym == key_sym: 43 if not type(key).__name__ == "Key":
172 return self.keys[key] 44 continue
45 print(key.key_sym, key_sym)
46 if key.key_sym == key_sym:
47 print("found")
48 return key
173 return None 49 return None
174 50
175 def stop_all_running(self): 51 def stop_all_running(self):
@@ -185,3 +61,78 @@ class Mapping:
185 if (key, start_time) in self.running: 61 if (key, start_time) in self.running:
186 self.running.remove((key, start_time)) 62 self.running.remove((key, start_time))
187 63
64 def parse_config(self):
65 stream = open("config.yml", "r")
66 config = yaml.load(stream)
67 stream.close()
68
69 aliases = config['aliases']
70 seen_files = {}
71
72 file_lock = Lock("file")
73
74 channel_id = 0
75
76 key_properties = {}
77
78 for key in config['key_properties']:
79 if key not in key_properties:
80 key_properties[key] = {
81 "actions": [],
82 "properties": config['key_properties'][key],
83 "files": []
84 }
85
86 for mapped_key in config['keys']:
87 if mapped_key not in key_properties:
88 key_properties[mapped_key] = {
89 "actions": [],
90 "properties": {},
91 "files": []
92 }
93 for action in config['keys'][mapped_key]:
94 action_name = list(action)[0]
95 action_args = {}
96 if action[action_name] is None:
97 action[action_name] = []
98
99 if 'include' in action[action_name]:
100 included = action[action_name]['include']
101 del(action[action_name]['include'])
102
103 if isinstance(included, str):
104 action[action_name].update(aliases[included], **action[action_name])
105 else:
106 for included_ in included:
107 action[action_name].update(aliases[included_], **action[action_name])
108
109 for argument in action[action_name]:
110 if argument == 'file':
111 filename = action[action_name]['file']
112 if filename not in seen_files:
113 if filename in config['music_properties']:
114 seen_files[filename] = MusicFile(
115 filename,
116 file_lock,
117 channel_id,
118 **config['music_properties'][filename])
119 else:
120 seen_files[filename] = MusicFile(
121 filename,
122 file_lock,
123 channel_id)
124 channel_id = channel_id + 1
125
126 if filename not in key_properties[mapped_key]['files']:
127 key_properties[mapped_key]['files'].append(seen_files[filename])
128
129 action_args['music'] = seen_files[filename]
130
131 else:
132 action_args[argument] = action[action_name][argument]
133
134 key_properties[mapped_key]['actions'].append([action_name, action_args])
135
136 return (key_properties, channel_id + 1, seen_files)
137
138
diff --git a/helpers/rounded_rect.py b/helpers/rounded_rect.py
deleted file mode 100644
index f168e0e..0000000
--- a/helpers/rounded_rect.py
+++ /dev/null
@@ -1,62 +0,0 @@
1import pygame
2
3class RoundedRect:
4 def __init__(self, rect, outer_color, inner_color, linewidth = 2, radius = 0.4):
5 self.rect = pygame.Rect(rect)
6 self.outer_color = pygame.Color(*outer_color)
7 self.inner_color = pygame.Color(*inner_color)
8 self.linewidth = linewidth
9 self.radius = radius
10
11 def surface(self):
12 rectangle = self.filledRoundedRect(self.rect, self.outer_color, self.radius)
13
14 inner_rect = pygame.Rect((
15 self.rect.left + 2 * self.linewidth,
16 self.rect.top + 2 * self.linewidth,
17 self.rect.right - 2 * self.linewidth,
18 self.rect.bottom - 2 * self.linewidth
19 ))
20
21 inner_rectangle = self.filledRoundedRect(inner_rect, self.inner_color, self.radius)
22
23 rectangle.blit(inner_rectangle, (self.linewidth, self.linewidth))
24
25 return rectangle
26
27 def filledRoundedRect(self, rect, color, radius=0.4):
28 """
29 filledRoundedRect(rect,color,radius=0.4)
30
31 rect : rectangle
32 color : rgb or rgba
33 radius : 0 <= radius <= 1
34 """
35
36 alpha = color.a
37 color.a = 0
38 pos = rect.topleft
39 rect.topleft = 0,0
40 rectangle = pygame.Surface(rect.size,pygame.SRCALPHA)
41
42 circle = pygame.Surface([min(rect.size)*3]*2,pygame.SRCALPHA)
43 pygame.draw.ellipse(circle,(0,0,0),circle.get_rect(),0)
44 circle = pygame.transform.smoothscale(circle,[int(min(rect.size)*radius)]*2)
45
46 radius = rectangle.blit(circle,(0,0))
47 radius.bottomright = rect.bottomright
48 rectangle.blit(circle,radius)
49 radius.topright = rect.topright
50 rectangle.blit(circle,radius)
51 radius.bottomleft = rect.bottomleft
52 rectangle.blit(circle,radius)
53
54 rectangle.fill((0,0,0),rect.inflate(-radius.w,0))
55 rectangle.fill((0,0,0),rect.inflate(0,-radius.h))
56
57 rectangle.fill(color,special_flags=pygame.BLEND_RGBA_MAX)
58 rectangle.fill((255,255,255,alpha),special_flags=pygame.BLEND_RGBA_MIN)
59
60 return rectangle
61
62
diff --git a/keyboard.py b/keyboard.py
deleted file mode 100644
index 0c1115f..0000000
--- a/keyboard.py
+++ /dev/null
@@ -1,222 +0,0 @@
1from kivy.app import App
2from kivy.uix.widget import Widget
3from kivy.uix.floatlayout import FloatLayout
4from kivy.uix.relativelayout import RelativeLayout
5from kivy.properties import AliasProperty, ReferenceListProperty, BooleanProperty, NumericProperty, ListProperty, StringProperty, ObjectProperty
6from kivy.vector import Vector
7from kivy.clock import Clock
8from kivy.uix.behaviors import ButtonBehavior
9from kivy.uix.label import Label
10from kivy.core.window import Window
11
12from helpers.action import *
13import time
14import sys
15import math
16import pygame
17import helpers
18import threading
19
20class KeyDescription(Label):
21 pass
22
23class Key(ButtonBehavior, Widget):
24 key_sym = StringProperty(None)
25 custom_color = ListProperty([0, 1, 0, 1])
26 custom_unready_color = ListProperty([0, 1, 0, 100/255])
27 description_title = StringProperty("")
28 description = ListProperty([])
29 is_key_ready = BooleanProperty(True)
30
31 def get_color(self):
32 if not self.has_actions:
33 return [1, 1, 1, 1]
34 elif self.all_actions_ready:
35 return self.custom_color
36 else:
37 return self.custom_unready_color
38 def set_color(self):
39 pass
40
41 color = AliasProperty(get_color, set_color, bind=['is_key_ready'])
42
43 def __init__(self, **kwargs):
44 super(Key, self).__init__(**kwargs)
45 self.actions = []
46
47 def on_key_sym(self, key, key_sym):
48 if key_sym in self.parent.key_config:
49 self.is_key_ready = False
50
51 self.config = self.parent.key_config[key_sym]
52
53 self.actions = []
54 for key_action in self.config['actions']:
55 self.add_action(key_action[0], **key_action[1])
56
57 if 'description' in self.config['properties']:
58 key.set_description(self.config['properties']['description'])
59 if 'color' in self.config['properties']:
60 key.set_color(self.config['properties']['color'])
61
62 Clock.schedule_interval(self.check_all_active, 1)
63
64 def check_all_active(self, dt):
65 if self.all_actions_ready:
66 self.is_key_ready = True
67 return False
68
69 def set_description(self, description):
70 if description[0] is not None:
71 self.description_title = str(description[0])
72 for desc in description[1:]:
73 if desc is None:
74 self.description.append("")
75 else:
76 self.description.append(str(desc).replace(" ", " "))
77
78 def set_color(self, color):
79 color = [x / 255 for x in color]
80 color.append(1)
81 self.custom_color = color
82 color[3] = 100 / 255
83 self.custom_unready_color = tuple(color)
84
85 @property
86 def has_actions(self):
87 return len(self.actions) > 0
88
89 @property
90 def all_actions_ready(self):
91 return all(action.ready() for action in self.actions)
92
93 def add_action(self, action_name, **arguments):
94 self.actions.append(Action(action_name, self, **arguments))
95
96 def do_actions(self):
97 print("running actions for {}".format(self.key_sym))
98 start_time = time.time()
99 self.parent.start_running(self, start_time)
100 action_number = 0
101 for action in self.actions:
102 if self.parent.keep_running(self, start_time):
103 self.list_actions(action_number = action_number + 0.5)
104 action.run()
105 action_number += 1
106 self.list_actions(action_number = action_number)
107
108 self.parent.finished_running(self, start_time)
109
110 def list_actions(self, action_number = 0):
111 self.parent.parent.ids['ActionList'].update_list(self, action_number)
112
113 def on_press(self):
114 self.list_actions()
115 pass
116
117class PlayList(RelativeLayout):
118 playlist = ListProperty([])
119
120 def __init__(self, **kwargs):
121 super(PlayList, self).__init__(**kwargs)
122 Clock.schedule_interval(self.update_playlist, 0.5)
123
124 def update_playlist(self, dt):
125 if self.parent is None or 'Mapping' not in self.parent.ids:
126 return True
127
128 open_files = self.parent.ids['Mapping'].open_files
129 self.playlist = []
130 for music_file in open_files.values():
131 if not music_file.is_playing():
132 continue
133 if music_file.is_paused():
134 self.playlist.append(["⏸", music_file.name, False])
135 else:
136 self.playlist.append(["⏵", music_file.name, True])
137
138
139class ActionList(RelativeLayout):
140 action_title = StringProperty("")
141 action_list = ListProperty([])
142
143 def update_list(self, key, action_number = 0):
144 self.action_title = "actions linked to key {}:".format(key.key_sym)
145 self.action_list = []
146
147 action_descriptions = [action.description() for action in key.actions]
148
149 for index, description in enumerate(action_descriptions):
150 if index < int(action_number):
151 icon = "✓"
152 elif index + 0.5 == action_number:
153 icon = "✅"
154 else:
155 icon = " "
156
157 self.action_list.append([icon, description])
158
159class Mapping(RelativeLayout):
160 expected_keys = NumericProperty(0)
161
162 def __init__(self, **kwargs):
163 self.key_config, self.channel_number, self.open_files = helpers.parse_config2()
164 super(Mapping, self).__init__(**kwargs)
165 self._keyboard = Window.request_keyboard(self._keyboard_closed, self)
166 self._keyboard.bind(on_key_down=self._on_keyboard_down)
167 self.running = []
168
169
170 pygame.mixer.init(frequency = 44100)
171 pygame.mixer.set_num_channels(self.channel_number)
172
173 def _keyboard_closed(self):
174 self._keyboard.unbind(on_key_down=self._on_keyboard_down)
175 self._keyboard = None
176
177 def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
178 key = self.find_by_key_code(keycode)
179 if key is not None:
180 threading.Thread(name = "MSKeyAction", target=key.do_actions).start()
181 return True
182
183 def find_by_key_code(self, key_code):
184 if "Key_" + str(key_code[0]) in self.ids:
185 return self.ids["Key_" + str(key_code[0])]
186 return None
187
188 def find_by_unicode(self, key_sym):
189 for key in self.children:
190 if not type(key).__name__ == "Key":
191 continue
192 print(key.key_sym, key_sym)
193 if key.key_sym == key_sym:
194 print("found")
195 return key
196 return None
197
198 def stop_all_running(self):
199 self.running = []
200
201 def start_running(self, key, start_time):
202 self.running.append((key, start_time))
203
204 def keep_running(self, key, start_time):
205 return (key, start_time) in self.running
206
207 def finished_running(self, key, start_time):
208 if (key, start_time) in self.running:
209 self.running.remove((key, start_time))
210
211
212class Screen(FloatLayout):
213 pass
214
215class MusicSamplerApp(App):
216 def build(self):
217 Window.size = (913, 563)
218
219 return Screen()
220
221if __name__ == '__main__':
222 MusicSamplerApp().run()
diff --git a/music_sampler.py b/music_sampler.py
index 42c01e3..0d9a7a9 100644
--- a/music_sampler.py
+++ b/music_sampler.py
@@ -1,80 +1,63 @@
1import sys 1from kivy.app import App
2import pygame 2from kivy.uix.floatlayout import FloatLayout
3import helpers 3from kivy.uix.relativelayout import RelativeLayout
4import threading 4from kivy.properties import ListProperty, StringProperty
5 5from kivy.clock import Clock
6pygame.mixer.pre_init(frequency = 44100) 6from kivy.core.window import Window
7pygame.init() 7
8 8from helpers.key import Key
9size = width, height = 913, 563 9from helpers.mapping import Mapping
10screen = pygame.display.set_mode(size) 10
11screen.fill((229, 228, 226)) 11class PlayList(RelativeLayout):
12 12 playlist = ListProperty([])
13draw_lock = helpers.Lock("draw") 13
14 14 def __init__(self, **kwargs):
15mapping = helpers.Mapping(screen, draw_lock) 15 super(PlayList, self).__init__(**kwargs)
16channel_number, open_files = helpers.parse_config(mapping) 16 Clock.schedule_interval(self.update_playlist, 0.5)
17pygame.mixer.set_num_channels(channel_number) 17
18 18 def update_playlist(self, dt):
19mapping.draw() 19 if self.parent is None or 'Mapping' not in self.parent.ids:
20 return True
21
22 open_files = self.parent.ids['Mapping'].open_files
23 self.playlist = []
24 for music_file in open_files.values():
25 if not music_file.is_playing():
26 continue
27 if music_file.is_paused():
28 self.playlist.append(["⏸", music_file.name, False])
29 else:
30 self.playlist.append(["⏵", music_file.name, True])
20 31
21draw_lock.acquire()
22pygame.display.flip()
23draw_lock.release()
24 32
25contexts = [ 33class ActionList(RelativeLayout):
26 'normal' 34 action_title = StringProperty("")
27] 35 action_list = ListProperty([])
28 36
29context = 'normal' 37 def update_list(self, key, action_number = 0):
38 self.action_title = "actions linked to key {}:".format(key.key_sym)
39 self.action_list = []
30 40
31#### Normal workflow #### 41 action_descriptions = [action.description() for action in key.actions]
32while 1:
33 event = pygame.event.wait()
34 42
35 if event.type == pygame.QUIT or ( 43 for index, description in enumerate(action_descriptions):
36 event.type == pygame.KEYDOWN and 44 if index < int(action_number):
37 event.mod == 4160 and 45 icon = "✓"
38 event.key == pygame.K_c): 46 elif index + 0.5 == action_number:
39 for thread in threading.enumerate(): 47 icon = "✅"
40 if thread.getName()[0:2] != "MS": 48 else:
41 continue 49 icon = " "
42 thread.join()
43 50
44 pygame.quit() 51 self.action_list.append([icon, description])
45 sys.exit()
46 52
47 if context == 'normal': 53class Screen(FloatLayout):
48 if event.type == pygame.KEYDOWN: 54 pass
49 key = mapping.find_by_key_num(event.key)
50 if key is not None and not key.disabled:
51 threading.Thread(name = "MSKeyAction", target=key.do_actions, args = [screen]).start()
52 threading.Thread(name = "MSClic", target=key.list_actions, args = [screen]).start()
53 elif event.type == pygame.MOUSEBUTTONUP:
54 key = mapping.find_by_collidepoint(pygame.mouse.get_pos())
55 if key is not None:
56 threading.Thread(name = "MSClic", target=key.list_actions, args = [screen]).start()
57 55
58 draw_lock.acquire() 56class MusicSamplerApp(App):
59 police = helpers.font(14) 57 def build(self):
60 icon_police = helpers.font(14, font = "Symbola") 58 Window.size = (913, 563)
61 59
62 surface = pygame.Surface((208, 250)).convert() 60 return Screen()
63 surface.fill((250, 250, 250))
64 offset = 0
65 for music_file in open_files.values():
66 police.set_bold(False)
67 if music_file.is_playing():
68 if music_file.is_paused():
69 icon = icon_police.render("⏸", True, (0,0,0))
70 else:
71 icon = icon_police.render("⏵", True, (0,0,0))
72 police.set_bold(True)
73 text = police.render(music_file.name, True, (0,0,0))
74 surface.blit(icon, (0, offset))
75 surface.blit(text, (20, offset))
76 offset += police.get_linesize()
77 screen.blit(surface, (700, 308))
78 61
79 pygame.display.flip() 62if __name__ == '__main__':
80 draw_lock.release() 63 MusicSamplerApp().run()
diff --git a/musicsampler.kv b/musicsampler.kv
index 3c1964b..17be1f5 100644
--- a/musicsampler.kv
+++ b/musicsampler.kv
@@ -67,6 +67,13 @@
67 size: 2 * self.parent.width - 2 * self.parent.line_width, self.parent.height - key_label.font_size 67 size: 2 * self.parent.width - 2 * self.parent.line_width, self.parent.height - key_label.font_size
68 68
69<Screen>: 69<Screen>:
70 canvas:
71 Color:
72 rgba: 229/255, 228/255, 226/255, 1
73 Rectangle:
74 pos: 0, 0
75 size: self.width, self.height
76
70 key_size: int( (3 * self.width - 16) / 56) 77 key_size: int( (3 * self.width - 16) / 56)
71 key_sep: int( self.key_size / 24) 78 key_sep: int( self.key_size / 24)
72 key_pad_sep: int( self.key_size / 7) + 1 79 key_pad_sep: int( self.key_size / 7) + 1