import audioop
from .lock import Lock
-from . import Config, gain, debug_print, error_print
+from .helpers import Config, gain, debug_print, error_print
from .mixer import Mixer
from .music_effect import GainEffect
{
'name': 'loaded',
'children': [
+ 'stopped',
'playing',
'paused',
'stopping'
TRANSITIONS = [
{
'trigger': 'load',
- 'source': 'initial',
- 'dest': 'loading',
- 'after': 'poll_loaded'
+ 'source': ['initial', 'failed'],
+ 'dest': 'loading'
},
{
'trigger': 'fail',
'source': 'loading',
'dest': 'failed'
},
+ {
+ 'trigger': 'unload',
+ 'source': ['failed', 'loaded_stopped'],
+ 'dest': 'initial',
+ },
{
'trigger': 'success',
'source': 'loading',
- 'dest': 'loaded'
+ 'dest': 'loaded_stopped'
},
{
'trigger': 'start_playing',
- 'source': 'loaded',
- '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']
+ 'source': 'loaded_stopped',
+ 'dest': 'loaded_playing'
},
{
'trigger': 'pause',
},
{
'trigger': 'stopped',
- 'source': '*',
- 'dest': 'loaded',
+ 'source': 'loaded',
+ 'dest': 'loaded_stopped',
'before': 'trigger_stopped_events',
- 'conditions': ['is_in_use']
+ 'unless': 'is_loaded_stopped',
}
]
def __init__(self, filename, mapping, name=None, gain=1):
- Machine(model=self, states=self.STATES,
+ machine = Machine(model=self, states=self.STATES,
transitions=self.TRANSITIONS, initial='initial',
- ignore_invalid_triggers=True)
+ auto_transitions=False,
+ after_state_change=self.notify_state_change)
- self.loaded_callbacks = []
+ self.state_change_callbacks = []
self.mapping = mapping
self.filename = filename
self.name = name or filename
self.initial_volume_factor = gain
self.music_lock = Lock("music__" + filename)
- threading.Thread(name="MSMusicLoad", target=self.load).start()
+ if Config.load_all_musics:
+ threading.Thread(name="MSMusicLoad", target=self.load).start()
def reload_properties(self, name=None, gain=1):
self.name = name or self.filename
if gain != self.initial_volume_factor:
self.initial_volume_factor = gain
- self.reload_music_file()
+ self.stopped()
+ self.unload()
+ self.load(reloading=True)
- def reload_music_file(self):
- with file_lock:
- try:
- if self.filename.startswith("/"):
- filename = self.filename
- else:
- filename = Config.music_path + self.filename
+ # Machine related events
+ def on_enter_initial(self):
+ self.audio_segment = None
- debug_print("Reloading « {} »".format(self.name))
- initial_db_gain = gain(self.initial_volume_factor * 100)
- self.audio_segment = pydub.AudioSegment \
- .from_file(filename) \
- .set_frame_rate(Config.frame_rate) \
- .set_channels(Config.channels) \
- .set_sample_width(Config.sample_width) \
- .apply_gain(initial_db_gain)
- except Exception as e:
- error_print("failed to reload « {} »: {}"\
- .format(self.name, e))
- self.loading_error = e
- self.to_failed()
- else:
- debug_print("Reloaded « {} »".format(self.name))
+ def on_enter_loading(self, reloading=False):
+ if reloading:
+ prefix = 'Rel'
+ prefix_s = 'rel'
+ else:
+ prefix = 'L'
+ prefix_s = 'l'
- # Machine related events
- def on_enter_loading(self):
with file_lock:
+ if self.mapping.is_leaving_application:
+ self.fail()
+ return
+
try:
if self.filename.startswith("/"):
filename = self.filename
else:
filename = Config.music_path + self.filename
- debug_print("Loading « {} »".format(self.name))
+ debug_print("{}oading « {} »".format(prefix, self.name))
self.mixer = self.mapping.mixer or Mixer()
initial_db_gain = gain(self.initial_volume_factor * 100)
self.audio_segment = pydub.AudioSegment \
.apply_gain(initial_db_gain)
self.sound_duration = self.audio_segment.duration_seconds
except Exception as e:
- error_print("failed to load « {} »: {}".format(self.name, e))
+ error_print("failed to {}oad « {} »: {}".format(
+ prefix_s, self.name, e))
self.loading_error = e
self.fail()
else:
self.success()
- debug_print("Loaded « {} »".format(self.name))
+ debug_print("{}oaded « {} »".format(prefix, self.name))
def on_enter_loaded(self):
self.cleanup()
# Machine related states
def is_in_use(self):
- return self.is_loaded(allow_substates=True) and not self.is_loaded()
+ return self.is_loaded(allow_substates=True) and\
+ not self.is_loaded_stopped()
def is_in_use_not_stopping(self):
return self.is_loaded_playing() or self.is_loaded_paused()
+ def is_unloadable(self):
+ return self.is_loaded_stopped() or self.is_failed()
+
# Machine related triggers
def trigger_stopped_events(self):
self.mixer.remove_file(self)
self.current_audio_segment = new_audio_segment
self.stop_playing()
if wait:
- if set_wait_id is not None:
- self.mapping.add_wait_id(set_wait_id, self.wait_event)
+ self.mapping.add_wait(self.wait_event, wait_id=set_wait_id)
self.wait_end()
- else:
+ elif self.is_loaded(allow_substates=True):
self.stopped()
def abandon_all_effects(self):
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)
+ # Let other subscribe for state change
+ def notify_state_change(self, **kwargs):
+ for callback in self.state_change_callbacks:
+ callback(self.state)
+
+ def subscribe_state_change(self, callback):
+ if callback not in self.state_change_callbacks:
+ self.state_change_callbacks.append(callback)
+ callback(self.state)
- def poll_loaded(self):
- for callback in self.loaded_callbacks:
- callback(self.is_loaded())
- self.loaded_callbacks = []
+ def unsubscribe_state_change(self, callback):
+ if callback in self.state_change_callbacks:
+ self.state_change_callbacks.remove(callback)
# Callbacks
def finished_callback(self):