Been bothered by how obvious most adaptive music implementations feel in smaller games. The typical approach (swap out your AudioStreamPlayer when entering combat, swap it back on exit) works, but transitions are usually abrupt, or you do a long fade that doesn't feel intentional.
I wanted stems to crossfade smoothly based on game state. Combat layer fades in, ambient fades out, tension stacks on top during boss phases. Ended up writing a small MusicManager singleton. You register named AudioStreamPlayer nodes as layers at startup, then call set_state() with whatever layer names should be active. Everything else fades out.
class_name MusicManager
extends Node
@export var fade_duration: float = 1.2
var _layers: Dictionary = {}
func register_layer(layer_name: String, player: AudioStreamPlayer) -> void:
_layers[layer_name] = player
player.volume_db = -80.0
func set_state(active_layers: Array[String]) -> void:
for layer_name: String in _layers:
var player := _layers[layer_name] as AudioStreamPlayer
var target_db := 0.0 if layer_name in active_layers else -80.0
var tween := create_tween()
tween.tween_property(player, "volume_db", target_db, fade_duration).set_trans(Tween.TRANS_SINE)
if layer_name in active_layers and not player.playing:
player.play()
All layers need to be the same length and set to loop. Standard stem-based audio design. I'm using AudioStreamSynchronized so stems that were already playing stay in phase when new ones join. That part works better than I expected honestly.
The unsolved problem: starting a new layer mid-track without phase drift. A layer fading in from a stopped state begins at position 0, which can land anywhere relative to currently-playing stems. My workaround reads the playback position from an active layer and seeks the new one to match, but it's brittle and breaks when nothing is playing yet.
Has anyone done bar-aware or beat-quantized switching in Godot 4 without reaching for FMOD? Curious if there's a clean way to hook into AudioServer for timing, or if anyone's built something similar.