Files
settled-reach/docs/audio/dialogue-ambient-dip.md
T
jpmschweitzerandClaude Opus 4.6 d0d545f44d feat(audio): add dip spec and synthesis tooling for #440
dialogue-ambient-dip.md: full Godot implementation spec for dialogue
(-7dB ambient) and confrontation (-11dB ambient + LP 800Hz, -5dB world
SFX) audio dips with tween code and edge case handling. synth_ui_sounds
.py: FM/noise/impulse synthesis for insert-tech UI sounds (cursor hover,
weapon aim, monologue chimes).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:04:44 +01:00

8.7 KiB

Dialogue & Confrontation Ambient Dip — Implementation Spec

Audio bus volume/filter changes for dialogue and confrontation states. These are NOT audio files — they're mix parameter changes applied to AudioBus volumes and effects in Godot's AudioServer.

Ticket: #440 Decisions: D-068 (5-bus architecture), D-069 (dip profiles), D-070 (confrontation as cognitive vulnerability) Branch: client (AudioManager implementation), audio (this spec)

Bus Architecture Reference

Bus Index Name Purpose
0 Master Final mix output
1 Music Score (empty for now)
2 Ambient amb_* loops, environmental background
3 World SFX Positional sounds in physical space
4 Player Actions Combat, footsteps, interaction SFX
5 UI Sounds Non-positional interface feedback

Dialogue Dip

Trigger: Dialogue box opens (client #434) Release: Dialogue box closes

Parameter Value
Ambient bus volume -6 to -8 dB (from current)
Ease-in duration 300ms
Ease-out duration 500ms
Easing curve Cubic ease-in-out
Affected buses Ambient only

Godot Implementation

# In AudioManager (autoload singleton)

const DIALOGUE_DIP_DB := -7.0  # midpoint of -6 to -8 range
const DIALOGUE_DIP_IN_MS := 300.0
const DIALOGUE_DIP_OUT_MS := 500.0

var _ambient_bus_idx: int
var _ambient_base_volume_db: float
var _dip_tween: Tween

func _ready() -> void:
    _ambient_bus_idx = AudioServer.get_bus_index("Ambient")
    _ambient_base_volume_db = AudioServer.get_bus_volume_db(_ambient_bus_idx)

func dialogue_dip_start() -> void:
    _cancel_dip_tween()
    var target := _ambient_base_volume_db + DIALOGUE_DIP_DB
    _dip_tween = create_tween()
    _dip_tween.tween_method(
        _set_ambient_volume,
        AudioServer.get_bus_volume_db(_ambient_bus_idx),
        target,
        DIALOGUE_DIP_IN_MS / 1000.0
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

func dialogue_dip_end() -> void:
    _cancel_dip_tween()
    _dip_tween = create_tween()
    _dip_tween.tween_method(
        _set_ambient_volume,
        AudioServer.get_bus_volume_db(_ambient_bus_idx),
        _ambient_base_volume_db,
        DIALOGUE_DIP_OUT_MS / 1000.0
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

func _set_ambient_volume(db: float) -> void:
    AudioServer.set_bus_volume_db(_ambient_bus_idx, db)

func _cancel_dip_tween() -> void:
    if _dip_tween and _dip_tween.is_valid():
        _dip_tween.kill()

Signal Wiring

# In DialogueBox or wherever dialogue state is managed:
func _open_dialogue() -> void:
    # ... show dialogue UI ...
    AudioManager.dialogue_dip_start()

func _close_dialogue() -> void:
    # ... hide dialogue UI ...
    AudioManager.dialogue_dip_end()

Confrontation Dip

Trigger: Confrontation dialogue begins (client #434, confrontation variant) Release: Confrontation dialogue ends Design intent (D-070): "Felt, not computed." The player's focus narrows — the world acoustically recedes. This is cognitive vulnerability made audible.

Parameter Value
Ambient bus volume -11 dB (midpoint of -10 to -12)
Ambient bus low-pass filter 800 Hz cutoff, 6 dB resonance
World SFX bus volume -5 dB (midpoint of -4 to -6)
Ease-in duration 500ms
Ease-out duration 1000ms
Easing curve Cubic ease-in-out
Affected buses Ambient, World SFX

Godot Implementation

The confrontation dip adds a low-pass filter effect to the Ambient bus. This must be set up in the Godot AudioBus layout (Project → Audio Bus Layout):

Bus setup (audio bus layout .tres):

  1. Add AudioEffectLowPassFilter to the Ambient bus
  2. Set it to bypassed by default (effect is inactive until confrontation)
  3. Default cutoff: 20500 Hz (fully open)
const CONFRONTATION_AMBIENT_DIP_DB := -11.0
const CONFRONTATION_SFX_DIP_DB := -5.0
const CONFRONTATION_LP_CUTOFF_HZ := 800.0
const CONFRONTATION_LP_OPEN_HZ := 20500.0
const CONFRONTATION_DIP_IN_MS := 500.0
const CONFRONTATION_DIP_OUT_MS := 1000.0

var _world_sfx_bus_idx: int
var _world_sfx_base_volume_db: float
var _ambient_lp_effect_idx: int  # index of the LP filter on Ambient bus
var _confrontation_tween: Tween

func _ready() -> void:
    # ... (ambient bus setup from dialogue dip above) ...
    _world_sfx_bus_idx = AudioServer.get_bus_index("World SFX")
    _world_sfx_base_volume_db = AudioServer.get_bus_volume_db(_world_sfx_bus_idx)
    # Find the LP filter effect index on the Ambient bus
    for i in range(AudioServer.get_bus_effect_count(_ambient_bus_idx)):
        if AudioServer.get_bus_effect(_ambient_bus_idx, i) is AudioEffectLowPassFilter:
            _ambient_lp_effect_idx = i
            break

func confrontation_dip_start() -> void:
    _cancel_confrontation_tween()

    # Enable the LP filter
    AudioServer.set_bus_effect_enabled(_ambient_bus_idx, _ambient_lp_effect_idx, true)

    var amb_target := _ambient_base_volume_db + CONFRONTATION_AMBIENT_DIP_DB
    var sfx_target := _world_sfx_base_volume_db + CONFRONTATION_SFX_DIP_DB
    var dur := CONFRONTATION_DIP_IN_MS / 1000.0

    _confrontation_tween = create_tween()
    _confrontation_tween.set_parallel(true)

    # Ambient volume dip
    _confrontation_tween.tween_method(
        _set_ambient_volume,
        AudioServer.get_bus_volume_db(_ambient_bus_idx),
        amb_target, dur
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

    # World SFX volume dip
    _confrontation_tween.tween_method(
        _set_world_sfx_volume,
        AudioServer.get_bus_volume_db(_world_sfx_bus_idx),
        sfx_target, dur
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

    # Low-pass filter sweep
    var lp_effect: AudioEffectLowPassFilter = AudioServer.get_bus_effect(
        _ambient_bus_idx, _ambient_lp_effect_idx
    )
    _confrontation_tween.tween_property(
        lp_effect, "cutoff_hz",
        CONFRONTATION_LP_CUTOFF_HZ, dur
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

func confrontation_dip_end() -> void:
    _cancel_confrontation_tween()
    var dur := CONFRONTATION_DIP_OUT_MS / 1000.0

    _confrontation_tween = create_tween()
    _confrontation_tween.set_parallel(true)

    # Restore ambient volume
    _confrontation_tween.tween_method(
        _set_ambient_volume,
        AudioServer.get_bus_volume_db(_ambient_bus_idx),
        _ambient_base_volume_db, dur
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

    # Restore world SFX volume
    _confrontation_tween.tween_method(
        _set_world_sfx_volume,
        AudioServer.get_bus_volume_db(_world_sfx_bus_idx),
        _world_sfx_base_volume_db, dur
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

    # Open LP filter back up
    var lp_effect: AudioEffectLowPassFilter = AudioServer.get_bus_effect(
        _ambient_bus_idx, _ambient_lp_effect_idx
    )
    _confrontation_tween.tween_property(
        lp_effect, "cutoff_hz",
        CONFRONTATION_LP_OPEN_HZ, dur
    ).set_ease(Tween.EASE_IN_OUT).set_trans(Tween.TRANS_CUBIC)

    # Disable LP filter after tween completes
    _confrontation_tween.chain().tween_callback(func():
        AudioServer.set_bus_effect_enabled(
            _ambient_bus_idx, _ambient_lp_effect_idx, false
        )
    )

func _set_world_sfx_volume(db: float) -> void:
    AudioServer.set_bus_volume_db(_world_sfx_bus_idx, db)

func _cancel_confrontation_tween() -> void:
    if _confrontation_tween and _confrontation_tween.is_valid():
        _confrontation_tween.kill()

Edge Cases

Confrontation during dialogue

Confrontation dip supersedes dialogue dip (it's deeper). If dialogue is active when confrontation starts, skip directly to confrontation levels. When confrontation ends, restore to dialogue dip levels (not base), then to base when dialogue ends.

Rapid open/close

The tween-kill-and-restart pattern handles this — a new dip start/end always kills the current tween and starts from the current actual volume, preventing jarring jumps.

Player volume slider interaction

Dips are RELATIVE to _ambient_base_volume_db. If the player adjusts their Ambient slider mid-dip, update _ambient_base_volume_db and recalculate the target. The AudioManager settings save/load system should call a method to refresh base volumes.

ListeningFocus Boost (D-069)

When the player is in active listening mode (future sprint), World SFX gets a +2-3 dB boost instead of a dip. This is the inverse of confrontation — the character is paying MORE attention to the environment.

Parameter Value
World SFX bus volume +2.5 dB (midpoint of +2 to +3)
Ease-in 200ms
Ease-out 300ms

Implementation follows the same tween pattern. Mutually exclusive with confrontation dip.