diff options
-rw-r--r-- | config.yml | 80 | ||||
-rw-r--r-- | music_sampler/action.py | 70 | ||||
-rw-r--r-- | music_sampler/actions/__init__.py | 2 | ||||
-rw-r--r-- | music_sampler/actions/load_music.py | 12 | ||||
-rw-r--r-- | music_sampler/actions/unload_music.py | 10 | ||||
-rw-r--r-- | music_sampler/app_blocks/actionlist.py | 3 | ||||
-rw-r--r-- | music_sampler/app_blocks/playlist.py | 3 | ||||
-rw-r--r-- | music_sampler/helpers.py | 8 | ||||
-rw-r--r-- | music_sampler/key.py | 59 | ||||
-rw-r--r-- | music_sampler/mapping.py | 50 | ||||
-rw-r--r-- | music_sampler/music_file.py | 119 | ||||
-rw-r--r-- | music_sampler/music_sampler.kv | 4 |
12 files changed, 265 insertions, 155 deletions
@@ -92,6 +92,42 @@ key_properties: | |||
92 | - | 92 | - |
93 | - Noise | 93 | - Noise |
94 | include: light_blue | 94 | include: light_blue |
95 | 'q': | ||
96 | description: | ||
97 | - | ||
98 | - Load | ||
99 | - Music 1 | ||
100 | include: light_blue | ||
101 | 's': | ||
102 | description: | ||
103 | - | ||
104 | - Load | ||
105 | - Music 2 | ||
106 | include: light_blue | ||
107 | 'd': | ||
108 | description: | ||
109 | - | ||
110 | - Load | ||
111 | - Noise | ||
112 | include: light_blue | ||
113 | 'w': | ||
114 | description: | ||
115 | - | ||
116 | - Unload | ||
117 | - Music 1 | ||
118 | include: light_blue | ||
119 | 'x': | ||
120 | description: | ||
121 | - | ||
122 | - Unload | ||
123 | - Music 2 | ||
124 | include: light_blue | ||
125 | 'c': | ||
126 | description: | ||
127 | - | ||
128 | - Unload | ||
129 | - Noise | ||
130 | include: light_blue | ||
95 | 't': | 131 | 't': |
96 | description: | 132 | description: |
97 | - | 133 | - |
@@ -105,12 +141,6 @@ key_properties: | |||
105 | - Stop wait | 141 | - Stop wait |
106 | - music 2 | 142 | - music 2 |
107 | include: green | 143 | include: green |
108 | 'u': | ||
109 | description: | ||
110 | - | ||
111 | - Noise | ||
112 | - + Music 2 | ||
113 | include: green | ||
114 | 144 | ||
115 | 'g': | 145 | 'g': |
116 | description: | 146 | description: |
@@ -266,6 +296,28 @@ keys: | |||
266 | - play: | 296 | - play: |
267 | include: noise | 297 | include: noise |
268 | 298 | ||
299 | # Load the songs | ||
300 | 'q': | ||
301 | - load_music: | ||
302 | include: music1 | ||
303 | 's': | ||
304 | - load_music: | ||
305 | include: music2 | ||
306 | 'd': | ||
307 | - load_music: | ||
308 | include: noise | ||
309 | |||
310 | # Unload the songs | ||
311 | 'w': | ||
312 | - unload_music: | ||
313 | include: music1 | ||
314 | 'x': | ||
315 | - unload_music: | ||
316 | include: music2 | ||
317 | 'c': | ||
318 | - unload_music: | ||
319 | include: noise | ||
320 | |||
269 | # Crossfade from any music to music 2 | 321 | # Crossfade from any music to music 2 |
270 | 't': | 322 | 't': |
271 | - stop: | 323 | - stop: |
@@ -276,26 +328,10 @@ keys: | |||
276 | 328 | ||
277 | # Fade out, then wait and start music 2 | 329 | # Fade out, then wait and start music 2 |
278 | 'y': | 330 | 'y': |
279 | - stop: | ||
280 | fade_out: 3 | ||
281 | wait: true | ||
282 | - wait: | 331 | - wait: |
283 | duration: 3 | ||
284 | - play: | ||
285 | include: music2 | 332 | include: music2 |
286 | 333 | ||
287 | # Play the noise, stop music 1, wait a bit then start music 2 | 334 | # Play the noise, stop music 1, wait a bit then start music 2 |
288 | 'u': | ||
289 | - play: | ||
290 | include: noise | ||
291 | - wait: | ||
292 | duration: 0.2 | ||
293 | - stop: | ||
294 | include: music1 | ||
295 | - wait: | ||
296 | duration: 3 | ||
297 | - play: | ||
298 | include: music2 | ||
299 | 335 | ||
300 | # Play music 1, starting at 30 seconds then seeks at 60 after 5 seconds | 336 | # Play music 1, starting at 30 seconds then seeks at 60 after 5 seconds |
301 | 'g': | 337 | 'g': |
diff --git a/music_sampler/action.py b/music_sampler/action.py index 22a2bdc..bc62f33 100644 --- a/music_sampler/action.py +++ b/music_sampler/action.py | |||
@@ -9,8 +9,9 @@ class Action: | |||
9 | 'failed', | 9 | 'failed', |
10 | { | 10 | { |
11 | 'name': 'loaded', | 11 | 'name': 'loaded', |
12 | 'children': ['running'] | 12 | 'children': ['stopped', 'running'] |
13 | } | 13 | }, |
14 | 'destroyed' | ||
14 | ] | 15 | ] |
15 | 16 | ||
16 | TRANSITIONS = [ | 17 | TRANSITIONS = [ |
@@ -21,36 +22,42 @@ class Action: | |||
21 | }, | 22 | }, |
22 | { | 23 | { |
23 | 'trigger': 'fail', | 24 | 'trigger': 'fail', |
24 | 'source': 'loading', | 25 | 'source': ['loading', 'loaded'], |
25 | 'dest': 'failed', | 26 | 'dest': 'failed', |
26 | 'after': 'poll_loaded' | ||
27 | }, | 27 | }, |
28 | { | 28 | { |
29 | 'trigger': 'success', | 29 | 'trigger': 'success', |
30 | 'source': 'loading', | 30 | 'source': 'loading', |
31 | 'dest': 'loaded', | 31 | 'dest': 'loaded_stopped', |
32 | 'after': 'poll_loaded' | ||
33 | }, | 32 | }, |
34 | { | 33 | { |
35 | 'trigger': 'run', | 34 | 'trigger': 'reload', |
36 | 'source': 'loaded', | 35 | 'source': 'loaded', |
36 | 'dest': 'loading', | ||
37 | }, | ||
38 | { | ||
39 | 'trigger': 'run', | ||
40 | 'source': 'loaded_stopped', | ||
37 | 'dest': 'loaded_running', | 41 | 'dest': 'loaded_running', |
38 | 'after': 'finish_action', | 42 | 'after': 'finish_action', |
39 | # if a child has no transitions, then it is bubbled to the parent, | ||
40 | # and we don't want that. Not useful in that machine precisely. | ||
41 | 'conditions': ['is_loaded'] | ||
42 | }, | 43 | }, |
43 | { | 44 | { |
44 | 'trigger': 'finish_action', | 45 | 'trigger': 'finish_action', |
45 | 'source': 'loaded_running', | 46 | 'source': 'loaded_running', |
46 | 'dest': 'loaded' | 47 | 'dest': 'loaded_stopped' |
48 | }, | ||
49 | { | ||
50 | 'trigger': 'destroy', | ||
51 | 'source': '*', | ||
52 | 'dest': 'destroyed' | ||
47 | } | 53 | } |
48 | ] | 54 | ] |
49 | 55 | ||
50 | def __init__(self, action, key, **kwargs): | 56 | def __init__(self, action, key, **kwargs): |
51 | Machine(model=self, states=self.STATES, | 57 | Machine(model=self, states=self.STATES, |
52 | transitions=self.TRANSITIONS, initial='initial', | 58 | transitions=self.TRANSITIONS, initial='initial', |
53 | ignore_invalid_triggers=True, queued=True) | 59 | ignore_invalid_triggers=True, queued=True, |
60 | after_state_change=self.notify_state_change) | ||
54 | 61 | ||
55 | self.action = action | 62 | self.action = action |
56 | self.key = key | 63 | self.key = key |
@@ -62,18 +69,31 @@ class Action: | |||
62 | def is_loaded_or_failed(self): | 69 | def is_loaded_or_failed(self): |
63 | return self.is_loaded(allow_substates=True) or self.is_failed() | 70 | return self.is_loaded(allow_substates=True) or self.is_failed() |
64 | 71 | ||
65 | def callback_music_loaded(self, success): | 72 | def callback_music_state(self, new_state): |
66 | if success: | 73 | # If a music gets unloaded while the action is loaded_running and |
67 | self.success() | 74 | # depending on the music, it won't be able to do the finish_action. |
68 | else: | 75 | # Can that happen? |
76 | # a: play 'mp3'; | ||
77 | # z: wait 'mp3'; | ||
78 | # e: pause 'mp3'; | ||
79 | # r: stop 'mp3'; unload_music 'mp3' | ||
80 | if new_state == 'failed': | ||
69 | self.fail() | 81 | self.fail() |
82 | elif self.is_loaded(allow_substates=True) and\ | ||
83 | new_state in ['initial', 'loading']: | ||
84 | self.reload(reloading=True) | ||
85 | elif self.is_loading() and new_state.startswith('loaded_'): | ||
86 | self.success() | ||
70 | 87 | ||
71 | # Machine states / events | 88 | # Machine states / events |
72 | def on_enter_loading(self): | 89 | def on_enter_loading(self, reloading=False): |
90 | if reloading: | ||
91 | return | ||
73 | if hasattr(actions, self.action): | 92 | if hasattr(actions, self.action): |
74 | if 'music' in self.arguments: | 93 | if 'music' in self.arguments and\ |
75 | self.arguments['music'].subscribe_loaded( | 94 | self.action not in ['unload_music', 'load_music']: |
76 | self.callback_music_loaded) | 95 | self.arguments['music'].subscribe_state_change( |
96 | self.callback_music_state) | ||
77 | else: | 97 | else: |
78 | self.success() | 98 | self.success() |
79 | else: | 99 | else: |
@@ -86,9 +106,13 @@ class Action: | |||
86 | getattr(actions, self.action).run(self, | 106 | getattr(actions, self.action).run(self, |
87 | key_start_time=key_start_time, **self.arguments) | 107 | key_start_time=key_start_time, **self.arguments) |
88 | 108 | ||
89 | def poll_loaded(self): | 109 | def on_enter_destroyed(self): |
90 | self.key.callback_action_ready(self, | 110 | if 'music' in self.arguments: |
91 | self.is_loaded(allow_substates=True)) | 111 | self.arguments['music'].unsubscribe_state_change( |
112 | self.callback_music_state) | ||
113 | |||
114 | def notify_state_change(self, *args, **kwargs): | ||
115 | self.key.callback_action_state_changed() | ||
92 | 116 | ||
93 | # This one cannot be in the Machine state since it would be queued to run | 117 | # This one cannot be in the Machine state since it would be queued to run |
94 | # *after* the wait is ended... | 118 | # *after* the wait is ended... |
diff --git a/music_sampler/actions/__init__.py b/music_sampler/actions/__init__.py index 7c812cb..e0671fe 100644 --- a/music_sampler/actions/__init__.py +++ b/music_sampler/actions/__init__.py | |||
@@ -1,4 +1,5 @@ | |||
1 | from . import interrupt_wait | 1 | from . import interrupt_wait |
2 | from . import load_music | ||
2 | from . import pause | 3 | from . import pause |
3 | from . import pause_wait | 4 | from . import pause_wait |
4 | from . import play | 5 | from . import play |
@@ -7,6 +8,7 @@ from . import run_command | |||
7 | from . import seek | 8 | from . import seek |
8 | from . import stop | 9 | from . import stop |
9 | from . import stop_all_actions | 10 | from . import stop_all_actions |
11 | from . import unload_music | ||
10 | from . import unpause | 12 | from . import unpause |
11 | from . import unpause_wait | 13 | from . import unpause_wait |
12 | from . import volume | 14 | from . import volume |
diff --git a/music_sampler/actions/load_music.py b/music_sampler/actions/load_music.py new file mode 100644 index 0000000..f3e02ba --- /dev/null +++ b/music_sampler/actions/load_music.py | |||
@@ -0,0 +1,12 @@ | |||
1 | import threading | ||
2 | |||
3 | def run(action, music=None, **kwargs): | ||
4 | for music in action.music_list(music): | ||
5 | if not music.is_loaded(allow_substates=True): | ||
6 | threading.Thread(name="MSMusicLoad", target=music.load).start() | ||
7 | |||
8 | def description(action, music=None, **kwargs): | ||
9 | if music is not None: | ||
10 | return "load music « {} » to memory".format(music.name) | ||
11 | else: | ||
12 | return "load all music to memory" | ||
diff --git a/music_sampler/actions/unload_music.py b/music_sampler/actions/unload_music.py new file mode 100644 index 0000000..b3de316 --- /dev/null +++ b/music_sampler/actions/unload_music.py | |||
@@ -0,0 +1,10 @@ | |||
1 | def run(action, music=None, **kwargs): | ||
2 | for music in action.music_list(music): | ||
3 | if music.is_unloadable(): | ||
4 | music.unload() | ||
5 | |||
6 | def description(action, music=None, **kwargs): | ||
7 | if music is not None: | ||
8 | return "unload music « {} » from memory".format(music.name) | ||
9 | else: | ||
10 | return "unload all music from memory" | ||
diff --git a/music_sampler/app_blocks/actionlist.py b/music_sampler/app_blocks/actionlist.py index f48072f..59315de 100644 --- a/music_sampler/app_blocks/actionlist.py +++ b/music_sampler/app_blocks/actionlist.py | |||
@@ -4,6 +4,8 @@ from kivy.uix.relativelayout import RelativeLayout | |||
4 | from kivy.properties import ListProperty, StringProperty | 4 | from kivy.properties import ListProperty, StringProperty |
5 | from ..lock import Lock | 5 | from ..lock import Lock |
6 | 6 | ||
7 | from kivy.clock import mainthread | ||
8 | |||
7 | __all__ = ["ActionList", | 9 | __all__ = ["ActionList", |
8 | "ActionListIcons", "ActionListIcon", | 10 | "ActionListIcons", "ActionListIcon", |
9 | "ActionListDescriptions", "ActionListDescription"] | 11 | "ActionListDescriptions", "ActionListDescription"] |
@@ -14,6 +16,7 @@ class ActionList(RelativeLayout): | |||
14 | action_title = StringProperty("") | 16 | action_title = StringProperty("") |
15 | action_list = ListProperty([]) | 17 | action_list = ListProperty([]) |
16 | 18 | ||
19 | @mainthread | ||
17 | def update_list(self, key, action_descriptions): | 20 | def update_list(self, key, action_descriptions): |
18 | if key.repeat_delay > 0: | 21 | if key.repeat_delay > 0: |
19 | self.action_title = _( | 22 | self.action_title = _( |
diff --git a/music_sampler/app_blocks/playlist.py b/music_sampler/app_blocks/playlist.py index 5894995..706e4fc 100644 --- a/music_sampler/app_blocks/playlist.py +++ b/music_sampler/app_blocks/playlist.py | |||
@@ -2,7 +2,7 @@ from kivy.uix.label import Label | |||
2 | from kivy.uix.stacklayout import StackLayout | 2 | from kivy.uix.stacklayout import StackLayout |
3 | from kivy.uix.relativelayout import RelativeLayout | 3 | from kivy.uix.relativelayout import RelativeLayout |
4 | from kivy.properties import ListProperty | 4 | from kivy.properties import ListProperty |
5 | from kivy.clock import Clock | 5 | from kivy.clock import Clock, mainthread |
6 | from ..helpers import duration_to_min_sec | 6 | from ..helpers import duration_to_min_sec |
7 | from ..lock import Lock | 7 | from ..lock import Lock |
8 | 8 | ||
@@ -20,6 +20,7 @@ class PlayList(RelativeLayout): | |||
20 | super(PlayList, self).__init__(**kwargs) | 20 | super(PlayList, self).__init__(**kwargs) |
21 | Clock.schedule_interval(self.update_playlist, 0.5) | 21 | Clock.schedule_interval(self.update_playlist, 0.5) |
22 | 22 | ||
23 | @mainthread | ||
23 | def update_playlist(self, dt): | 24 | def update_playlist(self, dt): |
24 | if self.parent is None or 'Mapping' not in self.parent.ids: | 25 | if self.parent is None or 'Mapping' not in self.parent.ids: |
25 | return True | 26 | return True |
diff --git a/music_sampler/helpers.py b/music_sampler/helpers.py index 9403875..fbd338b 100644 --- a/music_sampler/helpers.py +++ b/music_sampler/helpers.py | |||
@@ -124,6 +124,13 @@ Configs = { | |||
124 | 'help_no': _("Don't show warning when focus is lost"), | 124 | 'help_no': _("Don't show warning when focus is lost"), |
125 | 'type': 'boolean' | 125 | 'type': 'boolean' |
126 | }, | 126 | }, |
127 | 'load_all_musics': { | ||
128 | 'default': True, | ||
129 | 'help_yes': _("Load all the musics at launch time (default)"), | ||
130 | 'help_no': _("Don't load all the musics at launch time (use it if you \ | ||
131 | have memory problems)"), | ||
132 | 'type': 'boolean' | ||
133 | }, | ||
127 | 'list_devices': { | 134 | 'list_devices': { |
128 | 'help': _("List available sound devices"), | 135 | 'help': _("List available sound devices"), |
129 | 'type': 'action' | 136 | 'type': 'action' |
@@ -142,6 +149,7 @@ Configs_order = [ | |||
142 | 'language', | 149 | 'language', |
143 | 'list_devices', | 150 | 'list_devices', |
144 | 'device', | 151 | 'device', |
152 | 'load_all_musics', | ||
145 | ] | 153 | ] |
146 | def parse_args(): | 154 | def parse_args(): |
147 | argv = sys.argv[1 :] | 155 | argv = sys.argv[1 :] |
diff --git a/music_sampler/key.py b/music_sampler/key.py index ce2f45b..e05bb16 100644 --- a/music_sampler/key.py +++ b/music_sampler/key.py | |||
@@ -9,7 +9,11 @@ import time | |||
9 | import threading | 9 | import threading |
10 | from transitions.extensions import HierarchicalMachine as Machine | 10 | from transitions.extensions import HierarchicalMachine as Machine |
11 | 11 | ||
12 | class KeyMachine(Widget): | 12 | # All drawing operations should happen in the main thread |
13 | # https://github.com/kivy/kivy/wiki/Working-with-Python-threads-inside-a-Kivy-application | ||
14 | from kivy.clock import mainthread | ||
15 | |||
16 | class KeyMachine(): | ||
13 | STATES = [ | 17 | STATES = [ |
14 | 'initial', | 18 | 'initial', |
15 | 'configuring', | 19 | 'configuring', |
@@ -97,11 +101,15 @@ class KeyMachine(Widget): | |||
97 | { | 101 | { |
98 | 'trigger': 'repeat_protection_finished', | 102 | 'trigger': 'repeat_protection_finished', |
99 | 'source': 'loaded_protecting_repeat', | 103 | 'source': 'loaded_protecting_repeat', |
100 | 'dest': 'loaded' | 104 | 'dest': 'loaded', |
105 | 'after': 'callback_action_state_changed' | ||
101 | }, | 106 | }, |
102 | ] | 107 | ] |
103 | 108 | ||
104 | state = StringProperty("") | 109 | def __setattr__(self, name, value): |
110 | if hasattr(self, 'initialized') and name == 'state': | ||
111 | self.key.update_state(value) | ||
112 | super().__setattr__(name, value) | ||
105 | 113 | ||
106 | def __init__(self, key, **kwargs): | 114 | def __init__(self, key, **kwargs): |
107 | self.key = key | 115 | self.key = key |
@@ -109,7 +117,8 @@ class KeyMachine(Widget): | |||
109 | Machine(model=self, states=self.STATES, | 117 | Machine(model=self, states=self.STATES, |
110 | transitions=self.TRANSITIONS, initial='initial', | 118 | transitions=self.TRANSITIONS, initial='initial', |
111 | ignore_invalid_triggers=True, queued=True) | 119 | ignore_invalid_triggers=True, queued=True) |
112 | super(KeyMachine, self).__init__(**kwargs) | 120 | |
121 | self.initialized = True | ||
113 | 122 | ||
114 | # Machine states / events | 123 | # Machine states / events |
115 | def is_loaded_or_failed(self): | 124 | def is_loaded_or_failed(self): |
@@ -118,22 +127,22 @@ class KeyMachine(Widget): | |||
118 | def is_loaded_inactive(self): | 127 | def is_loaded_inactive(self): |
119 | return self.is_loaded_no_config() or self.is_loaded_no_actions() | 128 | return self.is_loaded_no_config() or self.is_loaded_no_actions() |
120 | 129 | ||
130 | @mainthread | ||
121 | def on_enter_configuring(self): | 131 | def on_enter_configuring(self): |
132 | self.destroy_actions() | ||
133 | self.key.unset_description() | ||
134 | self.key.unset_color() | ||
135 | |||
122 | if self.key.key_sym in self.key.parent.key_config: | 136 | if self.key.key_sym in self.key.parent.key_config: |
123 | self.key.config = self.key.parent.key_config[self.key.key_sym] | 137 | self.key.config = self.key.parent.key_config[self.key.key_sym] |
124 | 138 | ||
125 | self.key.actions = [] | ||
126 | for key_action in self.key.config['actions']: | 139 | for key_action in self.key.config['actions']: |
127 | self.key.add_action(key_action[0], **key_action[1]) | 140 | self.key.add_action(key_action[0], **key_action[1]) |
128 | 141 | ||
129 | if 'description' in self.key.config['properties']: | 142 | if 'description' in self.key.config['properties']: |
130 | self.key.set_description(self.key.config['properties']['description']) | 143 | self.key.set_description(self.key.config['properties']['description']) |
131 | else: | ||
132 | self.key.unset_description() | ||
133 | if 'color' in self.key.config['properties']: | 144 | if 'color' in self.key.config['properties']: |
134 | self.key.set_color(self.key.config['properties']['color']) | 145 | self.key.set_color(self.key.config['properties']['color']) |
135 | else: | ||
136 | self.key.unset_color() | ||
137 | self.success() | 146 | self.success() |
138 | else: | 147 | else: |
139 | self.no_config() | 148 | self.no_config() |
@@ -145,6 +154,11 @@ class KeyMachine(Widget): | |||
145 | else: | 154 | else: |
146 | self.no_actions() | 155 | self.no_actions() |
147 | 156 | ||
157 | def destroy_actions(self): | ||
158 | for action in self.key.actions: | ||
159 | action.destroy() | ||
160 | self.key.actions = [] | ||
161 | |||
148 | def run_actions(self, modifiers): | 162 | def run_actions(self, modifiers): |
149 | self.key.parent.parent.ids['KeyList'].append(self.key.key_sym) | 163 | self.key.parent.parent.ids['KeyList'].append(self.key.key_sym) |
150 | debug_print("running actions for {}".format(self.key.key_sym)) | 164 | debug_print("running actions for {}".format(self.key.key_sym)) |
@@ -168,9 +182,21 @@ class KeyMachine(Widget): | |||
168 | self.key.repeat_protection_finished() | 182 | self.key.repeat_protection_finished() |
169 | 183 | ||
170 | # Callbacks | 184 | # Callbacks |
185 | @mainthread | ||
171 | def key_loaded_callback(self): | 186 | def key_loaded_callback(self): |
172 | self.key.parent.key_loaded_callback() | 187 | self.key.parent.key_loaded_callback() |
173 | 188 | ||
189 | def callback_action_state_changed(self): | ||
190 | if self.state not in ['failed', 'loading', 'loaded']: | ||
191 | return | ||
192 | |||
193 | if any(action.is_failed() for action in self.key.actions): | ||
194 | self.to_failed() | ||
195 | elif any(action.is_loading() for action in self.key.actions): | ||
196 | self.to_loading() | ||
197 | else: | ||
198 | self.to_loaded() | ||
199 | self.key_loaded_callback() | ||
174 | 200 | ||
175 | class Key(ButtonBehavior, Widget): | 201 | class Key(ButtonBehavior, Widget): |
176 | 202 | ||
@@ -234,18 +260,18 @@ class Key(ButtonBehavior, Widget): | |||
234 | else: | 260 | else: |
235 | raise AttributeError | 261 | raise AttributeError |
236 | 262 | ||
237 | def machine_state_changed(self, instance, machine_state): | ||
238 | self.machine_state = self.machine.state | ||
239 | |||
240 | def __init__(self, **kwargs): | 263 | def __init__(self, **kwargs): |
241 | self.actions = [] | 264 | self.actions = [] |
242 | self.current_action = None | 265 | self.current_action = None |
243 | self.machine = KeyMachine(self) | 266 | self.machine = KeyMachine(self) |
244 | self.machine.bind(state=self.machine_state_changed) | ||
245 | 267 | ||
246 | super(Key, self).__init__(**kwargs) | 268 | super(Key, self).__init__(**kwargs) |
247 | 269 | ||
248 | # Kivy events | 270 | # Kivy events |
271 | @mainthread | ||
272 | def update_state(self, value): | ||
273 | self.machine_state = value | ||
274 | |||
249 | def on_key_sym(self, key, key_sym): | 275 | def on_key_sym(self, key, key_sym): |
250 | if key_sym != "": | 276 | if key_sym != "": |
251 | self.configure() | 277 | self.configure() |
@@ -258,13 +284,6 @@ class Key(ButtonBehavior, Widget): | |||
258 | def interrupt(self): | 284 | def interrupt(self): |
259 | self.current_action.interrupt() | 285 | self.current_action.interrupt() |
260 | 286 | ||
261 | # Callbacks | ||
262 | def callback_action_ready(self, action, success): | ||
263 | if not success: | ||
264 | self.fail() | ||
265 | elif all(action.is_loaded_or_failed() for action in self.actions): | ||
266 | self.success() | ||
267 | |||
268 | # Setters | 287 | # Setters |
269 | def set_description(self, description): | 288 | def set_description(self, description): |
270 | if description[0] is not None: | 289 | if description[0] is not None: |
diff --git a/music_sampler/mapping.py b/music_sampler/mapping.py index 9e40d40..a526ad2 100644 --- a/music_sampler/mapping.py +++ b/music_sampler/mapping.py | |||
@@ -22,8 +22,7 @@ class Mapping(RelativeLayout): | |||
22 | 'configuring', | 22 | 'configuring', |
23 | 'configured', | 23 | 'configured', |
24 | 'loading', | 24 | 'loading', |
25 | 'loaded', | 25 | 'loaded' |
26 | 'failed' | ||
27 | ] | 26 | ] |
28 | 27 | ||
29 | TRANSITIONS = [ | 28 | TRANSITIONS = [ |
@@ -33,11 +32,6 @@ class Mapping(RelativeLayout): | |||
33 | 'dest': 'configuring' | 32 | 'dest': 'configuring' |
34 | }, | 33 | }, |
35 | { | 34 | { |
36 | 'trigger': 'fail', | ||
37 | 'source': 'configuring', | ||
38 | 'dest': 'failed' | ||
39 | }, | ||
40 | { | ||
41 | 'trigger': 'success', | 35 | 'trigger': 'success', |
42 | 'source': 'configuring', | 36 | 'source': 'configuring', |
43 | 'dest': 'configured', | 37 | 'dest': 'configured', |
@@ -49,11 +43,6 @@ class Mapping(RelativeLayout): | |||
49 | 'dest': 'loading' | 43 | 'dest': 'loading' |
50 | }, | 44 | }, |
51 | { | 45 | { |
52 | 'trigger': 'fail', | ||
53 | 'source': 'loading', | ||
54 | 'dest': 'failed' | ||
55 | }, | ||
56 | { | ||
57 | 'trigger': 'success', | 46 | 'trigger': 'success', |
58 | 'source': 'loading', | 47 | 'source': 'loading', |
59 | 'dest': 'loaded' | 48 | 'dest': 'loaded' |
@@ -74,17 +63,18 @@ class Mapping(RelativeLayout): | |||
74 | self.running = [] | 63 | self.running = [] |
75 | self.wait_ids = {} | 64 | self.wait_ids = {} |
76 | self.open_files = {} | 65 | self.open_files = {} |
66 | self.is_leaving_application = False | ||
77 | 67 | ||
78 | Machine(model=self, states=self.STATES, | 68 | Machine(model=self, states=self.STATES, |
79 | transitions=self.TRANSITIONS, initial='initial', | 69 | transitions=self.TRANSITIONS, initial='initial', |
80 | ignore_invalid_triggers=True, queued=True) | 70 | auto_transitions=False, queued=True) |
81 | super(Mapping, self).__init__(**kwargs) | 71 | super(Mapping, self).__init__(**kwargs) |
82 | self.keyboard = Window.request_keyboard(self.on_keyboard_closed, self) | 72 | self.keyboard = Window.request_keyboard(self.on_keyboard_closed, self) |
83 | self.keyboard.bind(on_key_down=self.on_keyboard_down) | 73 | self.keyboard.bind(on_key_down=self.on_keyboard_down) |
84 | 74 | ||
85 | self.configure() | 75 | self.configure(initial=True) |
86 | 76 | ||
87 | def on_enter_configuring(self): | 77 | def on_enter_configuring(self, initial=True): |
88 | if Config.builtin_mixing: | 78 | if Config.builtin_mixing: |
89 | self.mixer = Mixer() | 79 | self.mixer = Mixer() |
90 | else: | 80 | else: |
@@ -94,9 +84,9 @@ class Mapping(RelativeLayout): | |||
94 | self.key_config, self.open_files = self.parse_config() | 84 | self.key_config, self.open_files = self.parse_config() |
95 | except Exception as e: | 85 | except Exception as e: |
96 | error_print("Error while loading configuration: {}".format(e), | 86 | error_print("Error while loading configuration: {}".format(e), |
97 | with_trace=True, exit=True) | 87 | with_trace=False, exit=initial) |
98 | else: | 88 | |
99 | self.success() | 89 | self.success() |
100 | 90 | ||
101 | def on_enter_loading(self): | 91 | def on_enter_loading(self): |
102 | for key in self.keys: | 92 | for key in self.keys: |
@@ -127,13 +117,14 @@ class Mapping(RelativeLayout): | |||
127 | elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'): | 117 | elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'): |
128 | self.leave_application() | 118 | self.leave_application() |
129 | sys.exit() | 119 | sys.exit() |
130 | elif 'ctrl' in modifiers and keycode[0] == 114: | 120 | elif 'ctrl' in modifiers and keycode[0] == 114 and self.is_loaded(): |
131 | threading.Thread(name="MSReload", target=self.reload).start() | 121 | self.reload(initial=False) |
132 | return True | 122 | return True |
133 | 123 | ||
134 | def leave_application(self): | 124 | def leave_application(self): |
135 | self.keyboard.unbind(on_key_down=self.on_keyboard_down) | 125 | self.keyboard.unbind(on_key_down=self.on_keyboard_down) |
136 | self.stop_all_running() | 126 | self.stop_all_running() |
127 | self.is_leaving_application = True | ||
137 | for music in self.open_files.values(): | 128 | for music in self.open_files.values(): |
138 | music.stop() | 129 | music.stop() |
139 | for thread in threading.enumerate(): | 130 | for thread in threading.enumerate(): |
@@ -167,13 +158,20 @@ class Mapping(RelativeLayout): | |||
167 | 158 | ||
168 | # Callbacks | 159 | # Callbacks |
169 | def key_loaded_callback(self): | 160 | def key_loaded_callback(self): |
161 | if hasattr(self, 'finished_loading'): | ||
162 | return | ||
163 | |||
164 | opacity = int(Config.load_all_musics) | ||
165 | |||
170 | result = self.all_keys_ready() | 166 | result = self.all_keys_ready() |
171 | if result == "success": | 167 | if result == "success": |
172 | self.ready_color = [0, 1, 0, 1] | 168 | self.ready_color = [0, 1, 0, opacity] |
169 | self.finished_loading = True | ||
173 | elif result == "partial": | 170 | elif result == "partial": |
174 | self.ready_color = [1, 0, 0, 1] | 171 | self.ready_color = [1, 0, 0, opacity] |
172 | self.finished_loading = True | ||
175 | else: | 173 | else: |
176 | self.ready_color = [1, 165/255, 0, 1] | 174 | self.ready_color = [1, 165/255, 0, opacity] |
177 | 175 | ||
178 | ## Some global actions | 176 | ## Some global actions |
179 | def stop_all_running(self, except_key=None, key_start_time=0): | 177 | def stop_all_running(self, except_key=None, key_start_time=0): |
@@ -350,13 +348,11 @@ class Mapping(RelativeLayout): | |||
350 | try: | 348 | try: |
351 | config = yaml.safe_load(stream) | 349 | config = yaml.safe_load(stream) |
352 | except Exception as e: | 350 | except Exception as e: |
353 | error_print("Error while loading config file: {}".format(e), | 351 | raise Exception("Error while loading config file: {}".format(e)) from e |
354 | exit=True) | ||
355 | stream.close() | 352 | stream.close() |
356 | 353 | ||
357 | if not isinstance(config, dict): | 354 | if not isinstance(config, dict): |
358 | error_print("Top level config is supposed to be a hash", | 355 | raise Exception("Top level config is supposed to be a hash") |
359 | exit=True) | ||
360 | 356 | ||
361 | if 'aliases' in config and isinstance(config['aliases'], dict): | 357 | if 'aliases' in config and isinstance(config['aliases'], dict): |
362 | aliases = config['aliases'] | 358 | aliases = config['aliases'] |
diff --git a/music_sampler/music_file.py b/music_sampler/music_file.py index 4ba65e3..ec50951 100644 --- a/music_sampler/music_file.py +++ b/music_sampler/music_file.py | |||
@@ -22,6 +22,7 @@ class MusicFile: | |||
22 | { | 22 | { |
23 | 'name': 'loaded', | 23 | 'name': 'loaded', |
24 | 'children': [ | 24 | 'children': [ |
25 | 'stopped', | ||
25 | 'playing', | 26 | 'playing', |
26 | 'paused', | 27 | 'paused', |
27 | 'stopping' | 28 | 'stopping' |
@@ -31,9 +32,8 @@ class MusicFile: | |||
31 | TRANSITIONS = [ | 32 | TRANSITIONS = [ |
32 | { | 33 | { |
33 | 'trigger': 'load', | 34 | 'trigger': 'load', |
34 | 'source': 'initial', | 35 | 'source': ['initial', 'failed'], |
35 | 'dest': 'loading', | 36 | 'dest': 'loading' |
36 | 'after': 'poll_loaded' | ||
37 | }, | 37 | }, |
38 | { | 38 | { |
39 | 'trigger': 'fail', | 39 | 'trigger': 'fail', |
@@ -41,17 +41,19 @@ class MusicFile: | |||
41 | 'dest': 'failed' | 41 | 'dest': 'failed' |
42 | }, | 42 | }, |
43 | { | 43 | { |
44 | 'trigger': 'unload', | ||
45 | 'source': ['failed', 'loaded_stopped'], | ||
46 | 'dest': 'initial', | ||
47 | }, | ||
48 | { | ||
44 | 'trigger': 'success', | 49 | 'trigger': 'success', |
45 | 'source': 'loading', | 50 | 'source': 'loading', |
46 | 'dest': 'loaded' | 51 | 'dest': 'loaded_stopped' |
47 | }, | 52 | }, |
48 | { | 53 | { |
49 | 'trigger': 'start_playing', | 54 | 'trigger': 'start_playing', |
50 | 'source': 'loaded', | 55 | 'source': 'loaded_stopped', |
51 | 'dest': 'loaded_playing', | 56 | 'dest': 'loaded_playing' |
52 | # if a child has no transitions, then it is bubbled to the parent, | ||
53 | # and we don't want that. Not useful in that machine precisely. | ||
54 | 'conditions': ['is_loaded'] | ||
55 | }, | 57 | }, |
56 | { | 58 | { |
57 | 'trigger': 'pause', | 59 | 'trigger': 'pause', |
@@ -70,19 +72,20 @@ class MusicFile: | |||
70 | }, | 72 | }, |
71 | { | 73 | { |
72 | 'trigger': 'stopped', | 74 | 'trigger': 'stopped', |
73 | 'source': '*', | 75 | 'source': 'loaded', |
74 | 'dest': 'loaded', | 76 | 'dest': 'loaded_stopped', |
75 | 'before': 'trigger_stopped_events', | 77 | 'before': 'trigger_stopped_events', |
76 | 'conditions': ['is_in_use'] | 78 | 'unless': 'is_loaded_stopped', |
77 | } | 79 | } |
78 | ] | 80 | ] |
79 | 81 | ||
80 | def __init__(self, filename, mapping, name=None, gain=1): | 82 | def __init__(self, filename, mapping, name=None, gain=1): |
81 | Machine(model=self, states=self.STATES, | 83 | machine = Machine(model=self, states=self.STATES, |
82 | transitions=self.TRANSITIONS, initial='initial', | 84 | transitions=self.TRANSITIONS, initial='initial', |
83 | ignore_invalid_triggers=True) | 85 | auto_transitions=False, |
86 | after_state_change=self.notify_state_change) | ||
84 | 87 | ||
85 | self.loaded_callbacks = [] | 88 | self.state_change_callbacks = [] |
86 | self.mapping = mapping | 89 | self.mapping = mapping |
87 | self.filename = filename | 90 | self.filename = filename |
88 | self.name = name or filename | 91 | self.name = name or filename |
@@ -90,48 +93,41 @@ class MusicFile: | |||
90 | self.initial_volume_factor = gain | 93 | self.initial_volume_factor = gain |
91 | self.music_lock = Lock("music__" + filename) | 94 | self.music_lock = Lock("music__" + filename) |
92 | 95 | ||
93 | threading.Thread(name="MSMusicLoad", target=self.load).start() | 96 | if Config.load_all_musics: |
97 | threading.Thread(name="MSMusicLoad", target=self.load).start() | ||
94 | 98 | ||
95 | def reload_properties(self, name=None, gain=1): | 99 | def reload_properties(self, name=None, gain=1): |
96 | self.name = name or self.filename | 100 | self.name = name or self.filename |
97 | if gain != self.initial_volume_factor: | 101 | if gain != self.initial_volume_factor: |
98 | self.initial_volume_factor = gain | 102 | self.initial_volume_factor = gain |
99 | self.reload_music_file() | 103 | self.stopped() |
104 | self.unload() | ||
105 | self.load(reloading=True) | ||
100 | 106 | ||
101 | def reload_music_file(self): | 107 | # Machine related events |
102 | with file_lock: | 108 | def on_enter_initial(self): |
103 | try: | 109 | self.audio_segment = None |
104 | if self.filename.startswith("/"): | ||
105 | filename = self.filename | ||
106 | else: | ||
107 | filename = Config.music_path + self.filename | ||
108 | 110 | ||
109 | debug_print("Reloading « {} »".format(self.name)) | 111 | def on_enter_loading(self, reloading=False): |
110 | initial_db_gain = gain(self.initial_volume_factor * 100) | 112 | if reloading: |
111 | self.audio_segment = pydub.AudioSegment \ | 113 | prefix = 'Rel' |
112 | .from_file(filename) \ | 114 | prefix_s = 'rel' |
113 | .set_frame_rate(Config.frame_rate) \ | 115 | else: |
114 | .set_channels(Config.channels) \ | 116 | prefix = 'L' |
115 | .set_sample_width(Config.sample_width) \ | 117 | prefix_s = 'l' |
116 | .apply_gain(initial_db_gain) | ||
117 | except Exception as e: | ||
118 | error_print("failed to reload « {} »: {}"\ | ||
119 | .format(self.name, e)) | ||
120 | self.loading_error = e | ||
121 | self.to_failed() | ||
122 | else: | ||
123 | debug_print("Reloaded « {} »".format(self.name)) | ||
124 | 118 | ||
125 | # Machine related events | ||
126 | def on_enter_loading(self): | ||
127 | with file_lock: | 119 | with file_lock: |
120 | if self.mapping.is_leaving_application: | ||
121 | self.fail() | ||
122 | return | ||
123 | |||
128 | try: | 124 | try: |
129 | if self.filename.startswith("/"): | 125 | if self.filename.startswith("/"): |
130 | filename = self.filename | 126 | filename = self.filename |
131 | else: | 127 | else: |
132 | filename = Config.music_path + self.filename | 128 | filename = Config.music_path + self.filename |
133 | 129 | ||
134 | debug_print("Loading « {} »".format(self.name)) | 130 | debug_print("{}oading « {} »".format(prefix, self.name)) |
135 | self.mixer = self.mapping.mixer or Mixer() | 131 | self.mixer = self.mapping.mixer or Mixer() |
136 | initial_db_gain = gain(self.initial_volume_factor * 100) | 132 | initial_db_gain = gain(self.initial_volume_factor * 100) |
137 | self.audio_segment = pydub.AudioSegment \ | 133 | self.audio_segment = pydub.AudioSegment \ |
@@ -142,12 +138,13 @@ class MusicFile: | |||
142 | .apply_gain(initial_db_gain) | 138 | .apply_gain(initial_db_gain) |
143 | self.sound_duration = self.audio_segment.duration_seconds | 139 | self.sound_duration = self.audio_segment.duration_seconds |
144 | except Exception as e: | 140 | except Exception as e: |
145 | error_print("failed to load « {} »: {}".format(self.name, e)) | 141 | error_print("failed to {}oad « {} »: {}".format( |
142 | prefix_s, self.name, e)) | ||
146 | self.loading_error = e | 143 | self.loading_error = e |
147 | self.fail() | 144 | self.fail() |
148 | else: | 145 | else: |
149 | self.success() | 146 | self.success() |
150 | debug_print("Loaded « {} »".format(self.name)) | 147 | debug_print("{}oaded « {} »".format(prefix, self.name)) |
151 | 148 | ||
152 | def on_enter_loaded(self): | 149 | def on_enter_loaded(self): |
153 | self.cleanup() | 150 | self.cleanup() |
@@ -165,11 +162,15 @@ class MusicFile: | |||
165 | 162 | ||
166 | # Machine related states | 163 | # Machine related states |
167 | def is_in_use(self): | 164 | def is_in_use(self): |
168 | return self.is_loaded(allow_substates=True) and not self.is_loaded() | 165 | return self.is_loaded(allow_substates=True) and\ |
166 | not self.is_loaded_stopped() | ||
169 | 167 | ||
170 | def is_in_use_not_stopping(self): | 168 | def is_in_use_not_stopping(self): |
171 | return self.is_loaded_playing() or self.is_loaded_paused() | 169 | return self.is_loaded_playing() or self.is_loaded_paused() |
172 | 170 | ||
171 | def is_unloadable(self): | ||
172 | return self.is_loaded_stopped() or self.is_failed() | ||
173 | |||
173 | # Machine related triggers | 174 | # Machine related triggers |
174 | def trigger_stopped_events(self): | 175 | def trigger_stopped_events(self): |
175 | self.mixer.remove_file(self) | 176 | self.mixer.remove_file(self) |
@@ -243,7 +244,7 @@ class MusicFile: | |||
243 | if wait: | 244 | if wait: |
244 | self.mapping.add_wait(self.wait_event, wait_id=set_wait_id) | 245 | self.mapping.add_wait(self.wait_event, wait_id=set_wait_id) |
245 | self.wait_end() | 246 | self.wait_end() |
246 | else: | 247 | elif self.is_loaded(allow_substates=True): |
247 | self.stopped() | 248 | self.stopped() |
248 | 249 | ||
249 | def abandon_all_effects(self): | 250 | def abandon_all_effects(self): |
@@ -274,21 +275,19 @@ class MusicFile: | |||
274 | self.wait_event.clear() | 275 | self.wait_event.clear() |
275 | self.wait_event.wait() | 276 | self.wait_event.wait() |
276 | 277 | ||
277 | # Let other subscribe for an event when they are ready | 278 | # Let other subscribe for state change |
278 | def subscribe_loaded(self, callback): | 279 | def notify_state_change(self, **kwargs): |
279 | # FIXME: should lock to be sure we have no race, but it makes the | 280 | for callback in self.state_change_callbacks: |
280 | # initialization screen not showing until everything is loaded | 281 | callback(self.state) |
281 | if self.is_loaded(allow_substates=True): | 282 | |
282 | callback(True) | 283 | def subscribe_state_change(self, callback): |
283 | elif self.is_failed(): | 284 | if callback not in self.state_change_callbacks: |
284 | callback(False) | 285 | self.state_change_callbacks.append(callback) |
285 | else: | 286 | callback(self.state) |
286 | self.loaded_callbacks.append(callback) | ||
287 | 287 | ||
288 | def poll_loaded(self): | 288 | def unsubscribe_state_change(self, callback): |
289 | for callback in self.loaded_callbacks: | 289 | if callback in self.state_change_callbacks: |
290 | callback(self.is_loaded()) | 290 | self.state_change_callbacks.remove(callback) |
291 | self.loaded_callbacks = [] | ||
292 | 291 | ||
293 | # Callbacks | 292 | # Callbacks |
294 | def finished_callback(self): | 293 | def finished_callback(self): |
diff --git a/music_sampler/music_sampler.kv b/music_sampler/music_sampler.kv index 0432e14..839d2ce 100644 --- a/music_sampler/music_sampler.kv +++ b/music_sampler/music_sampler.kv | |||
@@ -611,6 +611,8 @@ | |||
611 | key_sym: "^" | 611 | key_sym: "^" |
612 | row: 3 | 612 | row: 3 |
613 | col: 12.5 | 613 | col: 12.5 |
614 | line_width: 1 | ||
615 | enabled: False | ||
614 | Key: | 616 | Key: |
615 | id: Key_36 | 617 | id: Key_36 |
616 | key_code: 36 | 618 | key_code: 36 |
@@ -632,8 +634,6 @@ | |||
632 | row: 4 | 634 | row: 4 |
633 | col: 1 | 635 | col: 1 |
634 | key_width: 1.75 | 636 | key_width: 1.75 |
635 | line_width: 1 | ||
636 | enabled: False | ||
637 | 637 | ||
638 | Key: | 638 | Key: |
639 | id: Key_113 | 639 | id: Key_113 |