Installs into .claude/skills of the current project.
Are you the author of Adaptive Music?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/summerengine-adaptive-music-summer)
---
name: adaptive-music
description: "Wire music stems to game state: combat, explore, boss and tension crossfades on a shared bus."
license: MIT
compatibility: [Cursor, Claude Code, Windsurf, Codex]
category: audio
user-invocable: true
allowed-tools: Read Grep Glob Write Edit summer_assets_generate_audio summer_assets_search summer_import_from_url summer_run_script summer_add_node summer_set_prop summer_connect_signal summer_inspect_node summer_get_scene_tree summer_project_setting summer_call
paths: ["audio/music/**", "scripts/audio/**", "**/*.tscn", "default_bus_layout.tres"]
---
# /adaptive-music — Wire Stems to Game State
## Overview
This skill wires music; it does not generate it. It connects 3–5 music stems (or full tracks) to a game state machine so the music follows the player's situation — exploration → tension → combat → victory → exploration — with crossfades, ducking, and bus routing.
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.
## When to use
- The audio bible specifies a dynamic music model (layered stems / vertical re-orchestration / horizontal sequencing).
- You have at least two music tracks for two different states and want the engine to switch between them.
- Player-state-driven crossfades, not random shuffling.
## When NOT to use
- Generating the stems themselves → `/music-track`.
- Single static loop with no state changes → `/music-track` and an `AudioStreamPlayer` is enough.
## Steps
### 1. Read the audio bible's dynamic music plan
```
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 dB` on dialogue.
If the plan is absent, run `/audio-direction` step 6 first.
### 2. Pick the wiring model
| 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 most games, **horizontal sequencing** is right. It is cheaper to author and easier to swap.
### 3. Confirm stems exist (or generate them)
```
assets.search({ query: "music", assetType: "music", scope: "all" })
```
You need (typical case):
- `audio/music/calm.mp3` — exploration loop, sparse
- `audio/music/tension.mp3` — same tempo/key, denser, no drums
- `audio/music/combat.mp3` — same tempo/key, full drums, urgent
- (optional) `audio/music/boss.mp3` — distinct from combat
- (optional) `audio/music/victory.mp3` — short linear sting (4–8s)
If any are missing, run `/music-track` for each. Use **the same BPM, key, time signature, and bar length** for all of them so they overlap cleanly.
### 4. Verify / extend the bus layout
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).
### 5. Build the player rig
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. One script builds the whole rig (`scenePath` is the exact scene to change; `ctx.save_scene()` writes it):
```
summer_run_script(scenePath="res://main.tscn", source="""
func run(ctx):
var music = ctx.get_or_create("Node", "Music")
var states := {"Calm": 0.0, "Tension": -60.0, "Combat": -60.0} # only the active state is audible
for state in states:
ctx.add_node("AudioStreamPlayer", state, music, {
"stream": load("res://audio/music/%s.mp3" % state.to_lower()),
"bus": "Music",
"volume_db": states[state],
"autoplay": true,
})
ctx.save_scene()
""")
```
A fourth state (Boss) is one more entry in `states`, not five more calls. Loop the streams on the imported files (`loop` in the import settings, or `AudioStreamMP3.loop`), not on the player.
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.
### 6. The MusicDirector script
```gdscript
# 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.
### 7. Drive state from gameplay signals
Where does `set_state()` get called from? The game state machine. Hook signals from combat / detection / encounter logic:
```gdscript
# 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.
### 8. Layered stems variant (alternative to step 6)
If the bible says **layered stems** instead of horizontal sequencing, the rig is the same (multiple `AudioStreamPlayer`s, 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.
### 9. Bar-locked snap (vertical re-orchestration)
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:
```gdscript
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.
### 10. Test the transitions
Run the game (`summer_play`) and trigger each transition. Listen for:
- Crossfade artifacts at `crossfade_seconds = 2.0`. Too fast: fades are choppy. Too slow: state changes feel disconnected from gameplay.
- Out-of-sync stems. If combat enters and the drums hit on the wrong beat, the stems weren't generated at the same BPM/bar length. Regenerate.
- Ducking that's too aggressive (music disappears) or too subtle (voice still buried). The `-6 dB` is a starting value; tune in step 7.
- Victory sting that loops (it shouldn't). Set `victory_sting` to a non-loop AudioStream.
### 11. Edge case: scene transitions
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.
## Reference card — bus map
```
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)
```
## Anti-patterns
- **Stems generated at different BPM/key.** They will not crossfade cleanly. Same BPM, same key, same bar count.
- **Restarting tracks on state change.** Players hear the restart. Always crossfade volumes; never `play()` mid-state.
- **No autoplay on the silent stems.** They can't fade in to phase if they never started.
- **Polling state in `_process`.** Use signals. Music director listens to `EventBus`, not to game state every frame.
- **Hardcoding volumes in `set_state`.** Use the table-driven `_apply_state` so adding a new state doesn't fork the function.
- **Ducking music to silence on dialogue.** The bible says `-6 dB` (so music drops from `-8` to `-14`), not muted. Voice should sit on a present bed, not a dead one.
- **Boss music as a louder combat track.** Distinct, named instrument; ideally different tempo or key. The bible's `Boss music is its own track` rule.
## Edge cases
- **Player triggers TENSION → COMBAT in 0.2s.** The `instant_states = [State.COMBAT]` makes COMBAT snap so it lands on the swing connect. The intermediate fade is wrong — combat must feel immediate.
- **Player flees combat back to TENSION → CALM.** Use the regular `crossfade_seconds = 2.0`; the de-escalation can breathe.
- **Multiple enemies aware → one dies → none aware.** The state machine should hysteresis: only fall back to CALM after `no_enemies_aware AND no_recent_damage_for=8s`.
- **Audio service not ready on `_ready`.** If `AudioServer.get_bus_index("Music")` returns `-1`, the bus layout failed to load. Verify `default_bus_layout.tres`.
- **Stems generated at 60s but you want a 5-minute combat.** They loop. Crossfade is volume-only, not time-based, so loops are invisible to the director.
## Fallback (no MCP)
Print the bus layout, node tree, and GDScript. The user constructs it manually
in Summer Engine.
## Handoff
> MusicDirector wired. Calm / Tension / Combat play in sync, only one audible. State driven by `EventBus`. Music ducks `-6 dB` on dialogue. Next:
> - Run `summer_play` and trigger each transition; listen for sync issues.
> - If COMBAT lands on the wrong beat, the stems are off-tempo — regenerate via `/music-track` with identical BPM and bar count.
> - For boss-specific music, generate `boss.mp3` with `/music-track` and the director picks it up automatically.
## See also
- `audio-direction` — defines the dynamic music plan and bus layout
- `music-track` — generates the stems
- `voice-line` — the voice bus that triggers ducking
- `summer_docs_read` id `reference/godot-version` — Summer compatibility for AudioServer
and Tween APIs
<!-- shell-commands: generated from the operation catalog by npm run generate:registry; do not edit by hand -->
## From a shell
No Summer MCP tools in this session? Run each one as a command; when `summer --version` is missing or below 4.0, use `npx -y summer-engine@4.3.0` in place of `summer`. Fields are flags in kebab-case (`--scene-path res://main.tscn`) or one `--input '<json>'`, and `summer ops describe <op>` prints the exact input. The `summer-cli` skill has the rest.
| In this skill | Command |
|---|---|
| `summer_assets_generate_audio` | `summer assets generate-audio` |
| `summer_assets_search` | `summer assets search` |
| `summer_docs_read` | `summer docs read` |
| `summer_add_node` | `summer engine add-node` |
| `summer_connect_signal` | `summer engine connect-signal` |
| `summer_get_scene_tree` | `summer engine get-scene-tree` |
| `summer_import_from_url` | `summer engine import-from-url` |
| `summer_inspect_node` | `summer engine inspect-node` |
| `summer_play` | `summer engine play` |
| `summer_project_setting` | `summer engine project-setting` |
| `summer_run_script` | `summer engine run-script` |
| `summer_set_prop` | `summer engine set-prop` |
<!-- /shell-commands -->