Adaptive Music logo

Adaptive Music

Organization
SummerEngine
adaptive-music

Wire music stems to game state: combat, explore, boss and tension crossfades on a shared bus.

Overview

PublisherSummerEngine
Repositorysummer
Skill nameadaptive-music
Stars
91
Forks
8
Bundled files
1
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by SummerEngine on GitHub. Read the source before you install it.

Installation

Install the Adaptive Music AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/SummerEngine/summer/tree/main/library/skills/adaptive-music
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/SummerEngine/summer.git /tmp/summer
mkdir -p .claude/skills
cp -r /tmp/summer/library/skills/adaptive-music .claude/skills/adaptive-music
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Adaptive Music in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Adaptive Music on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Adaptive Music is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

/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

ModelWhenImplementation
Horizontal sequencing3–5 full tracks, one per state, crossfadeOne AudioStreamPlayer per track on the Music bus, tween volumes on state change
Layered stemsOne base loop + drum/brass/strings stems that turn on with intensityAll stems play simultaneously at silence; mute/unmute via volume tween
Vertical re-orchestrationSame melody, different orchestration per state, snap on barAll variants play simultaneously muted; swap on next bar boundary
Procedural / generativeRules generate music in real-timeBeyond 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 AudioStreamPlayers, all looping in sync, all on Music bus) but the volume map is different:

StateBaseDrumsStringsBrass
CALM0-60-60-60
TENSION0-60-10-60
COMBAT00-3-10
BOSS-60000

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

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 skillCommand
summer_assets_generate_audiosummer assets generate-audio
summer_assets_searchsummer assets search
summer_docs_readsummer docs read
summer_add_nodesummer engine add-node
summer_connect_signalsummer engine connect-signal
summer_get_scene_treesummer engine get-scene-tree
summer_import_from_urlsummer engine import-from-url
summer_inspect_nodesummer engine inspect-node
summer_playsummer engine play
summer_project_settingsummer engine project-setting
summer_run_scriptsummer engine run-script
summer_set_propsummer engine set-prop

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Adaptive Music AI skill do?

Wire music stems to game state: combat, explore, boss and tension crossfades on a shared bus.

Why use Adaptive Music on TypingMind?

Because you install it once and use it with any model. Adaptive Music is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Adaptive Music in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/SummerEngine/summer/tree/main/library/skills/adaptive-music. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Adaptive Music?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Adaptive Music?

As many as you like. As long as a model supports skills, you can use Adaptive Music with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Adaptive Music AI skill free?

Yes. It is published on GitHub by SummerEngine under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇