aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsmaël Bouya <ismael.bouya@normalesup.org>2016-09-22 21:47:25 +0200
committerIsmaël Bouya <ismael.bouya@normalesup.org>2016-09-22 21:47:25 +0200
commitd028768179d4fd1555831e26daaa9aae9ac94e85 (patch)
treec7dc2e8b589c87d50961b60ddfa17b739ef4e9fb
parentd4217fda2ff3991eb1ee9a9bec6acff751798507 (diff)
parentf9aeecf1a00e0e632546db00cb0cfa31b078dbe9 (diff)
downloadMusicSampler-d028768179d4fd1555831e26daaa9aae9ac94e85.tar.gz
MusicSampler-d028768179d4fd1555831e26daaa9aae9ac94e85.tar.zst
MusicSampler-d028768179d4fd1555831e26daaa9aae9ac94e85.zip
Merge branch 'load_action'1.3.0
-rw-r--r--config.yml80
-rw-r--r--music_sampler/action.py70
-rw-r--r--music_sampler/actions/__init__.py2
-rw-r--r--music_sampler/actions/load_music.py12
-rw-r--r--music_sampler/actions/unload_music.py10
-rw-r--r--music_sampler/app_blocks/actionlist.py3
-rw-r--r--music_sampler/app_blocks/playlist.py3
-rw-r--r--music_sampler/helpers.py8
-rw-r--r--music_sampler/key.py59
-rw-r--r--music_sampler/mapping.py50
-rw-r--r--music_sampler/music_file.py119
-rw-r--r--music_sampler/music_sampler.kv4
12 files changed, 265 insertions, 155 deletions
diff --git a/config.yml b/config.yml
index 36c804b..2e89a0f 100644
--- a/config.yml
+++ b/config.yml
@@ -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 @@
1from . import interrupt_wait 1from . import interrupt_wait
2from . import load_music
2from . import pause 3from . import pause
3from . import pause_wait 4from . import pause_wait
4from . import play 5from . import play
@@ -7,6 +8,7 @@ from . import run_command
7from . import seek 8from . import seek
8from . import stop 9from . import stop
9from . import stop_all_actions 10from . import stop_all_actions
11from . import unload_music
10from . import unpause 12from . import unpause
11from . import unpause_wait 13from . import unpause_wait
12from . import volume 14from . 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 @@
1import threading
2
3def 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
8def 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 @@
1def run(action, music=None, **kwargs):
2 for music in action.music_list(music):
3 if music.is_unloadable():
4 music.unload()
5
6def 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
4from kivy.properties import ListProperty, StringProperty 4from kivy.properties import ListProperty, StringProperty
5from ..lock import Lock 5from ..lock import Lock
6 6
7from 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
2from kivy.uix.stacklayout import StackLayout 2from kivy.uix.stacklayout import StackLayout
3from kivy.uix.relativelayout import RelativeLayout 3from kivy.uix.relativelayout import RelativeLayout
4from kivy.properties import ListProperty 4from kivy.properties import ListProperty
5from kivy.clock import Clock 5from kivy.clock import Clock, mainthread
6from ..helpers import duration_to_min_sec 6from ..helpers import duration_to_min_sec
7from ..lock import Lock 7from ..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]
146def parse_args(): 154def 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
9import threading 9import threading
10from transitions.extensions import HierarchicalMachine as Machine 10from transitions.extensions import HierarchicalMachine as Machine
11 11
12class 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
14from kivy.clock import mainthread
15
16class 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
175class Key(ButtonBehavior, Widget): 201class 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