---
name: godot-audio-systems
description: "Routes Godot audio through AudioStreamPlayer, AudioStreamPlayer2D/3D, AudioBus mixing, AudioServer effects, voice pools, and music crossfade (AudioStreamInteractive/Synchronized). Use when the user wants spatial SFX, bus reverb/EQ, or BPM-synced stems. Trigger: linear_to_db, attenuation_model, AudioEffect. Not for AnimationPlayer TYPE_AUDIO subtitle sync as the mixer. Never set_bus_volume_db with linear 0-1 values or instantiate a node per one-shot clap."
version: 1.0.1
---

## When to Use
Use this skill when implementing audio systems in Godot 4.7+, including:
- Music systems, sound effects, spatial audio, or audio-reactive gameplay.
- Trigger keywords: AudioStreamPlayer, AudioStreamPlayer2D, AudioStreamPlayer3D, AudioBus, AudioServer, AudioEffect, music_crossfade, audio_pool, positional_audio, reverb, bus_volume.

## Prerequisites
- Godot 4.7+ project.
- Windows host (PowerShell) primary environment.
- This folder is the skill. Do not depend on a private on-demand cache path.

## Procedure

### 1. AudioStreamPlayer Variants

#### AudioStreamPlayer (Global/UI)
```gdscript
# No spatial positioning, same volume everywhere
# Use for: Music, UI sounds, voiceovers

@onready var music := AudioStreamPlayer.new()

func _ready() -> void:
    music.stream = load("res://audio/music_main.ogg")
    music.volume_db = -10  # Quieter
    music.autoplay = false
    music.bus = "Music"  # Route to Music bus
    add_child(music)
    music.play()
```

#### AudioStreamPlayer2D (Positional)
```gdscript
# 2D panning based on distance from camera
# Use for: 2D games, top-down audio cues

extends Area2D

@onready var footstep := AudioStreamPlayer2D.new()

func _ready() -> void:
    footstep.stream = load("res://audio/footstep.ogg")
    footstep.max_distance = 500  # Audible range (pixels)
    footstep.attenuation = 2.0  # Falloff curve (higher = faster fadeout)
    add_child(footstep)

func play_footstep() -> void:
    if not footstep.playing:
        footstep.play()
```

#### AudioStreamPlayer3D (Spatial)
```gdscript
# 3D spatial audio with doppler, reverb send
# Use for: 3D games, realistic sound positioning

extends Node3D

@onready var explosion := AudioStreamPlayer3D.new()

func _ready() -> void:
    explosion.stream = load("res://audio/explosion.ogg")
    explosion.unit_size = 10.0  # Size of sound source
    explosion.max_distance = 100.0  # Range
    explosion.attenuation_model = AudioStreamPlayer3D.ATTENUATION_INVERSE_DISTANCE
    explosion.doppler_tracking = AudioStreamPlayer3D.DOPPLER_TRACKING_PHYSICS_STEP
    add_child(explosion)
    
    explosion.play()
```

### 2. AudioBus Architecture

#### Bus Setup (Project Settings)
```
Master (always exists)
  ├─ Music
  │   └─ Effects: Compressor, EQ
  ├─ SFX
  │   └─ Effects: Reverb (for environment)
  └─ Ambient
      └─ Effects: LowPassFilter (muffled ambience)
```

#### Volume Control (Decibels)
```gdscript
# ❌ BAD: Linear volume (doesn't work)
AudioServer.set_bus_volume_db(music_bus_idx, 0.5)  # WRONG!

# ✅ GOOD: Use decibels
var music_bus := AudioServer.get_bus_index("Music")
AudioServer.set_bus_volume_db(music_bus, -10)  # -10 dB (quieter)

# Convert linear (0.0-1.0) to dB:
var linear_volume := 0.5  # 50%
var db := linear_to_db(linear_volume)  # ~-6 dB
AudioServer.set_bus_volume_db(music_bus, db)

# Convert dB to linear:
var current_db := AudioServer.get_bus_volume_db(music_bus)
var linear := db_to_linear(current_db)
print("Current volume: %d%%" % int(linear * 100))
```

#### Mute Bus
```gdscript
func toggle_mute(bus_name: String) -> void:
    var bus_idx := AudioServer.get_bus_index(bus_name)
    var is_muted := AudioServer.is_bus_mute(bus_idx)
    AudioServer.set_bus_mute(bus_idx, not is_muted)
```

### 3. Audio Pooling (Performance)

#### Problem: Creating Players Every Frame
```gdscript
# ❌ BAD: Creates 60 new nodes/second at 60 FPS
func play_footstep() -> void:
    var player := AudioStreamPlayer.new()
    add_child(player)
    player.stream = load("res://audio/footstep.ogg")
    player.finished.connect(player.queue_free)
    player.play()
    # Result: 3600 nodes created in 1 minute!
```

#### Solution: Audio Pool
```gdscript
# audio_pool.gd (AutoLoad)
extends Node

const POOL_SIZE = 10
var pool: Array[AudioStreamPlayer] = []
var pool_index := 0

func _ready() -> void:
    # Pre-create players
    for i in range(POOL_SIZE):
        var player := AudioStreamPlayer.new()
        player.bus = "SFX"
        add_child(player)
        pool.append(player)

func play_sound(stream: AudioStream, volume_db := 0.0) -> void:
    var player := pool[pool_index]
    pool_index = (pool_index + 1) % POOL_SIZE  # Round-robin
    
    # Stop previous sound if still playing
    if player.playing:
        player.stop()
    
    player.stream = stream
    player.volume_db = volume_db
    player.play()

# Usage:
AudioPool.play_sound(load("res://audio/coin.ogg"), -5.0)
```

### 4. Music Transitions

#### Crossfade Between Tracks
```gdscript
# music_manager.gd (AutoLoad)
extends Node

@onready var track_a := AudioStreamPlayer.new()
@onready var track_b := AudioStreamPlayer.new()

var current_track: AudioStreamPlayer
var fade_duration := 2.0

func _ready() -> void:
    track_a.bus = "Music"
    track_b.bus = "Music"
    add_child(track_a)
    add_child(track_b)
    current_track = track_a

func crossfade_to(new_stream: AudioStream) -> void:
    var next_track := track_b if current_track == track_a else track_a
    
    # Start new track at 0 dB
    next_track.stream = new_stream
    next_track.volume_db = -80  # Silent
    next_track.play()
    
    # Fade out current, fade in next
    var tween := create_tween().set_parallel(true)
    tween.tween_property(current_track, "volume_db", -80, fade_duration)
    tween.tween_property(next_track, "volume_db", 0, fade_duration)
    
    await tween.finished
    
    # Stop old track
    current_track.stop()
    current_track = next_track
```

#### BPM-Synced Transitions
```gdscript
# Transition on beat boundary
var bpm := 120.0  # Beats per minute
var beat_duration := 60.0 / bpm  # 0.5s per beat

func queue_transition_on_beat(new_stream: AudioStream) -> void:
    # Wait for next beat
    var current_time := current_track.get_playback_position()
    var time_to_next_beat := beat_duration - fmod(current_time, beat_duration)
    
    await get_tree().create_timer(time_to_next_beat).timeout
    crossfade_to(new_stream)
```

### 5. Dynamic Audio Effects

#### Add Effect at Runtime
```gdscript
# Add reverb to SFX bus
var sfx_bus := AudioServer.get_bus_index("SFX")
var reverb := AudioEffectReverb.new()
reverb.room_size = 0.8  # Large room
reverb.damping = 0.5
reverb.wet = 0.3  # 30% effect, 70% dry
AudioServer.add_bus_effect(sfx_bus, reverb)
```

#### Underwater Effect
```gdscript
func set_underwater(enabled: bool) -> void:
    var sfx_bus := AudioServer.get_bus_index("SFX")
    
    if enabled:
        # Add low-pass filter (muffled sound)
        var lowpass := AudioEffectLowPassFilter.new()
        lowpass.cutoff_hz = 500  # Cut frequencies above 500 Hz
        AudioServer.add_bus_effect(sfx_bus, lowpass)
    else:
        # Remove all effects
        for i in range(AudioServer.get_bus_effect_count(sfx_bus)):
            AudioServer.remove_bus_effect(sfx_bus, 0)
```

### 6. Procedural Audio

#### Synthesize Beep
```gdscript
# Generate simple sine wave
func create_beep(frequency: float, duration: float) -> AudioStreamGenerator:
    var stream := AudioStreamGenerator.new()
    stream.mix_rate = 44100  # Sample rate
    
    var playback := stream.instantiate_playback()
    
    var increment := frequency / stream.mix_rate
    var phase := 0.0
    
    for i in range(int(stream.mix_rate * duration)):
        var sample := sin(phase * TAU)
        playback.push_frame(Vector2(sample, sample))  # Stereo
        phase += increment
        phase = fmod(phase, 1.0)
    
    return stream

# Usage:
var beep_stream := create_beep(440.0, 0.1)  # 440 Hz (A4), 0.1s
$AudioStreamPlayer.stream = beep_stream
$AudioStreamPlayer.play()
```

### 7. Advanced Patterns

#### Audio Ducking (Lower Music During Dialogue)
```gdscript
# auto_duck.gd (on Dialogue AudioStreamPlayer)
extends AudioStreamPlayer

func _ready() -> void:
    playing.connect(_on_playing)
    finished.connect(_on_finished)

func _on_playing() -> void:
    # Duck music to -15 dB
    var music_bus := AudioServer.get_bus_index("Music")
    var tween := create_tween()
    tween.tween_method(set_music_volume, 0.0, -15.0, 0.5)

func _on_finished() -> void:
    # Restore music to 0 dB
    var tween := create_tween()
    tween.tween_method(set_music_volume, -15.0, 0.0, 0.5)

func set_music_volume(db: float) -> void:
    var music_bus := AudioServer.get_bus_index("Music")
    AudioServer.set_bus_volume_db(music_bus, db)
```

#### Randomize Pitch for Variation
```gdscript
# Prevent identical sounds (footsteps, gunshots)
func play_varied_sound(stream: AudioStream) -> void:
    $AudioStreamPlayer.stream = stream
    $AudioStreamPlayer.pitch_scale = randf_range(0.9, 1.1)  # ±10% pitch
    $AudioStreamPlayer.play()
```

#### Layered Music (Adaptive)
```gdscript
# Intensity-based music layers (start quiet, add layers as intensity increases)
# Example: Peaceful exploration → Combat

@onready var layer_drums := $Music/Drums
@onready var layer_bass := $Music/Bass
@onready var layer_melody := $Music/Melody

var intensity := 0.0  # 0.0 = calm, 1.0 = intense

func _ready() -> void:
    # Start all layers in sync
    layer_drums.play()
    layer_bass.play()
    layer_melody.play()
    
    # Mute high-intensity layers
    layer_bass.volume_db = -80
    layer_melody.volume_db = -80

func set_music_intensity(new_intensity: float) -> void:
    intensity = clamp(new_intensity, 0.0, 1.0)
    
    # Fade in layers based on intensity
    var tween := create_tween().set_parallel(true)
    
    # Layer 1 (drums): always audible
    tween.tween_property(layer_drums, "volume_db", 0, 1.0)
    
    # Layer 2 (bass): fade in at 33% intensity
    var bass_db := -80 if intensity < 0.33 else lerp(-80.0, 0.0, (intensity - 0.33) / 0.67)
    tween.tween_property(layer_bass, "volume_db", bass_db, 1.0)
    
    # Layer 3 (melody): fade in at 66% intensity
    var melody_db := -80 if intensity < 0.66 else lerp(-80.0, 0.0, (intensity - 0.66) / 0.34)
    tween.tween_property(layer_melody, "volume_db", melody_db, 1.0)

# Usage (combat system):
func _on_enemy_spotted() -> void:
    MusicManager.set_music_intensity(1.0)  # Full intensity

func _on_all_enemies_defeated() -> void:
    MusicManager.set_music_intensity(0.0)  # Back to calm
```

### 8. Expert Audio Patterns

#### Subtitle-Sync-System
Standard pattern for perfectly timed dialogue using `AnimationPlayer`.
- **Tracks**: Use an **Audio Playback Track** for the voice-over and a **Call Method Track** to trigger subtitles.
- **Localization**: Pass a translation key (e.g., `show_subtitle("VO_LINE_01")`) as the method argument. The subtitle script uses `tr()` to fetch the localized text, ensuring perfect sync across all languages.

#### Audio-Occlusion-Profiling (Dynamic Muffling)
Simulate sound muffling behind obstacles using built-in filters.
- **Occlusion Detection**: Use a `RayCast3D` from the `AudioStreamPlayer3D` towards the listener.
- **Filter Modulation**: If the raycast is blocked, use a `Tween` to lower the `attenuation_filter_cutoff_hz` (e.g., from 20500 Hz to 1500 Hz) to create a muffled effect.
- **Room Occlusion**: Use `Area3D` with **Audio Bus Overrides** to redirect sound to a bus with a `LowPassFilter` when the source/listener enters a specific room.

#### Interactive-Music-Graph (Non-Linear Scoring)
Godot 4's advanced music resources for dynamic transitions.
- **Horizontal Re-sequencing**: Use `AudioStreamInteractive`. It functions as a state machine where you can define transitions between clips (e.g., "Exploration" to "Combat") that snap to the next beat or bar automatically.
- **Vertical Layering**: Use `AudioStreamSynchronized` to play multiple stems in perfect sync. Dynamically fade stem volumes (e.g., adding drums during intensity) using `set_sync_stream_volume()`.

### 9. Performance Optimization

#### Disable Far Audio
```gdscript
# Don't play sounds the player can't hear
extends AudioStreamPlayer3D

func _process(delta: float) -> void:
    var listener := get_viewport().get_camera_3d()
    if not listener:
        return
    
    var distance := global_position.distance_to(listener.global_position)
    
    if distance > max_distance * 1.5:  # 1.5x max range
        if playing:
            stop()
```

### 10. High-Precision Subtitle Sync
To prevent subtitle drift in long audio tracks, don't rely on simple timers.
- **Formula**: `Position = get_playback_position() + get_time_since_last_mix() - get_output_latency()`.
- **Implementation**: Poll this value in `_process` and compare against a timestamped subtitle track.

### 11. Vertical Music Layering
Use `AudioStreamSynchronized` to keep multiple stems (drums, bass, melodies) perfectly aligned.
- **Dynamic Mix**: Use `set_sync_stream_volume(index, db)` to fade layers in/out based on combat intensity or exploration progress.
- **Tweening**: Always use a `Tween` to interpolate volume changes over 1-2 seconds for a natural transition.

## Available Scripts

> **MANDATORY**: Read the appropriate script before implementing the corresponding pattern.

- **[scripts/audio_voice_pool_manager.gd](scripts/audio_voice_pool_manager.gd)**: Load before implementing high-performance voice pooling with priority-based voice stealing.
- **[scripts/audio_occlusion_raycast.gd](scripts/audio_occlusion_raycast.gd)**: Load before implementing Raycast-based audio occlusion for dynamic muffling.
- **[scripts/audio_interactive_music_manager.gd](scripts/audio_interactive_music_manager.gd)**: Load before implementing vertical music layering using AudioStreamSynchronized.
- **[scripts/audio_reactive_visualizer_component.gd](scripts/audio_reactive_visualizer_component.gd)**: Load before implementing FFT spectrum analysis for visuals.
- **[scripts/audio_bus_ducker_logic.gd](scripts/audio_bus_ducker_logic.gd)**: Load before implementing sidechain-style bus ducking.
- **[scripts/audio_procedural_generator_synth.gd](scripts/audio_procedural_generator_synth.gd)**: Load before implementing real-time synthesizer for procedural hums, engines, and signals.
- **[scripts/audio_environmental_reverb_zone.gd](scripts/audio_environmental_reverb_zone.gd)**: Load before implementing dynamic reverb/bus effect management via Area3D trigger zones.
- **[scripts/audio_voice_limiter_manager.gd](scripts/audio_voice_limiter_manager.gd)**: Load before implementing concurrency manager to cap identical SFX instances.
- **[scripts/audio_linear_volume_interpolator.gd](scripts/audio_linear_volume_interpolator.gd)**: Load before implementing smooth UI volume slider mapping.
- **[scripts/audio_footstep_surface_selector.gd](scripts/audio_footstep_surface_selector.gd)**: Load before implementing physics-driven surface detection for footsteps.

## Pitfalls

### Mixing & Buses
- **NEVER set bus volume with linear values** — `set_bus_volume_db()` is logarithmic. Use `linear_to_db()` for sliders OR everything will sound too loud until the last 5%.
- **NEVER skip 'Bus Routing'** — Playing music on the 'SFX' bus makes volume menus useless. Strictly route every player to its dedicated sub-bus (Music, SFX, UI, Voice).
- **NEVER use 'Master' for gameplay sounds** — Dedicate Master to final limiting. Route all gameplay to sub-groups so you can mute/duck categories.

### Positional & Spatial
- **NEVER use 3D players without an Attenuation Model** — Default is NONE. If you don't set it to `Inverse Distance`, a whisper on the other side of the map will be global volume.
- **NEVER play 3D sounds exactly on top of the listener** — Causes "Panning Jitter" where the sound snaps between Left/Right speakers. Offset by `0.1` units.
- **NEVER forget Doppler for high-speed objects** — A car flying by without `DOPPLER_TRACKING_PHYSICS_STEP` feels flat and static.

### Performance & Polish
- **NEVER spam same-frame sounds** — Playing 50 explosions at once causes constructive interference (clipping/distortion). Use a `Limiter` (`audio_voice_limiter_manager.gd`).
- **NEVER instantiate nodes for one-shots** — Creating a node, playing a 0.5s clap, and `queue_free()`ing causes frame-time spikes. Use a Pool.
- **NEVER skip Crossfades/Transitions** — Abrupt music cuts break immersion. Always use a 0.5s-1.0s `Tween` to bridge tracks.

### Godot 4.7 Breaking Changes
- `AudioEffectSpectrumAnalyzer.tap_back_pos` **removed** — migrate analyzers to alternative tap APIs.
- `AudioStreamPlayer` default `area_mask` is now **0** (disabled), not layer 1. If using `Area2D`/`Area3D` `audio_bus_override`, explicitly set `area_mask` to layer 1 or your bus layer.

## Verification

### Check if audio doesn't play
```gdscript
# Check:
# 1. Is stream assigned?
if not $AudioStreamPlayer.stream:
    push_error("No audio stream assigned!")

# 2. Is bus muted?
var bus_idx := AudioServer.get_bus_index($AudioStreamPlayer.bus)
if AudioServer.is_bus_mute(bus_idx):
    print("Bus is muted!")

# 3. Is volume too low?
if $AudioStreamPlayer.volume_db < -60:
    print("Volume too quiet (< -60 dB)")
```

### Decision Matrix: Which AudioStreamPlayer?

| Feature | AudioStreamPlayer | AudioStreamPlayer2D | AudioStreamPlayer3D |
|---------|------------------|---------------------|---------------------|
| **Spatial** | ❌ Global | ✅ 2D panning | ✅ 3D positioning |
| **Doppler** | ❌ | ❌ | ✅ |
| **Attenuation** | ❌ | ✅ Distance-based | ✅ 3D falloff |
| **Reverb send** | ❌ | ❌ | ✅ |
| **Use for** | Music, UI | 2D games | 3D games |
| **Performance** | Fastest | Medium | Slowest |

## Related skills
- Master Skill: [godot-master](../godot-master/SKILL.md)
