## Class for playing music and providing an interface to keep events in sync. class_name Conductor extends Node ## If true, start immediately when ready. @export var autostart: bool = false ## Music to play. @export var music: Music signal started signal ticked(beat_update: BeatUpdate) signal ticked_forward(new_beat: float) #signal ticked_backward(new_beat: float) signal finished func start() -> void: assert(music != null) assert(_audio_player != null) _stop_playback_and_initialize() _active = true started.emit() func is_active() -> bool: return _active func current_time() -> float: return(_audio_player.get_playback_position() + AudioServer.get_time_since_last_mix() + _music_offset - _remaining_silence) func previous_time() -> float: return _previous_time func current_beat() -> float: return _sync_track.to_beat(current_time()) func previous_beat() -> float: return _previous_beat func current_bpm() -> float: return _sync_track.bpm_at_time(current_time()) func add_time(seconds: float) -> void: if _audio_player == null: return _audio_player.seek(_audio_player.get_playback_position() + seconds) _previous_time = current_time() _previous_beat = current_beat() # ====== IMPLEMENTATION ====== # var _active: bool = false var _previous_time: float = -1.0 var _previous_beat: float = -1.0 var _audio_player: AudioStreamPlayer = null var _sync_track: SyncTrack = SyncTrack.new() var _music_offset: float var _music_playing: bool = false var _remaining_silence: float = 0.0 func _ready() -> void: _audio_player = AudioStreamPlayer.new() add_child(_audio_player) _audio_player.finished.connect(_on_music_finished) if autostart: start() func _on_music_finished() -> void: _music_playing = false _active = false finished.emit() func _process(delta: float) -> void: if not _active: return if _music_playing: _beat_update() else: _update_silence(delta) func _update_silence(elapsed_time: float) -> void: _remaining_silence -= elapsed_time if _remaining_silence < 0: _play_music_from_offset(-_remaining_silence) _remaining_silence = 0.0 func _beat_update() -> void: var _current_time = current_time() # _audio_player.get_playback_position() used in current_time() # is not perfect and may return a lesser value than last frame. # This makes sure the beat never updates backward. if _current_time <= _previous_time: return var _current_beat = _sync_track.to_beat(_current_time) ticked.emit(BeatUpdate.new(_current_beat, _previous_beat)) ticked_forward.emit(_current_beat) _previous_beat = _current_beat _previous_time = _current_time func _stop_playback_and_initialize() -> void: if _audio_player.playing: _audio_player.stop() _active = false _music_playing = false _previous_time = -1.0 _previous_beat = -1.0 _music_offset = music.offset _remaining_silence = _music_offset _audio_player.stream = music.stream _sync_track.initialize(music.tempo) func _play_music_from_offset(offset: float) -> void: _audio_player.play(offset) _music_playing = true