add SkipMode enum (UseReverbTail / CrossFade / Jump)
advance_to_next_song / advance_to_track now branch on the new SkipMode export: - UseReverbTail (default): existing behavior. Accelerate the outgoing song so remaining time hits ReverbTail, both songs play during the overlap, outgoing stops naturally. - CrossFade: programmatic volume crossfade. Outgoing fades from current volume to silent over CrossFadeDuration seconds (then stops), incoming starts silent and fades up to full over the same duration. Independent of any global fade. - Jump: hard cut. Outgoing song's streams are stopped immediately and removed from the active list before the incoming starts. For CrossFade we needed per-song fade state (the global _fade_db can't drive two songs in opposite directions at once), so _ActiveSong gained fade_db / fade_from_db / fade_to_db / fade_duration / fade_elapsed / fading / stop_after_fade fields. _apply_volumes_to_song now adds both the global and per-song fade_db to volume_db; process() advances each song's fade per-frame and stops songs whose fades reach the silent target with stop_after_fade set. CrossFadeDuration is hidden in the inspector via _validate_property unless SkipMode == CrossFade. The natural end-of-song advance is unchanged — it still uses the song's reverb tail. SkipMode only affects programmatic skips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
e792572635
commit
df3e73b32e
@@ -65,6 +65,8 @@ Plays through a playlist one song at a time, blending three intensity layers per
|
|||||||
| `Playlist` | `DynamicSoundPlaylist` | `null` | Songs to play in order. Read-only as songs play — the player tracks an internal index. |
|
| `Playlist` | `DynamicSoundPlaylist` | `null` | Songs to play in order. Read-only as songs play — the player tracks an internal index. |
|
||||||
| `Intensity` | `float` (0–1) | `0` | Runtime mix between the three layers. `0` = layer 1 dominates, `1` = layer 3 dominates. |
|
| `Intensity` | `float` (0–1) | `0` | Runtime mix between the three layers. `0` = layer 1 dominates, `1` = layer 3 dominates. |
|
||||||
| `LoopMode` | enum | `All` | `Single` keeps repeating the current song. `All` advances through the playlist and wraps back to index 0 after the last song. Both modes loop forever; there's no "play once and stop" mode. |
|
| `LoopMode` | enum | `All` | `Single` keeps repeating the current song. `All` advances through the playlist and wraps back to index 0 after the last song. Both modes loop forever; there's no "play once and stop" mode. |
|
||||||
|
| `SkipMode` | enum | `UseReverbTail` | How `advance_to_next_song()` / `advance_to_track()` transition. `UseReverbTail` accelerates the outgoing song so its remaining time hits `ReverbTail` and the new song plays on top — relies on the song's audio having a natural tail. `CrossFade` programmatically fades the outgoing song down and the incoming song up over `CrossFadeDuration` seconds. `Jump` is a hard cut. Does **not** affect natural end-of-song advancement (that always uses reverb tail). |
|
||||||
|
| `CrossFadeDuration` | `float` | `1.0` | Duration of the volume crossfade in seconds when `SkipMode = CrossFade`. Hidden in the inspector for the other modes. |
|
||||||
|
|
||||||
When the node enters the tree in the editor, `stream` is auto-assigned an `AudioStreamPolyphonic` and `bus` is set to `"Music"` if a bus by that name exists in the project (otherwise it's left at `"Master"`). User-customised buses are not overridden.
|
When the node enters the tree in the editor, `stream` is auto-assigned an `AudioStreamPolyphonic` and `bus` is set to `"Music"` if a bus by that name exists in the project (otherwise it's left at `"Master"`). User-customised buses are not overridden.
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ When the node enters the tree in the editor, `stream` is auto-assigned an `Audio
|
|||||||
| `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_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_out(duration: float)` | Ramps volume from current level to silent over `duration` seconds, then stops the polyphonic session. A subsequent `fade_in` reinitialises. |
|
||||||
| `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). Triggers a smooth transition through the current song's reverb tail — accelerates the outgoing song so its remaining time hits `ReverbTail` immediately, then starts the new song on top. With `ReverbTail = 0` this is effectively a hard cut. |
|
| `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. |
|
||||||
|
|
||||||
#### Behaviour
|
#### Behaviour
|
||||||
|
|||||||
@@ -14,3 +14,19 @@ enum PitchMode {
|
|||||||
Constant = 0,
|
Constant = 0,
|
||||||
Random = 1
|
Random = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## How [DynamicSoundPlayer] handles a programmatic skip ([code]advance_to_next_song[/code]
|
||||||
|
## or [code]advance_to_track[/code]).
|
||||||
|
##
|
||||||
|
## [b]UseReverbTail[/b] — accelerate the outgoing song so its remaining time hits
|
||||||
|
## [code]ReverbTail[/code], then start the new song on top. Both songs play at full
|
||||||
|
## volume during the overlap; the outgoing tail naturally decays in the audio.[br]
|
||||||
|
## [b]CrossFade[/b] — explicitly fade the outgoing song down and the incoming
|
||||||
|
## song up in volume over [code]CrossFadeDuration[/code] seconds.[br]
|
||||||
|
## [b]Jump[/b] — hard cut. Outgoing song stops immediately, incoming starts at
|
||||||
|
## full volume.
|
||||||
|
enum SkipMode {
|
||||||
|
UseReverbTail = 0,
|
||||||
|
CrossFade = 1,
|
||||||
|
Jump = 2
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ extends AudioStreamPlayer
|
|||||||
## through the playlist and wraps from the last song back to the first.
|
## through the playlist and wraps from the last song back to the first.
|
||||||
@export var LoopMode : DynamicSoundConstants.LoopMode = DynamicSoundConstants.LoopMode.All;
|
@export var LoopMode : DynamicSoundConstants.LoopMode = DynamicSoundConstants.LoopMode.All;
|
||||||
|
|
||||||
|
## How [method advance_to_next_song] and [method advance_to_track] transition between songs.
|
||||||
|
## Does not affect the natural end-of-song advance, which always uses the song's reverb tail.
|
||||||
|
@export var SkipMode : DynamicSoundConstants.SkipMode = DynamicSoundConstants.SkipMode.UseReverbTail:
|
||||||
|
set(value):
|
||||||
|
SkipMode = value;
|
||||||
|
notify_property_list_changed();
|
||||||
|
|
||||||
|
## Crossfade duration in seconds when [member SkipMode] is [code]CrossFade[/code].
|
||||||
|
@export var CrossFadeDuration : float = 1.0;
|
||||||
|
|
||||||
var _core : DynamicSoundPlayerCore;
|
var _core : DynamicSoundPlayerCore;
|
||||||
|
|
||||||
func _set(property: StringName, value: Variant) -> bool:
|
func _set(property: StringName, value: Variant) -> bool:
|
||||||
@@ -40,6 +50,10 @@ func _enter_tree() -> void:
|
|||||||
if bus == &"Master" and AudioServer.get_bus_index("Music") != -1:
|
if bus == &"Master" and AudioServer.get_bus_index("Music") != -1:
|
||||||
bus = "Music";
|
bus = "Music";
|
||||||
|
|
||||||
|
func _validate_property(property: Dictionary) -> void:
|
||||||
|
if property.name == "CrossFadeDuration" and SkipMode != DynamicSoundConstants.SkipMode.CrossFade:
|
||||||
|
property.usage &= ~PROPERTY_USAGE_EDITOR;
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_core = DynamicSoundPlayerCore.new(self);
|
_core = DynamicSoundPlayerCore.new(self);
|
||||||
_core.ready();
|
_core.ready();
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ extends AudioStreamPlayer2D
|
|||||||
## through the playlist and wraps from the last song back to the first.
|
## through the playlist and wraps from the last song back to the first.
|
||||||
@export var LoopMode : DynamicSoundConstants.LoopMode = DynamicSoundConstants.LoopMode.All;
|
@export var LoopMode : DynamicSoundConstants.LoopMode = DynamicSoundConstants.LoopMode.All;
|
||||||
|
|
||||||
|
## How [method advance_to_next_song] and [method advance_to_track] transition between songs.
|
||||||
|
## Does not affect the natural end-of-song advance, which always uses the song's reverb tail.
|
||||||
|
@export var SkipMode : DynamicSoundConstants.SkipMode = DynamicSoundConstants.SkipMode.UseReverbTail:
|
||||||
|
set(value):
|
||||||
|
SkipMode = value;
|
||||||
|
notify_property_list_changed();
|
||||||
|
|
||||||
|
## Crossfade duration in seconds when [member SkipMode] is [code]CrossFade[/code].
|
||||||
|
@export var CrossFadeDuration : float = 1.0;
|
||||||
|
|
||||||
var _core : DynamicSoundPlayerCore;
|
var _core : DynamicSoundPlayerCore;
|
||||||
|
|
||||||
func _set(property: StringName, value: Variant) -> bool:
|
func _set(property: StringName, value: Variant) -> bool:
|
||||||
@@ -39,6 +49,10 @@ func _enter_tree() -> void:
|
|||||||
if bus == &"Master" and AudioServer.get_bus_index("Music") != -1:
|
if bus == &"Master" and AudioServer.get_bus_index("Music") != -1:
|
||||||
bus = "Music";
|
bus = "Music";
|
||||||
|
|
||||||
|
func _validate_property(property: Dictionary) -> void:
|
||||||
|
if property.name == "CrossFadeDuration" and SkipMode != DynamicSoundConstants.SkipMode.CrossFade:
|
||||||
|
property.usage &= ~PROPERTY_USAGE_EDITOR;
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_core = DynamicSoundPlayerCore.new(self);
|
_core = DynamicSoundPlayerCore.new(self);
|
||||||
_core.ready();
|
_core.ready();
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ extends AudioStreamPlayer3D
|
|||||||
## through the playlist and wraps from the last song back to the first.
|
## through the playlist and wraps from the last song back to the first.
|
||||||
@export var LoopMode : DynamicSoundConstants.LoopMode = DynamicSoundConstants.LoopMode.All;
|
@export var LoopMode : DynamicSoundConstants.LoopMode = DynamicSoundConstants.LoopMode.All;
|
||||||
|
|
||||||
|
## How [method advance_to_next_song] and [method advance_to_track] transition between songs.
|
||||||
|
## Does not affect the natural end-of-song advance, which always uses the song's reverb tail.
|
||||||
|
@export var SkipMode : DynamicSoundConstants.SkipMode = DynamicSoundConstants.SkipMode.UseReverbTail:
|
||||||
|
set(value):
|
||||||
|
SkipMode = value;
|
||||||
|
notify_property_list_changed();
|
||||||
|
|
||||||
|
## Crossfade duration in seconds when [member SkipMode] is [code]CrossFade[/code].
|
||||||
|
@export var CrossFadeDuration : float = 1.0;
|
||||||
|
|
||||||
var _core : DynamicSoundPlayerCore;
|
var _core : DynamicSoundPlayerCore;
|
||||||
|
|
||||||
func _set(property: StringName, value: Variant) -> bool:
|
func _set(property: StringName, value: Variant) -> bool:
|
||||||
@@ -39,6 +49,10 @@ func _enter_tree() -> void:
|
|||||||
if bus == &"Master" and AudioServer.get_bus_index("Music") != -1:
|
if bus == &"Master" and AudioServer.get_bus_index("Music") != -1:
|
||||||
bus = "Music";
|
bus = "Music";
|
||||||
|
|
||||||
|
func _validate_property(property: Dictionary) -> void:
|
||||||
|
if property.name == "CrossFadeDuration" and SkipMode != DynamicSoundConstants.SkipMode.CrossFade:
|
||||||
|
property.usage &= ~PROPERTY_USAGE_EDITOR;
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_core = DynamicSoundPlayerCore.new(self);
|
_core = DynamicSoundPlayerCore.new(self);
|
||||||
_core.ready();
|
_core.ready();
|
||||||
|
|||||||
@@ -17,13 +17,22 @@ extends RefCounted
|
|||||||
const _FADE_SILENT_DB : float = -80.0;
|
const _FADE_SILENT_DB : float = -80.0;
|
||||||
|
|
||||||
## Per-song playback record. One exists per song currently audible — usually one,
|
## Per-song playback record. One exists per song currently audible — usually one,
|
||||||
## briefly two during a reverb-tail crossover.
|
## briefly two during a reverb-tail crossover or a crossfade.
|
||||||
class _ActiveSong extends RefCounted:
|
class _ActiveSong extends RefCounted:
|
||||||
var ids : Array[int];
|
var ids : Array[int];
|
||||||
var start_time : float;
|
var start_time : float;
|
||||||
var song_length : float;
|
var song_length : float;
|
||||||
var reverb_tail : float;
|
var reverb_tail : float;
|
||||||
var started_next : bool = false;
|
var started_next : bool = false;
|
||||||
|
# Per-song fade state — independent of the player-wide fade. Used by
|
||||||
|
# CrossFade-mode skips so two songs can fade in opposite directions at once.
|
||||||
|
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_after_fade : bool = false;
|
||||||
|
|
||||||
var _player;
|
var _player;
|
||||||
|
|
||||||
@@ -68,6 +77,18 @@ func process(delta: float) -> void:
|
|||||||
return;
|
return;
|
||||||
# iterate over a snapshot since we may modify _active_songs in the loop
|
# iterate over a snapshot since we may modify _active_songs in the loop
|
||||||
for active in _active_songs.duplicate():
|
for active in _active_songs.duplicate():
|
||||||
|
# advance per-song fade
|
||||||
|
if active.fading:
|
||||||
|
active.fade_elapsed += delta;
|
||||||
|
var t : float = clampf(active.fade_elapsed / active.fade_duration, 0.0, 1.0);
|
||||||
|
active.fade_db = lerp(active.fade_from_db, active.fade_to_db, t);
|
||||||
|
_apply_volumes_to_song(active, _player.volume_db);
|
||||||
|
if t >= 1.0:
|
||||||
|
active.fading = false;
|
||||||
|
if active.stop_after_fade:
|
||||||
|
_stop_active_song(active);
|
||||||
|
_active_songs.erase(active);
|
||||||
|
continue;
|
||||||
var remaining : float = (active.start_time + active.song_length) - _cur_time;
|
var remaining : float = (active.start_time + active.song_length) - _cur_time;
|
||||||
if remaining < active.reverb_tail and not active.started_next:
|
if remaining < active.reverb_tail and not active.started_next:
|
||||||
active.started_next = true;
|
active.started_next = true;
|
||||||
@@ -118,7 +139,7 @@ func advance_to_next_song() -> void:
|
|||||||
advance_to_track((_current_index + 1) % playlist.QueuedSongs.size());
|
advance_to_track((_current_index + 1) % playlist.QueuedSongs.size());
|
||||||
|
|
||||||
## Jump to the song at [param index]. Ignored if the index is out of range.
|
## Jump to the song at [param index]. Ignored if the index is out of range.
|
||||||
## Triggers a smooth transition through the current song's reverb tail.
|
## Transition style is controlled by [code]_player.SkipMode[/code].
|
||||||
func advance_to_track(index: int) -> void:
|
func advance_to_track(index: int) -> void:
|
||||||
var playlist : DynamicSoundPlaylist = _player.Playlist;
|
var playlist : DynamicSoundPlaylist = _player.Playlist;
|
||||||
if playlist == null or index < 0 or index >= playlist.QueuedSongs.size():
|
if playlist == null or index < 0 or index >= playlist.QueuedSongs.size():
|
||||||
@@ -126,14 +147,28 @@ func advance_to_track(index: int) -> void:
|
|||||||
if _playback == null:
|
if _playback == null:
|
||||||
_player.play();
|
_player.play();
|
||||||
_playback = _player.get_stream_playback();
|
_playback = _player.get_stream_playback();
|
||||||
# accelerate the most-recent active song so its remaining time hits the
|
var mode : int = _player.SkipMode;
|
||||||
# reverb tail right now — the outgoing song's tail covers the new one's start
|
var outgoing : _ActiveSong = null;
|
||||||
if not _active_songs.is_empty():
|
if not _active_songs.is_empty():
|
||||||
var current_active : _ActiveSong = _active_songs[_active_songs.size() - 1];
|
outgoing = _active_songs[_active_songs.size() - 1];
|
||||||
current_active.start_time = _cur_time - current_active.song_length + current_active.reverb_tail;
|
if outgoing != null:
|
||||||
current_active.started_next = true;
|
outgoing.started_next = true;
|
||||||
|
match mode:
|
||||||
|
DynamicSoundConstants.SkipMode.UseReverbTail:
|
||||||
|
# accelerate so remaining hits reverb_tail now; the existing
|
||||||
|
# overlap logic in process() will stop the song after that
|
||||||
|
outgoing.start_time = _cur_time - outgoing.song_length + outgoing.reverb_tail;
|
||||||
|
DynamicSoundConstants.SkipMode.CrossFade:
|
||||||
|
_start_song_fade(outgoing, outgoing.fade_db, _FADE_SILENT_DB, _player.CrossFadeDuration, true);
|
||||||
|
DynamicSoundConstants.SkipMode.Jump:
|
||||||
|
_stop_active_song(outgoing);
|
||||||
|
_active_songs.erase(outgoing);
|
||||||
_current_index = index;
|
_current_index = index;
|
||||||
_start_song_at_index(index);
|
_start_song_at_index(index);
|
||||||
|
if mode == DynamicSoundConstants.SkipMode.CrossFade and not _active_songs.is_empty():
|
||||||
|
var incoming : _ActiveSong = _active_songs[_active_songs.size() - 1];
|
||||||
|
_start_song_fade(incoming, _FADE_SILENT_DB, 0.0, _player.CrossFadeDuration, false);
|
||||||
|
_apply_volumes_to_song(incoming, _player.volume_db);
|
||||||
|
|
||||||
func fade_in(duration: float) -> void:
|
func fade_in(duration: float) -> void:
|
||||||
_start_playback();
|
_start_playback();
|
||||||
@@ -151,9 +186,8 @@ func fade_out(duration: float) -> void:
|
|||||||
func apply_stream_volumes(value: float) -> void:
|
func apply_stream_volumes(value: float) -> void:
|
||||||
if _playback == null:
|
if _playback == null:
|
||||||
return;
|
return;
|
||||||
var effective_db : float = value + _fade_db;
|
|
||||||
for active in _active_songs:
|
for active in _active_songs:
|
||||||
_apply_volumes_to_song(active, effective_db);
|
_apply_volumes_to_song(active, value);
|
||||||
|
|
||||||
func _start_playback() -> void:
|
func _start_playback() -> void:
|
||||||
var playlist : DynamicSoundPlaylist = _player.Playlist;
|
var playlist : DynamicSoundPlaylist = _player.Playlist;
|
||||||
@@ -193,13 +227,14 @@ func _start_song_at_index(index: int) -> void:
|
|||||||
if song.Intensity3 != null:
|
if song.Intensity3 != null:
|
||||||
active.ids.append(_playback.play_stream(song.Intensity3));
|
active.ids.append(_playback.play_stream(song.Intensity3));
|
||||||
_active_songs.append(active);
|
_active_songs.append(active);
|
||||||
_apply_volumes_to_song(active, _player.volume_db + _fade_db);
|
_apply_volumes_to_song(active, _player.volume_db);
|
||||||
|
|
||||||
func _stop_active_song(active: _ActiveSong) -> void:
|
func _stop_active_song(active: _ActiveSong) -> void:
|
||||||
for id in active.ids:
|
for id in active.ids:
|
||||||
_playback.stop_stream(id);
|
_playback.stop_stream(id);
|
||||||
|
|
||||||
func _apply_volumes_to_song(active: _ActiveSong, effective_db: float) -> void:
|
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 realIntensity : float;
|
var realIntensity : float;
|
||||||
if active.ids.size() == 3:
|
if active.ids.size() == 3:
|
||||||
realIntensity = _player.Intensity;
|
realIntensity = _player.Intensity;
|
||||||
@@ -219,6 +254,15 @@ func _start_fade(from_db: float, to_db: float, duration: float, stop_after: bool
|
|||||||
_stop_when_faded = stop_after;
|
_stop_when_faded = stop_after;
|
||||||
apply_stream_volumes(_player.volume_db);
|
apply_stream_volumes(_player.volume_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;
|
||||||
|
song.fade_to_db = to_db;
|
||||||
|
song.fade_db = from_db;
|
||||||
|
song.fade_duration = max(duration, 0.0001);
|
||||||
|
song.fade_elapsed = 0.0;
|
||||||
|
song.fading = true;
|
||||||
|
song.stop_after_fade = stop_after;
|
||||||
|
|
||||||
func _stop_after_fade() -> void:
|
func _stop_after_fade() -> void:
|
||||||
for active in _active_songs:
|
for active in _active_songs:
|
||||||
_stop_active_song(active);
|
_stop_active_song(active);
|
||||||
|
|||||||
Reference in New Issue
Block a user