diff options
-rw-r--r-- | helpers/__init__.py | 14 | ||||
-rw-r--r-- | helpers/action.py | 324 | ||||
-rw-r--r-- | helpers/actions/__init__.py | 10 | ||||
-rw-r--r-- | helpers/actions/command.py | 6 | ||||
-rw-r--r-- | helpers/actions/interrupt_wait.py | 5 | ||||
-rw-r--r-- | helpers/actions/pause.py | 10 | ||||
-rw-r--r-- | helpers/actions/play.py | 44 | ||||
-rw-r--r-- | helpers/actions/seek.py | 19 | ||||
-rw-r--r-- | helpers/actions/stop.py | 42 | ||||
-rw-r--r-- | helpers/actions/stop_all_actions.py | 5 | ||||
-rw-r--r-- | helpers/actions/unpause.py | 10 | ||||
-rw-r--r-- | helpers/actions/volume.py | 28 | ||||
-rw-r--r-- | helpers/actions/wait.py | 38 | ||||
-rw-r--r-- | helpers/key.py | 205 | ||||
-rw-r--r-- | helpers/mapping.py | 209 | ||||
-rw-r--r-- | helpers/music_file.py | 28 |
16 files changed, 634 insertions, 363 deletions
diff --git a/helpers/__init__.py b/helpers/__init__.py index f5ad848..534e168 100644 --- a/helpers/__init__.py +++ b/helpers/__init__.py | |||
@@ -86,7 +86,7 @@ def parse_args(): | |||
86 | by Kivy. Pass \"-- --help\" to get Kivy's usage.") | 86 | by Kivy. Pass \"-- --help\" to get Kivy's usage.") |
87 | 87 | ||
88 | from kivy.logger import Logger | 88 | from kivy.logger import Logger |
89 | Logger.setLevel(logging.ERROR) | 89 | Logger.setLevel(logging.WARN) |
90 | 90 | ||
91 | args = parser.parse_args(argv) | 91 | args = parser.parse_args(argv) |
92 | 92 | ||
@@ -137,10 +137,14 @@ def gain(volume, old_volume=None): | |||
137 | 20 * math.log10(max(volume, 0.1) / max(old_volume, 0.1)), | 137 | 20 * math.log10(max(volume, 0.1) / max(old_volume, 0.1)), |
138 | max(volume, 0)] | 138 | max(volume, 0)] |
139 | 139 | ||
140 | def debug_print(message): | 140 | def debug_print(message, with_trace=False): |
141 | from kivy.logger import Logger | 141 | from kivy.logger import Logger |
142 | Logger.debug('MusicSampler: ' + message) | 142 | Logger.debug('MusicSampler: ' + message, exc_info=with_trace) |
143 | 143 | ||
144 | def error_print(message): | 144 | def error_print(message, with_trace=False): |
145 | from kivy.logger import Logger | 145 | from kivy.logger import Logger |
146 | Logger.error('MusicSampler: ' + message) | 146 | Logger.error('MusicSampler: ' + message, exc_info=with_trace) |
147 | |||
148 | def warn_print(message, with_trace=False): | ||
149 | from kivy.logger import Logger | ||
150 | Logger.warn('MusicSampler: ' + message, exc_info=with_trace) | ||
diff --git a/helpers/action.py b/helpers/action.py index ec8fcb6..1f374ec 100644 --- a/helpers/action.py +++ b/helpers/action.py | |||
@@ -1,263 +1,111 @@ | |||
1 | import threading | 1 | from transitions.extensions import HierarchicalMachine as Machine |
2 | import time | 2 | from . import debug_print, error_print |
3 | 3 | from . import actions | |
4 | from . import debug_print | ||
5 | 4 | ||
6 | class Action: | 5 | class Action: |
7 | action_types = [ | 6 | STATES = [ |
8 | 'command', | 7 | 'initial', |
9 | 'interrupt_wait', | 8 | 'loading', |
10 | 'pause', | 9 | 'failed', |
11 | 'play', | 10 | { |
12 | 'seek', | 11 | 'name': 'loaded', |
13 | 'stop', | 12 | 'children': ['running'] |
14 | 'stop_all_actions', | 13 | } |
15 | 'unpause', | 14 | ] |
16 | 'volume', | 15 | |
17 | 'wait', | 16 | TRANSITIONS = [ |
17 | { | ||
18 | 'trigger': 'load', | ||
19 | 'source': 'initial', | ||
20 | 'dest': 'loading' | ||
21 | }, | ||
22 | { | ||
23 | 'trigger': 'fail', | ||
24 | 'source': 'loading', | ||
25 | 'dest': 'failed', | ||
26 | 'after': 'poll_loaded' | ||
27 | }, | ||
28 | { | ||
29 | 'trigger': 'success', | ||
30 | 'source': 'loading', | ||
31 | 'dest': 'loaded', | ||
32 | 'after': 'poll_loaded' | ||
33 | }, | ||
34 | { | ||
35 | 'trigger': 'run', | ||
36 | 'source': 'loaded', | ||
37 | 'dest': 'loaded_running', | ||
38 | '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 | { | ||
44 | 'trigger': 'finish_action', | ||
45 | 'source': 'loaded_running', | ||
46 | 'dest': 'loaded' | ||
47 | } | ||
18 | ] | 48 | ] |
19 | 49 | ||
20 | def __init__(self, action, key, **kwargs): | 50 | def __init__(self, action, key, **kwargs): |
21 | if action in self.action_types: | 51 | Machine(model=self, states=self.STATES, |
22 | self.action = action | 52 | transitions=self.TRANSITIONS, initial='initial', |
23 | else: | 53 | ignore_invalid_triggers=True, queued=True) |
24 | raise Exception("Unknown action {}".format(action)) | ||
25 | 54 | ||
55 | self.action = action | ||
26 | self.key = key | 56 | self.key = key |
27 | self.mapping = key.parent | 57 | self.mapping = key.parent |
28 | self.arguments = kwargs | 58 | self.arguments = kwargs |
29 | self.sleep_event = None | 59 | self.sleep_event = None |
60 | self.waiting_music = None | ||
61 | |||
62 | def is_loaded_or_failed(self): | ||
63 | return self.is_loaded(allow_substates=True) or self.is_failed() | ||
30 | 64 | ||
31 | def ready(self): | 65 | def callback_music_loaded(self, success): |
32 | if 'music' in self.arguments: | 66 | if success: |
33 | return self.arguments['music'].is_loaded(allow_substates=True) | 67 | self.success() |
34 | else: | 68 | else: |
35 | return True | 69 | self.fail() |
36 | 70 | ||
37 | def run(self): | 71 | # Machine states / events |
72 | def on_enter_loading(self): | ||
73 | if hasattr(actions, self.action): | ||
74 | if 'music' in self.arguments: | ||
75 | self.arguments['music'].subscribe_loaded(self.callback_music_loaded) | ||
76 | else: | ||
77 | self.success() | ||
78 | else: | ||
79 | error_print("Unknown action {}".format(self.action)) | ||
80 | self.fail() | ||
81 | |||
82 | def on_enter_loaded_running(self): | ||
38 | debug_print(self.description()) | 83 | debug_print(self.description()) |
39 | getattr(self, self.action)(**self.arguments) | 84 | if hasattr(actions, self.action): |
85 | getattr(actions, self.action).run(self, **self.arguments) | ||
40 | 86 | ||
41 | def description(self): | 87 | def poll_loaded(self): |
42 | return getattr(self, self.action + "_print")(**self.arguments) | 88 | self.key.callback_action_ready(self, |
89 | self.is_loaded(allow_substates=True)) | ||
43 | 90 | ||
91 | # This one cannot be in the Machine state since it would be queued to run | ||
92 | # *after* the wait is ended... | ||
44 | def interrupt(self): | 93 | def interrupt(self): |
45 | if getattr(self, self.action + "_interrupt", None): | 94 | if getattr(actions, self.action, None) and\ |
46 | return getattr(self, self.action + "_interrupt")(**self.arguments) | 95 | hasattr(getattr(actions, self.action), 'interrupt'): |
96 | return getattr(getattr(actions, self.action), 'interrupt')( | ||
97 | self, **self.arguments) | ||
47 | 98 | ||
99 | # Helpers | ||
48 | def music_list(self, music): | 100 | def music_list(self, music): |
49 | if music is not None: | 101 | if music is not None: |
50 | return [music] | 102 | return [music] |
51 | else: | 103 | else: |
52 | return self.mapping.open_files.values() | 104 | return self.mapping.open_files.values() |
53 | 105 | ||
54 | # Actions | 106 | def description(self): |
55 | def command(self, command="", **kwargs): | 107 | if hasattr(actions, self.action): |
56 | # FIXME: todo | 108 | return getattr(actions, self.action)\ |
57 | pass | 109 | .description(self, **self.arguments) |
58 | |||
59 | def pause(self, music=None, **kwargs): | ||
60 | for music in self.music_list(music): | ||
61 | if music.is_loaded_playing(): | ||
62 | music.pause() | ||
63 | |||
64 | def unpause(self, music=None, **kwargs): | ||
65 | for music in self.music_list(music): | ||
66 | if music.is_loaded_paused(): | ||
67 | music.unpause() | ||
68 | |||
69 | def play(self, music=None, fade_in=0, start_at=0, | ||
70 | restart_if_running=False, volume=100, | ||
71 | loop=0, **kwargs): | ||
72 | for music in self.music_list(music): | ||
73 | if restart_if_running: | ||
74 | if music.is_in_use(): | ||
75 | music.stop() | ||
76 | music.play( | ||
77 | volume=volume, | ||
78 | fade_in=fade_in, | ||
79 | start_at=start_at, | ||
80 | loop=loop) | ||
81 | elif not music.is_in_use(): | ||
82 | music.play( | ||
83 | volume=volume, | ||
84 | fade_in=fade_in, | ||
85 | start_at=start_at, | ||
86 | loop=loop) | ||
87 | |||
88 | def seek(self, music=None, value=0, delta=False, **kwargs): | ||
89 | for music in self.music_list(music): | ||
90 | music.seek(value=value, delta=delta) | ||
91 | |||
92 | def interrupt_wait(self, wait_id=None): | ||
93 | self.mapping.interrupt_wait(wait_id) | ||
94 | |||
95 | def stop(self, music=None, fade_out=0, wait=False, | ||
96 | set_wait_id=None, **kwargs): | ||
97 | previous = None | ||
98 | for music in self.music_list(music): | ||
99 | if music.is_loaded_paused() or music.is_loaded_playing(): | ||
100 | if previous is not None: | ||
101 | previous.stop(fade_out=fade_out) | ||
102 | previous = music | ||
103 | else: | ||
104 | music.stop(fade_out=fade_out) | ||
105 | |||
106 | if previous is not None: | ||
107 | previous.stop( | ||
108 | fade_out=fade_out, | ||
109 | wait=wait, | ||
110 | set_wait_id=set_wait_id) | ||
111 | |||
112 | def stop_all_actions(self, **kwargs): | ||
113 | self.mapping.stop_all_running() | ||
114 | |||
115 | def volume(self, music=None, value=100, fade=0, delta=False, **kwargs): | ||
116 | if music is not None: | ||
117 | music.set_volume(value, delta=delta, fade=fade) | ||
118 | else: | ||
119 | self.mapping.set_master_volume(value, delta=delta, fade=fade) | ||
120 | |||
121 | def wait(self, duration=0, music=None, set_wait_id=None, **kwargs): | ||
122 | if set_wait_id is not None: | ||
123 | self.mapping.add_wait_id(set_wait_id, self) | ||
124 | |||
125 | self.sleep_event = threading.Event() | ||
126 | |||
127 | if music is not None: | ||
128 | music.wait_end() | ||
129 | |||
130 | threading.Timer(duration, self.sleep_event.set).start() | ||
131 | self.sleep_event.wait() | ||
132 | |||
133 | # Action messages | ||
134 | def command_print(self, command="", **kwargs): | ||
135 | return "running command {}".format(command) | ||
136 | |||
137 | def interrupt_wait_print(self, wait_id=None, **kwargs): | ||
138 | return "interrupt wait with id {}".format(wait_id) | ||
139 | |||
140 | def pause_print(self, music=None, **kwargs): | ||
141 | if music is not None: | ||
142 | return "pausing « {} »".format(music.name) | ||
143 | else: | ||
144 | return "pausing all musics" | ||
145 | |||
146 | def unpause_print(self, music=None, **kwargs): | ||
147 | if music is not None: | ||
148 | return "unpausing « {} »".format(music.name) | ||
149 | else: | ||
150 | return "unpausing all musics" | ||
151 | |||
152 | def play_print(self, music=None, fade_in=0, start_at=0, | ||
153 | restart_if_running=False, volume=100, loop=0, **kwargs): | ||
154 | message = "starting " | ||
155 | if music is not None: | ||
156 | message += "« {} »".format(music.name) | ||
157 | else: | ||
158 | message += "all musics" | ||
159 | |||
160 | if start_at != 0: | ||
161 | message += " at {}s".format(start_at) | ||
162 | |||
163 | if fade_in != 0: | ||
164 | message += " with {}s fade_in".format(fade_in) | ||
165 | |||
166 | message += " at volume {}%".format(volume) | ||
167 | |||
168 | if loop > 0: | ||
169 | message += " {} times".format(loop + 1) | ||
170 | elif loop < 0: | ||
171 | message += " in loop" | ||
172 | |||
173 | if restart_if_running: | ||
174 | message += " (restarting if already running)" | ||
175 | |||
176 | return message | ||
177 | |||
178 | def stop_print(self, music=None, fade_out=0, wait=False, | ||
179 | set_wait_id=None, **kwargs): | ||
180 | |||
181 | message = "stopping " | ||
182 | if music is not None: | ||
183 | message += "music « {} »".format(music.name) | ||
184 | else: | ||
185 | message += "all musics" | ||
186 | |||
187 | if fade_out > 0: | ||
188 | message += " with {}s fadeout".format(fade_out) | ||
189 | if wait: | ||
190 | if set_wait_id is not None: | ||
191 | message += " (waiting the end of fadeout, with id {})"\ | ||
192 | .format(set_wait_id) | ||
193 | else: | ||
194 | message += " (waiting the end of fadeout)" | ||
195 | |||
196 | return message | ||
197 | |||
198 | def stop_all_actions_print(self, **kwargs): | ||
199 | return "stopping all actions" | ||
200 | |||
201 | def seek_print(self, music=None, value=0, delta=False, **kwargs): | ||
202 | if delta: | ||
203 | if music is not None: | ||
204 | return "moving music « {} » by {:+d}s" \ | ||
205 | .format(music.name, value) | ||
206 | else: | ||
207 | return "moving all musics by {:+d}s" \ | ||
208 | .format(value) | ||
209 | else: | ||
210 | if music is not None: | ||
211 | return "moving music « {} » to position {}s" \ | ||
212 | .format(music.name, value) | ||
213 | else: | ||
214 | return "moving all musics to position {}s" \ | ||
215 | .format(value) | ||
216 | |||
217 | def volume_print(self, music=None, | ||
218 | value=100, delta=False, fade=0, **kwargs): | ||
219 | message = "" | ||
220 | if delta: | ||
221 | if music is not None: | ||
222 | message += "{:+d}% to volume of « {} »" \ | ||
223 | .format(value, music.name) | ||
224 | else: | ||
225 | message += "{:+d}% to volume" \ | ||
226 | .format(value) | ||
227 | else: | ||
228 | if music is not None: | ||
229 | message += "setting volume of « {} » to {}%" \ | ||
230 | .format(music.name, value) | ||
231 | else: | ||
232 | message += "setting volume to {}%" \ | ||
233 | .format(value) | ||
234 | |||
235 | if fade > 0: | ||
236 | message += " with {}s fade".format(fade) | ||
237 | |||
238 | return message | ||
239 | |||
240 | def wait_print(self, duration=0, music=None, set_wait_id=None, **kwargs): | ||
241 | message = "" | ||
242 | if music is None: | ||
243 | message += "waiting {}s" \ | ||
244 | .format(duration) | ||
245 | elif duration == 0: | ||
246 | message += "waiting the end of « {} »" \ | ||
247 | .format(music.name) | ||
248 | else: | 110 | else: |
249 | message += "waiting the end of « {} » + {}s" \ | 111 | return "unknown action {}".format(self.action) |
250 | .format(music.name, duration) | ||
251 | |||
252 | if set_wait_id is not None: | ||
253 | message += " (setting id = {})".format(set_wait_id) | ||
254 | |||
255 | return message | ||
256 | |||
257 | # Interruptions | ||
258 | def wait_interrupt(self, duration=0, music=None, **kwargs): | ||
259 | if self.sleep_event is not None: | ||
260 | self.sleep_event.set() | ||
261 | if music is not None: | ||
262 | music.wait_event.set() | ||
263 | |||
diff --git a/helpers/actions/__init__.py b/helpers/actions/__init__.py new file mode 100644 index 0000000..ea1e800 --- /dev/null +++ b/helpers/actions/__init__.py | |||
@@ -0,0 +1,10 @@ | |||
1 | from . import command | ||
2 | from . import interrupt_wait | ||
3 | from . import pause | ||
4 | from . import play | ||
5 | from . import seek | ||
6 | from . import stop | ||
7 | from . import stop_all_actions | ||
8 | from . import unpause | ||
9 | from . import volume | ||
10 | from . import wait | ||
diff --git a/helpers/actions/command.py b/helpers/actions/command.py new file mode 100644 index 0000000..96f72fe --- /dev/null +++ b/helpers/actions/command.py | |||
@@ -0,0 +1,6 @@ | |||
1 | def run(action, command="", **kwargs): | ||
2 | # FIXME: todo | ||
3 | pass | ||
4 | |||
5 | def description(action, command="", **kwargs): | ||
6 | return "running command {}".format(command) | ||
diff --git a/helpers/actions/interrupt_wait.py b/helpers/actions/interrupt_wait.py new file mode 100644 index 0000000..36766a2 --- /dev/null +++ b/helpers/actions/interrupt_wait.py | |||
@@ -0,0 +1,5 @@ | |||
1 | def run(action, wait_id=None): | ||
2 | action.mapping.interrupt_wait(wait_id) | ||
3 | |||
4 | def description(action, wait_id=None, **kwargs): | ||
5 | return "interrupt wait with id {}".format(wait_id) | ||
diff --git a/helpers/actions/pause.py b/helpers/actions/pause.py new file mode 100644 index 0000000..bb27734 --- /dev/null +++ b/helpers/actions/pause.py | |||
@@ -0,0 +1,10 @@ | |||
1 | def run(action, music=None, **kwargs): | ||
2 | for music in action.music_list(music): | ||
3 | if music.is_loaded_playing(): | ||
4 | music.pause() | ||
5 | |||
6 | def description(action, music=None, **kwargs): | ||
7 | if music is not None: | ||
8 | return "pausing « {} »".format(music.name) | ||
9 | else: | ||
10 | return "pausing all musics" | ||
diff --git a/helpers/actions/play.py b/helpers/actions/play.py new file mode 100644 index 0000000..fdba95b --- /dev/null +++ b/helpers/actions/play.py | |||
@@ -0,0 +1,44 @@ | |||
1 | def run(action, music=None, fade_in=0, start_at=0, | ||
2 | restart_if_running=False, volume=100, | ||
3 | loop=0, **kwargs): | ||
4 | for music in action.music_list(music): | ||
5 | if restart_if_running: | ||
6 | if music.is_in_use(): | ||
7 | music.stop() | ||
8 | music.play( | ||
9 | volume=volume, | ||
10 | fade_in=fade_in, | ||
11 | start_at=start_at, | ||
12 | loop=loop) | ||
13 | elif not music.is_in_use(): | ||
14 | music.play( | ||
15 | volume=volume, | ||
16 | fade_in=fade_in, | ||
17 | start_at=start_at, | ||
18 | loop=loop) | ||
19 | |||
20 | def description(action, music=None, fade_in=0, start_at=0, | ||
21 | restart_if_running=False, volume=100, loop=0, **kwargs): | ||
22 | message = "starting " | ||
23 | if music is not None: | ||
24 | message += "« {} »".format(music.name) | ||
25 | else: | ||
26 | message += "all musics" | ||
27 | |||
28 | if start_at != 0: | ||
29 | message += " at {}s".format(start_at) | ||
30 | |||
31 | if fade_in != 0: | ||
32 | message += " with {}s fade_in".format(fade_in) | ||
33 | |||
34 | message += " at volume {}%".format(volume) | ||
35 | |||
36 | if loop > 0: | ||
37 | message += " {} times".format(loop + 1) | ||
38 | elif loop < 0: | ||
39 | message += " in loop" | ||
40 | |||
41 | if restart_if_running: | ||
42 | message += " (restarting if already running)" | ||
43 | |||
44 | return message | ||
diff --git a/helpers/actions/seek.py b/helpers/actions/seek.py new file mode 100644 index 0000000..467af7d --- /dev/null +++ b/helpers/actions/seek.py | |||
@@ -0,0 +1,19 @@ | |||
1 | def run(action, music=None, value=0, delta=False, **kwargs): | ||
2 | for music in action.music_list(music): | ||
3 | music.seek(value=value, delta=delta) | ||
4 | |||
5 | def description(action, music=None, value=0, delta=False, **kwargs): | ||
6 | if delta: | ||
7 | if music is not None: | ||
8 | return "moving music « {} » by {:+d}s" \ | ||
9 | .format(music.name, value) | ||
10 | else: | ||
11 | return "moving all musics by {:+d}s" \ | ||
12 | .format(value) | ||
13 | else: | ||
14 | if music is not None: | ||
15 | return "moving music « {} » to position {}s" \ | ||
16 | .format(music.name, value) | ||
17 | else: | ||
18 | return "moving all musics to position {}s" \ | ||
19 | .format(value) | ||
diff --git a/helpers/actions/stop.py b/helpers/actions/stop.py new file mode 100644 index 0000000..88cc66d --- /dev/null +++ b/helpers/actions/stop.py | |||
@@ -0,0 +1,42 @@ | |||
1 | def run(action, music=None, fade_out=0, wait=False, | ||
2 | set_wait_id=None, **kwargs): | ||
3 | previous = None | ||
4 | for music in action.music_list(music): | ||
5 | if music.is_loaded_paused() or music.is_loaded_playing(): | ||
6 | if previous is not None: | ||
7 | previous.stop(fade_out=fade_out) | ||
8 | previous = music | ||
9 | else: | ||
10 | music.stop(fade_out=fade_out) | ||
11 | |||
12 | if previous is not None: | ||
13 | action.waiting_music = previous | ||
14 | previous.stop( | ||
15 | fade_out=fade_out, | ||
16 | wait=wait, | ||
17 | set_wait_id=set_wait_id) | ||
18 | |||
19 | def description(action, music=None, fade_out=0, wait=False, | ||
20 | set_wait_id=None, **kwargs): | ||
21 | |||
22 | message = "stopping " | ||
23 | if music is not None: | ||
24 | message += "music « {} »".format(music.name) | ||
25 | else: | ||
26 | message += "all musics" | ||
27 | |||
28 | if fade_out > 0: | ||
29 | message += " with {}s fadeout".format(fade_out) | ||
30 | if wait: | ||
31 | if set_wait_id is not None: | ||
32 | message += " (waiting the end of fadeout, with id {})"\ | ||
33 | .format(set_wait_id) | ||
34 | else: | ||
35 | message += " (waiting the end of fadeout)" | ||
36 | |||
37 | return message | ||
38 | |||
39 | def interrupt(action, music=None, fade_out=0, wait=False, | ||
40 | set_wait_id=None, **kwargs): | ||
41 | if action.waiting_music is not None: | ||
42 | action.waiting_music.wait_event.set() | ||
diff --git a/helpers/actions/stop_all_actions.py b/helpers/actions/stop_all_actions.py new file mode 100644 index 0000000..f3fc5fb --- /dev/null +++ b/helpers/actions/stop_all_actions.py | |||
@@ -0,0 +1,5 @@ | |||
1 | def run(action, **kwargs): | ||
2 | action.mapping.stop_all_running() | ||
3 | |||
4 | def description(action, **kwargs): | ||
5 | return "stopping all actions" | ||
diff --git a/helpers/actions/unpause.py b/helpers/actions/unpause.py new file mode 100644 index 0000000..5fa88c3 --- /dev/null +++ b/helpers/actions/unpause.py | |||
@@ -0,0 +1,10 @@ | |||
1 | def run(action, music=None, **kwargs): | ||
2 | for music in action.music_list(music): | ||
3 | if music.is_loaded_paused(): | ||
4 | music.unpause() | ||
5 | |||
6 | def description(action, music=None, **kwargs): | ||
7 | if music is not None: | ||
8 | return "unpausing « {} »".format(music.name) | ||
9 | else: | ||
10 | return "unpausing all musics" | ||
diff --git a/helpers/actions/volume.py b/helpers/actions/volume.py new file mode 100644 index 0000000..7dda3c1 --- /dev/null +++ b/helpers/actions/volume.py | |||
@@ -0,0 +1,28 @@ | |||
1 | def run(action, music=None, value=100, fade=0, delta=False, **kwargs): | ||
2 | if music is not None: | ||
3 | music.set_volume(value, delta=delta, fade=fade) | ||
4 | else: | ||
5 | action.mapping.set_master_volume(value, delta=delta, fade=fade) | ||
6 | |||
7 | def description(action, music=None, | ||
8 | value=100, delta=False, fade=0, **kwargs): | ||
9 | message = "" | ||
10 | if delta: | ||
11 | if music is not None: | ||
12 | message += "{:+d}% to volume of « {} »" \ | ||
13 | .format(value, music.name) | ||
14 | else: | ||
15 | message += "{:+d}% to volume" \ | ||
16 | .format(value) | ||
17 | else: | ||
18 | if music is not None: | ||
19 | message += "setting volume of « {} » to {}%" \ | ||
20 | .format(music.name, value) | ||
21 | else: | ||
22 | message += "setting volume to {}%" \ | ||
23 | .format(value) | ||
24 | |||
25 | if fade > 0: | ||
26 | message += " with {}s fade".format(fade) | ||
27 | |||
28 | return message | ||
diff --git a/helpers/actions/wait.py b/helpers/actions/wait.py new file mode 100644 index 0000000..f7d2a78 --- /dev/null +++ b/helpers/actions/wait.py | |||
@@ -0,0 +1,38 @@ | |||
1 | import threading | ||
2 | |||
3 | def run(action, duration=0, music=None, set_wait_id=None, **kwargs): | ||
4 | if set_wait_id is not None: | ||
5 | action.mapping.add_wait_id(set_wait_id, action) | ||
6 | |||
7 | action.sleep_event = threading.Event() | ||
8 | action.sleep_event_timer = threading.Timer(duration, action.sleep_event.set) | ||
9 | |||
10 | if music is not None: | ||
11 | music.wait_end() | ||
12 | |||
13 | action.sleep_event_timer.start() | ||
14 | action.sleep_event.wait() | ||
15 | |||
16 | def description(action, duration=0, music=None, set_wait_id=None, **kwargs): | ||
17 | message = "" | ||
18 | if music is None: | ||
19 | message += "waiting {}s" \ | ||
20 | .format(duration) | ||
21 | elif duration == 0: | ||
22 | message += "waiting the end of « {} »" \ | ||
23 | .format(music.name) | ||
24 | else: | ||
25 | message += "waiting the end of « {} » + {}s" \ | ||
26 | .format(music.name, duration) | ||
27 | |||
28 | if set_wait_id is not None: | ||
29 | message += " (setting id = {})".format(set_wait_id) | ||
30 | |||
31 | return message | ||
32 | |||
33 | def interrupt(action, duration=0, music=None, **kwargs): | ||
34 | if action.sleep_event is not None: | ||
35 | action.sleep_event.set() | ||
36 | action.sleep_event_timer.cancel() | ||
37 | if music is not None: | ||
38 | music.wait_event.set() | ||
diff --git a/helpers/key.py b/helpers/key.py index 34c5140..bf46eeb 100644 --- a/helpers/key.py +++ b/helpers/key.py | |||
@@ -1,59 +1,183 @@ | |||
1 | from kivy.uix.widget import Widget | 1 | from kivy.uix.widget import Widget |
2 | from kivy.properties import AliasProperty, BooleanProperty, \ | 2 | from kivy.properties import AliasProperty, BooleanProperty, \ |
3 | ListProperty, StringProperty | 3 | ListProperty, StringProperty |
4 | from kivy.clock import Clock | ||
5 | from kivy.uix.behaviors import ButtonBehavior | 4 | from kivy.uix.behaviors import ButtonBehavior |
6 | 5 | ||
7 | from .action import * | 6 | from .action import Action |
8 | from . import debug_print | 7 | from . import debug_print |
9 | import time | 8 | import time |
9 | from transitions.extensions import HierarchicalMachine as Machine | ||
10 | 10 | ||
11 | class Key(ButtonBehavior, Widget): | 11 | class Key(ButtonBehavior, Widget): |
12 | STATES = [ | ||
13 | 'initial', | ||
14 | 'configuring', | ||
15 | 'configured', | ||
16 | 'loading', | ||
17 | 'failed', | ||
18 | { | ||
19 | 'name': 'loaded', | ||
20 | 'children': ['no_config', 'no_actions', 'running'] | ||
21 | } | ||
22 | ] | ||
23 | |||
24 | TRANSITIONS = [ | ||
25 | { | ||
26 | 'trigger': 'configure', | ||
27 | 'source': 'initial', | ||
28 | 'dest': 'configuring' | ||
29 | }, | ||
30 | { | ||
31 | 'trigger': 'fail', | ||
32 | 'source': 'configuring', | ||
33 | 'dest': 'failed' | ||
34 | }, | ||
35 | { | ||
36 | 'trigger': 'success', | ||
37 | 'source': 'configuring', | ||
38 | 'dest': 'configured', | ||
39 | 'after': 'load' | ||
40 | }, | ||
41 | { | ||
42 | 'trigger': 'no_config', | ||
43 | 'source': 'configuring', | ||
44 | 'dest': 'loaded_no_config', | ||
45 | }, | ||
46 | { | ||
47 | 'trigger': 'load', | ||
48 | 'source': 'configured', | ||
49 | 'dest': 'loading' | ||
50 | }, | ||
51 | { | ||
52 | 'trigger': 'fail', | ||
53 | 'source': 'loading', | ||
54 | 'dest': 'failed' | ||
55 | }, | ||
56 | { | ||
57 | 'trigger': 'success', | ||
58 | 'source': 'loading', | ||
59 | 'dest': 'loaded' | ||
60 | }, | ||
61 | { | ||
62 | 'trigger': 'no_actions', | ||
63 | 'source': 'loading', | ||
64 | 'dest': 'loaded_no_actions', | ||
65 | }, | ||
66 | { | ||
67 | 'trigger': 'reload', | ||
68 | 'source': 'loaded', | ||
69 | 'dest': 'configuring' | ||
70 | }, | ||
71 | { | ||
72 | 'trigger': 'run', | ||
73 | 'source': 'loaded', | ||
74 | 'dest': 'loaded_running', | ||
75 | 'after': 'finish', | ||
76 | # if a child, like loaded_no_actions, has no transitions, then it is | ||
77 | # bubbled to the parent, and we don't want that. | ||
78 | 'conditions': ['is_loaded'] | ||
79 | }, | ||
80 | { | ||
81 | 'trigger': 'finish', | ||
82 | 'source': 'loaded_running', | ||
83 | 'dest': 'loaded' | ||
84 | } | ||
85 | ] | ||
86 | |||
12 | key_sym = StringProperty(None) | 87 | key_sym = StringProperty(None) |
13 | custom_color = ListProperty([0, 1, 0, 1]) | 88 | custom_color = ListProperty([0, 1, 0]) |
14 | custom_unready_color = ListProperty([0, 1, 0, 100/255]) | ||
15 | description_title = StringProperty("") | 89 | description_title = StringProperty("") |
16 | description = ListProperty([]) | 90 | description = ListProperty([]) |
17 | is_key_ready = BooleanProperty(True) | 91 | state = StringProperty("") |
18 | 92 | ||
19 | def get_color(self): | 93 | def get_alias_color(self): |
20 | if not self.has_actions: | 94 | if self.is_loaded_inactive(): |
21 | return [1, 1, 1, 1] | 95 | return [1, 1, 1, 1] |
22 | elif self.all_actions_ready: | 96 | elif self.is_loaded(allow_substates=True): |
23 | return self.custom_color | 97 | return [*self.custom_color, 1] |
98 | elif self.is_failed(): | ||
99 | return [0, 0, 0, 1] | ||
24 | else: | 100 | else: |
25 | return self.custom_unready_color | 101 | return [*self.custom_color, 100/255] |
26 | def set_color(self): | 102 | def set_alias_color(self): |
27 | pass | 103 | pass |
28 | 104 | ||
29 | color = AliasProperty(get_color, set_color, bind=['is_key_ready']) | 105 | color = AliasProperty(get_alias_color, set_alias_color, |
106 | bind=['state', 'custom_color']) | ||
30 | 107 | ||
31 | def __init__(self, **kwargs): | 108 | def __init__(self, **kwargs): |
32 | super(Key, self).__init__(**kwargs) | ||
33 | self.actions = [] | 109 | self.actions = [] |
110 | Machine(model=self, states=self.STATES, | ||
111 | transitions=self.TRANSITIONS, initial='initial', | ||
112 | ignore_invalid_triggers=True, queued=True) | ||
113 | super(Key, self).__init__(**kwargs) | ||
34 | 114 | ||
115 | # Kivy events | ||
35 | def on_key_sym(self, key, key_sym): | 116 | def on_key_sym(self, key, key_sym): |
36 | if key_sym in self.parent.key_config: | 117 | if key_sym != "": |
37 | self.is_key_ready = False | 118 | self.configure() |
119 | |||
120 | def on_press(self): | ||
121 | self.list_actions() | ||
38 | 122 | ||
39 | self.config = self.parent.key_config[key_sym] | 123 | # Machine states / events |
124 | def is_loaded_or_failed(self): | ||
125 | return self.is_loaded(allow_substates=True) or self.is_failed() | ||
126 | |||
127 | def is_loaded_inactive(self): | ||
128 | return self.is_loaded_no_config() or self.is_loaded_no_actions() | ||
129 | |||
130 | def on_enter_configuring(self): | ||
131 | if self.key_sym in self.parent.key_config: | ||
132 | self.config = self.parent.key_config[self.key_sym] | ||
40 | 133 | ||
41 | self.actions = [] | 134 | self.actions = [] |
42 | for key_action in self.config['actions']: | 135 | for key_action in self.config['actions']: |
43 | self.add_action(key_action[0], **key_action[1]) | 136 | self.add_action(key_action[0], **key_action[1]) |
44 | 137 | ||
45 | if 'description' in self.config['properties']: | 138 | if 'description' in self.config['properties']: |
46 | key.set_description(self.config['properties']['description']) | 139 | self.set_description(self.config['properties']['description']) |
47 | if 'color' in self.config['properties']: | 140 | if 'color' in self.config['properties']: |
48 | key.set_color(self.config['properties']['color']) | 141 | self.set_color(self.config['properties']['color']) |
142 | self.success() | ||
143 | else: | ||
144 | self.no_config() | ||
49 | 145 | ||
50 | Clock.schedule_interval(self.check_all_active, 1) | 146 | def on_enter_loading(self): |
147 | if len(self.actions) > 0: | ||
148 | for action in self.actions: | ||
149 | action.load() | ||
150 | else: | ||
151 | self.no_actions() | ||
152 | |||
153 | def on_enter_loaded_running(self): | ||
154 | self.parent.parent.ids['KeyList'].append(self.key_sym) | ||
155 | debug_print("running actions for {}".format(self.key_sym)) | ||
156 | start_time = time.time() | ||
157 | self.parent.start_running(self, start_time) | ||
158 | action_number = 0 | ||
159 | for self.current_action in self.actions: | ||
160 | if self.parent.keep_running(self, start_time): | ||
161 | self.list_actions(action_number=action_number + 0.5) | ||
162 | self.current_action.run() | ||
163 | action_number += 1 | ||
164 | self.list_actions(action_number=action_number) | ||
51 | 165 | ||
52 | def check_all_active(self, dt): | 166 | self.parent.finished_running(self, start_time) |
53 | if self.all_actions_ready: | ||
54 | self.is_key_ready = True | ||
55 | return False | ||
56 | 167 | ||
168 | # This one cannot be in the Machine state since it would be queued to run | ||
169 | # *after* the loop is ended... | ||
170 | def interrupt(self): | ||
171 | self.current_action.interrupt() | ||
172 | |||
173 | # Callbacks | ||
174 | def callback_action_ready(self, action, success): | ||
175 | if not success: | ||
176 | self.fail() | ||
177 | elif all(action.is_loaded_or_failed() for action in self.actions): | ||
178 | self.success() | ||
179 | |||
180 | # Setters | ||
57 | def set_description(self, description): | 181 | def set_description(self, description): |
58 | if description[0] is not None: | 182 | if description[0] is not None: |
59 | self.description_title = str(description[0]) | 183 | self.description_title = str(description[0]) |
@@ -65,45 +189,12 @@ class Key(ButtonBehavior, Widget): | |||
65 | 189 | ||
66 | def set_color(self, color): | 190 | def set_color(self, color): |
67 | color = [x / 255 for x in color] | 191 | color = [x / 255 for x in color] |
68 | color.append(1) | ||
69 | self.custom_color = color | 192 | self.custom_color = color |
70 | color[3] = 100 / 255 | ||
71 | self.custom_unready_color = tuple(color) | ||
72 | |||
73 | @property | ||
74 | def has_actions(self): | ||
75 | return len(self.actions) > 0 | ||
76 | |||
77 | @property | ||
78 | def all_actions_ready(self): | ||
79 | return all(action.ready() for action in self.actions) | ||
80 | 193 | ||
194 | # Actions handling | ||
81 | def add_action(self, action_name, **arguments): | 195 | def add_action(self, action_name, **arguments): |
82 | self.actions.append(Action(action_name, self, **arguments)) | 196 | self.actions.append(Action(action_name, self, **arguments)) |
83 | 197 | ||
84 | def interrupt_action(self): | ||
85 | self.current_action.interrupt() | ||
86 | |||
87 | def do_actions(self): | ||
88 | if not self.enabled: | ||
89 | return None | ||
90 | |||
91 | self.parent.parent.ids['KeyList'].append(self.key_sym) | ||
92 | debug_print("running actions for {}".format(self.key_sym)) | ||
93 | start_time = time.time() | ||
94 | self.parent.start_running(self, start_time) | ||
95 | action_number = 0 | ||
96 | for self.current_action in self.actions: | ||
97 | if self.parent.keep_running(self, start_time): | ||
98 | self.list_actions(action_number=action_number + 0.5) | ||
99 | self.current_action.run() | ||
100 | action_number += 1 | ||
101 | self.list_actions(action_number=action_number) | ||
102 | |||
103 | self.parent.finished_running(self, start_time) | ||
104 | |||
105 | def list_actions(self, action_number=0): | 198 | def list_actions(self, action_number=0): |
106 | self.parent.parent.ids['ActionList'].update_list(self, action_number) | 199 | self.parent.parent.ids['ActionList'].update_list(self, action_number) |
107 | 200 | ||
108 | def on_press(self): | ||
109 | self.list_actions() | ||
diff --git a/helpers/mapping.py b/helpers/mapping.py index c2a94e6..6e3b291 100644 --- a/helpers/mapping.py +++ b/helpers/mapping.py | |||
@@ -6,10 +6,11 @@ from kivy.clock import Clock | |||
6 | import threading | 6 | import threading |
7 | import yaml | 7 | import yaml |
8 | import sys | 8 | import sys |
9 | from collections import defaultdict | ||
9 | 10 | ||
10 | from .music_file import * | 11 | from .music_file import MusicFile |
11 | from .mixer import Mixer | 12 | from .mixer import Mixer |
12 | from . import Config, gain, error_print | 13 | from . import Config, gain, error_print, warn_print |
13 | from .action import Action | 14 | from .action import Action |
14 | 15 | ||
15 | class Mapping(RelativeLayout): | 16 | class Mapping(RelativeLayout): |
@@ -26,7 +27,8 @@ class Mapping(RelativeLayout): | |||
26 | try: | 27 | try: |
27 | self.key_config, self.open_files = self.parse_config() | 28 | self.key_config, self.open_files = self.parse_config() |
28 | except Exception as e: | 29 | except Exception as e: |
29 | error_print("Error while loading configuration: {}".format(e)) | 30 | error_print("Error while loading configuration: {}".format(e), |
31 | with_trace=True) | ||
30 | sys.exit() | 32 | sys.exit() |
31 | 33 | ||
32 | super(Mapping, self).__init__(**kwargs) | 34 | super(Mapping, self).__init__(**kwargs) |
@@ -67,8 +69,9 @@ class Mapping(RelativeLayout): | |||
67 | def _on_keyboard_down(self, keyboard, keycode, text, modifiers): | 69 | def _on_keyboard_down(self, keyboard, keycode, text, modifiers): |
68 | key = self.find_by_key_code(keycode) | 70 | key = self.find_by_key_code(keycode) |
69 | if len(modifiers) == 0 and key is not None: | 71 | if len(modifiers) == 0 and key is not None: |
70 | threading.Thread(name="MSKeyAction", target=key.do_actions).start() | 72 | threading.Thread(name="MSKeyAction", target=key.run).start() |
71 | elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'): | 73 | elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'): |
74 | self.stop_all_running() | ||
72 | for thread in threading.enumerate(): | 75 | for thread in threading.enumerate(): |
73 | if thread.getName()[0:2] != "MS": | 76 | if thread.getName()[0:2] != "MS": |
74 | continue | 77 | continue |
@@ -86,7 +89,7 @@ class Mapping(RelativeLayout): | |||
86 | for key in self.children: | 89 | for key in self.children: |
87 | if not type(key).__name__ == "Key": | 90 | if not type(key).__name__ == "Key": |
88 | continue | 91 | continue |
89 | if not key.is_key_ready: | 92 | if not key.is_loaded_or_failed(): |
90 | return True | 93 | return True |
91 | self.ready_color = [0, 1, 0, 1] | 94 | self.ready_color = [0, 1, 0, 1] |
92 | return False | 95 | return False |
@@ -95,7 +98,7 @@ class Mapping(RelativeLayout): | |||
95 | running = self.running | 98 | running = self.running |
96 | self.running = [] | 99 | self.running = [] |
97 | for (key, start_time) in running: | 100 | for (key, start_time) in running: |
98 | key.interrupt_action() | 101 | key.interrupt() |
99 | 102 | ||
100 | def start_running(self, key, start_time): | 103 | def start_running(self, key, start_time): |
101 | self.running.append((key, start_time)) | 104 | self.running.append((key, start_time)) |
@@ -108,85 +111,171 @@ class Mapping(RelativeLayout): | |||
108 | self.running.remove((key, start_time)) | 111 | self.running.remove((key, start_time)) |
109 | 112 | ||
110 | def parse_config(self): | 113 | def parse_config(self): |
114 | def update_alias(prop_hash, aliases, key): | ||
115 | if isinstance(aliases[key], dict): | ||
116 | prop_hash.update(aliases[key], **prop_hash) | ||
117 | else: | ||
118 | warn_print("Alias {} is not a hash, ignored".format(key)) | ||
119 | |||
120 | def include_aliases(prop_hash, aliases): | ||
121 | if 'include' not in prop_hash: | ||
122 | return | ||
123 | |||
124 | included = prop_hash['include'] | ||
125 | del(prop_hash['include']) | ||
126 | if isinstance(included, str): | ||
127 | update_alias(prop_hash, aliases, included) | ||
128 | elif isinstance(included, list): | ||
129 | for included_ in included: | ||
130 | if isinstance(included_, str): | ||
131 | update_alias(prop_hash, aliases, included_) | ||
132 | else: | ||
133 | warn_print("Unkown alias include type, ignored: " | ||
134 | "{} in {}".format(included_, included)) | ||
135 | else: | ||
136 | warn_print("Unkown alias include type, ignored: {}" | ||
137 | .format(included)) | ||
138 | |||
139 | def check_key_property(key_property, key): | ||
140 | if 'description' in key_property: | ||
141 | desc = key_property['description'] | ||
142 | if not isinstance(desc, list): | ||
143 | warn_print("description in key_property '{}' is not " | ||
144 | "a list, ignored".format(key)) | ||
145 | del(key_property['description']) | ||
146 | if 'color' in key_property: | ||
147 | color = key_property['color'] | ||
148 | if not isinstance(color, list)\ | ||
149 | or len(color) != 3\ | ||
150 | or not all(isinstance(item, int) for item in color)\ | ||
151 | or any(item < 0 or item > 255 for item in color): | ||
152 | warn_print("color in key_property '{}' is not " | ||
153 | "a list of 3 valid integers, ignored".format(key)) | ||
154 | del(key_property['color']) | ||
155 | |||
156 | def check_key_properties(config): | ||
157 | if 'key_properties' in config: | ||
158 | if isinstance(config['key_properties'], dict): | ||
159 | return config['key_properties'] | ||
160 | else: | ||
161 | warn_print("key_properties config is not a hash, ignored") | ||
162 | return {} | ||
163 | else: | ||
164 | return {} | ||
165 | |||
166 | def check_mapped_keys(config): | ||
167 | if 'keys' in config: | ||
168 | if isinstance(config['keys'], dict): | ||
169 | return config['keys'] | ||
170 | else: | ||
171 | warn_print("keys config is not a hash, ignored") | ||
172 | return {} | ||
173 | else: | ||
174 | return {} | ||
175 | |||
176 | def check_mapped_key(mapped_keys, key): | ||
177 | if not isinstance(mapped_keys[key], list): | ||
178 | warn_print("key config '{}' is not an array, ignored" | ||
179 | .format(key)) | ||
180 | return [] | ||
181 | else: | ||
182 | return mapped_keys[key] | ||
183 | |||
184 | def check_music_property(music_property, filename): | ||
185 | if not isinstance(music_property, dict): | ||
186 | warn_print("music_property config '{}' is not a hash, ignored" | ||
187 | .format(filename)) | ||
188 | return {} | ||
189 | if 'name' in music_property: | ||
190 | music_property['name'] = str(music_property['name']) | ||
191 | if 'gain' in music_property: | ||
192 | try: | ||
193 | music_property['gain'] = float(music_property['gain']) | ||
194 | except ValueError as e: | ||
195 | del(music_property['gain']) | ||
196 | warn_print("gain for music_property '{}' is not " | ||
197 | "a float, ignored".format(filename)) | ||
198 | return music_property | ||
199 | |||
111 | stream = open(Config.yml_file, "r") | 200 | stream = open(Config.yml_file, "r") |
112 | try: | 201 | try: |
113 | config = yaml.load(stream) | 202 | config = yaml.safe_load(stream) |
114 | except Exception as e: | 203 | except Exception as e: |
115 | error_print("Error while loading config file: {}".format(e)) | 204 | error_print("Error while loading config file: {}".format(e)) |
116 | sys.exit() | 205 | sys.exit() |
117 | stream.close() | 206 | stream.close() |
118 | 207 | ||
119 | aliases = config['aliases'] | 208 | if not isinstance(config, dict): |
209 | raise Exception("Top level config is supposed to be a hash") | ||
210 | |||
211 | if 'aliases' in config and isinstance(config['aliases'], dict): | ||
212 | aliases = config['aliases'] | ||
213 | else: | ||
214 | aliases = defaultdict(dict) | ||
215 | if 'aliases' in config: | ||
216 | warn_print("aliases config is not a hash, ignored") | ||
217 | |||
218 | music_properties = defaultdict(dict) | ||
219 | if 'music_properties' in config and\ | ||
220 | isinstance(config['music_properties'], dict): | ||
221 | music_properties.update(config['music_properties']) | ||
222 | elif 'music_properties' in config: | ||
223 | warn_print("music_properties config is not a hash, ignored") | ||
224 | |||
120 | seen_files = {} | 225 | seen_files = {} |
121 | 226 | ||
122 | key_properties = {} | 227 | key_properties = defaultdict(lambda: { |
228 | "actions": [], | ||
229 | "properties": {}, | ||
230 | "files": [] | ||
231 | }) | ||
123 | 232 | ||
124 | for key in config['key_properties']: | 233 | for key in check_key_properties(config): |
125 | if key not in key_properties: | 234 | key_prop = config['key_properties'][key] |
126 | key_prop = config['key_properties'][key] | 235 | |
127 | if 'include' in key_prop: | 236 | if not isinstance(key_prop, dict): |
128 | included = key_prop['include'] | 237 | warn_print("key_property '{}' is not a hash, ignored" |
129 | del(key_prop['include']) | 238 | .format(key)) |
239 | continue | ||
240 | |||
241 | include_aliases(key_prop, aliases) | ||
242 | check_key_property(key_prop, key) | ||
243 | |||
244 | key_properties[key]["properties"] = key_prop | ||
245 | |||
246 | for mapped_key in check_mapped_keys(config): | ||
247 | for index, action in enumerate(check_mapped_key( | ||
248 | config['keys'], mapped_key)): | ||
249 | if not isinstance(action, dict) or\ | ||
250 | not len(action) == 1 or\ | ||
251 | not isinstance(list(action.values())[0] or {}, dict): | ||
252 | warn_print("action number {} of key '{}' is invalid, " | ||
253 | "ignored".format(index + 1, mapped_key)) | ||
254 | continue | ||
130 | 255 | ||
131 | if isinstance(included, str): | ||
132 | key_prop.update(aliases[included], **key_prop) | ||
133 | else: | ||
134 | for included_ in included: | ||
135 | key_prop.update(aliases[included_], **key_prop) | ||
136 | |||
137 | key_properties[key] = { | ||
138 | "actions": [], | ||
139 | "properties": key_prop, | ||
140 | "files": [] | ||
141 | } | ||
142 | |||
143 | for mapped_key in config['keys']: | ||
144 | if mapped_key not in key_properties: | ||
145 | key_properties[mapped_key] = { | ||
146 | "actions": [], | ||
147 | "properties": {}, | ||
148 | "files": [] | ||
149 | } | ||
150 | for action in config['keys'][mapped_key]: | ||
151 | action_name = list(action)[0] | 256 | action_name = list(action)[0] |
152 | action_args = {} | 257 | action_args = {} |
153 | if action[action_name] is None: | 258 | if action[action_name] is None: |
154 | action[action_name] = [] | 259 | action[action_name] = {} |
155 | |||
156 | if 'include' in action[action_name]: | ||
157 | included = action[action_name]['include'] | ||
158 | del(action[action_name]['include']) | ||
159 | 260 | ||
160 | if isinstance(included, str): | 261 | include_aliases(action[action_name], aliases) |
161 | action[action_name].update( | ||
162 | aliases[included], | ||
163 | **action[action_name]) | ||
164 | else: | ||
165 | for included_ in included: | ||
166 | action[action_name].update( | ||
167 | aliases[included_], | ||
168 | **action[action_name]) | ||
169 | 262 | ||
170 | for argument in action[action_name]: | 263 | for argument in action[action_name]: |
171 | if argument == 'file': | 264 | if argument == 'file': |
172 | filename = action[action_name]['file'] | 265 | filename = str(action[action_name]['file']) |
173 | if filename not in seen_files: | 266 | if filename not in seen_files: |
174 | if filename in config['music_properties']: | 267 | music_property = check_music_property( |
175 | seen_files[filename] = MusicFile( | 268 | music_properties[filename], |
176 | filename, | 269 | filename) |
177 | self, | 270 | |
178 | **config['music_properties'][filename]) | 271 | seen_files[filename] = MusicFile( |
179 | else: | 272 | filename, self, **music_property) |
180 | seen_files[filename] = MusicFile( | ||
181 | self, | ||
182 | filename) | ||
183 | 273 | ||
184 | if filename not in key_properties[mapped_key]['files']: | 274 | if filename not in key_properties[mapped_key]['files']: |
185 | key_properties[mapped_key]['files'] \ | 275 | key_properties[mapped_key]['files'] \ |
186 | .append(seen_files[filename]) | 276 | .append(seen_files[filename]) |
187 | 277 | ||
188 | action_args['music'] = seen_files[filename] | 278 | action_args['music'] = seen_files[filename] |
189 | |||
190 | else: | 279 | else: |
191 | action_args[argument] = action[action_name][argument] | 280 | action_args[argument] = action[action_name][argument] |
192 | 281 | ||
diff --git a/helpers/music_file.py b/helpers/music_file.py index ccf60ce..a972bc5 100644 --- a/helpers/music_file.py +++ b/helpers/music_file.py | |||
@@ -32,7 +32,8 @@ class MusicFile: | |||
32 | { | 32 | { |
33 | 'trigger': 'load', | 33 | 'trigger': 'load', |
34 | 'source': 'initial', | 34 | 'source': 'initial', |
35 | 'dest': 'loading' | 35 | 'dest': 'loading', |
36 | 'after': 'poll_loaded' | ||
36 | }, | 37 | }, |
37 | { | 38 | { |
38 | 'trigger': 'fail', | 39 | 'trigger': 'fail', |
@@ -47,7 +48,10 @@ class MusicFile: | |||
47 | { | 48 | { |
48 | 'trigger': 'start_playing', | 49 | 'trigger': 'start_playing', |
49 | 'source': 'loaded', | 50 | 'source': 'loaded', |
50 | 'dest': 'loaded_playing' | 51 | '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'] | ||
51 | }, | 55 | }, |
52 | { | 56 | { |
53 | 'trigger': 'pause', | 57 | 'trigger': 'pause', |
@@ -68,7 +72,8 @@ class MusicFile: | |||
68 | 'trigger': 'stopped', | 72 | 'trigger': 'stopped', |
69 | 'source': '*', | 73 | 'source': '*', |
70 | 'dest': 'loaded', | 74 | 'dest': 'loaded', |
71 | 'before': 'trigger_stopped_events' | 75 | 'before': 'trigger_stopped_events', |
76 | 'conditions': ['is_in_use'] | ||
72 | } | 77 | } |
73 | ] | 78 | ] |
74 | 79 | ||
@@ -77,6 +82,7 @@ class MusicFile: | |||
77 | transitions=self.TRANSITIONS, initial='initial', | 82 | transitions=self.TRANSITIONS, initial='initial', |
78 | ignore_invalid_triggers=True) | 83 | ignore_invalid_triggers=True) |
79 | 84 | ||
85 | self.loaded_callbacks = [] | ||
80 | self.mapping = mapping | 86 | self.mapping = mapping |
81 | self.filename = filename | 87 | self.filename = filename |
82 | self.name = name or filename | 88 | self.name = name or filename |
@@ -230,6 +236,22 @@ class MusicFile: | |||
230 | self.wait_event.clear() | 236 | self.wait_event.clear() |
231 | self.wait_event.wait() | 237 | self.wait_event.wait() |
232 | 238 | ||
239 | # Let other subscribe for an event when they are ready | ||
240 | def subscribe_loaded(self, callback): | ||
241 | # FIXME: should lock to be sure we have no race, but it makes the | ||
242 | # initialization screen not showing until everything is loaded | ||
243 | if self.is_loaded(allow_substates=True): | ||
244 | callback(True) | ||
245 | elif self.is_failed(): | ||
246 | callback(False) | ||
247 | else: | ||
248 | self.loaded_callbacks.append(callback) | ||
249 | |||
250 | def poll_loaded(self): | ||
251 | for callback in self.loaded_callbacks: | ||
252 | callback(self.is_loaded()) | ||
253 | self.loaded_callbacks = [] | ||
254 | |||
233 | # Callbacks | 255 | # Callbacks |
234 | def finished_callback(self): | 256 | def finished_callback(self): |
235 | self.stopped() | 257 | self.stopped() |