DynamicSound and DynamicSoundPlaylist were still pointing @icon at the non-existent res://addons/gd-dynamic-audio/src/*.png paths, causing "Failed loading resource" errors at editor load. Pointed both at the placeholder icon.svg used by the player wrappers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gd-dynamic-sound
A Godot 4 audio addon for dynamic music (intensity-layered, seamless playlist transitions, fade in/out) and overlapping one-shot SFX — both built on AudioStreamPolyphonic.
Installation
- Copy the
addons/gd-dynamic-sound/folder into your project'saddons/directory. - Enable the plugin in Project Settings → Plugins.
- (Optional, recommended) Wire up the
MusicandFXbuses — see below.
Audio buses
When a player enters the tree in the editor, it auto-routes to a named bus if one exists:
DynamicSoundPlayerfamily → looks for a bus namedMusicDynamicSoundFXPlayerfamily → looks for a bus namedFX
If the named bus doesn't exist, the player stays on Master. User-customised buses are never overridden — the auto-route only fires when bus is still the default Master.
A ready-made layout containing both buses (each routing into Master) ships with the addon at addons/gd-dynamic-sound/default_bus_layout.tres. To use it:
- Whole-project replacement: copy it to your project root as
default_bus_layout.tres, or point Godot at the addon's copy via Project Settings → General → Audio → Buses → Default Bus Layout. - Merge into an existing layout: open Godot's audio bus panel and add
MusicandFXbuses manually, both sending toMaster.
Concepts
DynamicSound (Resource)
A single piece of audio with up to three intensity layers that play simultaneously and are blended at runtime.
| Property | Type | Description |
|---|---|---|
Intensity1 |
AudioStream |
Least-intense layer (e.g. ambient). Required for length tracking. |
Intensity2 |
AudioStream |
Mid-intensity layer. Optional. |
Intensity3 |
AudioStream |
Most-intense layer. Optional. |
ReverbTail |
float |
Length (seconds) of the song's audio that is pure reverb tail. The next song is started this far ahead of the current song's full length, so the new opening covers the outgoing tail without a perceptible gap. Set to 0 to disable overlap (hard cut). |
DynamicSoundPlaylist (Resource)
An ordered list of DynamicSounds.
| Property | Type | Description |
|---|---|---|
QueuedSongs |
Array[DynamicSound] |
Songs to play in order. The continuous player tracks an internal index; the resource is not mutated as songs play. |
Nodes
Two node families, each with three positional variants:
| Family | Non-positional | 2D | 3D |
|---|---|---|---|
| Continuous music | DynamicSoundPlayer |
DynamicSoundPlayer2D |
DynamicSoundPlayer3D |
| One-shot SFX | DynamicSoundFXPlayer |
DynamicSoundFXPlayer2D |
DynamicSoundFXPlayer3D |
The 2D variants apply distance attenuation from a 2D position. The 3D variants are fully spatialised — distance, directionality, and Doppler.
DynamicSoundPlayer family — continuous music
Plays through a playlist one song at a time, blending three intensity layers per song.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
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. |
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. |
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.
Methods
| Method | Description |
|---|---|
play_playlist() |
Starts (or resumes) the playlist. Use this when autoplay is false and you want to start playback programmatically. |
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. |
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_track(index: int) |
Same as advance_to_next_song, but jumps to a specific index. Out-of-range indices are ignored. |
Behaviour
- Autoplay. On
_ready, plays the song at the current index only ifautoplayis true (the inheritedAudioStreamPlayer.autoplayproperty; defaulttrue). Setautoplay = falseto defer playback to aplay_playlist()orfade_in()call. - Reverb-tail transitions. When the current song is within
ReverbTailseconds of its full length, the next song (chosen perLoopMode) 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_dbis 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. - Editor. All scripts are
@tool, but_readyand_processearly-return in the editor so audio doesn't play in the inspector.
DynamicSoundFXPlayer family — overlapping one-shot SFX
Each play_fx() call fires a fresh trigger of the assigned DynamicSound. Triggers overlap freely — the underlying AudioStreamPolyphonic cleans up each trigger when its audio ends.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
Sound |
DynamicSound |
null |
The sound to trigger. Layers with intensity blending if all three are set. |
Intensity |
float (0–1) |
0 |
Volume mix between layers. Only blended when all three layers are present; otherwise the first non-null layer plays at full volume. |
PitchMode |
enum | Constant |
Constant plays each trigger at pitch 1.0. Random picks a fresh pitch per trigger via 1.0 + randf_range(RandomMinPitchScale, RandomMaxPitchScale). |
RandomMinPitchScale |
float |
-0.5 |
Minimum pitch offset (added to 1.0) when PitchMode = Random. Hidden in the inspector when PitchMode = Constant. |
RandomMaxPitchScale |
float |
0.5 |
Maximum pitch offset (added to 1.0) when PitchMode = Random. Hidden in the inspector when PitchMode = Constant. |
When the node enters the tree in the editor, stream is auto-assigned an AudioStreamPolyphonic and bus is set to "FX" if a bus by that name exists in the project (otherwise it's left at "Master"). User-customised buses are not overridden.
For a constant non-1.0 pitch, set the player's built-in pitch_scale property — that multiplies with whatever PitchMode produces, so e.g. pitch_scale = 0.8 + PitchMode = Random with default range gives an effective pitch in [0.4, 1.2].
Methods
| Method | Description |
|---|---|
play_fx() |
Triggers a fresh play of Sound. Pitch is sampled at trigger time per PitchMode. Volumes are computed at trigger time and locked in — later changes to Intensity or volume_db don't affect already-playing triggers. |
Example: collision SFX
@onready var fire_fx : DynamicSoundFXPlayer2D = $FireFX
func _on_body_entered(body) -> void:
fire_fx.play_fx()
Architecture
The six player classes are thin wrappers (~70 lines each) around two shared RefCounted cores:
DynamicSoundPlayerCore— continuous-music logic (advancement, fades, pause, intensity blending).DynamicSoundFXPlayerCore— one-shot trigger logic.
Each wrapper declares its exports, sets up a core in _ready, and forwards public methods. The cores access their owner via duck-typed property lookups (_player.Playlist, _player.volume_db, etc.), which is what lets the same core drive non-positional, 2D, and 3D wrappers without inheritance.
Honourable mentions
This addon was heavily inspired by the Ovani Plugin 1.4. Please consider supporting OvaniSound — these players are designed to drive their dynamic sounds.