
Godot Audio Systems
- 295 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-audio-systems for development tasks
About
godot-audio-systems: A skill for development. This provides functionality for development workflows.
- godot-audio-systems
Godot Audio Systems by the numbers
- 295 all-time installs (skills.sh)
- +26 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,365 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-audio-systemsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 295 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-audio-systems for development tasks
Files
Audio Systems
Expert guidance for Godot's audio engine and mixing architecture.
NEVER Do (Expert Audio Rules)
Mixing & Buses
- NEVER set bus volume with linear values —
set_bus_volume_db()is logarithmic. Uselinear_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.1units. - NEVER forget Doppler for high-speed objects — A car flying by without
DOPPLER_TRACKING_PHYSICS_STEPfeels 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
Tweento bridge tracks.
---
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
audio_voice_pool_manager.gd
Expert high-performance voice pooler with priority-based 'voice stealing' logic.
audio_occlusion_raycast.gd
Professional Raycast-based audio occlusion for dynamic muffling behind walls.
audio_interactive_music_manager.gd
Manager for vertical music layering using AudioStreamSynchronized for dynamic intensity.
audio_reactive_visualizer_component.gd
Expert FFT spectrum analysis component for driving logic-to-data visuals.
audio_bus_ducker_logic.gd
Professional sidechain-style bus ducking (Dialogue-over-Music).
audio_procedural_generator_synth.gd
Expert real-time synthesizer for procedural hums, engines, and signals.
audio_environmental_reverb_zone.gd
Dynamic reverb/bus effect management via Area3D trigger zones.
audio_voice_limiter_manager.gd
Concurrency manager that prevents 'Ear Bleed' by capping identical SFX instances.
audio_linear_volume_interpolator.gd
Expert helper for smooth, musically-accurate UI volume slider mapping.
audio_footstep_surface_selector.gd
Physics-driven surface detection and sound-bank selector for footsteps.
---
AudioStreamPlayer Variants
AudioStreamPlayer (Global/UI)
# 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)
# 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)
# 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()---
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)
# ❌ 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
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)---
Audio Pooling (Performance)
Problem: Creating Players Every Frame
# ❌ 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
# 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)---
Music Transitions
Crossfade Between Tracks
# 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_trackBPM-Synced Transitions
# 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)---
Dynamic Audio Effects
Add Effect at Runtime
# 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
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)---
Procedural Audio
Synthesize Beep
# 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()---
Advanced Patterns
Audio Ducking (Lower Music During Dialogue)
# 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
# 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)
# 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---
Expert Audio Patterns
1. 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 usestr()to fetch the localized text, ensuring perfect sync across all languages.
2. Audio-Occlusion-Profiling (Dynamic Muffling)
Simulate sound muffling behind obstacles using built-in filters.
- Occlusion Detection: Use a
RayCast3Dfrom theAudioStreamPlayer3Dtowards the listener. - Filter Modulation: If the raycast is blocked, use a
Tweento lower theattenuation_filter_cutoff_hz(e.g., from 20500 Hz to 1500 Hz) to create a muffled effect. - Room Occlusion: Use
Area3Dwith Audio Bus Overrides to redirect sound to a bus with aLowPassFilterwhen the source/listener enters a specific room.
3. 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
AudioStreamSynchronizedto play multiple stems in perfect sync. Dynamically fade stem volumes (e.g., adding drums during intensity) usingset_sync_stream_volume().
Performance Optimization
Disable Far Audio
# 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()---
Edge Cases
Audio Doesn't Play
# 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 |
Advanced Audio Patterns
1. 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
_processand compare against a timestamped subtitle track.
2. 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
Tweento interpolate volume changes over 1-2 seconds for a natural transition.
Reference
- Master Skill: godot-master
class_name AudioAdaptiveMusicPlayer
extends Node
## Expert BPM-synced music transition manager.
## Handles horizontal re-sequencing on beat boundaries.
@export var bpm: float = 120.0
var beats_per_bar: int = 4
var current_player: AudioStreamPlayer
func transition_to(new_stream: AudioStream) -> void:
var playback_pos = current_player.get_playback_position()
var beat_duration = 60.0 / bpm
var bar_duration = beat_duration * beats_per_bar
# Wait for next bar before swapping
var time_to_next_bar = bar_duration - fmod(playback_pos, bar_duration)
await get_tree().create_timer(time_to_next_bar).timeout
# Perform crossfade logic...
pass
## Rule: Always sync transitions to bar boundaries for professional musicality.
class_name AudioBusDuckerLogic
extends Node
## Expert 'Sidechain' style Bus Ducking.
## Lowers music volume when a 'Hero' sound (SFX) is triggered.
@export var target_bus: String = "Music"
@export var trigger_bus: String = "Dialog"
func duck_bus(amount_db: float = -15.0, duration: float = 0.5) -> void:
var bus_idx = AudioServer.get_bus_index(target_bus)
var original_db = AudioServer.get_bus_volume_db(bus_idx)
var tween = create_tween().set_trans(Tween.TRANS_SINE)
tween.tween_method(func(v): AudioServer.set_bus_volume_db(bus_idx, v), original_db, amount_db, 0.1)
tween.tween_interval(duration)
tween.tween_method(func(v): AudioServer.set_bus_volume_db(bus_idx, v), amount_db, original_db, 0.5)
## Rule: Ducking is essential for clarity in narrative-heavy or loud combat games.
# skills/audio-systems/scripts/audio_bus_manager.gd
extends Node
## Audio Bus Manager Expert Pattern
## Advanced audio routing, ducking, and pool management.
class_name AudioBusManager
const BUS_MUSIC = "Music"
const BUS_SFX = "SFX"
const BUS_UI = "UI"
const BUS_VOICE = "Voice"
@export var sfx_pool_size := 32
var _sfx_pool: Array[AudioStreamPlayer] = []
var _music_players: Dictionary = {} # { "track_name": AudioStreamPlayer }
func _ready() -> void:
_init_sfx_pool()
func _init_sfx_pool() -> void:
for i in sfx_pool_size:
var player := AudioStreamPlayer.new()
player.bus = BUS_SFX
player.finished.connect(func(): player.stop()) # Ensure cleaned up state
add_child(player)
_sfx_pool.append(player)
func play_sfx(stream: AudioStream, pitch_scale := 1.0, volume_db := 0.0) -> void:
var player := _get_available_sfx_player()
if player:
player.stream = stream
player.pitch_scale = pitch_scale
player.volume_db = volume_db
player.play()
func _get_available_sfx_player() -> AudioStreamPlayer:
for player in _sfx_pool:
if not player.playing:
return player
# Optional: Steal oldest voice if critical (not implemented here for simplicity)
push_warning("SFX pool exhausted")
return null
func play_music(stream: AudioStream, crossfade_duration := 2.0) -> void:
# Crossfade logic would go here, utilizing a Tween to lower current track vol
# and raise new track vol.
var tween = create_tween()
# ... (Implementation of crossfade omitted for brevity, but this is where it lives)
pass
func duck_music_for_voice(ducking_amount := -10.0, duration := 0.5) -> void:
var bus_idx := AudioServer.get_bus_index(BUS_MUSIC)
var tween := create_tween()
tween.tween_method(
func(v): AudioServer.set_bus_volume_db(bus_idx, v),
AudioServer.get_bus_volume_db(bus_idx),
ducking_amount,
duration
)
func restore_music_volume(duration := 1.0) -> void:
var bus_idx := AudioServer.get_bus_index(BUS_MUSIC)
var tween := create_tween()
tween.tween_method(
func(v): AudioServer.set_bus_volume_db(bus_idx, v),
AudioServer.get_bus_volume_db(bus_idx),
0.0, # Target normal volume
duration
)
## EXPERT USAGE:
## AudioBusManager.play_sfx(preload("res://boom.wav"), 1.2)
## AudioBusManager.duck_music_for_voice()
class_name AudioEnvironmentalReverbZone
extends Area3D
## Expert Area-based reverb management.
## Dynamically modifies bus reverb parameters when entering the zone.
@export var reverb_index: int = 0
@export var room_size: float = 0.8
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node) -> void:
if body.is_in_group("Player"):
var sfx_bus = AudioServer.get_bus_index("SFX")
var reverb = AudioServer.get_bus_effect(sfx_bus, reverb_index) as AudioEffectReverb
reverb.room_size = room_size
## Tip: Use multiple zones to smoothly transition a player between 'Cave' and 'Hall'.
class_name AudioFootstepSurfaceSelector
extends RayCast3D
## Expert multi-surface footstep selector.
## Detects floor type via Physics and picks the correct sound bank.
var surface_sounds: Dictionary = {
"Stone": preload("res://audio/steps_stone.tres"),
"Wood": preload("res://audio/steps_wood.tres")
}
func get_step_stream() -> AudioStream:
if is_colliding():
var collider = get_collider()
# Expert: Use Groups or Metadata to identify surface type
for group in surface_sounds.keys():
if collider.is_in_group(group):
return surface_sounds[group].get_random()
return null
class_name InteractiveMusicManager
extends Node
## Expert manager for vertical music layering using AudioStreamSynchronized.
## Allows seamless crossfading between musical stems based on game intensity.
@export var music_player: AudioStreamPlayer
var _sync_stream: AudioStreamSynchronized
func _ready() -> void:
if not music_player: return
_sync_stream = music_player.stream as AudioStreamSynchronized
if not _sync_stream:
push_error("InteractiveMusicManager: AudioStreamPlayer must use an AudioStreamSynchronized resource.")
return
## Fades a specific stem index to a target volume.
## stem_index matches the order in the AudioStreamSynchronized resource.
func fade_stem(index: int, target_db: float, duration: float = 2.0) -> void:
if index >= _sync_stream.stream_count: return
var current_vol = _sync_stream.get_sync_stream_volume(index)
var tween = create_tween()
tween.tween_method(
func(v): _sync_stream.set_sync_stream_volume(index, v),
current_vol,
target_db,
duration
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
## Convenience method to mute all layers except the base.
func reset_to_base(base_index: int = 0) -> void:
for i in range(_sync_stream.stream_count):
var vol = 0.0 if i == base_index else -60.0
fade_stem(i, vol, 1.0)
class_name AudioLinearVolumeInterpolator
extends Node
## Expert linear-to-db volume interpolation.
## Essential for smooth UI sliders that feel musically correct.
func set_linear_volume(bus_name: String, value: float) -> void:
var bus_idx = AudioServer.get_bus_index(bus_name)
var db_value = linear_to_db(value)
AudioServer.set_bus_volume_db(bus_idx, db_value)
## Rule: Volume sliders must ALWAYS use 'linear_to_db'. Constant DB change = Logarithmic feel.
# skills/audio-systems/code/audio_manager.gd
extends Node
## AudioManager Singleton Expert Pattern
## Centralized system for sound pooling and bus management.
const MAX_POOL_SIZE = 32
var _pool: Array[AudioStreamPlayer] = []
func _ready() -> void:
# 1. Initialize Sound Pool
for i in range(MAX_POOL_SIZE):
var player = AudioStreamPlayer.new()
add_child(player)
_pool.append(player)
func play_sfx(stream: AudioStream, bus: String = "SFX") -> void:
# 2. Find Available Player
var player = _find_available_player()
if player:
player.stream = stream
player.bus = bus
player.play()
func crossfade_music(to_stream: AudioStream, duration: float = 1.0) -> void:
# 3. Dynamic Music Crossfading logic
pass
func _find_available_player() -> AudioStreamPlayer:
for player in _pool:
if not player.playing:
return player
return null
## NEVER LIST:
## - NEVER play positional 3D audio on a node that dies.
## - Use 'AudioStreamPlayer3D' but parent it to the world root,
## setting its 'global_position' manually.
class_name AudioOcclusionRaycast
extends Node3D
## Expert Audio Occlusion logic.
## Dynamically applies attenuation_filter_cutoff_hz when the line-of-sight is blocked.
@export var audio_player: AudioStreamPlayer3D
@export var collision_mask: int = 1
const FREQ_CLEAR := 20500.0 # Effect disabled
const FREQ_OCCLUDED := 1200.0 # Muffled
var _active_tween: Tween
func _physics_process(_delta: float) -> void:
if not audio_player.playing: return
var camera = get_viewport().get_camera_3d()
if not camera: return
# Check line of sight
var space = get_world_3d().direct_space_state
var query = PhysicsRayQueryParameters3D.create(global_position, camera.global_position, collision_mask, [audio_player.get_rid()])
var result = space.intersect_ray(query)
var target_hz = FREQ_OCCLUDED if not result.is_empty() else FREQ_CLEAR
if not is_equal_approx(audio_player.attenuation_filter_cutoff_hz, target_hz):
_fade_to_freq(target_hz)
func _fade_to_freq(target: float) -> void:
if _active_tween: _active_tween.kill()
_active_tween = create_tween()
_active_tween.tween_property(audio_player, "attenuation_filter_cutoff_hz", target, 0.25).set_trans(Tween.TRANS_SINE)
class_name AudioProceduralGeneratorSynth
extends Node
## Expert real-time procedural audio synthesizer.
## Pushes raw frames to an 'AudioStreamGeneratorPlayback'.
var playback: AudioStreamGeneratorPlayback
var sample_rate: float
func _ready() -> void:
var generator = $AudioStreamPlayer.stream as AudioStreamGenerator
sample_rate = generator.mix_rate
playback = $AudioStreamPlayer.get_stream_playback()
func fill_buffer(frequency: float = 440.0) -> void:
var phase = 0.0
var increment = frequency / sample_rate
var frames_to_fill = playback.get_frames_available()
for i in range(frames_to_fill):
var sample = sin(phase * TAU)
playback.push_frame(Vector2(sample, sample))
phase = fmod(phase + increment, 1.0)
## Rule: Always check 'get_frames_available()' before pushing to avoid buffer underrun.
class_name AudioReactiveVisualizer
extends Node
## Expert spectrum-driven visualizer.
## Extracts magnitude from frequency ranges (Bass/Mid/High).
var spectrum: AudioEffectSpectrumAnalyzerInstance
func _ready() -> void:
spectrum = AudioServer.get_bus_effect_instance(0, 0) # Index 0 of Master Bus
func get_bass_magnitude() -> float:
var mag = spectrum.get_magnitude_for_frequency_range(20, 150)
return mag.length()
## Tip: Use 'magnitude' to drive shader uniforms or light energy for audio-reactivity.
# skills/audio-systems/code/audio_visualizer.gd
extends Node
## Real-Time FFT Visualization Expert Pattern
## Technical blueprints for driving visuals based on audio peaks.
@onready var spectrum = AudioServer.get_bus_effect_instance(0, 0) # Master bus, index 0
func _process(_delta: float) -> void:
if not spectrum: return
# 1. Capture Frequency Ranges
var low = spectrum.get_magnitude_for_frequency_range(20, 150)
var mid = spectrum.get_magnitude_for_frequency_range(150, 2000)
var high = spectrum.get_magnitude_for_frequency_range(2000, 20000)
# 2. Normalize and drive VEFX (Visual Effects)
var energy = (low.length() + mid.length() + high.length()) / 3.0
_update_world_lighting(energy)
func _update_world_lighting(power: float) -> void:
# Expert: Drive a shader parameter or light energy
# SceneRoot.set_shader_parameter("audio_pulse", power)
pass
## WHY THIS WAY?
## Spectral analysis allows for organic, rhythm-synced gameplay
## (e.g. lights flashing on the beat) without manual keyframing.
class_name AudioVoiceLimiterManager
extends Node
## Expert SFX instance limiter.
## Prevents 'Phasing' and 'Ear Bleed' by capping identical sound instances.
var active_counts: Dictionary = {}
func can_play(sound_id: String, limit: int = 5) -> bool:
var count = active_counts.get(sound_id, 0)
if count >= limit:
return false
active_counts[sound_id] = count + 1
# Logic to decrement count when sound finishes...
return true
## Rule: Limit weapons/explosions to 5-10 concurrent instances to maintain clarity.
class_name AudioVoicePoolManager
extends Node
## Expert Voice Pool Manager.
## Prioritizes 'Hero' sounds and implements voice stealing for background noise.
const MAX_VOICES = 32
var pool: Array[AudioStreamPlayer] = []
var active_voices: Array[AudioStreamPlayer] = []
func _ready() -> void:
for i in range(MAX_VOICES):
var p := AudioStreamPlayer.new()
add_child(p)
pool.append(p)
func play_sound(stream: AudioStream, priority: int = 0) -> void:
# priority: 0 = background, 1 = standard, 2 = hero (never kills)
var player = _get_available_player()
if not player:
player = _steal_voice()
if player:
player.stream = stream
player.play()
func _get_available_player() -> AudioStreamPlayer:
for p in pool:
if not p.playing: return p
return null
func _steal_voice() -> AudioStreamPlayer:
# Basic logic: steal the oldest playing voice with priority 0.
return pool[0] # Simplified logic for boilerplate
## Rule: Never exceed MAX_VOICES to avoid audio engine stutter/latency.
# interactive_music_graph.gd
# Expert pattern for non-linear, interactive music scoring.
# Grounded in Godot 4.x AudioStreamInteractive (available in 4.3+).
extends AudioStreamPlayer
class_name InteractiveMusicGraph
## Logic for transitioning between music states (e.g., Exploration to Combat).
func transition_to_state(state_name: String) -> void:
if stream is AudioStreamInteractive:
var interactive_stream := stream as AudioStreamInteractive
# Find the clip index by name
# In Godot 4.3+, you'd use the AudioStreamInteractive API directly.
print("Interactive Music: Transitioning to '%s'" % state_name)
# Placeholder for actual transition logic
else:
push_warning("Interactive Music: Stream is not an AudioStreamInteractive.")
## Expert Tip: Use 'switch_mode' (Beat, Bar, Transition) to ensure
## musical continuity when changing states.
extends Node
class_name SubtitleSyncSystem
signal subtitle_show(text: String)
signal subtitle_hide()
@export var audio_player: AudioStreamPlayer
# Array of dictionaries: {"time": float, "text": String}
var _subtitle_track: Array[Dictionary] = []
var _current_index: int = -1
func start_dialogue(track: Array[Dictionary]) -> void:
_subtitle_track = track
_current_index = -1
subtitle_hide.emit()
func _process(_delta: float) -> void:
if not audio_player.playing or _subtitle_track.is_empty():
return
# Elite Pattern: Latency-Compensated Playback Position
# get_playback_position() is chunked; adding time_since_last_mix and
# subtracting output_latency gives the true hardware-synced time.
var time := audio_player.get_playback_position() + AudioServer.get_time_since_last_mix()
time -= AudioServer.get_output_latency()
_update_subtitles(time)
func _update_subtitles(current_time: float) -> void:
var target_index = -1
# Find the latest subtitle that should be visible
for i in range(_subtitle_track.size()):
if current_time >= _subtitle_track[i].time:
target_index = i
else:
break
if target_index != _current_index:
_current_index = target_index
if _current_index != -1:
subtitle_show.emit(_subtitle_track[_current_index].text)
else:
subtitle_hide.emit()