Files

143 lines
3.0 KiB
GDScript3
Raw Permalink Normal View History

## 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