# Godot 4 — Audio Layering & SFX

> **Purpose**: Reference document for Claude Code. Bus hierarchy tuning, impact layering, pitch curves, ducking patterns, spatial values, and adaptive music techniques that make a game *feel* like it sounds expensive. The marinade — not the AudioServer API.

---

## 1. Bus Hierarchy — The dB Ladder

### Why the Default Flat Layout Kills Your Mix

Most devs create Master > Music / SFX / UI and call it done. The problem: combat SFX drown the music, UI clicks compete with explosions, ambient loops fight footsteps. The fix is a **hierarchy with sub-buses** and pre-set dB offsets.

### The Shipped-Game Bus Layout

```
Master (0 dB, Compressor + Limiter)
  |- Music (-8 dB)
  |- SFX (-3 dB)
  |    |- Combat (0 dB relative to SFX)
  |    |- Movement (-4 dB relative to SFX)
  |    |- Environment (-6 dB relative to SFX)
  |- UI (-10 dB)
  |- Ambiance (-14 dB)
```

**Why these numbers work**: Music at -8 dB sits "behind" gameplay — present but never competing with player actions. SFX at -3 dB is the loudest category because player feedback is sacred. UI at -10 dB: clicks and hovers should feel tactile but never intrusive. Ambiance at -14 dB is wallpaper — you notice when it stops, not when it plays.

**Sub-bus rationale**: Combat sounds (sword swings, explosions, hit impacts) share the same SFX bus but need independent volume control. Movement (footsteps, dash whoosh, landing thuds) is quieter than combat but louder than ambiance. Without sub-buses, lowering SFX to tame explosions also kills footsteps.

### dB Hierarchy Per Sound Type (Tested Values)

| Sound | Volume (dB) | Bus | Reference game |
|-------|------------|-----|----------------|
| Player hit received | 0 dB | SFX > Combat | Hollow Knight |
| Sword swing | -4 dB | SFX > Combat | Dead Cells |
| Explosion (large) | -2 dB | SFX > Combat | Nuclear Throne |
| Footstep | -10 dB | SFX > Movement | Celeste |
| Dash / dodge | -6 dB | SFX > Movement | Hades |
| Landing (light) | -8 dB | SFX > Movement | Celeste |
| Landing (heavy) | -4 dB | SFX > Movement | Hollow Knight |
| UI click | -12 dB | UI | Hades |
| UI confirm | -10 dB | UI | Undertale |
| UI cancel / back | -14 dB | UI | Universal |
| Coin / pickup | -8 dB | SFX > Combat | Dead Cells |
| Music (exploration) | -8 dB | Music | Hollow Knight |
| Music (combat) | -6 dB | Music | Hades |
| Wind / rain loop | -14 dB | Ambiance | Celeste |
| Torch / campfire | -12 dB | Ambiance | Hollow Knight |
| Dialogue blip | -6 dB | UI | Undertale |

**The rule**: the louder the sound, the more important it is to the player's immediate survival. Player-hit at 0 dB because you MUST hear you're dying. Footsteps at -10 dB because they confirm movement without demanding attention.

### Setup Code (Sub-Buses)

```gdscript
## audio_bus_setup.gd — Run once at boot
extends Node

func _ready():
    _ensure_bus("Music", "Master", -8.0)
    _ensure_bus("SFX", "Master", -3.0)
    _ensure_bus("Combat", "SFX", 0.0)
    _ensure_bus("Movement", "SFX", -4.0)
    _ensure_bus("Environment", "SFX", -6.0)
    _ensure_bus("UI", "Master", -10.0)
    _ensure_bus("Ambiance", "Master", -14.0)

    _add_master_compressor()
    _add_master_limiter()

func _ensure_bus(bus_name: String, parent: String, volume_db: float) -> void:
    if AudioServer.get_bus_index(bus_name) != -1:
        return
    var idx: int = AudioServer.bus_count
    AudioServer.add_bus(idx)
    AudioServer.set_bus_name(idx, bus_name)
    AudioServer.set_bus_send(idx, parent)
    AudioServer.set_bus_volume_db(idx, volume_db)

func _add_master_compressor() -> void:
    var master_idx: int = AudioServer.get_bus_index("Master")
    var comp := AudioEffectCompressor.new()
    comp.threshold = -10.0       # Start compressing at -10 dB
    comp.ratio = 4.0             # 4:1 — transparent, not pumping
    comp.gain = 4.0              # Makeup gain to recover perceived volume
    comp.attack_us = 20.0        # Fast enough to catch transients
    comp.release_ms = 150.0      # Smooth release, no pumping artifacts
    AudioServer.add_bus_effect(master_idx, comp)

func _add_master_limiter() -> void:
    var master_idx: int = AudioServer.get_bus_index("Master")
    var limiter := AudioEffectLimiter.new()
    limiter.threshold_db = -1.0  # Ceiling at -1 dB — prevents clipping
    limiter.ceiling_db = -0.3    # Hard wall — nothing escapes above this
    limiter.soft_clip_db = 2.0   # Soft clip zone before the wall
    AudioServer.add_bus_effect(master_idx, limiter)
```

**Compressor values explained**: -10 dB threshold with 4:1 ratio catches most peaks without audible pumping. Attack at 20us lets transients punch through (you hear the snap of a sword, then the body gets compressed). Release at 150ms prevents the "breathing" effect where volume audibly rides up and down.

**Limiter as safety net**: the compressor shapes dynamics, the limiter prevents clipping. Ceiling at -0.3 dB leaves headroom for DAC conversion. Without this, 4+ simultaneous combat sounds will clip on the Master bus.

---

## 2. SFX Pitch Variation — The Anti-Fatigue Machine

### Why Identical Sounds Kill Immersion

A footstep played 200 times per minute at the exact same pitch and volume triggers pattern recognition in the auditory cortex. The brain tags it as "fake" within 5-10 repetitions. The fix: pitch and volume micro-variation.

### The Ranges That Work

| Sound type | Pitch range | Volume range | Why |
|-----------|-------------|-------------|-----|
| Footsteps | 0.90 - 1.10 (+-10%) | -3.0 to 0.0 dB | Wide range — feet never sound identical IRL |
| Sword swing | 0.92 - 1.08 (+-8%) | -2.0 to 0.0 dB | Tighter — must still read as "same weapon" |
| UI click | 0.95 - 1.05 (+-5%) | -1.0 to 0.0 dB | Minimal — UI needs consistency |
| Gunshot | 0.88 - 1.12 (+-12%) | -2.0 to 0.0 dB | Wide — echoes and barrel variance |
| Coin pickup | 0.85 - 1.15 (+-15%) | -2.0 to 0.0 dB | Very wide — satisfying chaos |
| Jump | 0.90 - 1.10 (+-10%) | -2.0 to 0.0 dB | Tied to physics feel |
| Hit impact | 0.85 - 1.15 (+-15%) | 0.0 dB (fixed) | Volume stays constant — hit must always read |

### AudioStreamRandomizer Setup (The Right Way)

```gdscript
## Create a randomized footstep stream with 3 variants
func _create_footstep_randomizer() -> AudioStreamRandomizer:
    var randomizer := AudioStreamRandomizer.new()
    randomizer.add_stream(0, preload("res://audio/sfx/step_01.wav"))
    randomizer.add_stream(1, preload("res://audio/sfx/step_02.wav"))
    randomizer.add_stream(2, preload("res://audio/sfx/step_03.wav"))

    # random_pitch is a MULTIPLIER around 1.0
    # 1.1 means pitch varies from 0.9 to 1.1 (10% each direction)
    randomizer.random_pitch = 1.1

    # random_volume_offset_db is a RANGE from -N to 0
    # 3.0 means volume varies from -3 dB to 0 dB
    randomizer.random_volume_offset_db = 3.0

    # Playback mode — sequential avoids repeating the same sample twice
    randomizer.playback_mode = AudioStreamRandomizer.PLAYBACK_RANDOM_NO_REPEATS

    return randomizer
```

**Gotcha**: `random_pitch = 1.2` does NOT mean "pitch between 1.0 and 1.2." It means "pitch between 0.8 and 1.2" (symmetric around 1.0). A value of 1.0 means NO variation.

### Combo Pitch Escalation (The Hades Pattern)

When the player chains hits, each successive hit sounds higher-pitched. This creates a musical crescendo that rewards skill. One semitone up per hit = a pitch multiplier of ~1.06.

```gdscript
## combo_audio.gd — Attach to player or combat manager
extends Node

const SEMITONE: float = 1.059463  # 2^(1/12) — one semitone up
const MAX_COMBO_PITCH: float = 1.5  # Cap at ~7 semitones up
const COMBO_RESET_TIME: float = 1.5  # Seconds before combo breaks

var _combo_count: int = 0
var _combo_timer: float = 0.0

var hit_sound: AudioStream = preload("res://audio/sfx/hit_base.wav")

func _process(delta: float) -> void:
    if _combo_count > 0:
        _combo_timer -= delta
        if _combo_timer <= 0.0:
            _reset_combo()

func register_hit() -> void:
    _combo_count += 1
    _combo_timer = COMBO_RESET_TIME

    # Pitch rises with each hit, capped
    var pitch: float = minf(1.0 * pow(SEMITONE, _combo_count - 1), MAX_COMBO_PITCH)

    # Volume stays constant — every hit must be heard equally
    SFXPool.play(hit_sound, pitch, 0.0)

func _reset_combo() -> void:
    _combo_count = 0
```

**Feeling**: the first hit is a low thud. By hit 5, it's a sharp crack. By hit 8, it's almost a yelp. The player hears their combo building without looking at a counter.

**Design trap**: don't escalate volume alongside pitch. Higher pitch already sounds louder perceptually (Fletcher-Munson curve). Escalating both = ear fatigue by hit 4.

### Descending Pitch for Weight (The Hollow Knight Pattern)

Inverse of combos: when something heavy happens (boss slam, boulder landing, gate closing), pitch goes DOWN. Lower = heavier = more threatening.

```gdscript
## Heavy impact with descending pitch based on weight
func play_heavy_impact(weight: float) -> void:
    # weight: 0.0 = light, 1.0 = earth-shattering
    var pitch: float = lerpf(1.0, 0.65, weight)
    var volume_db: float = lerpf(-6.0, 0.0, weight)
    SFXPool.play(impact_sound, pitch, volume_db)
```

---

## 3. Impact Layering — The 3-Layer Hit

### Why a Single .wav Sounds Flat

A sword hitting an enemy is not one sound. It's three overlapping sounds that happen within 10-50ms of each other. When you play a single .wav, the brain hears "speaker noise." When you layer three, it hears "impact."

### The Three Layers

| Layer | Name | Frequency range | Duration | What it does |
|-------|------|----------------|----------|-------------|
| 1 | **Transient** | High (2-8 kHz) | 5-30ms | The snap / click. Tells the brain "something happened NOW." |
| 2 | **Body** | Low-mid (80-500 Hz) | 50-200ms | The weight. Tells the brain "this thing has mass." |
| 3 | **Tail** | Full range, fading | 200ms-1s | The space. Tells the brain "this happened in a room/cave/arena." |

**Game examples**:
- **Hollow Knight nail hit**: transient = sharp metallic clink, body = meaty thump, tail = ring that decays with slight reverb (the caverns)
- **Dead Cells sword**: transient = fast blade whistle, body = flesh slap, tail = short echo
- **Hades attack**: transient = weapon-specific click, body = low punch, tail = mythological reverb (Underworld acoustics)

### Implementation

```gdscript
## impact_layer.gd — Reusable impact sound system
extends Node

## Transient: the sharp attack. Short, high-frequency.
@export var transient: AudioStream
## Body: the meaty weight. Low-mid, longer.
@export var body: AudioStream
## Tail: reverb/ring/echo. Full spectrum, longest.
@export var tail: AudioStream

@export var transient_volume_db: float = -2.0
@export var body_volume_db: float = 0.0
@export var tail_volume_db: float = -8.0

## Delay between layers (seconds). 0 = simultaneous.
@export var body_delay: float = 0.005    # 5ms after transient
@export var tail_delay: float = 0.02     # 20ms after transient

func play_impact(intensity: float = 1.0) -> void:
    ## intensity: 0.0 = glancing blow, 1.0 = full hit
    var pitch_variance: float = randf_range(0.92, 1.08)

    # Transient — always plays, pitch varies
    SFXPool.play(transient, pitch_variance, transient_volume_db)

    # Body — louder at higher intensity, slightly delayed
    if intensity > 0.3:
        var body_vol: float = body_volume_db - (1.0 - intensity) * 6.0
        get_tree().create_timer(body_delay).timeout.connect(
            func(): SFXPool.play(body, pitch_variance * 0.95, body_vol)
        )

    # Tail — only on strong hits, longest delay
    if intensity > 0.6:
        var tail_vol: float = tail_volume_db - (1.0 - intensity) * 4.0
        get_tree().create_timer(tail_delay).timeout.connect(
            func(): SFXPool.play(tail, pitch_variance * 0.9, tail_vol)
        )
```

**Why the body pitch is 0.95x of transient**: the body should sound slightly lower than the attack. It reinforces the sense of mass. Same pitch on all three layers = one blurry sound. Slightly offset pitches = three distinct textures that fuse into one believable impact.

### Impact Variants by Context

| Hit type | Transient | Body | Tail | Pitch offset |
|---------|-----------|------|------|-------------|
| Blade on flesh | Metal clink | Wet slap | Short thud | body 0.95x |
| Blade on armor | Hard clang | Metallic ring | Long ring | body 0.90x |
| Blunt on flesh | Soft thump | Deep punch | Reverb | body 0.85x |
| Blunt on stone | Sharp crack | Bass rumble | Long echo | body 0.80x |
| Magic projectile | Sparkle/zap | Electric hum | Shimmer tail | body 1.05x |
| Explosion | Air snap | Bass boom | Rolling rumble | body 0.70x |

---

## 4. Music Ducking — Give Space to What Matters

### The Principle

When something important happens (dialogue, a big SFX moment, a boss intro), the music should step back. Not mute — step back. The player doesn't consciously notice; they just hear the important thing clearly.

### Ducking Targets (Tested Values)

| Trigger | Music duck (dB) | Duck speed (s) | Recovery speed (s) | Also duck |
|---------|----------------|----------------|-------------------|-----------|
| Dialogue starts | -12 dB | 0.3 | 0.8 | Ambiance -6 dB |
| Boss intro cutscene | -18 dB | 0.5 | 1.5 | SFX -6 dB |
| Heavy SFX moment | -6 dB | 0.15 | 0.5 | Nothing |
| Pause menu opens | -8 dB | 0.3 | 0.3 | SFX -20 dB |
| Death screen | -12 dB | 0.5 | 2.0 | SFX -12 dB |
| Item pickup (important) | -4 dB | 0.1 | 0.3 | Nothing |

**Speed asymmetry matters**: ducking IN should always be faster than recovery. Quick duck = you instantly hear the dialogue. Slow recovery = the music fades back naturally without an audible "bump." The Celeste approach: 0.3s in, 0.8s out. The brain perceives the duck as "the room got quiet" rather than "someone turned a knob."

### Volume Ducking Implementation

```gdscript
## audio_ducker.gd — Autoload for bus-level ducking
extends Node

var _active_ducks: Dictionary = {}  # bus_name -> Tween

func duck(bus_name: String, target_db: float, duck_time: float = 0.3, recover_time: float = 0.8) -> void:
    var bus_idx: int = AudioServer.get_bus_index(bus_name)
    if bus_idx == -1:
        return

    # Kill any existing tween on this bus
    if _active_ducks.has(bus_name) and is_instance_valid(_active_ducks[bus_name]):
        _active_ducks[bus_name].kill()

    var current_db: float = AudioServer.get_bus_volume_db(bus_idx)
    var tween := create_tween()
    tween.tween_method(
        func(db: float): AudioServer.set_bus_volume_db(bus_idx, db),
        current_db, target_db, duck_time
    ).set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_QUAD)

    _active_ducks[bus_name] = tween

func unduck(bus_name: String, original_db: float, recover_time: float = 0.8) -> void:
    var bus_idx: int = AudioServer.get_bus_index(bus_name)
    if bus_idx == -1:
        return

    if _active_ducks.has(bus_name) and is_instance_valid(_active_ducks[bus_name]):
        _active_ducks[bus_name].kill()

    var current_db: float = AudioServer.get_bus_volume_db(bus_idx)
    var tween := create_tween()
    tween.tween_method(
        func(db: float): AudioServer.set_bus_volume_db(bus_idx, db),
        current_db, original_db, recover_time
    ).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_QUAD)

    _active_ducks[bus_name] = tween
```

### Low-Pass Ducking (The "Underwater" Alternative)

Instead of lowering volume, lower the frequency cutoff. The music becomes muffled instead of quiet. Feels more natural — like hearing music through a wall.

```gdscript
## lowpass_duck.gd — Alternative to volume ducking
extends Node

## The LowPassFilter effect must already exist on the Music bus (add in editor or at boot)
var _music_lowpass_idx: int = -1  # Index of the effect on the bus

func _ready() -> void:
    # Find the LowPassFilter effect on the Music bus
    var music_idx: int = AudioServer.get_bus_index("Music")
    for i in AudioServer.get_bus_effect_count(music_idx):
        if AudioServer.get_bus_effect(music_idx, i) is AudioEffectLowPassFilter:
            _music_lowpass_idx = i
            break

    # If none exists, create one
    if _music_lowpass_idx == -1:
        var lpf := AudioEffectLowPassFilter.new()
        lpf.cutoff_hz = 20500.0  # Fully open = no filtering
        AudioServer.add_bus_effect(music_idx, lpf)
        _music_lowpass_idx = AudioServer.get_bus_effect_count(music_idx) - 1

func duck_lowpass(target_hz: float = 600.0, duration: float = 0.3) -> void:
    var music_idx: int = AudioServer.get_bus_index("Music")
    var effect := AudioServer.get_bus_effect(music_idx, _music_lowpass_idx) as AudioEffectLowPassFilter

    var tween := create_tween()
    tween.tween_property(effect, "cutoff_hz", target_hz, duration)

func unduck_lowpass(duration: float = 0.8) -> void:
    var music_idx: int = AudioServer.get_bus_index("Music")
    var effect := AudioServer.get_bus_effect(music_idx, _music_lowpass_idx) as AudioEffectLowPassFilter

    var tween := create_tween()
    tween.tween_property(effect, "cutoff_hz", 20500.0, duration)
```

**Low-pass target values**:

| Context | Cutoff Hz | Feeling |
|---------|----------|---------|
| Normal (no duck) | 20500 | Full spectrum, no filter |
| Dialogue active | 800 | Music becomes warm rumble, dialogue crystal clear |
| Pause menu | 600 | Heavily muffled, "behind a door" |
| Underwater / submerged | 400 | Deep, oppressive, claustrophobic |
| Death / slow-mo | 300 | Almost bass-only, dreamlike |
| Boss intro cinematic | 500 | Dramatic, "the world goes quiet" |

**Combine both**: duck volume by -6 dB AND drop low-pass to 800 Hz during dialogue. The music becomes both quieter and muffled. This is what Hades does.

---

## 5. Spatial Audio 2D — Making Sounds Live in the World

### max_distance Values That Work

The `max_distance` on `AudioStreamPlayer2D` defines the radius beyond which the sound is silent. Too small = sounds pop in/out. Too large = everything is audible everywhere. These values assume a viewport of ~640x360 (pixel art) or ~1920x1080 (HD).

| Sound type | max_distance (px) | attenuation | panning_strength | Why |
|-----------|-------------------|-------------|-----------------|-----|
| Footstep (enemy) | 400 | 1.5 | 0.8 | Audible when near, directional |
| Sword clash | 600 | 1.0 | 1.0 | Must hear fights happening nearby |
| Explosion | 1200 | 1.0 | 0.6 | Distant booms, less panning |
| Campfire crackle | 300 | 2.0 | 0.5 | Intimate, falls off fast |
| Waterfall / river | 800 | 1.5 | 0.4 | Fills space, gentle panning |
| NPC dialogue blip | 250 | 2.0 | 0.9 | Must be close to hear, very directional |
| Torch / lantern hum | 200 | 2.5 | 0.3 | Background texture, minimal panning |
| Boss roar | 2000 | 0.5 | 0.3 | Heard from far, not very directional |
| Coin pickup | 150 | 3.0 | 0.6 | Tiny radius, player is already on top of it |
| Door / mechanism | 500 | 1.5 | 0.7 | Practical, mid-range |

### Attenuation Curve Explained

- **1.0 (linear)**: volume drops proportionally with distance. Predictable but unrealistic.
- **1.5**: slightly more aggressive than linear. Good default for most game sounds.
- **2.0 (inverse square)**: realistic physical falloff. Sounds drop fast then plateau. Good for small, intimate sounds (fire, whispers).
- **3.0+**: very aggressive falloff. Sound is loud up close, near-silent at half max_distance. Use for tiny sounds that shouldn't travel (UI-like feedback in world space).
- **0.5**: slow falloff. Sound carries far. Use for alarms, boss roars, explosions.

### Panning Strength Guidelines

Panning_strength = 1.0 means full stereo panning (hard left/right). This is disorienting for many sound types.

- **0.3 - 0.5**: ambient sounds (wind, water, general atmosphere). You feel the direction without it pulling your ear.
- **0.6 - 0.8**: gameplay sounds (enemy footsteps, item drops). Directional enough to be useful.
- **0.9 - 1.0**: critical spatial sounds (offscreen enemy attack, NPC calling out). The player NEEDS to know direction.

---

## 6. Silence as a Sound — The Celeste Principle

### When to Remove Sound

Silence is the most powerful sound in your toolbox. Games that never shut up exhaust the player. Strategic silence creates contrast that makes the next sound hit harder.

**Game examples of strategic silence**:
- **Celeste Chapter 5 (Mirror Temple)**: music drops to nothing before the Badeline encounters. The silence amplifies dread. When the chase music kicks in, it hits like a truck because the ear was resting.
- **Hollow Knight (Resting Grounds)**: long ambient silence with only footsteps. After the dense audio of combat areas, the silence communicates grief and solemnity without dialogue.
- **Undertale (Genocide route)**: after certain events, the music doesn't just stop — it's replaced with near-silence and a low drone. The absence of the familiar melody IS the emotion.
- **Dead Cells (between biomes)**: transition rooms have minimal audio. This resets the listener's ear for the next biome's sonic identity.

### The Implementation Pattern

```gdscript
## silence_manager.gd — Orchestrate dramatic silence
extends Node

func enter_silence(fade_out: float = 1.0, ambient_floor_db: float = -30.0) -> void:
    ## Fade all buses to near-silence, keep a low ambient floor
    AudioDucker.duck("Music", -80.0, fade_out)
    AudioDucker.duck("SFX", -80.0, fade_out)
    AudioDucker.duck("Ambiance", ambient_floor_db, fade_out)
    # UI stays audible — player must hear menu feedback
    AudioDucker.duck("UI", -6.0, fade_out)

func exit_silence(music_stream: AudioStream, fade_in: float = 2.0) -> void:
    ## Restore audio — music comes back last for maximum impact
    AudioDucker.unduck("Ambiance", -14.0, fade_in * 0.5)
    AudioDucker.unduck("SFX", -3.0, fade_in * 0.5)
    AudioDucker.unduck("UI", -10.0, fade_in * 0.3)

    # Music delayed by half the fade_in — ambiance returns first, then music hits
    get_tree().create_timer(fade_in * 0.5).timeout.connect(
        func():
            MusicManager.change_music(music_stream, fade_in * 0.5)
    )
```

**The silence budget**: in a 5-minute gameplay loop, aim for 10-30 seconds of near-silence (not counting pause). Safe rooms, transition corridors, post-boss moments. The ear needs rest to maintain dynamic range.

### Pre-Impact Silence (The "Breath Before the Storm")

The 0.3-0.5 second silence before a boss attack lands. The music dips, ambient goes quiet, then SLAM — the hit sound fills the entire dynamic range.

```gdscript
## boss_attack_audio.gd
func telegraph_heavy_attack() -> void:
    # Step 1: Brief silence (0.3s)
    AudioDucker.duck("Music", -20.0, 0.15)
    AudioDucker.duck("Ambiance", -40.0, 0.15)

    await get_tree().create_timer(0.3).timeout

    # Step 2: The hit (full volume, layered)
    impact_layer.play_impact(1.0)

    # Step 3: Recovery (slow unduck)
    AudioDucker.unduck("Music", -8.0, 1.0)
    AudioDucker.unduck("Ambiance", -14.0, 0.8)
```

---

## 7. Adaptive Music — Horizontal vs Vertical

### Horizontal Re-sequencing (Switching Between Tracks)

Different game states = different music tracks. Transition between them on beat boundaries. Godot 4.3+ provides `AudioStreamInteractive` for this.

**When to use**: exploration -> combat -> boss -> victory. Each state has a fundamentally different composition.

### Vertical Layering (Adding/Removing Stems)

Same composition, different intensity. The drums stem enters when combat starts. The strings swell when health is low. The melody drops during stealth.

**When to use**: a single area where tension rises and falls continuously (Hades chambers, roguelike runs).

### AudioStreamSynchronized (Layer Control)

```gdscript
## adaptive_music.gd — Vertical layering with AudioStreamSynchronized
extends Node

var _player: AudioStreamPlayer
var _synced_stream: AudioStreamSynchronized

## Layer indices (match the order added to AudioStreamSynchronized resource)
const LAYER_BASE: int = 0       # Ambient pad, always on
const LAYER_DRUMS: int = 1      # Combat percussion
const LAYER_MELODY: int = 2     # Exploration melody
const LAYER_TENSION: int = 3    # Low strings, danger

func _ready() -> void:
    _player = $AudioStreamPlayer
    _player.bus = "Music"
    # The AudioStreamSynchronized resource is set up in the editor
    # with 4 OGG streams of identical length and tempo
    _synced_stream = _player.stream as AudioStreamSynchronized

func set_combat_layers(in_combat: bool) -> void:
    if not _player.playing:
        return
    # Drums ON in combat, melody OFF
    _synced_stream.set_sync_stream_volume(LAYER_DRUMS,
        0.0 if in_combat else -80.0)
    _synced_stream.set_sync_stream_volume(LAYER_MELODY,
        -80.0 if in_combat else 0.0)

func set_tension(danger: float) -> void:
    ## danger: 0.0 = chill, 1.0 = critical
    if not _player.playing:
        return
    var tension_db: float = lerpf(-80.0, 0.0, danger)
    _synced_stream.set_sync_stream_volume(LAYER_TENSION, tension_db)
```

**Gotcha**: AudioStreamSynchronized requires all streams to be the same length and tempo. Record/export your stems at identical bar counts. Even 1ms drift = audible phasing artifacts after a few loops.

### AudioStreamInteractive (Horizontal Transitions)

Best configured in the editor as a `.tres` resource. The transition table defines how clips switch:

| Transition type | When to use | Feel |
|----------------|-------------|------|
| Immediate | Emergency (death, boss phase change) | Jarring, dramatic |
| At end of bar | Normal state changes (explore -> combat) | Musical, smooth |
| At end of loop | Low-priority changes (ambient shift) | Seamless |
| Crossfade | Between similar moods | Soft, film-like |

**Godot setup**: create an `AudioStreamInteractive` resource in the inspector. Add clips (exploration, combat, boss, victory). Configure the transition table — "exploration to combat" = transition at next bar boundary with 0.5s crossfade. "Combat to boss" = immediate cut for dramatic effect.

---

## 8. Audio Feedback Hierarchy — What MUST Be Heard

### The Priority Stack

Not all sounds are equal. When 15 sounds try to play simultaneously, which ones survive?

| Priority | Sound category | Can be culled? | Can be delayed? | Bus |
|---------|---------------|---------------|----------------|-----|
| 1 (highest) | Player death | Never | Never | SFX > Combat |
| 2 | Player hit received | Never | Never | SFX > Combat |
| 3 | UI confirm / error | Never | Never | UI |
| 4 | Player attack / ability | Rarely | Never | SFX > Combat |
| 5 | Enemy death | Rarely | Up to 1 frame | SFX > Combat |
| 6 | Pickup / collectible | Sometimes | Up to 2 frames | SFX > Combat |
| 7 | Enemy attack | Sometimes | Up to 1 frame | SFX > Combat |
| 8 | Footsteps (player) | Often | Up to 3 frames | SFX > Movement |
| 9 | Footsteps (enemy) | Often | Freely | SFX > Movement |
| 10 (lowest) | Ambient detail | Always | Freely | Ambiance |

**The rule**: if the sound tells the player "you are about to die" or "you just died," it plays unconditionally. If it's flavor, it can wait or be dropped.

### Polyphony Limits

```gdscript
## Per-bus voice limits — set in editor or code
func _setup_polyphony() -> void:
    # Combat bus: max 6 simultaneous sounds
    # Movement bus: max 4 simultaneous sounds
    # Ambiance bus: max 3 simultaneous sounds
    # UI: max 2 simultaneous sounds
    #
    # Godot controls this via AudioStreamPlayer.max_polyphony
    # and the AudioServer voice limit (Project Settings > Audio > Max Polyphony)
    #
    # Project Settings > Audio > General > Max Polyphony = 32 (default)
    # This is the TOTAL across ALL buses. 32 is fine for 2D games.
    # Raise to 48-64 for audio-heavy action games.
    pass
```

---

## 9. Combining Techniques (Stack Examples)

### Stack: "Entering Combat"

Trigger sequence over 0.5-1.0 seconds:

1. Music: vertical layer — drums stem fades in over 0.5s (LAYER_DRUMS from -80 to 0 dB)
2. Music: melody stem fades out over 0.8s (LAYER_MELODY from 0 to -80 dB)
3. Ambiance: duck to -24 dB over 0.3s (birds/wind fade)
4. SFX > Combat bus: unmute if it was muted during exploration
5. First enemy attack SFX plays at full volume — the "punctuation mark"

### Stack: "Boss Entrance"

Trigger sequence over 2-3 seconds:

1. Silence: all buses duck to near-zero over 0.5s (except UI)
2. 0.5s of pure silence — the breath
3. Boss roar: single layered impact (transient + body + tail) at 0 dB
4. Screen shake synced to roar body layer
5. 0.3s after roar: boss music kicks in at full volume (AudioStreamInteractive immediate transition)
6. Ambiance slowly recovers to -14 dB over 2s

### Stack: "Player Death"

1. Time slows (Engine.time_scale = 0.3)
2. Low-pass filter drops to 400 Hz on all buses over 0.2s
3. Death SFX plays at pitch 0.7 (heavy, slowed)
4. Music volume ducks to -18 dB over 0.3s
5. All SFX cease except the death sound tail
6. 1.5s later: silence. Full silence for 1.0s.
7. Respawn jingle fades in — a short, rising phrase. The "you're back" signal.

### Stack: "Picking Up a Rare Item"

1. Pre-silence: 0.15s micro-duck on Music (-4 dB) and SFX (-6 dB)
2. Pickup SFX: ascending 3-note chime (each note one semitone higher)
3. Music recovers over 0.3s
4. If legendary rarity: add a subtle reverb tail (0.5s) and deeper music duck (-8 dB)

---

## Gotchas

1. **`linear_to_db(0.0)` = -INF** — this crashes tweens and produces silence that can't be un-silenced. Always clamp slider minimums to 0.01. Use `maxf(value, 0.01)` before conversion.

2. **AudioStreamRandomizer `random_pitch` is symmetric** — a value of 1.2 means the range is 0.8-1.2, NOT 1.0-1.2. A value of 1.0 = zero variation. This confuses everyone the first time.

3. **Ducking tweens stack if not killed** — calling `duck()` twice without killing the first tween creates two tweens fighting over the same bus volume. Always kill previous tweens on the same bus before creating new ones.

4. **Low-pass filter on Master affects UI sounds too** — if you low-pass the Master bus for a "death muffled" effect, UI clicks also get muffled. Solution: apply low-pass only to Music + SFX + Ambiance buses, not Master. Or add the effect to a "Gameplay" parent bus that UI doesn't route through.

5. **AudioStreamSynchronized streams MUST be identical length** — even 10ms difference causes drift after a few loops. Export all stems from the same project/session at the exact same bar count. Verify with an audio editor before importing.

6. **`volume_db = -80` is not muted** — it's very quiet but not silent. For true mute, use `AudioServer.set_bus_mute()`. For fade-outs that end in silence, tween to -80 then call `.stop()` in the callback.

7. **Bus effects persist across scenes** — effects added via `AudioServer.add_bus_effect()` at runtime stay until explicitly removed. Scene transitions don't clean them up. Track what you added and remove it in `_exit_tree()` or use a dedicated audio setup autoload.

8. **Compression before limiter, always** — compressor shapes dynamics, limiter prevents clipping. Reversed order = the limiter clips first, then the compressor tries to compress an already-clipped signal. Useless.

9. **Spatial audio without AudioListener2D = screen center** — by default, Godot uses the screen center as the listener position. If your camera moves but you don't have an AudioListener2D following the player, spatial sounds pan relative to world center, not camera center. Add AudioListener2D as a child of the camera or player.

10. **Game screenshot timeout from heavy audio processing** — too many bus effects (5+ on Master) can cause frame drops that trigger GolemCapture timeouts. Profile your audio bus chain. If screenshots fail, temporarily disable non-essential effects.

11. **OGG loop points** — Godot uses the `LOOPSTART` and `LOOPLENGTH` metadata tags in OGG files for seamless loops. Without them, there's a tiny gap at the loop point. Set these in your audio editor (Audacity: Edit > Metadata > add LOOPSTART=0 and LOOPLENGTH=total_samples) or in the Godot import settings.

12. **Pitch scaling affects playback duration** — a sound at pitch 0.5 takes twice as long to finish. This matters for SFX pool recycling: a "heavy" hit at 0.65 pitch occupies its pool slot 54% longer than normal. Size your pool accordingly for pitch-heavy designs.
