make fade_in/fade_out idempotent — repeat calls toward the same target are no-ops

Without this, scripts that re-fire fade_in on every Area2D body_entered (or
similar repeating signal) restart the fade clock each call, which can stutter
audibly when a per-song crossfade is also running. Now a fade_in while already
fading to the user's set level — or fade_out while already fading to silent —
is a no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Daniel Samson 2026-04-29 20:32:06 +01:00
parent 5204295864
commit 7bcc12b45b
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
2 changed files with 9 additions and 3 deletions

View File

@ -90,11 +90,11 @@ When the node enters the tree in the editor, `stream` is auto-assigned an `Audio
| `pause()` | Pauses playback. No-op if not playing or already paused. | | `pause()` | Pauses playback. No-op if not playing or already paused. |
| `resume()` | Resumes playback after a pause. | | `resume()` | Resumes playback after a pause. |
| `is_song_playing() -> bool` | True iff a song's layers are active and not paused. Distinct from `is_playing()`, which stays true between songs because the polyphonic playback session itself is still live. | | `is_song_playing() -> bool` | True iff a song's layers are active and not paused. Distinct from `is_playing()`, which stays true between songs because the polyphonic playback session itself is still live. |
| `fade_in(duration: float)` | Animates `volume_db` up to the user's set level over `duration` seconds. **Pure volume operation** — does not start playback, unpause, or otherwise change playing state. The "set level" is the last value written to `volume_db` outside of an active fade, so `fade_in``fade_out``fade_in` round-trips back to the original volume. If any fade is in flight (player-wide `fade_out` or per-song crossfade from `SkipMode = CrossFade`), the ramp continues from the current `volume_db` so audible state is preserved. Otherwise it snaps to silent and ramps up. Pair with `play_playlist()` / `resume()` if the player isn't already playing. | | `fade_in(duration: float)` | Animates `volume_db` up to the user's set level over `duration` seconds. **Pure volume operation** — does not start playback, unpause, or otherwise change playing state. The "set level" is the last value written to `volume_db` outside of an active fade, so `fade_in``fade_out``fade_in` round-trips back to the original volume. If any fade is in flight (player-wide `fade_out` or per-song crossfade from `SkipMode = CrossFade`), the ramp continues from the current `volume_db` so audible state is preserved. Otherwise it snaps to silent and ramps up. **Idempotent** — calling it while already fading to the same target is a no-op (won't restart the fade clock). Pair with `play_playlist()` / `resume()` if the player isn't already playing. |
| `fade_out(duration: float)` | Animates `volume_db` from its current value down to silent over `duration` seconds. **Pure volume operation** — does not stop the polyphonic session or pause. Call `pause()` / `stop()` yourself afterwards if you want playback to halt. | | `fade_out(duration: float)` | Animates `volume_db` from its current value down to silent over `duration` seconds. **Pure volume operation** — does not stop the polyphonic session or pause. Call `pause()` / `stop()` yourself afterwards if you want playback to halt. **Idempotent** — calling it while already fading to silent is a no-op. |
| `get_current_song_index() -> int` | Index of the song most recently kicked off in `Playlist.QueuedSongs`. During a reverb-tail crossover this becomes the incoming song the moment it starts, even though the outgoing song is still audible. | | `get_current_song_index() -> int` | Index of the song most recently kicked off in `Playlist.QueuedSongs`. During a reverb-tail crossover this becomes the incoming song the moment it starts, even though the outgoing song is still audible. |
| `advance_to_next_song()` | Skip to the next song in the playlist (wraps from the end back to index 0). Transition style is controlled by `SkipMode`. | | `advance_to_next_song()` | Skip to the next song in the playlist (wraps from the end back to index 0). Transition style is controlled by `SkipMode`. |
| `advance_to_track(index: int)` | Same as `advance_to_next_song`, but jumps to a specific index. Out-of-range indices are ignored. | | `advance_to_track(index: int)` | Same as `advance_to_next_song`, but jumps to a specific index. Out-of-range indices are ignored. **No-op when called with the index of the song already playing** — won't restart it. |
#### Behaviour #### Behaviour

View File

@ -176,10 +176,13 @@ func advance_to_track(index: int) -> void:
## crossfade), the ramp continues from the current [code]volume_db[/code] — ## crossfade), the ramp continues from the current [code]volume_db[/code] —
## no snap to silent, so audible crossfade state is preserved. Otherwise the ## no snap to silent, so audible crossfade state is preserved. Otherwise the
## ramp snaps to silent first and rises to the user's set level. ## ramp snaps to silent first and rises to the user's set level.
## Idempotent: no-op if a fade is already heading to this same target.
func fade_in(duration: float) -> void: func fade_in(duration: float) -> void:
var target_db : float = _target_volume_db; var target_db : float = _target_volume_db;
if target_db <= _FADE_SILENT_DB + 0.001: if target_db <= _FADE_SILENT_DB + 0.001:
target_db = 0.0; target_db = 0.0;
if _fading and is_equal_approx(_fade_to_db, target_db):
return;
var any_song_fading : bool = false; var any_song_fading : bool = false;
for active in _active_songs: for active in _active_songs:
if active.fading: if active.fading:
@ -192,7 +195,10 @@ func fade_in(duration: float) -> void:
## [param duration] seconds. Pure volume operation — does not stop the ## [param duration] seconds. Pure volume operation — does not stop the
## polyphonic session or pause. Call [method pause] or [code]stop()[/code] ## polyphonic session or pause. Call [method pause] or [code]stop()[/code]
## yourself if you want playback to halt at the end. ## yourself if you want playback to halt at the end.
## Idempotent: no-op if a fade is already heading to silent.
func fade_out(duration: float) -> void: func fade_out(duration: float) -> void:
if _fading and is_equal_approx(_fade_to_db, _FADE_SILENT_DB):
return;
_start_fade(_player.volume_db, _FADE_SILENT_DB, duration); _start_fade(_player.volume_db, _FADE_SILENT_DB, duration);
func apply_stream_volumes(value: float) -> void: func apply_stream_volumes(value: float) -> void: