aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--helpers/__init__.py14
-rw-r--r--helpers/action.py324
-rw-r--r--helpers/actions/__init__.py10
-rw-r--r--helpers/actions/command.py6
-rw-r--r--helpers/actions/interrupt_wait.py5
-rw-r--r--helpers/actions/pause.py10
-rw-r--r--helpers/actions/play.py44
-rw-r--r--helpers/actions/seek.py19
-rw-r--r--helpers/actions/stop.py42
-rw-r--r--helpers/actions/stop_all_actions.py5
-rw-r--r--helpers/actions/unpause.py10
-rw-r--r--helpers/actions/volume.py28
-rw-r--r--helpers/actions/wait.py38
-rw-r--r--helpers/key.py205
-rw-r--r--helpers/mapping.py209
-rw-r--r--helpers/music_file.py28
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
140def debug_print(message): 140def 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
144def error_print(message): 144def 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
148def 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 @@
1import threading 1from transitions.extensions import HierarchicalMachine as Machine
2import time 2from . import debug_print, error_print
3 3from . import actions
4from . import debug_print
5 4
6class Action: 5class 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 @@
1from . import command
2from . import interrupt_wait
3from . import pause
4from . import play
5from . import seek
6from . import stop
7from . import stop_all_actions
8from . import unpause
9from . import volume
10from . 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 @@
1def run(action, command="", **kwargs):
2 # FIXME: todo
3 pass
4
5def 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 @@
1def run(action, wait_id=None):
2 action.mapping.interrupt_wait(wait_id)
3
4def 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 @@
1def run(action, music=None, **kwargs):
2 for music in action.music_list(music):
3 if music.is_loaded_playing():
4 music.pause()
5
6def 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 @@
1def 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
20def 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 @@
1def 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
5def 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 @@
1def 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
19def 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
39def 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 @@
1def run(action, **kwargs):
2 action.mapping.stop_all_running()
3
4def 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 @@
1def run(action, music=None, **kwargs):
2 for music in action.music_list(music):
3 if music.is_loaded_paused():
4 music.unpause()
5
6def 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 @@
1def 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
7def 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 @@
1import threading
2
3def 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
16def 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
33def 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 @@
1from kivy.uix.widget import Widget 1from kivy.uix.widget import Widget
2from kivy.properties import AliasProperty, BooleanProperty, \ 2from kivy.properties import AliasProperty, BooleanProperty, \
3 ListProperty, StringProperty 3 ListProperty, StringProperty
4from kivy.clock import Clock
5from kivy.uix.behaviors import ButtonBehavior 4from kivy.uix.behaviors import ButtonBehavior
6 5
7from .action import * 6from .action import Action
8from . import debug_print 7from . import debug_print
9import time 8import time
9from transitions.extensions import HierarchicalMachine as Machine
10 10
11class Key(ButtonBehavior, Widget): 11class 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
6import threading 6import threading
7import yaml 7import yaml
8import sys 8import sys
9from collections import defaultdict
9 10
10from .music_file import * 11from .music_file import MusicFile
11from .mixer import Mixer 12from .mixer import Mixer
12from . import Config, gain, error_print 13from . import Config, gain, error_print, warn_print
13from .action import Action 14from .action import Action
14 15
15class Mapping(RelativeLayout): 16class 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()