Skills/godot-audio/SKILL.md

299 lines
8.4 KiB
Markdown

---
name: godot-audio
description: "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."
license: MIT
compatibility:
- godot-4.0
- godot-4.1
- godot-4.2
- godot-4.3
- godot-4.4
- godot-4.5
- godot-4.6
- godot-4.7
metadata:
author: vl4dt
version: 0.1.0
tags:
- godot
- audio
- sound
- music
- spatial-audio
- bus-mixing
created: 2026-06-21
---
# Audio in Godot
Patterns for game audio implementation.
## AudioStreamPlayer Hierarchy
Godot provides separate nodes for 2D and 3D audio:
```gdscript
# 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
```gdscript
# 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:
```gdscript
# 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
```gdscript
# 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:
```gdscript
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:
```gdscript
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:
```gdscript
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
```gdscript
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
```gdscript
# 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
- [Audio Bus Configuration](references/audio-buses.md)
- [Spatial Audio Deep Dive](references/spatial-audio.md)
- [Dynamic Music Patterns](references/dynamic-music.md)