Skills/godot-audio/SKILL.md

8.4 KiB

name description license compatibility metadata
godot-audio Audio patterns for Godot 4.x: AudioStreamPlayer hierarchy, bus mixing, spatial audio (2D/3D), randomization, dynamic music, and AudioListener positioning. Use when implementing sound effects, music systems, or audio mixing in Godot games. MIT
godot-4.0
godot-4.1
godot-4.2
godot-4.3
godot-4.4
godot-4.5
godot-4.6
godot-4.7
author version tags created
vl4dt 0.1.0
godot
audio
sound
music
spatial-audio
bus-mixing
2026-06-21

Audio in Godot

Patterns for game audio implementation.

AudioStreamPlayer Hierarchy

Godot provides separate nodes for 2D and 3D audio:

# 2D audio — attached near the sound source
# AudioStreamPlayer2D (auto-pans based on position relative to AudioListener2D)
var sfx_player = AudioStreamPlayer2D.new()
sfx_player.stream = preload("res://sounds/footstep.ogg")
add_child(sfx_player)
sfx_player.play()

# 3D audio — auto-attenuates and pans based on distance to AudioListener3D
var sfx_player_3d = AudioStreamPlayer3D.new()
sfx_player_3d.stream = preload("res://sounds/explosion.ogg")
sfx_player_3d.unit_size = 1.0  # Meters per audio unit
add_child(sfx_player_3d)
sfx_player_3d.play()

AudioListener Placement

# Camera typically has AudioListener3D as a child
# The camera's position determines where the "ears" are in 3D space
$Camera3D.add_child(AudioListener3D.new())

# For 2D, attach to the main camera or viewport center
$Camera2D.add_child(AudioListener2D.new())

Bus Mixing

Route audio through buses for independent volume/effects control:

# Configure buses via code (or use Project Settings → Audio)
func setup_audio_buses() -> void:
    var bus_count = AudioServer.bus_count

    # Find bus index by name
    var music_bus = AudioServer.get_bus_index("Music")
    var sfx_bus = AudioServer.get_bus_index("SFX")
    var ambient_bus = AudioServer.get_bus_index("Ambient")

    # Set volume (linear, not dB)
    AudioServer.set_bus_volume_db(music_bus, linear_to_db(0.7))
    AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(1.0))
    AudioServer.set_bus_volume_db(ambient_bus, linear_to_db(0.5))

    # Mute individual buses
    AudioServer.set_bus_mute(sfx_bus, false)

    # Solo for debugging
    AudioServer.set_bus_solo(music_bus, true)

Per-Player Bus Assignment

# Assign a player node to a specific bus
$AudioStreamPlayer.bus = "SFX"  # By name
# Or by index:
$AudioStreamPlayer.bus = AudioServer.get_bus_index("SFX")

Spatial Audio (3D)

AudioStreamPlayer3D automatically handles distance attenuation and stereo panning:

class_name EnemyExplosion extends Node3D

var explosion_player: AudioStreamPlayer3D

func _ready() -> void:
    explosion_player = AudioStreamPlayer3D.new()
    explosion_player.stream = preload("res://sounds/explosion.ogg")
    explosion_player.unit_size = 1.0  # 1 unit = 1 meter
    explosion_player.max_distance = 50.0
    explosion_player.attenuation_model = AudioStreamPlayer3D.ATTENUATION_INVERSE_DISTANCE
    add_child(explosion_player)

func explode() -> void:
    explosion_player.play()
    # Visual effects here...

Spatial Audio Models

Model Use Case
ATTENUATION_DISABLED UI sounds, non-spatial audio in 3D scenes
ATTENUATION_INVERSE_DISTANCE Default — realistic falloff
ATTENUATION_LINEAR_DISTANCE Linear volume drop with distance

Sound Effect Randomization

Avoid the "machine gun" effect by randomizing pitch and playback:

class_name SFXPool extends Node

@export var sounds: Array[AudioStream] = []
var _players: Array[AudioStreamPlayer] = []
@export var pool_size: int = 8

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

func play_random() -> void:
    if sounds.is_empty():
        return

    # Find first available player
    for player in _players:
        if not player.playing:
            player.stream = sounds[randi() % sounds.size()]
            # Randomize pitch slightly (0.9 to 1.1)
            player.pitch_scale = randf_range(0.9, 1.1)
            player.play()
            return

    # All busy — steal the first one
    _players[0].stream = sounds[randi() % sounds.size()]
    _players[0].pitch_scale = randf_range(0.9, 1.1)
    _players[0].play()

Dynamic Music System

Crossfade between music tracks based on game state:

class_name MusicManager extends Node

var _current_player: AudioStreamPlayer = null
var _next_player: AudioStreamPlayer = null
const FADE_DURATION := 1.5

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

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

func play(track: AudioStream, fade_in: bool = true) -> void:
    if _current_player.stream == track and _current_player.playing:
        return

    # Start next player at silent
    _next_player.stream = track
    _next_player.volume_db = -80.0  # Effectively silent
    _next_player.play()

    # Crossfade
    var tween = create_tween()
    if _current_player.playing:
        tween.tween_method(_set_current_volume, 0.0, -80.0, FADE_DURATION)
    tween.tween_method(_set_next_volume, -80.0, 0.0, FADE_DURATION)

    # Swap roles after fade
    await tween.finished
    var temp = _current_player
    _current_player = _next_player
    _next_player = temp
    _next_player.stop()
    _next_player.volume_db = -80.0

func _set_current_volume(vol: float) -> void:
    if _current_player:
        _current_player.volume_db = vol

func _set_next_volume(vol: float) -> void:
    if _next_player:
        _next_player.volume_db = vol

func stop(fade_out: bool = true) -> void:
    if not _current_player or not _current_player.playing:
        return
    if fade_out:
        var tween = create_tween()
        tween.tween_method(_set_current_volume, _current_player.volume_db, -80.0, FADE_DURATION)
        await tween.finished
        _current_player.stop()
    else:
        _current_player.stop()

Accessibility Patterns

Subtitle/SRT Sync

class_name DialogueAudio extends AudioStreamPlayer

signal subtitle_shown(text: String)
signal subtitle_hidden()

@export var subtitles: Array[SubtitleCue] = []
var _active_cue_index: int = -1

class_name SubtitleCue extends RefCounted
    var time: float = 0.0
    var text: String = ""

func play() -> void:
    super.play()
    _active_cue_index = -1

func _process(_delta: float) -> void:
    if not playing:
        return
    var elapsed = get_playback_position()
    while _active_cue_index + 1 < subtitles.size():
        var cue = subtitles[_active_cue_index + 1]
        if elapsed >= cue.time:
            _active_cue_index += 1
            subtitle_shown.emit(cue.text)
        else:
            break

Audio Cues for Visually Impaired Players

# Emit directional audio cues for important events
func play_directional_cue(direction: Vector3, sound: AudioStream) -> void:
    var player = AudioStreamPlayer3D.new()
    player.stream = sound
    player.unit_size = 1.0
    player.max_distance = 20.0
    # Position relative to listener
    player.global_position = $AudioListener3D.global_position + direction.normalized() * 2.0
    add_child(player)
    player.play()

    # Auto-cleanup after sound ends
    player.finished.connect(func(): player.queue_free())

Godot 4.7 Audio Notes

  • AudioServer — Improved bus effect chain for real-time DSP processing
  • Vorbis/Opus — Opus codec support for higher quality at lower bitrates
  • Spatial audio — Enhanced HRTF filtering for headphone-based 3D audio

MCP Bridge Tools (Optional — Live Editor Integration)

When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:

  • script_read / script_edit — Read and modify audio scripts
  • project_info — Check configured audio buses and settings

Note: MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.

References