## Class for converting beats to time (since start of music) and vice versa. class_name SyncTrack extends Node func initialize(tempo: Tempo) -> void: _set_bpm_data(tempo) _calculate_cache() ## Get the number of valid BPM changes. func size() -> int: return min(_bpms.size(), _lengths.size() + 1) ## Get the BPM at the given time (since music start). func bpm_at_time(time: float) -> float: var i: int = _get_bpm_index_at_time(time) if _types[i] == Tempo.BPM_TYPE.LINEAR: var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time) return lerp(_bpms[i], _bpms[i+1], delta) else: return _bpms.get(i) ## Get the beat at the given time (since music start). func to_beat(time: float) -> float: var i: int = _get_bpm_index_at_time(time) # If i is not the last BPM: if i < _bpms.size() - 1: var delta = inverse_lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], time) return lerp(_elapsed_beats[i], _elapsed_beats[i+1], delta) # Else i is the last BPM: else: var bps: float = _bpms[i] / 60.0 var remaining_seconds: float = time - _elapsed_seconds[i] var remaining_beats: float = remaining_seconds * bps return _elapsed_beats[i] + remaining_beats ## Get the time (since music start) at the given beat. func to_time(beat: float) -> float: var i: int = _get_bpm_index_at_beat(beat) if i == -1: return -1.0 # Else if i is not the last BPM: elif i < _bpms.size() - 1: var delta = inverse_lerp(_elapsed_beats[i], _elapsed_beats[i+1], beat) return lerp(_elapsed_seconds[i], _elapsed_seconds[i+1], delta) # Else i is the last BPM: else: var bps: float = _bpms[i] / 60.0 var remaining_beats: float = beat - _elapsed_beats[i] var remaining_seconds: float = remaining_beats / bps return _elapsed_seconds[i] + remaining_seconds func _init(tempo: Tempo = Tempo.new()) -> void: initialize(tempo) # IMPLEMENTATION # === Note about BPM data === # The data for each BPM is stored in the same index # across the different arrays. # =========================== ## BPM array (Beats per Minute). var _bpms: Array[float] ## The duration of each BPM in beats. var _lengths: Array[float] ## The BpmData.BPM_TYPE of each BPM. var _types: Array[Tempo.BPM_TYPE] # Cache variables that make calculations easier. ## The duration of each BPM in seconds. var _length_seconds: Array[float] ## Seconds passed before the corresponding BPM. var _elapsed_seconds: Array[float] = [0.0] ## Beats passed before the corresponding BPM. var _elapsed_beats: Array[float] = [0.0] ## Set the BPM data arrays. func _set_bpm_data(tempo: Tempo) -> void: _clear_bpm_data() tempo.normalize() _bpms = tempo.bpms _lengths = tempo.lengths _types = tempo.types func _calculate_cache() -> void: _clear_cache() for i: int in range(0, _bpms.size() - 1): var bps: float var length_in_beats: float = _lengths[i] if _types[i] == Tempo.BPM_TYPE.LINEAR: # Average BPS. bps = (_bpms[i] + _bpms[i+1]) / 120.0 else: bps = _bpms[i] / 60.0 var length_in_seconds: float = length_in_beats / bps _length_seconds.append(length_in_seconds) _elapsed_seconds.append(_elapsed_seconds.back() + length_in_seconds) _elapsed_beats.append(_elapsed_beats.back() + length_in_beats) if _length_seconds.size() > 0: _length_seconds[-1] = -1.0 func _clear_bpm_data() -> void: _bpms = [] _lengths = [] _types = [] func _clear_cache() -> void: _length_seconds = [] _elapsed_seconds = [0.0] _elapsed_beats = [0.0] func _get_bpm_index_at_time(time: float) -> int: assert(_elapsed_seconds.size() > 0) assert(_bpms.size() > 0) var _find_index: Callable = func (p_elapsed_seconds: float) -> bool: return time < p_elapsed_seconds var index: int = _elapsed_seconds.find_custom(_find_index) if index == -1: index = _bpms.size() - 1 else: index -= 1 return index func _get_bpm_index_at_beat(beat: float) -> int: var _find_index: Callable = func (p_elapsed_beats: float) -> bool: return beat < p_elapsed_beats if _elapsed_beats.size() == 0: return -1 var index: int = _elapsed_beats.find_custom(_find_index) if index > 0: index -= 1 elif index == -1: index = max(0, _bpms.size()-1) return index func _print_data() -> void: print("=== SyncTrack ===") print("BPMs: ", _bpms) print("BPM Lengths (Beats): ", _lengths) print("BPM Types: ", _types) print("Elapsed Seconds: ", _elapsed_seconds) print("Elapsed Beats: ", _elapsed_beats)