@tool class_name TimedMusicAnimationPlayer extends AnimPlayerEditorCalls ## 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 slowsly. SPEED_SCALE_ANIMATION, ## Adjusts the [member AnimationPlayer.speed_scale] property when desynced. PITCH_SCALE_MUSIC ## Adjusts the [member AudioStreamPlayer.pitch_scale] property when desynced. } ## 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, because 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 # 0.02175 @export_group("Catching up") ## Defines in what way we want to sync the audio and animation back up. @export var catch_up_behaviour := CatchupBehaviour.SPEED_SCALE_ANIMATION #@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 @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 ## Helper variable in case you want to modify the [member AudioStreamPlayer.pitch_scale] property. @export var custom_pitch_scale: float = 1.0 @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 ## Helper variable in case you want to modify the [member AnimationPlayer.speed_scale] property. @export var custom_speed_scale: float = 1.0 var _audio_player: Node var _timer := Timer.new() var _track_time: float = 0.0 func _ready() -> void: super() if not Engine.is_editor_hint(): add_child(_timer) _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() 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 #and abs_difference > latency #min_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.seek(difference) _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 = custom_pitch_scale speed_scale = custom_speed_scale 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 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 = custom_speed_scale + (custom_speed_scale - max_speed_scale) var speed: float = custom_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_scale = speed#clampf(speed, min_speed_scale, max_speed_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 = custom_pitch_scale + (custom_pitch_scale - max_pitch_scale) var pitch: float = custom_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) 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: _timer.start(duration) var _signals: Array[Signal] = await SignalGroup.await_signals( [_timer.timeout, animation_finished, animation_changed], 1 ) if not _signals.front() == _timer.timeout: return #var latency: float = AudioServer.get_output_latency() - AudioServer.get_time_to_next_mix() _audio_player.stream = animation.audio_track_get_key_stream(music_anim_track_index, 0) _audio_player.play(current_animation_position - _track_time)# - latency) func _anim_finished(_anim_name: StringName) -> void: if Engine.is_editor_hint(): return if is_instance_valid(_audio_player): _audio_player.stop() _audio_player.stream = null