audio-system

Use when implementing audio — audio buses, AudioStreamPlayer, spatial audio, music management, SFX pooling, and dynamic mixing

Audio System in Godot 4.3+

All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.

Related skills: event-bus for decoupled audio triggers, save-load for persisting audio settings, resource-pattern for audio data containers.


1. Core Concepts

Audio Node Types

NodeDimensionsUse For
AudioStreamPlayerNon-positionalMusic, UI sounds, global SFX
AudioStreamPlayer2D2D positionalFootsteps, gunfire, environmental sounds
AudioStreamPlayer3D3D positionalSame as 2D but in 3D space

Audio Bus Architecture

Godot routes all audio through buses (like a mixing console).

Master (always exists)
├── Music          → volume, effects for background music
├── SFX            → volume, effects for sound effects
│   ├── Footsteps  → sub-bus for fine-tuning
│   └── Weapons    → sub-bus for fine-tuning
└── UI             → volume for menu sounds

Setup: Bottom panel → Audio tab → Add buses, set names, route outputs.

Every AudioStreamPlayer has a bus property — set it to the target bus name (e.g., "SFX", "Music").


2. Basic Audio Playback

GDScript

extends Node2D

@onready var sfx_player: AudioStreamPlayer2D = $AudioStreamPlayer2D
@onready var music_player: AudioStreamPlayer = $MusicPlayer

func _ready() -> void:
    # Play background music (looping is set on the AudioStream resource)
    music_player.play()

func play_jump_sound() -> void:
    sfx_player.stream = preload("res://audio/sfx/jump.wav")
    sfx_player.play()

C#

using Godot;

public partial class AudioExample : Node2D
{
    private AudioStreamPlayer2D _sfxPlayer;
    private AudioStreamPlayer _musicPlayer;

    public override void _Ready()
    {
        _sfxPlayer = GetNode<AudioStreamPlayer2D>("AudioStreamPlayer2D");
        _musicPlayer = GetNode<AudioStreamPlayer>("MusicPlayer");
        _musicPlayer.Play();
    }

    public void PlayJumpSound()
    {
        _sfxPlayer.Stream = GD.Load<AudioStream>("res://audio/sfx/jump.wav");
        _sfxPlayer.Play();
    }
}

Looping Audio

Looping is configured on the AudioStream resource, not the player node:

  • WAV: Import tab → Loop Mode → Forward (or Ping-Pong)
  • OGG: Import tab → Loop → On, set Loop Offset
  • MP3: Import tab → Loop → On

Always use OGG Vorbis for music (smaller files, good quality). Use WAV for short SFX (no decoding latency). Avoid MP3 for SFX — it adds silence at the start.


3. Audio Bus Management

Setting Volume from Code

Godot uses decibels (dB) for volume. Linear-to-dB conversion is required for sliders.

GDScript

# Get bus index by name
var bus_index: int = AudioServer.get_bus_index("SFX")

# Set volume in dB directly
AudioServer.set_bus_volume_db(bus_index, -6.0)  # -6 dB = ~50% perceived volume

# Convert linear (0.0–1.0) to dB — use for UI sliders
func set_bus_volume_linear(bus_name: String, linear: float) -> void:
    var index := AudioServer.get_bus_index(bus_name)
    AudioServer.set_bus_volume_db(index, linear_to_db(linear))

# Mute / unmute a bus
AudioServer.set_bus_mute(bus_index, true)

# Read current volume as linear (for displaying on a slider)
func get_bus_volume_linear(bus_name: String) -> float:
    var index := AudioServer.get_bus_index(bus_name)
    return db_to_linear(AudioServer.get_bus_volume_db(index))

C#

int busIndex = AudioServer.GetBusIndex("SFX");

// Set volume in dB
AudioServer.SetBusVolumeDb(busIndex, -6.0f);

// Linear to dB conversion for UI sliders
public void SetBusVolumeLinear(string busName, float linear)
{
    int index = AudioServer.GetBusIndex(busName);
    AudioServer.SetBusVolumeDb(index, Mathf.LinearToDb(linear));
}

// Mute / unmute
AudioServer.SetBusMute(busIndex, true);

// Read current volume as linear
public float GetBusVolumeLinear(string busName)
{
    int index = AudioServer.GetBusIndex(busName);
    return Mathf.DbToLinear(AudioServer.GetBusVolumeDb(index));
}

Audio Bus Effects

Add effects to buses in the Audio panel (bottom dock). Common effects:

EffectUse For
ReverbCave, cathedral, bathroom ambience
DelayEcho effects
CompressorNormalize loud/quiet sounds (master bus)
LimiterPrevent clipping on master bus
LowPassFilterMuffled sounds (underwater, behind walls)
HighPassFilterThin/tinny sound (radio, phone)
ChorusThicken sounds
DistortionGritty/overdrive effects
EQFine-tune frequency bands

Dynamic Effect Toggle

# Enable/disable an effect on a bus at runtime
var bus_index := AudioServer.get_bus_index("SFX")
var effect_index := 0  # First effect on the bus
AudioServer.set_bus_effect_enabled(bus_index, effect_index, true)

# Apply low-pass filter for "underwater" feel
func set_underwater(enabled: bool) -> void:
    var index := AudioServer.get_bus_index("SFX")
    # Assumes a LowPassFilter is the first effect on the SFX bus
    AudioServer.set_bus_effect_enabled(index, 0, enabled)

4. Spatial Audio (2D & 3D)

AudioStreamPlayer2D

Automatically adjusts volume and panning based on distance to the nearest AudioListener2D (or the Camera2D if no listener exists).

Enemy (CharacterBody2D)
├── Sprite2D
└── AudioStreamPlayer2D   ← positioned at enemy's location
    bus = "SFX"
    max_distance = 1000.0
    attenuation = 1.0

Key properties:

PropertyDescriptionDefault
max_distanceBeyond this distance, sound is silent2000.0
attenuationVolume falloff curve (1.0 = linear, higher = sharper)1.0
max_polyphonyMax simultaneous instances of this player1
panning_strengthHow much the sound pans left/right1.0

AudioStreamPlayer3D

Same concept but in 3D. Works with AudioListener3D (or the Camera3D).

Key additional properties:

PropertyDescription
unit_sizeDistance at which volume is 0 dB
max_dbMaximum volume cap
attenuation_modelInverse, InverseSquare, Logarithmic, Disabled
doppler_trackingEnable Doppler effect for moving sources

AudioListener

# Make a specific camera the audio listener
# 2D: add AudioListener2D as child of Camera2D, call make_current()
# 3D: add AudioListener3D as child of Camera3D, call make_current()

# By default, the current Camera2D/3D acts as the listener.
# Only add an explicit AudioListener if you need a different listening position.

5. Music Manager (Autoload)

A global music manager that handles crossfading between tracks.

GDScript

# music_manager.gd — add as autoload named MusicManager
extends Node

@export var crossfade_duration: float = 1.5

var _player_a: AudioStreamPlayer
var _player_b: AudioStreamPlayer
var _active_player: AudioStreamPlayer

func _ready() -> void:
    _player_a = AudioStreamPlayer.new()
    _player_a.bus = "Music"
    add_child(_player_a)

    _player_b = AudioStreamPlayer.new()
    _player_b.bus = "Music"
    add_child(_player_b)

    _active_player = _player_a


func play_music(stream: AudioStream, from_position: float = 0.0) -> void:
    # If already playing this track, do nothing
    if _active_player.stream == stream and _active_player.playing:
        return

    var next_player := _player_b if _active_player == _player_a else _player_a

    # Crossfade
    next_player.stream = stream
    next_player.volume_db = -80.0
    next_player.play(from_position)

    var tween := create_tween().set_parallel(true)
    tween.tween_property(_active_player, "volume_db", -80.0, crossfade_duration)
    tween.tween_property(next_player, "volume_db", 0.0, crossfade_duration)
    tween.chain().tween_callback(_active_player.stop)

    _active_player = next_player


func stop_music(fade_duration: float = 1.0) -> void:
    var tween := create_tween()
    tween.tween_property(_active_player, "volume_db", -80.0, fade_duration)
    tween.tween_callback(_active_player.stop)


func set_music_volume(linear: float) -> void:
    var index := AudioServer.get_bus_index("Music")
    AudioServer.set_bus_volume_db(index, linear_to_db(linear))

C#

using Godot;

public partial class MusicManager : Node
{
    [Export] public float CrossfadeDuration { get; set; } = 1.5f;

    private AudioStreamPlayer _playerA;
    private AudioStreamPlayer _playerB;
    private AudioStreamPlayer _activePlayer;

    public override void _Ready()
    {
        _playerA = new AudioStreamPlayer { Bus = "Music" };
        AddChild(_playerA);

        _playerB = new AudioStreamPlayer { Bus = "Music" };
        AddChild(_playerB);

        _activePlayer = _playerA;
    }

    public void PlayMusic(AudioStream stream, float fromPosition = 0.0f)
    {
        if (_activePlayer.Stream == stream && _activePlayer.Playing)
            return;

        var nextPlayer = _activePlayer == _playerA ? _playerB : _playerA;

        nextPlayer.Stream = stream;
        nextPlayer.VolumeDb = -80.0f;
        nextPlayer.Play(fromPosition);

        var tween = CreateTween().SetParallel(true);
        tween.TweenProperty(_activePlayer, "volume_db", -80.0f, CrossfadeDuration);
        tween.TweenProperty(nextPlayer, "volume_db", 0.0f, CrossfadeDuration);
        tween.Chain().TweenCallback(Callable.From(_activePlayer.Stop));

        _activePlayer = nextPlayer;
    }

    public void StopMusic(float fadeDuration = 1.0f)
    {
        var tween = CreateTween();
        tween.TweenProperty(_activePlayer, "volume_db", -80.0f, fadeDuration);
        tween.TweenCallback(Callable.From(_activePlayer.Stop));
    }

    public void SetMusicVolume(float linear)
    {
        int index = AudioServer.GetBusIndex("Music");
        AudioServer.SetBusVolumeDb(index, Mathf.LinearToDb(linear));
    }
}

Usage:

# From any script:
MusicManager.play_music(preload("res://audio/music/boss_theme.ogg"))
MusicManager.stop_music(2.0)

6. SFX Pool

Prevent overlapping sounds and manage polyphony with a reusable SFX pool.

GDScript

# sfx_pool.gd — add as autoload named SFXPool
extends Node

@export var pool_size: int = 16

var _players: Array[AudioStreamPlayer] = []
var _index: int = 0


func _ready() -> void:
    for i in pool_size:
        var player := AudioStreamPlayer.new()
        player.bus = "SFX"
        add_child(player)
        _players.append(player)


func play(stream: AudioStream, volume_db: float = 0.0, pitch_scale: float = 1.0) -> void:
    var player := _players[_index]
    _index = (_index + 1) % pool_size

    player.stream = stream
    player.volume_db = volume_db
    player.pitch_scale = pitch_scale
    player.play()


func play_random_pitch(stream: AudioStream, min_pitch: float = 0.9, max_pitch: float = 1.1) -> void:
    play(stream, 0.0, randf_range(min_pitch, max_pitch))

C#

using Godot;

public partial class SfxPool : Node
{
    [Export] public int PoolSize { get; set; } = 16;

    private AudioStreamPlayer[] _players;
    private int _index;

    public override void _Ready()
    {
        _players = new AudioStreamPlayer[PoolSize];
        for (int i = 0; i < PoolSize; i++)
        {
            var player = new AudioStreamPlayer { Bus = "SFX" };
            AddChild(player);
            _players[i] = player;
        }
    }

    public void Play(AudioStream stream, float volumeDb = 0.0f, float pitchScale = 1.0f)
    {
        var player = _players[_index];
        _index = (_index + 1) % PoolSize;

        player.Stream = stream;
        player.VolumeDb = volumeDb;
        player.PitchScale = pitchScale;
        player.Play();
    }

    public void PlayRandomPitch(AudioStream stream, float minPitch = 0.9f, float maxPitch = 1.1f)
    {
        Play(stream, 0.0f, (float)GD.RandRange(minPitch, maxPitch));
    }
}

Usage:

# Slightly randomize pitch to avoid repetitive sound
SFXPool.play_random_pitch(preload("res://audio/sfx/footstep.wav"))

# Fixed pitch and volume
SFXPool.play(preload("res://audio/sfx/explosion.wav"), -3.0, 1.0)

Why pool? Creating new AudioStreamPlayer nodes every frame is wasteful. A pool recycles a fixed number of players in round-robin order. If pool_size players are already playing, the oldest one gets interrupted — this is usually fine for SFX.


7. 2D Positional SFX Pool

For spatial sounds that need position in 2D space.

GDScript

# sfx_pool_2d.gd — add as autoload named SFXPool2D
extends Node

@export var pool_size: int = 16

var _players: Array[AudioStreamPlayer2D] = []
var _index: int = 0


func _ready() -> void:
    for i in pool_size:
        var player := AudioStreamPlayer2D.new()
        player.bus = "SFX"
        add_child(player)
        _players.append(player)


func play_at(stream: AudioStream, global_pos: Vector2, volume_db: float = 0.0, pitch_scale: float = 1.0) -> void:
    var player := _players[_index]
    _index = (_index + 1) % pool_size

    player.global_position = global_pos
    player.stream = stream
    player.volume_db = volume_db
    player.pitch_scale = pitch_scale
    player.play()

C#

using Godot;

public partial class SfxPool2D : Node
{
    [Export] public int PoolSize { get; set; } = 16;

    private AudioStreamPlayer2D[] _players;
    private int _index;

    public override void _Ready()
    {
        _players = new AudioStreamPlayer2D[PoolSize];
        for (int i = 0; i < PoolSize; i++)
        {
            var player = new AudioStreamPlayer2D { Bus = "SFX" };
            AddChild(player);
            _players[i] = player;
        }
    }

    public void PlayAt(AudioStream stream, Vector2 globalPos, float volumeDb = 0.0f, float pitchScale = 1.0f)
    {
        var player = _players[_index];
        _index = (_index + 1) % PoolSize;

        player.GlobalPosition = globalPos;
        player.Stream = stream;
        player.VolumeDb = volumeDb;
        player.PitchScale = pitchScale;
        player.Play();
    }
}

Usage:

# Play an explosion sound at the enemy's position
SFXPool2D.play_at(
    preload("res://audio/sfx/explosion.wav"),
    enemy.global_position,
    -3.0
)

8. Audio Settings Integration

Tie audio buses to a settings UI. Works well with the save-load skill's ConfigFile approach.

GDScript

# In your settings/audio menu script:
@onready var master_slider: HSlider = %MasterSlider
@onready var music_slider: HSlider = %MusicSlider
@onready var sfx_slider: HSlider = %SFXSlider

func _ready() -> void:
    # Load saved values (0.0–1.0 linear)
    master_slider.value = _get_saved_volume("master")
    music_slider.value = _get_saved_volume("music")
    sfx_slider.value = _get_saved_volume("sfx")

    # Apply to buses
    _apply_volume("Master", master_slider.value)
    _apply_volume("Music", music_slider.value)
    _apply_volume("SFX", sfx_slider.value)

func _on_master_slider_value_changed(value: float) -> void:
    _apply_volume("Master", value)
    _save_volume("master", value)

func _on_music_slider_value_changed(value: float) -> void:
    _apply_volume("Music", value)
    _save_volume("music", value)

func _on_sfx_slider_value_changed(value: float) -> void:
    _apply_volume("SFX", value)
    _save_volume("sfx", value)

func _apply_volume(bus_name: String, linear: float) -> void:
    var index := AudioServer.get_bus_index(bus_name)
    if linear <= 0.01:
        AudioServer.set_bus_mute(index, true)
    else:
        AudioServer.set_bus_mute(index, false)
        AudioServer.set_bus_volume_db(index, linear_to_db(linear))

func _get_saved_volume(key: String) -> float:
    # Integrate with your settings system (see save-load skill)
    return SettingsManager.get_setting("audio", "%s_volume" % key, 1.0)

func _save_volume(key: String, value: float) -> void:
    SettingsManager.set_setting("audio", "%s_volume" % key, value)

C#

using Godot;

public partial class AudioSettings : Control
{
    private HSlider _masterSlider;
    private HSlider _musicSlider;
    private HSlider _sfxSlider;

    public override void _Ready()
    {
        _masterSlider = GetNode<HSlider>("%MasterSlider");
        _musicSlider = GetNode<HSlider>("%MusicSlider");
        _sfxSlider = GetNode<HSlider>("%SFXSlider");

        _masterSlider.Value = GetSavedVolume("master");
        _musicSlider.Value = GetSavedVolume("music");
        _sfxSlider.Value = GetSavedVolume("sfx");

        ApplyVolume("Master", (float)_masterSlider.Value);
        ApplyVolume("Music", (float)_musicSlider.Value);
        ApplyVolume("SFX", (float)_sfxSlider.Value);

        _masterSlider.ValueChanged += v => { ApplyVolume("Master", (float)v); SaveVolume("master", (float)v); };
        _musicSlider.ValueChanged += v => { ApplyVolume("Music", (float)v); SaveVolume("music", (float)v); };
        _sfxSlider.ValueChanged += v => { ApplyVolume("SFX", (float)v); SaveVolume("sfx", (float)v); };
    }

    private void ApplyVolume(string busName, float linear)
    {
        int index = AudioServer.GetBusIndex(busName);
        if (linear <= 0.01f)
            AudioServer.SetBusMute(index, true);
        else
        {
            AudioServer.SetBusMute(index, false);
            AudioServer.SetBusVolumeDb(index, Mathf.LinearToDb(linear));
        }
    }

    private float GetSavedVolume(string key)
    {
        // Integrate with your settings system (see save-load skill)
        return 1.0f;
    }

    private void SaveVolume(string key, float value)
    {
        // Integrate with your settings system (see save-load skill)
    }
}

9. Audio Import Best Practices

FormatUse ForFile SizeDecode LatencyLoop Support
WAVShort SFXLargeNone (PCM)Via import
OGGMusic, long SFXSmallMinimalVia import
MP3Music (fallback)SmallHas paddingVia import

Import Settings

In the Import dock (select an audio file):

  • Loop: Enable for music and ambient loops
  • BPM / Beat Count / Bar Beats: Set for rhythm-synced games
  • Force Mono: Enable for 3D positional audio (stereo doesn't spatialize well)

Tip: Keep SFX as 16-bit WAV at 44.1kHz. Godot stores WAV uncompressed in PCK, so they play instantly with zero decode overhead. For music, OGG Vorbis at quality 6–8 is a good balance.


10. Interactive & Adaptive Music (Godot 4.3+)

Godot 4.3 introduced specialized AudioStream types for dynamic music that responds to gameplay.

AudioStreamPlaylist

Plays a sequence of audio streams in order. Use for level music with intro → loop → outro structure, or for jukebox-style playlists.

# Create a playlist with intro and loop
var playlist := AudioStreamPlaylist.new()
playlist.stream_count = 2
playlist.set_list_stream(0, preload("res://audio/music/boss_intro.ogg"))
playlist.set_list_stream(1, preload("res://audio/music/boss_loop.ogg"))
# BPM sync ensures transitions land on beat boundaries
playlist.bpm = 120.0
playlist.shuffle = false
playlist.loop = true    # loop the entire playlist

$MusicPlayer.stream = playlist
$MusicPlayer.play()
var playlist = new AudioStreamPlaylist();
playlist.StreamCount = 2;
playlist.SetListStream(0, GD.Load<AudioStream>("res://audio/music/boss_intro.ogg"));
playlist.SetListStream(1, GD.Load<AudioStream>("res://audio/music/boss_loop.ogg"));
playlist.Shuffle = false;
playlist.Loop = true;

GetNode<AudioStreamPlayer>("MusicPlayer").Stream = playlist;
GetNode<AudioStreamPlayer>("MusicPlayer").Play();

AudioStreamSynchronized

Plays multiple streams in perfect sync, allowing you to blend layers. Use for adaptive music — e.g. a base track with combat percussion that fades in when enemies appear.

# Three synchronized layers: base, drums, intensity
var sync := AudioStreamSynchronized.new()
sync.stream_count = 3
sync.set_sync_stream(0, preload("res://audio/music/ambient_base.ogg"))
sync.set_sync_stream(1, preload("res://audio/music/ambient_drums.ogg"))
sync.set_sync_stream(2, preload("res://audio/music/ambient_intense.ogg"))

# Set initial volumes — layer 0 full, others silent
sync.set_sync_stream_volume(0, 0.0)   # 0 dB
sync.set_sync_stream_volume(1, -80.0)  # silent
sync.set_sync_stream_volume(2, -80.0)  # silent

$MusicPlayer.stream = sync
$MusicPlayer.play()

# When combat starts — fade in drums layer
func _on_combat_started() -> void:
    var playback := $MusicPlayer.get_stream_playback() as AudioStreamPlaybackSynchronized
    # Tween the volume of layer 1 from silent to audible
    var tween := create_tween()
    tween.tween_method(
        func(db: float) -> void: playback.set_stream_volume(1, db),
        -80.0, 0.0, 1.5
    )
var sync = new AudioStreamSynchronized();
sync.StreamCount = 3;
sync.SetSyncStream(0, GD.Load<AudioStream>("res://audio/music/ambient_base.ogg"));
sync.SetSyncStream(1, GD.Load<AudioStream>("res://audio/music/ambient_drums.ogg"));
sync.SetSyncStream(2, GD.Load<AudioStream>("res://audio/music/ambient_intense.ogg"));
sync.SetSyncStreamVolume(0, 0.0f);
sync.SetSyncStreamVolume(1, -80.0f);
sync.SetSyncStreamVolume(2, -80.0f);

GetNode<AudioStreamPlayer>("MusicPlayer").Stream = sync;
GetNode<AudioStreamPlayer>("MusicPlayer").Play();

// When combat starts — fade in drums layer
public void OnCombatStarted()
{
    var playback = GetNode<AudioStreamPlayer>("MusicPlayer").GetStreamPlayback() as AudioStreamPlaybackSynchronized;
    var tween = CreateTween();
    tween.TweenMethod(Callable.From<float>(db => playback.SetStreamVolume(1, db)), -80.0f, 0.0f, 1.5);
}

AudioStreamInteractive

Transitions between music clips based on triggers (e.g. exploration → combat → boss). Define transition rules: crossfade, fade-to-silence, or immediate switch. Set transitions between clips by index with configurable fade times.

Note: AudioStreamInteractive is configured primarily through the Inspector — set up clips and transition rules in the AudioStreamInteractive resource editor. Code triggers the switch:

# Switch to clip index 2 (e.g. "combat" clip)
var playback: AudioStreamPlaybackInteractive = $MusicPlayer.get_stream_playback()
playback.switch_to_clip(2)
var playback = GetNode<AudioStreamPlayer>("MusicPlayer").GetStreamPlayback() as AudioStreamPlaybackInteractive;
playback.SwitchToClip(2);

When to Use Which

Stream TypeUse For
AudioStreamPlaylistSequential tracks (intro → loop), jukeboxes
AudioStreamSynchronizedLayered music with volume blending
AudioStreamInteractiveState-based music with designed transitions

Runtime WAV Loading (Godot 4.4+)

Godot 4.4 allows loading WAV files at runtime from user directories (mods, user-generated content):

func load_wav_from_path(path: String) -> AudioStreamWAV:
    var file := FileAccess.open(path, FileAccess.READ)
    if not file:
        push_error("Cannot open audio file: %s" % path)
        return null

    var wav := AudioStreamWAV.new()
    wav.data = file.get_buffer(file.get_length())
    wav.format = AudioStreamWAV.FORMAT_16_BITS
    wav.mix_rate = 44100
    wav.stereo = false
    return wav

Warning: Runtime-loaded audio bypasses Godot's import system. You must set format, mix_rate, and stereo manually to match the actual file. Incorrect values produce garbled audio.


11. Common Pitfalls

SymptomCauseFix
Sound doesn't playPlayer not in the scene treeEnsure the AudioStreamPlayer is add_child()'d before play()
Sound plays but no audio heardWrong bus name or bus is mutedCheck bus property matches a bus name exactly (case-sensitive)
Music restarts on scene changePlayer is part of the scene, not an autoloadMove music player to an autoload (MusicManager)
Positional audio has no panningNo AudioListener or Camera in the sceneAdd an AudioListener2D/3D or ensure a Camera is current
Sound clicks or popsAudio file has no fade-in/fade-outAdd a tiny fade (2–5ms) at start/end of WAV in audio editor
Too many sounds playing at onceNo polyphony limitSet max_polyphony on players or use an SFX pool
Volume slider feels non-linearUsing dB directly instead of linear conversionUse linear_to_db() / db_to_linear() for slider values
3D audio sounds mono/flatStereo source fileImport as mono (Force Mono in Import tab) for 3D spatialization
MP3 has silence at startMP3 format adds encoder paddingUse WAV for timing-critical SFX, OGG for music

12. Implementation Checklist

  • Audio buses are set up: Master, Music, SFX (minimum)
  • All AudioStreamPlayer nodes have the correct bus property assigned
  • Music uses OGG Vorbis format; short SFX uses WAV
  • Music player is in an autoload (survives scene changes)
  • Music crossfading is implemented for smooth transitions
  • SFX pool is used instead of creating new AudioStreamPlayer nodes dynamically
  • Volume sliders use linear_to_db() / db_to_linear() conversion
  • Near-zero slider values mute the bus (avoid linear_to_db(0.0) = -inf)
  • 3D audio sources use mono audio files for proper spatialization
  • Audio settings are saved and restored on game launch (ConfigFile or similar)
  • Looping is configured on the AudioStream resource, not in code
  • Interactive/adaptive music uses AudioStreamPlaylist, AudioStreamSynchronized, or AudioStreamInteractive (Godot 4.3+) instead of manual track-switching code