7.4 KiB
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 reverb tail, used for seamless looping. |
DynamicSoundPlaylist (Resource)
An ordered list of DynamicSounds.
| Property | Type | Description |
|---|---|---|
QueuedSongs |
Array[DynamicSound] |
Songs to play in order. The continuous player pops songs off the front as each finishes. |
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. Mutated as songs finish. |
Intensity |
float (0–1) |
0 |
Runtime mix between the three layers. 0 = layer 1 dominates, 1 = layer 3 dominates. |
Loop |
bool |
false |
When true, finished songs are re-queued at the back so the playlist cycles forever. |
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 |
|---|---|
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. |
fade_out(duration: float) |
Ramps volume from current level to silent over duration seconds, then stops the polyphonic session. A subsequent fade_in reinitialises. |
Behaviour
- Autoplay. On
_ready, plays the front song of the playlist. - Advancement. When a song's
Intensity1length elapses, the player pops it from the queue and plays the next. WithLoop = true, the popped song is re-appended to the back. - Volume.
volume_dbis honoured; the fade adjustment is layered on top internally, so manual volume changes during a fade still work correctly. - 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.