From summer
Wires music stems to game state with crossfades, bus routing, and state machine integration. For dynamic music systems (combat/explore/boss transitions) using horizontal sequencing or layered stems.
How this skill is triggered — by the user, by Claude, or both
Slash command
/summer:adaptive-musicaudio/music/**scripts/audio/****/*.tscndefault_bus_layout.tresThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
This is the **wiring** skill, not the generation skill. The job is to take 3–5 music stems (or full tracks) and connect them to a game state machine so the music shifts as the player's situation changes — exploration → tension → combat → victory → exploration — with crossfades, ducking, and bus routing the player never consciously notices but always feels.
This is the wiring skill, not the generation skill. The job is to take 3–5 music stems (or full tracks) and connect them to a game state machine so the music shifts as the player's situation changes — exploration → tension → combat → victory → exploration — with crossfades, ducking, and bus routing the player never consciously notices but always feels.
This skill assumes the stems already exist. If they don't, run /music-track first for each state, or generate matching stems (same tempo, same key, same bar count) so they layer/crossfade cleanly.
/music-track./music-track and an AudioStreamPlayer is enough.Read .summer/audio-bible.md
Quote the plan back. Example:
Bible plan: Layered stems. States CALM / TENSION / COMBAT / VICTORY. CALM → TENSION on enemy detect (2s crossfade). TENSION → COMBAT on first damage (instant drum stem). COMBAT → VICTORY on last enemy dies (drum cuts, brass swell). VICTORY → CALM after 4s. Music ducks
-6 dBon dialogue.
If the plan is absent, run /audio-direction step 6 first.
| Model | When | Implementation |
|---|---|---|
| Horizontal sequencing | 3–5 full tracks, one per state, crossfade | One AudioStreamPlayer per track on the Music bus, tween volumes on state change |
| Layered stems | One base loop + drum/brass/strings stems that turn on with intensity | All stems play simultaneously at silence; mute/unmute via volume tween |
| Vertical re-orchestration | Same melody, different orchestration per state, snap on bar | All variants play simultaneously muted; swap on next bar boundary |
| Procedural / generative | Rules generate music in real-time | Beyond this skill; needs a custom system |
For 90% of indie games, horizontal sequencing is correct. It's cheaper to author, easier to swap, and players don't notice the difference.
summer_search_assets(query="music", filter={ kind: "audio" })
You need (typical case):
audio/music/calm.mp3 — exploration loop, sparseaudio/music/tension.mp3 — same tempo/key, denser, no drumsaudio/music/combat.mp3 — same tempo/key, full drums, urgentaudio/music/boss.mp3 — distinct from combataudio/music/victory.mp3 — short linear sting (4–8s)If any are missing, run /music-track for each. Critically: same BPM, same key, same time signature, same bar length so they overlap cleanly.
The Music bus must exist and route to Master. If /audio-direction ran, it does. Verify:
Read default_bus_layout.tres # or wherever the project stores it
You want this structure:
Master
├── Music (-8 dB; ducks -6 dB when Voice is active)
├── SFX (0 dB; routes to Reverb buses)
├── UI (-3 dB)
├── Voice (0 dB)
├── Ambient (-12 dB)
├── Reverb_Small
├── Reverb_Large
└── Reverb_Outdoor
If Music bus is missing, run /audio-direction step 8.
For ducking, add a sidechain compressor on the Music bus with the Voice bus as the sidechain source. Or implement ducking in code (simpler — see step 7).
For horizontal sequencing, instantiate one AudioStreamPlayer per state, all on the Music bus, all autoplay, all looping, all starting at -60 dB except the active state:
summer_add_node(parentPath="/root/Game/Music", type="AudioStreamPlayer", name="Calm")
summer_set_prop(path="/root/Game/Music/Calm", property="stream", value="res://audio/music/calm.mp3")
summer_set_prop(path="/root/Game/Music/Calm", property="bus", value="Music")
summer_set_prop(path="/root/Game/Music/Calm", property="volume_db", value=0.0)
summer_set_prop(path="/root/Game/Music/Calm", property="autoplay", value=true)
summer_add_node(parentPath="/root/Game/Music", type="AudioStreamPlayer", name="Tension")
summer_set_prop(path="/root/Game/Music/Tension", property="stream", value="res://audio/music/tension.mp3")
summer_set_prop(path="/root/Game/Music/Tension", property="bus", value="Music")
summer_set_prop(path="/root/Game/Music/Tension", property="volume_db", value=-60.0)
summer_set_prop(path="/root/Game/Music/Tension", property="autoplay", value=true)
summer_add_node(parentPath="/root/Game/Music", type="AudioStreamPlayer", name="Combat")
summer_set_prop(path="/root/Game/Music/Combat", property="stream", value="res://audio/music/combat.mp3")
summer_set_prop(path="/root/Game/Music/Combat", property="bus", value="Music")
summer_set_prop(path="/root/Game/Music/Combat", property="volume_db", value=-60.0)
summer_set_prop(path="/root/Game/Music/Combat", property="autoplay", value=true)
All three play simultaneously, in sync, but only one is audible. State transitions tween volumes — the music never restarts, only fades.
This is why same BPM / same key / same bar length matter: all three are at the same playhead at all times.
# scripts/audio/MusicDirector.gd
class_name MusicDirector
extends Node
enum State { CALM, TENSION, COMBAT, VICTORY, BOSS }
@export var calm: AudioStreamPlayer
@export var tension: AudioStreamPlayer
@export var combat: AudioStreamPlayer
@export var boss: AudioStreamPlayer
@export var victory_sting: AudioStreamPlayer # one-shot, not looping
@export var crossfade_seconds: float = 2.0
@export var instant_states: Array[State] = [State.COMBAT] # snap, no fade
@export var active_db: float = 0.0
@export var inactive_db: float = -60.0
var _state: State = State.CALM
var _tween: Tween
func _ready() -> void:
_apply_state(_state, true)
func set_state(new_state: State) -> void:
if new_state == _state:
return
var instant := new_state in instant_states
_state = new_state
_apply_state(new_state, instant)
if new_state == State.VICTORY:
victory_sting.play()
await get_tree().create_timer(4.0).timeout
set_state(State.CALM)
func _apply_state(state: State, instant: bool) -> void:
if _tween and _tween.is_valid():
_tween.kill()
_tween = create_tween().set_parallel(true)
var dur := 0.0 if instant else crossfade_seconds
for pair in [
[calm, state == State.CALM],
[tension, state == State.TENSION],
[combat, state == State.COMBAT],
[boss, state == State.BOSS],
]:
var player: AudioStreamPlayer = pair[0]
var should_be_active: bool = pair[1]
if player == null:
continue
var target_db := active_db if should_be_active else inactive_db
_tween.tween_property(player, "volume_db", target_db, dur)
Attach it under /root/Game/Music, hook the exported player slots to the four players. The director is the single point of truth for music state.
Where does set_state() get called from? The game state machine. Hook signals from combat / detection / encounter logic:
# scripts/audio/MusicHooks.gd
extends Node
@export var director: MusicDirector
func _ready() -> void:
EventBus.enemy_detected_player.connect(_on_tension)
EventBus.player_took_damage.connect(_on_combat)
EventBus.encounter_cleared.connect(_on_victory)
EventBus.boss_started.connect(_on_boss)
EventBus.boss_ended.connect(_on_calm)
EventBus.dialogue_started.connect(_on_duck)
EventBus.dialogue_ended.connect(_on_unduck)
func _on_tension() -> void: director.set_state(MusicDirector.State.TENSION)
func _on_combat() -> void: director.set_state(MusicDirector.State.COMBAT)
func _on_victory() -> void: director.set_state(MusicDirector.State.VICTORY)
func _on_boss() -> void: director.set_state(MusicDirector.State.BOSS)
func _on_calm() -> void: director.set_state(MusicDirector.State.CALM)
func _on_duck() -> void:
var bus_idx := AudioServer.get_bus_index(&"Music")
create_tween().tween_method(
func(db: float) -> void: AudioServer.set_bus_volume_db(bus_idx, db),
AudioServer.get_bus_volume_db(bus_idx), -14.0, 0.3
)
func _on_unduck() -> void:
var bus_idx := AudioServer.get_bus_index(&"Music")
create_tween().tween_method(
func(db: float) -> void: AudioServer.set_bus_volume_db(bus_idx, db),
AudioServer.get_bus_volume_db(bus_idx), -8.0, 0.5
)
If your project doesn't have an EventBus autoload yet, add one — adaptive music depends on event-driven signals, not polling.
Wire the connections via summer_connect_signal so they survive a scene reload.
If the bible says layered stems instead of horizontal sequencing, the rig is the same (multiple AudioStreamPlayers, all looping in sync, all on Music bus) but the volume map is different:
| State | Base | Drums | Strings | Brass |
|---|---|---|---|---|
| CALM | 0 | -60 | -60 | -60 |
| TENSION | 0 | -60 | -10 | -60 |
| COMBAT | 0 | 0 | -3 | -10 |
| BOSS | -60 | 0 | 0 | 0 |
The director method is the same _apply_state pattern — just more players and per-player target_db tables.
For the third model, snapping must happen on a bar boundary (not mid-phrase). All players run in sync; on state change, schedule the snap for the next bar:
const BPM := 128.0
const BEATS_PER_BAR := 4
const SECONDS_PER_BAR := (60.0 / BPM) * BEATS_PER_BAR
func snap_at_next_bar(callable: Callable) -> void:
var pos := calm.get_playback_position()
var into_bar := fmod(pos, SECONDS_PER_BAR)
var wait := SECONDS_PER_BAR - into_bar
get_tree().create_timer(wait).timeout.connect(callable)
Use this for boss reveals, story beats — anywhere a snap-on-bar is more dramatic than a crossfade.
Run the game (summer_play) and trigger each transition. Listen for:
crossfade_seconds = 2.0. Too fast: fades are choppy. Too slow: state changes feel disconnected from gameplay.-6 dB is a starting value; tune in step 7.victory_sting to a non-loop AudioStream.When the player changes scenes, the MusicDirector either:
a) Persists as an autoload. New scenes call MusicDirector.set_state() to take over.
b) Per-scene, fades out the previous before the scene changes (set_state(CALM) then crossfade to -60 dB on all then change scene).
Persistent is preferred for open-world; per-scene for level-based games.
Master
├── Music -8 dB (ducks to -14 dB on Voice)
│ └── (one AudioStreamPlayer per state, all looping in sync)
├── SFX 0 dB
│ └── routes to Reverb_Small / Reverb_Large / Reverb_Outdoor sends
├── UI -3 dB
├── Voice 0 dB
├── Ambient -12 dB
├── Reverb_Small (return)
├── Reverb_Large (return)
└── Reverb_Outdoor (return)
play() mid-state._process. Use signals. Music director listens to EventBus, not to game state every frame.set_state. Use the table-driven _apply_state so adding a new state doesn't fork the function.-6 dB (so music drops from -8 to -14), not muted. Voice should sit on a present bed, not a dead one.Boss music is its own track rule.instant_states = [State.COMBAT] makes COMBAT snap so it lands on the swing connect. The intermediate fade is wrong — combat must feel immediate.crossfade_seconds = 2.0; the de-escalation can breathe.no_enemies_aware AND no_recent_damage_for=8s._ready. If AudioServer.get_bus_index("Music") returns -1, the bus layout failed to load. Verify default_bus_layout.tres.Print the bus layout, the node tree, and the GDScript. User constructs in the Godot editor manually.
MusicDirector wired. Calm / Tension / Combat play in sync, only one audible. State driven by
EventBus. Music ducks-6 dBon dialogue. Next:
- Run
summer_playand trigger each transition; listen for sync issues.- If COMBAT lands on the wrong beat, the stems are off-tempo — regenerate via
/music-trackwith identical BPM and bar count.- For boss-specific music, generate
boss.mp3with/music-trackand the director picks it up automatically.
audio/audio-direction — defines the dynamic music plan and bus layoutaudio/music-track — generates the stemsaudio/voice-line — the voice bus that triggers duckingreferences/godot-version.md — Godot 4.5 AudioServer and Tween APInpx claudepluginhub summerengine/summer-engine-agent --plugin summerGenerates looped or cinematic music tracks via ElevenLabs Music, wired as AudioStreamPlayer on the Music bus. Use for level loops, boss themes, cutscenes, or menu music.
Digital Audio Workstation usage, music composition, interactive music systems, and game audio implementation for immersive soundscapes.
Implements Godot 4.3+ audio systems using GDScript/C#: buses, AudioStreamPlayer/2D/3D nodes, spatial audio, music management, SFX pooling, dynamic mixing.