refactor fade_in/fade_out to drive volume_db directly and decouple from play/stop
Fades are now pure volume ramps: they no longer call play(), stop(), or unpause. The animation is visible on the volume_db property each frame. Track _target_volume_db separately so fade_in/fade_out round-trips return to the user's set level instead of a transient lerp value, and skip the "snap to silent" when interrupting an in-flight player or per-song crossfade so audible state is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1e53fb83d1
commit
5204295864
|
|
@ -90,8 +90,8 @@ 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)` | Starts playback (if needed), unpauses, and ramps volume from silent to `volume_db` over `duration` seconds. Works regardless of `autoplay`. |
|
||||
| `fade_out(duration: float)` | Ramps volume from current level to silent over `duration` seconds, then stops the polyphonic session. A subsequent `fade_in` reinitialises. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
|
@ -100,7 +100,7 @@ When the node enters the tree in the editor, `stream` is auto-assigned an `Audio
|
|||
|
||||
- **Autoplay.** On `_ready`, plays the song at the current index *only if `autoplay` is true* (the inherited `AudioStreamPlayer.autoplay` property; default `true`). Set `autoplay = false` to defer playback to a `play_playlist()` or `fade_in()` call.
|
||||
- **Reverb-tail transitions.** When the current song is within `ReverbTail` seconds of its full length, the next song (chosen per `LoopMode`) is started so its leading audio covers the outgoing tail. Both songs are audible during the overlap window; the outgoing one stops when its full length has elapsed.
|
||||
- **Volume.** `volume_db` is honoured; the fade adjustment is layered on top internally, so manual volume changes during a fade still work correctly. During a reverb-tail overlap, both songs share the same volume/intensity mix.
|
||||
- **Volume.** `fade_in` and `fade_out` animate `volume_db` directly — the property visibly changes each frame during a fade. Setting `volume_db` mid-fade will be overwritten on the next frame; let the fade complete (or stop it by triggering another fade) before nudging the property manually. During a reverb-tail overlap, both songs share the same volume/intensity mix.
|
||||
- **Editor.** All scripts are `@tool`, but `_ready` and `_process` early-return in the editor so audio doesn't play in the inspector.
|
||||
|
||||
### `DynamicSoundFXPlayer` family — overlapping one-shot SFX
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ var _core : DynamicSoundPlayerCore;
|
|||
|
||||
func _set(property: StringName, value: Variant) -> bool:
|
||||
if property == &"volume_db":
|
||||
if _core: _core.apply_stream_volumes(value);
|
||||
if _core: _core.on_volume_db_set(value);
|
||||
return false;
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ var _core : DynamicSoundPlayerCore;
|
|||
|
||||
func _set(property: StringName, value: Variant) -> bool:
|
||||
if property == &"volume_db":
|
||||
if _core: _core.apply_stream_volumes(value);
|
||||
if _core: _core.on_volume_db_set(value);
|
||||
return false;
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ var _core : DynamicSoundPlayerCore;
|
|||
|
||||
func _set(property: StringName, value: Variant) -> bool:
|
||||
if property == &"volume_db":
|
||||
if _core: _core.apply_stream_volumes(value);
|
||||
if _core: _core.on_volume_db_set(value);
|
||||
return false;
|
||||
return false;
|
||||
|
||||
|
|
|
|||
|
|
@ -41,16 +41,18 @@ var _playback : AudioStreamPlaybackPolyphonic;
|
|||
var _cur_time : float = 0.0;
|
||||
var _current_index : int = 0;
|
||||
var _paused : bool = false;
|
||||
var _fade_db : float = 0.0;
|
||||
var _fade_from_db : float = 0.0;
|
||||
var _fade_to_db : float = 0.0;
|
||||
var _fade_duration : float = 0.0;
|
||||
var _fade_elapsed : float = 0.0;
|
||||
var _fading : bool = false;
|
||||
var _stop_when_faded : bool = false;
|
||||
# Stable record of the user's set volume. Fades animate volume_db directly,
|
||||
# so without this we lose the original target across fade interruptions.
|
||||
var _target_volume_db : float = 0.0;
|
||||
|
||||
func _init(player) -> void:
|
||||
_player = player;
|
||||
_target_volume_db = player.volume_db;
|
||||
|
||||
func ready() -> void:
|
||||
if Engine.is_editor_hint():
|
||||
|
|
@ -68,13 +70,9 @@ func process(delta: float) -> void:
|
|||
if _fading:
|
||||
_fade_elapsed += delta;
|
||||
var t : float = clampf(_fade_elapsed / _fade_duration, 0.0, 1.0);
|
||||
_fade_db = lerp(_fade_from_db, _fade_to_db, t);
|
||||
apply_stream_volumes(_player.volume_db);
|
||||
_player.volume_db = lerp(_fade_from_db, _fade_to_db, t);
|
||||
if t >= 1.0:
|
||||
_fading = false;
|
||||
if _stop_when_faded:
|
||||
_stop_after_fade();
|
||||
return;
|
||||
# iterate over a snapshot since we may modify _active_songs in the loop
|
||||
for active in _active_songs.duplicate():
|
||||
# advance per-song fade
|
||||
|
|
@ -172,18 +170,30 @@ func advance_to_track(index: int) -> void:
|
|||
_start_song_fade(incoming, _FADE_SILENT_DB, 0.0, _player.CrossFadeDuration, false);
|
||||
_apply_volumes_to_song(incoming, _player.volume_db);
|
||||
|
||||
## Ramps [code]volume_db[/code] up to the user's set level over [param duration]
|
||||
## seconds. Pure volume operation — does not call [code]play()[/code], unpause,
|
||||
## or change playing state. If any fade is in flight (player-wide or per-song
|
||||
## 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.
|
||||
func fade_in(duration: float) -> void:
|
||||
_start_playback();
|
||||
if _paused:
|
||||
resume();
|
||||
_start_fade(_FADE_SILENT_DB, 0.0, duration, false);
|
||||
var target_db : float = _target_volume_db;
|
||||
if target_db <= _FADE_SILENT_DB + 0.001:
|
||||
target_db = 0.0;
|
||||
var any_song_fading : bool = false;
|
||||
for active in _active_songs:
|
||||
if active.fading:
|
||||
any_song_fading = true;
|
||||
break;
|
||||
var from_db : float = _player.volume_db if (_fading or any_song_fading) else _FADE_SILENT_DB;
|
||||
_start_fade(from_db, target_db, duration);
|
||||
|
||||
## Ramps [code]volume_db[/code] from its current value down to silent over
|
||||
## [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.
|
||||
func fade_out(duration: float) -> void:
|
||||
if _playback == null:
|
||||
return;
|
||||
if _paused:
|
||||
resume();
|
||||
_start_fade(_fade_db, _FADE_SILENT_DB, duration, true);
|
||||
_start_fade(_player.volume_db, _FADE_SILENT_DB, duration);
|
||||
|
||||
func apply_stream_volumes(value: float) -> void:
|
||||
if _playback == null:
|
||||
|
|
@ -191,6 +201,14 @@ func apply_stream_volumes(value: float) -> void:
|
|||
for active in _active_songs:
|
||||
_apply_volumes_to_song(active, value);
|
||||
|
||||
## Wrapper-side hook for [code]volume_db[/code] writes. Updates [code]_target_volume_db[/code]
|
||||
## (the user's set level) only when no fade is animating it, so transient lerp
|
||||
## values aren't captured as a new target.
|
||||
func on_volume_db_set(value: float) -> void:
|
||||
if not _fading:
|
||||
_target_volume_db = value;
|
||||
apply_stream_volumes(value);
|
||||
|
||||
func _start_playback() -> void:
|
||||
var playlist : DynamicSoundPlaylist = _player.Playlist;
|
||||
if playlist == null or playlist.QueuedSongs.is_empty():
|
||||
|
|
@ -239,7 +257,7 @@ func _stop_active_song(active: _ActiveSong) -> void:
|
|||
_playback.stop_stream(id);
|
||||
|
||||
func _apply_volumes_to_song(active: _ActiveSong, volume_db_value: float) -> void:
|
||||
var effective_db : float = volume_db_value + _fade_db + active.fade_db;
|
||||
var effective_db : float = volume_db_value + active.fade_db;
|
||||
var realIntensity : float;
|
||||
if active.ids.size() == 3:
|
||||
realIntensity = _player.Intensity;
|
||||
|
|
@ -249,15 +267,13 @@ func _apply_volumes_to_song(active: _ActiveSong, volume_db_value: float) -> void
|
|||
# set volume based on intensity; with a single layer it's just full volume
|
||||
_playback.set_stream_volume(active.ids[i], linear_to_db(db_to_linear(effective_db) * max((.5 - abs(float(i)/2 - realIntensity))/.5, 0)));
|
||||
|
||||
func _start_fade(from_db: float, to_db: float, duration: float, stop_after: bool) -> void:
|
||||
func _start_fade(from_db: float, to_db: float, duration: float) -> void:
|
||||
_fade_from_db = from_db;
|
||||
_fade_to_db = to_db;
|
||||
_fade_db = from_db;
|
||||
_fade_duration = max(duration, 0.0001);
|
||||
_fade_elapsed = 0.0;
|
||||
_fading = true;
|
||||
_stop_when_faded = stop_after;
|
||||
apply_stream_volumes(_player.volume_db);
|
||||
_player.volume_db = from_db;
|
||||
|
||||
func _start_song_fade(song: _ActiveSong, from_db: float, to_db: float, duration: float, stop_after: bool) -> void:
|
||||
song.fade_from_db = from_db;
|
||||
|
|
@ -267,12 +283,3 @@ func _start_song_fade(song: _ActiveSong, from_db: float, to_db: float, duration:
|
|||
song.fade_elapsed = 0.0;
|
||||
song.fading = true;
|
||||
song.stop_after_fade = stop_after;
|
||||
|
||||
func _stop_after_fade() -> void:
|
||||
for active in _active_songs:
|
||||
_stop_active_song(active);
|
||||
_active_songs.clear();
|
||||
_player.stop();
|
||||
_playback = null;
|
||||
_fade_db = 0.0;
|
||||
_stop_when_faded = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue