Continuous music players (DynamicSoundPlayer, 2D, 3D) with intensity-blended layers, playlist queue, Loop, fade in/out, pause/resume. One-shot SFX players (DynamicSoundFXPlayer, 2D, 3D) for overlapping triggers. All variants share RefCounted core classes via duck-typed property access. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
114 lines
5.7 KiB
Markdown
114 lines
5.7 KiB
Markdown
# 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
|
||
|
||
1. Copy the `addons/gd-dynamic-sound/` folder into your project's `addons/` directory.
|
||
2. Enable the plugin in **Project Settings → Plugins**.
|
||
|
||
## 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 `DynamicSound`s.
|
||
|
||
| 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. |
|
||
| `PitchMode` | enum | `Constant` | `Constant` or `Random` (placeholder — pitch logic not yet implemented). |
|
||
| `RandomMinPitchScale` | `float` | `-0.5` | Minimum pitch when `PitchMode = Random`. Hidden in the inspector when `PitchMode = Constant`. |
|
||
| `RandomMaxPitchScale` | `float` | `0.5` | Maximum pitch when `PitchMode = Random`. Hidden in the inspector when `PitchMode = Constant`. |
|
||
|
||
The `stream` property is auto-assigned an `AudioStreamPolyphonic` when the node enters the tree in the editor, so you don't need to configure it manually.
|
||
|
||
#### 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 `Intensity1` length elapses, the player pops it from the queue and plays the next. With `Loop = true`, the popped song is re-appended to the back.
|
||
- **Volume.** `volume_db` is 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 `_ready` and `_process` early-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. |
|
||
|
||
#### Methods
|
||
|
||
| Method | Description |
|
||
| --- | --- |
|
||
| `play_fx()` | Triggers a fresh play of `Sound`. 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
|
||
|
||
```gdscript
|
||
@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](https://ovanisound.com) — these players are designed to drive their dynamic sounds.
|