From 7bcc12b45b779550f5c91577938a6cdaa503ee59 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Wed, 29 Apr 2026 20:32:06 +0100 Subject: [PATCH] =?UTF-8?q?make=20fade=5Fin/fade=5Fout=20idempotent=20?= =?UTF-8?q?=E2=80=94=20repeat=20calls=20toward=20the=20same=20target=20are?= =?UTF-8?q?=20no-ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 6 +++--- src/DynamicSoundPlayerCore.gd | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d34a308..613704a 100644 --- a/README.md +++ b/README.md @@ -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. | | `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. | -| `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_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_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. **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. | | `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 diff --git a/src/DynamicSoundPlayerCore.gd b/src/DynamicSoundPlayerCore.gd index f7df179..cd687b0 100644 --- a/src/DynamicSoundPlayerCore.gd +++ b/src/DynamicSoundPlayerCore.gd @@ -176,10 +176,13 @@ func advance_to_track(index: int) -> void: ## crossfade), the ramp continues from the current [code]volume_db[/code] — ## 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. +## Idempotent: no-op if a fade is already heading to this same target. func fade_in(duration: float) -> void: var target_db : float = _target_volume_db; if target_db <= _FADE_SILENT_DB + 0.001: target_db = 0.0; + if _fading and is_equal_approx(_fade_to_db, target_db): + return; var any_song_fading : bool = false; for active in _active_songs: if active.fading: @@ -192,7 +195,10 @@ func fade_in(duration: float) -> void: ## [param duration] seconds. Pure volume operation — does not stop the ## polyphonic session or pause. Call [method pause] or [code]stop()[/code] ## 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: + if _fading and is_equal_approx(_fade_to_db, _FADE_SILENT_DB): + return; _start_fade(_player.volume_db, _FADE_SILENT_DB, duration); func apply_stream_volumes(value: float) -> void: