by Kivy. Pass \"-- --help\" to get Kivy's usage.")
from kivy.logger import Logger
- Logger.setLevel(logging.ERROR)
+ Logger.setLevel(logging.WARN)
args = parser.parse_args(argv)
20 * math.log10(max(volume, 0.1) / max(old_volume, 0.1)),
max(volume, 0)]
-def debug_print(message):
+def debug_print(message, with_trace=False):
from kivy.logger import Logger
- Logger.debug('MusicSampler: ' + message)
+ Logger.debug('MusicSampler: ' + message, exc_info=with_trace)
-def error_print(message):
+def error_print(message, with_trace=False):
from kivy.logger import Logger
- Logger.error('MusicSampler: ' + message)
+ Logger.error('MusicSampler: ' + message, exc_info=with_trace)
+
+def warn_print(message, with_trace=False):
+ from kivy.logger import Logger
+ Logger.warn('MusicSampler: ' + message, exc_info=with_trace)
-import threading
-import time
-
-from . import debug_print
+from transitions.extensions import HierarchicalMachine as Machine
+from . import debug_print, error_print
+from . import actions
class Action:
- action_types = [
- 'command',
- 'interrupt_wait',
- 'pause',
- 'play',
- 'seek',
- 'stop',
- 'stop_all_actions',
- 'unpause',
- 'volume',
- 'wait',
+ STATES = [
+ 'initial',
+ 'loading',
+ 'failed',
+ {
+ 'name': 'loaded',
+ 'children': ['running']
+ }
+ ]
+
+ TRANSITIONS = [
+ {
+ 'trigger': 'load',
+ 'source': 'initial',
+ 'dest': 'loading'
+ },
+ {
+ 'trigger': 'fail',
+ 'source': 'loading',
+ 'dest': 'failed',
+ 'after': 'poll_loaded'
+ },
+ {
+ 'trigger': 'success',
+ 'source': 'loading',
+ 'dest': 'loaded',
+ 'after': 'poll_loaded'
+ },
+ {
+ 'trigger': 'run',
+ 'source': 'loaded',
+ 'dest': 'loaded_running',
+ 'after': 'finish_action',
+ # if a child has no transitions, then it is bubbled to the parent,
+ # and we don't want that. Not useful in that machine precisely.
+ 'conditions': ['is_loaded']
+ },
+ {
+ 'trigger': 'finish_action',
+ 'source': 'loaded_running',
+ 'dest': 'loaded'
+ }
]
def __init__(self, action, key, **kwargs):
- if action in self.action_types:
- self.action = action
- else:
- raise Exception("Unknown action {}".format(action))
+ Machine(model=self, states=self.STATES,
+ transitions=self.TRANSITIONS, initial='initial',
+ ignore_invalid_triggers=True, queued=True)
+ self.action = action
self.key = key
self.mapping = key.parent
self.arguments = kwargs
self.sleep_event = None
+ self.waiting_music = None
+
+ def is_loaded_or_failed(self):
+ return self.is_loaded(allow_substates=True) or self.is_failed()
- def ready(self):
- if 'music' in self.arguments:
- return self.arguments['music'].is_loaded(allow_substates=True)
+ def callback_music_loaded(self, success):
+ if success:
+ self.success()
else:
- return True
+ self.fail()
- def run(self):
+ # Machine states / events
+ def on_enter_loading(self):
+ if hasattr(actions, self.action):
+ if 'music' in self.arguments:
+ self.arguments['music'].subscribe_loaded(self.callback_music_loaded)
+ else:
+ self.success()
+ else:
+ error_print("Unknown action {}".format(self.action))
+ self.fail()
+
+ def on_enter_loaded_running(self):
debug_print(self.description())
- getattr(self, self.action)(**self.arguments)
+ if hasattr(actions, self.action):
+ getattr(actions, self.action).run(self, **self.arguments)
- def description(self):
- return getattr(self, self.action + "_print")(**self.arguments)
+ def poll_loaded(self):
+ self.key.callback_action_ready(self,
+ self.is_loaded(allow_substates=True))
+ # This one cannot be in the Machine state since it would be queued to run
+ # *after* the wait is ended...
def interrupt(self):
- if getattr(self, self.action + "_interrupt", None):
- return getattr(self, self.action + "_interrupt")(**self.arguments)
+ if getattr(actions, self.action, None) and\
+ hasattr(getattr(actions, self.action), 'interrupt'):
+ return getattr(getattr(actions, self.action), 'interrupt')(
+ self, **self.arguments)
+ # Helpers
def music_list(self, music):
if music is not None:
return [music]
else:
return self.mapping.open_files.values()
- # Actions
- def command(self, command="", **kwargs):
- # FIXME: todo
- pass
-
- def pause(self, music=None, **kwargs):
- for music in self.music_list(music):
- if music.is_loaded_playing():
- music.pause()
-
- def unpause(self, music=None, **kwargs):
- for music in self.music_list(music):
- if music.is_loaded_paused():
- music.unpause()
-
- def play(self, music=None, fade_in=0, start_at=0,
- restart_if_running=False, volume=100,
- loop=0, **kwargs):
- for music in self.music_list(music):
- if restart_if_running:
- if music.is_in_use():
- music.stop()
- music.play(
- volume=volume,
- fade_in=fade_in,
- start_at=start_at,
- loop=loop)
- elif not music.is_in_use():
- music.play(
- volume=volume,
- fade_in=fade_in,
- start_at=start_at,
- loop=loop)
-
- def seek(self, music=None, value=0, delta=False, **kwargs):
- for music in self.music_list(music):
- music.seek(value=value, delta=delta)
-
- def interrupt_wait(self, wait_id=None):
- self.mapping.interrupt_wait(wait_id)
-
- def stop(self, music=None, fade_out=0, wait=False,
- set_wait_id=None, **kwargs):
- previous = None
- for music in self.music_list(music):
- if music.is_loaded_paused() or music.is_loaded_playing():
- if previous is not None:
- previous.stop(fade_out=fade_out)
- previous = music
- else:
- music.stop(fade_out=fade_out)
-
- if previous is not None:
- previous.stop(
- fade_out=fade_out,
- wait=wait,
- set_wait_id=set_wait_id)
-
- def stop_all_actions(self, **kwargs):
- self.mapping.stop_all_running()
-
- def volume(self, music=None, value=100, fade=0, delta=False, **kwargs):
- if music is not None:
- music.set_volume(value, delta=delta, fade=fade)
- else:
- self.mapping.set_master_volume(value, delta=delta, fade=fade)
-
- def wait(self, duration=0, music=None, set_wait_id=None, **kwargs):
- if set_wait_id is not None:
- self.mapping.add_wait_id(set_wait_id, self)
-
- self.sleep_event = threading.Event()
-
- if music is not None:
- music.wait_end()
-
- threading.Timer(duration, self.sleep_event.set).start()
- self.sleep_event.wait()
-
- # Action messages
- def command_print(self, command="", **kwargs):
- return "running command {}".format(command)
-
- def interrupt_wait_print(self, wait_id=None, **kwargs):
- return "interrupt wait with id {}".format(wait_id)
-
- def pause_print(self, music=None, **kwargs):
- if music is not None:
- return "pausing « {} »".format(music.name)
- else:
- return "pausing all musics"
-
- def unpause_print(self, music=None, **kwargs):
- if music is not None:
- return "unpausing « {} »".format(music.name)
- else:
- return "unpausing all musics"
-
- def play_print(self, music=None, fade_in=0, start_at=0,
- restart_if_running=False, volume=100, loop=0, **kwargs):
- message = "starting "
- if music is not None:
- message += "« {} »".format(music.name)
- else:
- message += "all musics"
-
- if start_at != 0:
- message += " at {}s".format(start_at)
-
- if fade_in != 0:
- message += " with {}s fade_in".format(fade_in)
-
- message += " at volume {}%".format(volume)
-
- if loop > 0:
- message += " {} times".format(loop + 1)
- elif loop < 0:
- message += " in loop"
-
- if restart_if_running:
- message += " (restarting if already running)"
-
- return message
-
- def stop_print(self, music=None, fade_out=0, wait=False,
- set_wait_id=None, **kwargs):
-
- message = "stopping "
- if music is not None:
- message += "music « {} »".format(music.name)
- else:
- message += "all musics"
-
- if fade_out > 0:
- message += " with {}s fadeout".format(fade_out)
- if wait:
- if set_wait_id is not None:
- message += " (waiting the end of fadeout, with id {})"\
- .format(set_wait_id)
- else:
- message += " (waiting the end of fadeout)"
-
- return message
-
- def stop_all_actions_print(self, **kwargs):
- return "stopping all actions"
-
- def seek_print(self, music=None, value=0, delta=False, **kwargs):
- if delta:
- if music is not None:
- return "moving music « {} » by {:+d}s" \
- .format(music.name, value)
- else:
- return "moving all musics by {:+d}s" \
- .format(value)
- else:
- if music is not None:
- return "moving music « {} » to position {}s" \
- .format(music.name, value)
- else:
- return "moving all musics to position {}s" \
- .format(value)
-
- def volume_print(self, music=None,
- value=100, delta=False, fade=0, **kwargs):
- message = ""
- if delta:
- if music is not None:
- message += "{:+d}% to volume of « {} »" \
- .format(value, music.name)
- else:
- message += "{:+d}% to volume" \
- .format(value)
- else:
- if music is not None:
- message += "setting volume of « {} » to {}%" \
- .format(music.name, value)
- else:
- message += "setting volume to {}%" \
- .format(value)
-
- if fade > 0:
- message += " with {}s fade".format(fade)
-
- return message
-
- def wait_print(self, duration=0, music=None, set_wait_id=None, **kwargs):
- message = ""
- if music is None:
- message += "waiting {}s" \
- .format(duration)
- elif duration == 0:
- message += "waiting the end of « {} »" \
- .format(music.name)
+ def description(self):
+ if hasattr(actions, self.action):
+ return getattr(actions, self.action)\
+ .description(self, **self.arguments)
else:
- message += "waiting the end of « {} » + {}s" \
- .format(music.name, duration)
-
- if set_wait_id is not None:
- message += " (setting id = {})".format(set_wait_id)
-
- return message
-
- # Interruptions
- def wait_interrupt(self, duration=0, music=None, **kwargs):
- if self.sleep_event is not None:
- self.sleep_event.set()
- if music is not None:
- music.wait_event.set()
-
+ return "unknown action {}".format(self.action)
--- /dev/null
+from . import command
+from . import interrupt_wait
+from . import pause
+from . import play
+from . import seek
+from . import stop
+from . import stop_all_actions
+from . import unpause
+from . import volume
+from . import wait
--- /dev/null
+def run(action, command="", **kwargs):
+ # FIXME: todo
+ pass
+
+def description(action, command="", **kwargs):
+ return "running command {}".format(command)
--- /dev/null
+def run(action, wait_id=None):
+ action.mapping.interrupt_wait(wait_id)
+
+def description(action, wait_id=None, **kwargs):
+ return "interrupt wait with id {}".format(wait_id)
--- /dev/null
+def run(action, music=None, **kwargs):
+ for music in action.music_list(music):
+ if music.is_loaded_playing():
+ music.pause()
+
+def description(action, music=None, **kwargs):
+ if music is not None:
+ return "pausing « {} »".format(music.name)
+ else:
+ return "pausing all musics"
--- /dev/null
+def run(action, music=None, fade_in=0, start_at=0,
+ restart_if_running=False, volume=100,
+ loop=0, **kwargs):
+ for music in action.music_list(music):
+ if restart_if_running:
+ if music.is_in_use():
+ music.stop()
+ music.play(
+ volume=volume,
+ fade_in=fade_in,
+ start_at=start_at,
+ loop=loop)
+ elif not music.is_in_use():
+ music.play(
+ volume=volume,
+ fade_in=fade_in,
+ start_at=start_at,
+ loop=loop)
+
+def description(action, music=None, fade_in=0, start_at=0,
+ restart_if_running=False, volume=100, loop=0, **kwargs):
+ message = "starting "
+ if music is not None:
+ message += "« {} »".format(music.name)
+ else:
+ message += "all musics"
+
+ if start_at != 0:
+ message += " at {}s".format(start_at)
+
+ if fade_in != 0:
+ message += " with {}s fade_in".format(fade_in)
+
+ message += " at volume {}%".format(volume)
+
+ if loop > 0:
+ message += " {} times".format(loop + 1)
+ elif loop < 0:
+ message += " in loop"
+
+ if restart_if_running:
+ message += " (restarting if already running)"
+
+ return message
--- /dev/null
+def run(action, music=None, value=0, delta=False, **kwargs):
+ for music in action.music_list(music):
+ music.seek(value=value, delta=delta)
+
+def description(action, music=None, value=0, delta=False, **kwargs):
+ if delta:
+ if music is not None:
+ return "moving music « {} » by {:+d}s" \
+ .format(music.name, value)
+ else:
+ return "moving all musics by {:+d}s" \
+ .format(value)
+ else:
+ if music is not None:
+ return "moving music « {} » to position {}s" \
+ .format(music.name, value)
+ else:
+ return "moving all musics to position {}s" \
+ .format(value)
--- /dev/null
+def run(action, music=None, fade_out=0, wait=False,
+ set_wait_id=None, **kwargs):
+ previous = None
+ for music in action.music_list(music):
+ if music.is_loaded_paused() or music.is_loaded_playing():
+ if previous is not None:
+ previous.stop(fade_out=fade_out)
+ previous = music
+ else:
+ music.stop(fade_out=fade_out)
+
+ if previous is not None:
+ action.waiting_music = previous
+ previous.stop(
+ fade_out=fade_out,
+ wait=wait,
+ set_wait_id=set_wait_id)
+
+def description(action, music=None, fade_out=0, wait=False,
+ set_wait_id=None, **kwargs):
+
+ message = "stopping "
+ if music is not None:
+ message += "music « {} »".format(music.name)
+ else:
+ message += "all musics"
+
+ if fade_out > 0:
+ message += " with {}s fadeout".format(fade_out)
+ if wait:
+ if set_wait_id is not None:
+ message += " (waiting the end of fadeout, with id {})"\
+ .format(set_wait_id)
+ else:
+ message += " (waiting the end of fadeout)"
+
+ return message
+
+def interrupt(action, music=None, fade_out=0, wait=False,
+ set_wait_id=None, **kwargs):
+ if action.waiting_music is not None:
+ action.waiting_music.wait_event.set()
--- /dev/null
+def run(action, **kwargs):
+ action.mapping.stop_all_running()
+
+def description(action, **kwargs):
+ return "stopping all actions"
--- /dev/null
+def run(action, music=None, **kwargs):
+ for music in action.music_list(music):
+ if music.is_loaded_paused():
+ music.unpause()
+
+def description(action, music=None, **kwargs):
+ if music is not None:
+ return "unpausing « {} »".format(music.name)
+ else:
+ return "unpausing all musics"
--- /dev/null
+def run(action, music=None, value=100, fade=0, delta=False, **kwargs):
+ if music is not None:
+ music.set_volume(value, delta=delta, fade=fade)
+ else:
+ action.mapping.set_master_volume(value, delta=delta, fade=fade)
+
+def description(action, music=None,
+ value=100, delta=False, fade=0, **kwargs):
+ message = ""
+ if delta:
+ if music is not None:
+ message += "{:+d}% to volume of « {} »" \
+ .format(value, music.name)
+ else:
+ message += "{:+d}% to volume" \
+ .format(value)
+ else:
+ if music is not None:
+ message += "setting volume of « {} » to {}%" \
+ .format(music.name, value)
+ else:
+ message += "setting volume to {}%" \
+ .format(value)
+
+ if fade > 0:
+ message += " with {}s fade".format(fade)
+
+ return message
--- /dev/null
+import threading
+
+def run(action, duration=0, music=None, set_wait_id=None, **kwargs):
+ if set_wait_id is not None:
+ action.mapping.add_wait_id(set_wait_id, action)
+
+ action.sleep_event = threading.Event()
+ action.sleep_event_timer = threading.Timer(duration, action.sleep_event.set)
+
+ if music is not None:
+ music.wait_end()
+
+ action.sleep_event_timer.start()
+ action.sleep_event.wait()
+
+def description(action, duration=0, music=None, set_wait_id=None, **kwargs):
+ message = ""
+ if music is None:
+ message += "waiting {}s" \
+ .format(duration)
+ elif duration == 0:
+ message += "waiting the end of « {} »" \
+ .format(music.name)
+ else:
+ message += "waiting the end of « {} » + {}s" \
+ .format(music.name, duration)
+
+ if set_wait_id is not None:
+ message += " (setting id = {})".format(set_wait_id)
+
+ return message
+
+def interrupt(action, duration=0, music=None, **kwargs):
+ if action.sleep_event is not None:
+ action.sleep_event.set()
+ action.sleep_event_timer.cancel()
+ if music is not None:
+ music.wait_event.set()
from kivy.uix.widget import Widget
from kivy.properties import AliasProperty, BooleanProperty, \
ListProperty, StringProperty
-from kivy.clock import Clock
from kivy.uix.behaviors import ButtonBehavior
-from .action import *
+from .action import Action
from . import debug_print
import time
+from transitions.extensions import HierarchicalMachine as Machine
class Key(ButtonBehavior, Widget):
+ STATES = [
+ 'initial',
+ 'configuring',
+ 'configured',
+ 'loading',
+ 'failed',
+ {
+ 'name': 'loaded',
+ 'children': ['no_config', 'no_actions', 'running']
+ }
+ ]
+
+ TRANSITIONS = [
+ {
+ 'trigger': 'configure',
+ 'source': 'initial',
+ 'dest': 'configuring'
+ },
+ {
+ 'trigger': 'fail',
+ 'source': 'configuring',
+ 'dest': 'failed'
+ },
+ {
+ 'trigger': 'success',
+ 'source': 'configuring',
+ 'dest': 'configured',
+ 'after': 'load'
+ },
+ {
+ 'trigger': 'no_config',
+ 'source': 'configuring',
+ 'dest': 'loaded_no_config',
+ },
+ {
+ 'trigger': 'load',
+ 'source': 'configured',
+ 'dest': 'loading'
+ },
+ {
+ 'trigger': 'fail',
+ 'source': 'loading',
+ 'dest': 'failed'
+ },
+ {
+ 'trigger': 'success',
+ 'source': 'loading',
+ 'dest': 'loaded'
+ },
+ {
+ 'trigger': 'no_actions',
+ 'source': 'loading',
+ 'dest': 'loaded_no_actions',
+ },
+ {
+ 'trigger': 'reload',
+ 'source': 'loaded',
+ 'dest': 'configuring'
+ },
+ {
+ 'trigger': 'run',
+ 'source': 'loaded',
+ 'dest': 'loaded_running',
+ 'after': 'finish',
+ # if a child, like loaded_no_actions, has no transitions, then it is
+ # bubbled to the parent, and we don't want that.
+ 'conditions': ['is_loaded']
+ },
+ {
+ 'trigger': 'finish',
+ 'source': 'loaded_running',
+ 'dest': 'loaded'
+ }
+ ]
+
key_sym = StringProperty(None)
- custom_color = ListProperty([0, 1, 0, 1])
- custom_unready_color = ListProperty([0, 1, 0, 100/255])
+ custom_color = ListProperty([0, 1, 0])
description_title = StringProperty("")
description = ListProperty([])
- is_key_ready = BooleanProperty(True)
+ state = StringProperty("")
- def get_color(self):
- if not self.has_actions:
+ def get_alias_color(self):
+ if self.is_loaded_inactive():
return [1, 1, 1, 1]
- elif self.all_actions_ready:
- return self.custom_color
+ elif self.is_loaded(allow_substates=True):
+ return [*self.custom_color, 1]
+ elif self.is_failed():
+ return [0, 0, 0, 1]
else:
- return self.custom_unready_color
- def set_color(self):
+ return [*self.custom_color, 100/255]
+ def set_alias_color(self):
pass
- color = AliasProperty(get_color, set_color, bind=['is_key_ready'])
+ color = AliasProperty(get_alias_color, set_alias_color,
+ bind=['state', 'custom_color'])
def __init__(self, **kwargs):
- super(Key, self).__init__(**kwargs)
self.actions = []
+ Machine(model=self, states=self.STATES,
+ transitions=self.TRANSITIONS, initial='initial',
+ ignore_invalid_triggers=True, queued=True)
+ super(Key, self).__init__(**kwargs)
+ # Kivy events
def on_key_sym(self, key, key_sym):
- if key_sym in self.parent.key_config:
- self.is_key_ready = False
+ if key_sym != "":
+ self.configure()
+
+ def on_press(self):
+ self.list_actions()
- self.config = self.parent.key_config[key_sym]
+ # Machine states / events
+ def is_loaded_or_failed(self):
+ return self.is_loaded(allow_substates=True) or self.is_failed()
+
+ def is_loaded_inactive(self):
+ return self.is_loaded_no_config() or self.is_loaded_no_actions()
+
+ def on_enter_configuring(self):
+ if self.key_sym in self.parent.key_config:
+ self.config = self.parent.key_config[self.key_sym]
self.actions = []
for key_action in self.config['actions']:
self.add_action(key_action[0], **key_action[1])
if 'description' in self.config['properties']:
- key.set_description(self.config['properties']['description'])
+ self.set_description(self.config['properties']['description'])
if 'color' in self.config['properties']:
- key.set_color(self.config['properties']['color'])
+ self.set_color(self.config['properties']['color'])
+ self.success()
+ else:
+ self.no_config()
- Clock.schedule_interval(self.check_all_active, 1)
+ def on_enter_loading(self):
+ if len(self.actions) > 0:
+ for action in self.actions:
+ action.load()
+ else:
+ self.no_actions()
+
+ def on_enter_loaded_running(self):
+ self.parent.parent.ids['KeyList'].append(self.key_sym)
+ debug_print("running actions for {}".format(self.key_sym))
+ start_time = time.time()
+ self.parent.start_running(self, start_time)
+ action_number = 0
+ for self.current_action in self.actions:
+ if self.parent.keep_running(self, start_time):
+ self.list_actions(action_number=action_number + 0.5)
+ self.current_action.run()
+ action_number += 1
+ self.list_actions(action_number=action_number)
- def check_all_active(self, dt):
- if self.all_actions_ready:
- self.is_key_ready = True
- return False
+ self.parent.finished_running(self, start_time)
+ # This one cannot be in the Machine state since it would be queued to run
+ # *after* the loop is ended...
+ def interrupt(self):
+ self.current_action.interrupt()
+
+ # Callbacks
+ def callback_action_ready(self, action, success):
+ if not success:
+ self.fail()
+ elif all(action.is_loaded_or_failed() for action in self.actions):
+ self.success()
+
+ # Setters
def set_description(self, description):
if description[0] is not None:
self.description_title = str(description[0])
def set_color(self, color):
color = [x / 255 for x in color]
- color.append(1)
self.custom_color = color
- color[3] = 100 / 255
- self.custom_unready_color = tuple(color)
-
- @property
- def has_actions(self):
- return len(self.actions) > 0
-
- @property
- def all_actions_ready(self):
- return all(action.ready() for action in self.actions)
+ # Actions handling
def add_action(self, action_name, **arguments):
self.actions.append(Action(action_name, self, **arguments))
- def interrupt_action(self):
- self.current_action.interrupt()
-
- def do_actions(self):
- if not self.enabled:
- return None
-
- self.parent.parent.ids['KeyList'].append(self.key_sym)
- debug_print("running actions for {}".format(self.key_sym))
- start_time = time.time()
- self.parent.start_running(self, start_time)
- action_number = 0
- for self.current_action in self.actions:
- if self.parent.keep_running(self, start_time):
- self.list_actions(action_number=action_number + 0.5)
- self.current_action.run()
- action_number += 1
- self.list_actions(action_number=action_number)
-
- self.parent.finished_running(self, start_time)
-
def list_actions(self, action_number=0):
self.parent.parent.ids['ActionList'].update_list(self, action_number)
- def on_press(self):
- self.list_actions()
import threading
import yaml
import sys
+from collections import defaultdict
-from .music_file import *
+from .music_file import MusicFile
from .mixer import Mixer
-from . import Config, gain, error_print
+from . import Config, gain, error_print, warn_print
from .action import Action
class Mapping(RelativeLayout):
try:
self.key_config, self.open_files = self.parse_config()
except Exception as e:
- error_print("Error while loading configuration: {}".format(e))
+ error_print("Error while loading configuration: {}".format(e),
+ with_trace=True)
sys.exit()
super(Mapping, self).__init__(**kwargs)
def _on_keyboard_down(self, keyboard, keycode, text, modifiers):
key = self.find_by_key_code(keycode)
if len(modifiers) == 0 and key is not None:
- threading.Thread(name="MSKeyAction", target=key.do_actions).start()
+ threading.Thread(name="MSKeyAction", target=key.run).start()
elif 'ctrl' in modifiers and (keycode[0] == 113 or keycode[0] == '99'):
+ self.stop_all_running()
for thread in threading.enumerate():
if thread.getName()[0:2] != "MS":
continue
for key in self.children:
if not type(key).__name__ == "Key":
continue
- if not key.is_key_ready:
+ if not key.is_loaded_or_failed():
return True
self.ready_color = [0, 1, 0, 1]
return False
running = self.running
self.running = []
for (key, start_time) in running:
- key.interrupt_action()
+ key.interrupt()
def start_running(self, key, start_time):
self.running.append((key, start_time))
self.running.remove((key, start_time))
def parse_config(self):
+ def update_alias(prop_hash, aliases, key):
+ if isinstance(aliases[key], dict):
+ prop_hash.update(aliases[key], **prop_hash)
+ else:
+ warn_print("Alias {} is not a hash, ignored".format(key))
+
+ def include_aliases(prop_hash, aliases):
+ if 'include' not in prop_hash:
+ return
+
+ included = prop_hash['include']
+ del(prop_hash['include'])
+ if isinstance(included, str):
+ update_alias(prop_hash, aliases, included)
+ elif isinstance(included, list):
+ for included_ in included:
+ if isinstance(included_, str):
+ update_alias(prop_hash, aliases, included_)
+ else:
+ warn_print("Unkown alias include type, ignored: "
+ "{} in {}".format(included_, included))
+ else:
+ warn_print("Unkown alias include type, ignored: {}"
+ .format(included))
+
+ def check_key_property(key_property, key):
+ if 'description' in key_property:
+ desc = key_property['description']
+ if not isinstance(desc, list):
+ warn_print("description in key_property '{}' is not "
+ "a list, ignored".format(key))
+ del(key_property['description'])
+ if 'color' in key_property:
+ color = key_property['color']
+ if not isinstance(color, list)\
+ or len(color) != 3\
+ or not all(isinstance(item, int) for item in color)\
+ or any(item < 0 or item > 255 for item in color):
+ warn_print("color in key_property '{}' is not "
+ "a list of 3 valid integers, ignored".format(key))
+ del(key_property['color'])
+
+ def check_key_properties(config):
+ if 'key_properties' in config:
+ if isinstance(config['key_properties'], dict):
+ return config['key_properties']
+ else:
+ warn_print("key_properties config is not a hash, ignored")
+ return {}
+ else:
+ return {}
+
+ def check_mapped_keys(config):
+ if 'keys' in config:
+ if isinstance(config['keys'], dict):
+ return config['keys']
+ else:
+ warn_print("keys config is not a hash, ignored")
+ return {}
+ else:
+ return {}
+
+ def check_mapped_key(mapped_keys, key):
+ if not isinstance(mapped_keys[key], list):
+ warn_print("key config '{}' is not an array, ignored"
+ .format(key))
+ return []
+ else:
+ return mapped_keys[key]
+
+ def check_music_property(music_property, filename):
+ if not isinstance(music_property, dict):
+ warn_print("music_property config '{}' is not a hash, ignored"
+ .format(filename))
+ return {}
+ if 'name' in music_property:
+ music_property['name'] = str(music_property['name'])
+ if 'gain' in music_property:
+ try:
+ music_property['gain'] = float(music_property['gain'])
+ except ValueError as e:
+ del(music_property['gain'])
+ warn_print("gain for music_property '{}' is not "
+ "a float, ignored".format(filename))
+ return music_property
+
stream = open(Config.yml_file, "r")
try:
- config = yaml.load(stream)
+ config = yaml.safe_load(stream)
except Exception as e:
error_print("Error while loading config file: {}".format(e))
sys.exit()
stream.close()
- aliases = config['aliases']
+ if not isinstance(config, dict):
+ raise Exception("Top level config is supposed to be a hash")
+
+ if 'aliases' in config and isinstance(config['aliases'], dict):
+ aliases = config['aliases']
+ else:
+ aliases = defaultdict(dict)
+ if 'aliases' in config:
+ warn_print("aliases config is not a hash, ignored")
+
+ music_properties = defaultdict(dict)
+ if 'music_properties' in config and\
+ isinstance(config['music_properties'], dict):
+ music_properties.update(config['music_properties'])
+ elif 'music_properties' in config:
+ warn_print("music_properties config is not a hash, ignored")
+
seen_files = {}
- key_properties = {}
+ key_properties = defaultdict(lambda: {
+ "actions": [],
+ "properties": {},
+ "files": []
+ })
- for key in config['key_properties']:
- if key not in key_properties:
- key_prop = config['key_properties'][key]
- if 'include' in key_prop:
- included = key_prop['include']
- del(key_prop['include'])
+ for key in check_key_properties(config):
+ key_prop = config['key_properties'][key]
+
+ if not isinstance(key_prop, dict):
+ warn_print("key_property '{}' is not a hash, ignored"
+ .format(key))
+ continue
+
+ include_aliases(key_prop, aliases)
+ check_key_property(key_prop, key)
+
+ key_properties[key]["properties"] = key_prop
+
+ for mapped_key in check_mapped_keys(config):
+ for index, action in enumerate(check_mapped_key(
+ config['keys'], mapped_key)):
+ if not isinstance(action, dict) or\
+ not len(action) == 1 or\
+ not isinstance(list(action.values())[0] or {}, dict):
+ warn_print("action number {} of key '{}' is invalid, "
+ "ignored".format(index + 1, mapped_key))
+ continue
- if isinstance(included, str):
- key_prop.update(aliases[included], **key_prop)
- else:
- for included_ in included:
- key_prop.update(aliases[included_], **key_prop)
-
- key_properties[key] = {
- "actions": [],
- "properties": key_prop,
- "files": []
- }
-
- for mapped_key in config['keys']:
- if mapped_key not in key_properties:
- key_properties[mapped_key] = {
- "actions": [],
- "properties": {},
- "files": []
- }
- for action in config['keys'][mapped_key]:
action_name = list(action)[0]
action_args = {}
if action[action_name] is None:
- action[action_name] = []
-
- if 'include' in action[action_name]:
- included = action[action_name]['include']
- del(action[action_name]['include'])
+ action[action_name] = {}
- if isinstance(included, str):
- action[action_name].update(
- aliases[included],
- **action[action_name])
- else:
- for included_ in included:
- action[action_name].update(
- aliases[included_],
- **action[action_name])
+ include_aliases(action[action_name], aliases)
for argument in action[action_name]:
if argument == 'file':
- filename = action[action_name]['file']
+ filename = str(action[action_name]['file'])
if filename not in seen_files:
- if filename in config['music_properties']:
- seen_files[filename] = MusicFile(
- filename,
- self,
- **config['music_properties'][filename])
- else:
- seen_files[filename] = MusicFile(
- self,
- filename)
+ music_property = check_music_property(
+ music_properties[filename],
+ filename)
+
+ seen_files[filename] = MusicFile(
+ filename, self, **music_property)
if filename not in key_properties[mapped_key]['files']:
key_properties[mapped_key]['files'] \
.append(seen_files[filename])
action_args['music'] = seen_files[filename]
-
else:
action_args[argument] = action[action_name][argument]
{
'trigger': 'load',
'source': 'initial',
- 'dest': 'loading'
+ 'dest': 'loading',
+ 'after': 'poll_loaded'
},
{
'trigger': 'fail',
{
'trigger': 'start_playing',
'source': 'loaded',
- 'dest': 'loaded_playing'
+ 'dest': 'loaded_playing',
+ # if a child has no transitions, then it is bubbled to the parent,
+ # and we don't want that. Not useful in that machine precisely.
+ 'conditions': ['is_loaded']
},
{
'trigger': 'pause',
'trigger': 'stopped',
'source': '*',
'dest': 'loaded',
- 'before': 'trigger_stopped_events'
+ 'before': 'trigger_stopped_events',
+ 'conditions': ['is_in_use']
}
]
transitions=self.TRANSITIONS, initial='initial',
ignore_invalid_triggers=True)
+ self.loaded_callbacks = []
self.mapping = mapping
self.filename = filename
self.name = name or filename
self.wait_event.clear()
self.wait_event.wait()
+ # Let other subscribe for an event when they are ready
+ def subscribe_loaded(self, callback):
+ # FIXME: should lock to be sure we have no race, but it makes the
+ # initialization screen not showing until everything is loaded
+ if self.is_loaded(allow_substates=True):
+ callback(True)
+ elif self.is_failed():
+ callback(False)
+ else:
+ self.loaded_callbacks.append(callback)
+
+ def poll_loaded(self):
+ for callback in self.loaded_callbacks:
+ callback(self.is_loaded())
+ self.loaded_callbacks = []
+
# Callbacks
def finished_callback(self):
self.stopped()