Files
gd-dynamic-sound/README.md
T
Daniel SamsonandClaude Opus 4.7 cb47d89377 Initial commit: dynamic music & FX players
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>
2026-04-29 14:46:48 +01:00

114 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.