@tool class_name BPMTimer extends Timer @export var bpm: float = 120: set(beats): bpm = beats beat_length = bpm / 60.0 var beat_length: float = (120.0/60.0) @export var beats_per_bar: int = 4 @export var grid_offset: float = 0 var sync_to: AudioStreamPlayback var current_beat: int var current_bar: int signal on_beat(beat_count: int) signal on_bar(bar_count: int) @warning_ignore("shadowed_variable") func _init(bpm:float = 120, beats_per_bar:int = 4, grid_offset:float = 0, sync_to_playback:AudioStreamPlayback = null) -> void: self.bpm = bpm self.beats_per_bar = beats_per_bar self.grid_offset = grid_offset self.sync_to = sync_to_playback func _ready() -> void: start(0.5) func _process(delta: float) -> void: if not is_stopped(): if sync_to != null: if not sync_to.is_playing(): stop() if fmod(sync_to.get_playback_position()-grid_offset + delta, beat_length) < delta * 2.0: _on_beat() _last_beat_timing = sync_to.get_playback_position() else: if not Engine.is_editor_hint(): print(time_left) func start(time_sec: float = 0, starting_beat: int = 0, starting_bar: int = 0, sync_to_playback:AudioStreamPlayback = null) -> void: if starting_bar == 0 and starting_beat > 0: starting_bar = starting_beat / beats_per_bar current_beat = starting_beat - 1 current_bar = starting_bar - 1 _last_bar = starting_beat - ( starting_beat % beats_per_bar ) wait_time = beat_length grid_offset = time_sec if not sync_to_playback == null: sync_to = sync_to_playback if sync_to != null: await get_tree().process_frame if sync_to.is_playing(): on_beat.connect(_on_beat) return super.start() else: await get_tree().create_timer(time_sec).timeout _on_beat() super.start() func stop(): if timeout.get_connections().has(_on_beat): timeout.disconnect(_on_beat) super.stop() var _last_bar = 0 func _on_beat(): current_beat += 1 on_beat.emit(current_beat) if current_beat - _last_bar > beats_per_bar: _last_bar = current_bar current_bar += 1 on_bar.emit(current_bar) var _last_beat_timing func get_beat_progress(from_beat:int = -1, to_beat:int = -1, countdown:bool = false) -> float: if is_stopped(): return 0 if not countdown else 1 if from_beat == -1: from_beat == current_beat if to_beat == -1: to_beat == current_beat +1 assert(to_beat > from_beat) if current_beat <= from_beat: return 0 if not countdown else 1 if current_beat >= to_beat: return 1 if not countdown else 0 var beat_range: float = from_beat - to_beat var beat_progress: float = (current_beat - to_beat) / beat_range if sync_to == null: var result: float = beat_progress + (time_left / bpm) / beat_range return result if not countdown else 1 - result else: var result: float = beat_progress + (sync_to.get_playback_position() - _last_beat_timing) / bpm return result if not countdown else 1 - result return 0 func get_bar_progress(from_bar:int = 0, to_bar:int = -1, countdown:bool = false) -> float: if from_bar == -1: from_bar == current_beat if to_bar == -1: to_bar == current_beat +1 return get_beat_progress(from_bar * beats_per_bar, to_bar * beats_per_bar) func get_time_to_beat(to_beat:int = -1) -> float: if to_beat == -1: to_beat == current_beat +1 var result if sync_to == null: result = time_left else: result = sync_to.get_playback_position() - _last_beat_timing return result + (to_beat - current_bar + 1) / bpm