- Refactored VibrationComponent (RumbleComponent) -- Now supports multiple vibrations and adjustable curves. -- Inspired by the RumblePak addon. - Added a new startup scene.
223 lines
8.7 KiB
GDScript
223 lines
8.7 KiB
GDScript
@tool
|
|
class_name TimedMusicAnimationPlayer
|
|
extends AnimationPlayerEditorCalls
|
|
|
|
## A animation player that tries to sync the music with the animation as best as it can.
|
|
##
|
|
## This is a animation player, that tries to sync up the music and animation as best as it can,
|
|
## in case they get desynced.[br]
|
|
## This can happen due to a lag, stuff loading in,
|
|
## or other stuff that blocks the main thread.[br][br]
|
|
## It first tries to sync them back up again by adjusting
|
|
## the [member AnimationPlayer.speed_scale] or [member AudioStreamPlayer.pitch_scale] property
|
|
## [i](depending on [member catch_up_behaviour])[/i].[br]
|
|
## However, if the difference between them is too big,
|
|
## it will just snap audio playback back to the animation, because the audio continues to
|
|
## run, even if the main thread is blocked.[br][br]
|
|
## [b]Note:[/b] Catching up happens before the audio get's snapped,[br]
|
|
## so [member max_error] is only effective, if the difference between audio and animation
|
|
## is bigger than the [member max_catchup_threshold],
|
|
## or lower than the [method AudioServer.get_output_latency].
|
|
|
|
enum CatchUpBehaviour {
|
|
NONE, ## Don't catch up slowly.
|
|
SPEED_SCALE_ANIMATION, ## Adjusts the [member AnimationPlayer.speed_scale] property when desynced.
|
|
PITCH_SCALE_MUSIC, ## Adjusts the [member AudioStreamPlayer.pitch_scale] property when desynced.
|
|
}
|
|
|
|
## If this is set, it start's the [RhythmPlayer] instead of the [member audio_player].[br]
|
|
## [member audio_player] should still be set to the same node as in the [member rhythm_player].
|
|
@export var rhythm_player: RhythmPlayer: set = set_rhythm_player
|
|
## The [AudioStreamPlayer], [AudioStreamPlayer2D]
|
|
## or [AudioStreamPlayer3D] the music should be played on.[br]
|
|
## [b]Should be set to the same player as defined in your animation.[/b]
|
|
@export_node_path("AudioStreamPlayer", "AudioStreamPlayer2D", "AudioStreamPlayer3D")
|
|
var audio_player: NodePath = ^"": set = set_audio_player
|
|
## The track-index in the animation,
|
|
## where the music is played from [i](should be of type [constant Animation.TYPE_AUDIO])[/i].
|
|
@export var music_anim_track_index: int = 0
|
|
# The offset from 0.0 seconds when the music starts in the animation.
|
|
#@export var music_anim_offset: float = 0.0
|
|
## @deprecated: Actually reduntant. This should always happen if above [member max_catchup_threshold].
|
|
## The max difference audio and animation can have to each other,
|
|
## until the audio is snapped back to the animation.[br]
|
|
## However, [member catch_up_behaviour] takes priority, if this is set to value lower than that.
|
|
@export var max_error: float = 0.375
|
|
## Helper variable in case you want to modify the [member AudioStreamPlayer.pitch_scale] property.
|
|
@export var audio_pitch_scale: float = 1.0
|
|
## Helper variable in case you want to modify the [member AnimationPlayer.speed_scale] property.
|
|
@export var anim_speed_scale: float = 1.0
|
|
@export_group("Catching up")
|
|
#@export var min_catchup_threshold: float = 0.0015
|
|
## Should be set to a value above- or equal to [member max_error].
|
|
@export var max_catchup_threshold: float = 0.375
|
|
## Defines in what way we want to sync the audio and animation back up.
|
|
@export var catch_up_behaviour := CatchUpBehaviour.SPEED_SCALE_ANIMATION:
|
|
set(value):
|
|
catch_up_behaviour = value
|
|
notify_property_list_changed()
|
|
#@export_subgroup("Pitch Scale")
|
|
## The maximum pitch scale change the audio can be set to, when they are desynced.
|
|
@export var max_pitch_scale_difference: float = 1.01
|
|
#@export_subgroup("Speed Scale")
|
|
## The maximum speed scale change the animation can be set to, when they are desynced.
|
|
@export var max_speed_scale_difference: float = 1.15
|
|
|
|
var _audio_player: Node
|
|
var _audio_playback_delay_timer := Timer.new()
|
|
var _track_time: float = 0.0
|
|
|
|
|
|
func _ready() -> void:
|
|
super()
|
|
|
|
if not Engine.is_editor_hint():
|
|
add_child(_audio_playback_delay_timer)
|
|
_audio_playback_delay_timer.one_shot = false
|
|
|
|
set_audio_player(audio_player)
|
|
animation_started.connect(_anim_started)
|
|
animation_finished.connect(_anim_finished)
|
|
|
|
|
|
func _process(delta: float) -> void:
|
|
if Engine.is_editor_hint():
|
|
super(delta)
|
|
return
|
|
|
|
if not active or not is_playing() or not is_instance_valid(_audio_player):
|
|
return
|
|
|
|
var latency: float = AudioServer.get_output_latency()
|
|
var audio_position: float = _audio_player.get_playback_position()
|
|
var anim_position: float = current_animation_position + AudioServer.get_time_since_last_mix()
|
|
|
|
if anim_position < _track_time:
|
|
return
|
|
|
|
var difference: float = anim_position - (audio_position + _track_time)
|
|
var abs_difference: float = absf(difference)
|
|
|
|
SPrint.print_msgf("Audio-Position: %s\nAnim-Position: %s" % [audio_position, anim_position])
|
|
SPrint.print_msgf("Audio-Anim Diff: %s" % difference)#[audio_position - anim_position])
|
|
SPrint.print_msgf("ABS-Difference: %s" % abs_difference)
|
|
SPrint.print_msgf("Anim-Speed-Scale: %s" % [speed_scale])
|
|
SPrint.print_msgf("Audio-Pitch: %s\nLatency: %s" % [_audio_player.pitch_scale, latency])
|
|
|
|
if (
|
|
not catch_up_behaviour == CatchUpBehaviour.NONE
|
|
and abs_difference < max_catchup_threshold
|
|
):
|
|
match catch_up_behaviour:
|
|
CatchUpBehaviour.SPEED_SCALE_ANIMATION:
|
|
_sync_anim_speed_scale(difference, delta)
|
|
CatchUpBehaviour.PITCH_SCALE_MUSIC:
|
|
_sync_music_pitch_scale(latency, difference, delta)
|
|
elif abs_difference > max_error:
|
|
_audio_player.play(anim_position - _track_time + latency)
|
|
SPrint.print_msg("Snapped Audio to: %s" % _audio_player.get_playback_position())
|
|
else:
|
|
_audio_player.pitch_scale = audio_pitch_scale
|
|
speed_scale = anim_speed_scale
|
|
|
|
|
|
func _validate_property(property: Dictionary) -> void:
|
|
if property.name == &"max_pitch_scale_difference":
|
|
if catch_up_behaviour != CatchUpBehaviour.PITCH_SCALE_MUSIC:
|
|
property.usage = PROPERTY_USAGE_NO_EDITOR
|
|
elif property.name == &"max_speed_scale_difference":
|
|
if catch_up_behaviour != CatchUpBehaviour.SPEED_SCALE_ANIMATION:
|
|
property.usage = PROPERTY_USAGE_NO_EDITOR
|
|
|
|
|
|
func set_audio_player(path: NodePath) -> void:
|
|
audio_player = path
|
|
|
|
var node: Node = get_node_or_null(path)
|
|
_audio_player = node if Utils.is_node_audioplayer(node) else null
|
|
|
|
|
|
func set_rhythm_player(player: RhythmPlayer) -> void:
|
|
rhythm_player = player
|
|
|
|
|
|
#func play_timed(
|
|
#anim_name: StringName,
|
|
#from_marker: StringName = &"",
|
|
#end_marker: StringName = &"",
|
|
#custom_blend: float = -1.0,
|
|
#custom_speed: float = 1.0,
|
|
#from_end: bool = false
|
|
#) -> void:
|
|
#_anim_started(anim_name)
|
|
#play_section_with_markers(anim_name, from_marker,
|
|
#end_marker, custom_blend, custom_speed, from_end)
|
|
|
|
|
|
func _sync_anim_speed_scale(difference: float, delta: float) -> void:
|
|
var max_speed_scale: float = max_speed_scale_difference
|
|
var min_speed_scale: float = anim_speed_scale + (anim_speed_scale - max_speed_scale)
|
|
var speed: float = anim_speed_scale - difference
|
|
|
|
speed = clampf(speed, min_speed_scale, max_speed_scale)
|
|
|
|
if absf(difference) >= 0.05:
|
|
speed = move_toward(speed_scale, speed, delta * 0.01)
|
|
#else:
|
|
#speed = lerp(speed_scale, speed, 1.0 - pow(0.5, delta)) #0.001
|
|
#speed = clampf(speed, min_speed_scale, max_speed_scale)
|
|
|
|
speed_scale = speed * _audio_player.pitch_scale
|
|
|
|
|
|
func _sync_music_pitch_scale(latency: float, difference: float, delta: float) -> void:
|
|
var max_pitch_scale: float = max_pitch_scale_difference
|
|
var min_pitch_scale: float = audio_pitch_scale + (audio_pitch_scale - max_pitch_scale)
|
|
|
|
var pitch: float = audio_pitch_scale + difference + latency
|
|
pitch = clampf(pitch, min_pitch_scale, max_pitch_scale)
|
|
pitch = lerp(_audio_player.pitch_scale, pitch, 1.0 - pow(0.5, delta))
|
|
|
|
_audio_player.pitch_scale = clampf(pitch, min_pitch_scale, max_pitch_scale) * anim_speed_scale
|
|
|
|
|
|
func _anim_started(anim_name: StringName) -> void:
|
|
if Engine.is_editor_hint():
|
|
return
|
|
|
|
var animation: Animation = get_animation(anim_name)
|
|
animation.track_set_enabled(music_anim_track_index, false)
|
|
_audio_player.stream = null
|
|
|
|
_track_time = animation.track_get_key_time(music_anim_track_index, 0)
|
|
var duration: float = _track_time - current_animation_position
|
|
|
|
if duration > 0.0:
|
|
_audio_playback_delay_timer.start(duration)
|
|
|
|
var _signals: Array[Signal] = await SignalGroup.await_signals(
|
|
[_audio_playback_delay_timer.timeout, animation_finished, animation_changed], 1
|
|
)
|
|
|
|
if not _signals.front() == _audio_playback_delay_timer.timeout:
|
|
return
|
|
|
|
var playback_position: float = current_animation_position - _track_time
|
|
|
|
if is_instance_valid(rhythm_player):
|
|
rhythm_player.play(playback_position)
|
|
elif is_instance_valid(_audio_player):
|
|
_audio_player.stream = animation.audio_track_get_key_stream(music_anim_track_index, 0)
|
|
_audio_player.play(playback_position)
|
|
|
|
|
|
func _anim_finished(_anim_name: StringName) -> void:
|
|
if Engine.is_editor_hint():
|
|
return
|
|
|
|
if is_instance_valid(rhythm_player):
|
|
rhythm_player.stop()
|
|
elif is_instance_valid(_audio_player):
|
|
_audio_player.stop()
|
|
_audio_player.stream = null
|