168 lines
4.3 KiB
GDScript
168 lines
4.3 KiB
GDScript
## 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)
|