> ## Documentation Index
> Fetch the complete documentation index at: https://docs.summerengine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Adaptive music skill for AI agents

> Wire music stems to game state: combat, explore, boss and tension crossfades on a shared bus. A Summer Engine skill for AI agents.

`adaptive-music` is a skill from the [Summer skill library](/mcp/skills): a guide your AI agent reads when the task matches, so it builds the good version on the first try. Wire music stems to game state: combat, explore, boss and tension crossfades on a shared bus.

| | |
| - | - |
| Status | Stable |
| Domains | Audio, Gameplay |
| Version | `1.0.2` |
| Engine version | `>=4.6` |
| Recommended set | No, installed by `--all` or by name |

## When your agent uses it

* Wiring music stems to game state — combat / explore / boss / tension crossfades on a shared bus
* Layered or vertical music that changes when combat starts

## Install

`summer setup` installs every skill for your agent. To add only this one:

```bash theme={null}
npx -y summer-engine@latest skills install adaptive-music --agent claude-code
```

Swap `claude-code` for your agent, such as `codex` or `cursor`. See [Skills](/mcp/skills#quick-start) for every option.

## The skill

## 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 theme={null}
# 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 theme={null}
# 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 theme={null}
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

## 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@latest` 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` |

## Related skills

* [`audio-direction`](/skills/audio-direction): Define the game's sound (music style, SFX vocabulary, dynamic music) as .summer/audio-bible.md.
* [`music-track`](/skills/music-track): Generate a music track: game loops of 30s or more with a loop point, or linear cinematic cues.

***

Source: [`library/skills/adaptive-music/SKILL.md`](https://github.com/SummerEngine/summer/blob/main/library/skills/adaptive-music/SKILL.md), rendered from `summer-engine@4.3.0` on npm.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.