Summary
A game can switch its music between emotional states (explore, tension, combat, victory) without FMOD or Wwise. Unity’s built-in audio offers four tools, each matching one of the adaptive-music techniques described in interactive-music-techniques:
| Technique | Unity tool | What it gives you |
|---|---|---|
| Simple crossfade between two cues | Two AudioSources and a fade coroutine | Smooth state changes in about 30 lines of C# |
| Mood changes across many sources at once | AudioMixer snapshots with AudioMixerSnapshot.TransitionTo | Timed mixes (volume, effects) set up in the Editor, not in code |
| Musically timed switches (horizontal re-sequencing) | AudioSource.PlayScheduled with AudioSettings.dspTime | A new cue that starts exactly on the next bar line |
| Intensity layers (vertical layering) | One looping AudioSource per stem, all started on the same dspTime | Instant intensity changes with no tempo drift |
Short answer to “C# or middleware?” For a student or small project with two to four music states, plain C# is enough: crossfade two AudioSources, or move between mixer snapshots. Add bar-aligned scheduling if switches must land on the beat. Move to middleware when the composer needs to author the transition rules, quantisation and layer curves themselves, or when the music logic grows beyond a few states (see the checklist under Trade-offs).
Implementation
1. Two-AudioSource crossfade (the minimum)
Put two AudioSource components on a music object, with Loop on and Play On Awake off. One plays; the other waits. To change cue, start the idle one silently and fade the volumes in opposite directions. The AudioSource.volume property runs from 0 to 1 (see unity-audiosource).
using System.Collections;
using UnityEngine;
public class SimpleMusicCrossfade : MonoBehaviour
{
[SerializeField] private AudioSource current; // the track playing, or being faded towards
[SerializeField] private AudioSource other; // idle, or fading out
[SerializeField, Range(0f, 1f)] private float musicVolume = 0.8f;
public void CrossfadeTo(AudioClip clip, float seconds)
{
if (current.clip == clip && current.isPlaying) return; // already there or on the way
StopAllCoroutines(); // safe: the new fade starts from the current volumes
if (other.clip != clip || !other.isPlaying) // a new track, not the one fading out
{
if (other.isPlaying && other.volume > current.volume)
(current, other) = (other, current); // keep the louder track audible
other.clip = clip; // reuse the quieter source
other.volume = 0f;
other.Play();
}
(current, other) = (other, current); // the requested track becomes current
StartCoroutine(Fade(other, current, seconds));
}
private IEnumerator Fade(AudioSource from, AudioSource to, float seconds)
{
float fromStart = from.volume, toStart = to.volume;
for (float t = 0f; t < 1f; t += Time.unscaledDeltaTime / seconds)
{
float angle = t * Mathf.PI * 0.5f; // equal-power curve
from.volume = fromStart * Mathf.Cos(angle);
to.volume = Mathf.Lerp(toStart, musicVolume, Mathf.Sin(angle));
yield return null;
}
from.Stop();
to.volume = musicVolume;
}
}Game code then calls music.CrossfadeTo(combatTheme, 1.5f) when the state changes. Three details matter:
-
Interrupting a fade. If a new request arrives mid-fade, the next fade starts from the volumes the two sources have at that moment, so nothing jumps. Asking for the track that is fading out fades it back in without restarting it. With only two sources, a third track replaces whichever of the two is quieter at that moment.
-
Time.unscaledDeltaTimekeeps the fade running when the game is paused withTime.timeScale = 0. WithTime.deltaTimethe fade would freeze halfway. -
Call it from a state change, not every frame. Trigger it from the event that changes the game state (see observer-pattern and ai-state-machine-pattern). Add a minimum time between switches so a player hovering on a trigger boundary does not flip the music back and forth. Sweet calls this a timeout (Sweet, Writing Interactive Music for Video Games, see source-writing-interactive-music).
2. Equal-power vs linear fades
| Curve | Outgoing gain | Incoming gain | Gain of each at the midpoint | Use for |
|---|---|---|---|---|
| Linear | 1 - t | t | 0.5 (−6 dB) | Very short fades, or two versions of the same recording kept in sync |
| Equal-power | cos(t·π/2) | sin(t·π/2) | 0.707 (−3 dB) | Crossfading between different pieces of music: the default choice |
A linear crossfade keeps the two gains summing to 1, but two different signals at half amplitude are heard as roughly 3 dB quieter than one signal at full level, so the music audibly dips in the middle of the fade. Quarter-sine and quarter-cosine curves hold each signal at about 0.707 (−3 dB) at the midpoint, which removes the dip (Dobrian, Max Cookbook: Stereo balance and panning, UC Irvine). Arguably a linear or near-linear fade suits two beat-synchronised mixes of the same material, because those signals add together rather than as independent sounds. Treat that as a rule of thumb and check it by ear.
3. AudioMixer snapshots and TransitionTo
When several sources must change together (music, ambience, a low-pass filter on everything during a “danger” state), do the mixing in an Audio Mixer asset rather than in C#:
- Create the mixer. Assets → Create → Audio Mixer. Add groups such as
Music_Explore,Music_CombatandAmbience. - Route each source. Set each
AudioSource’s Output (outputAudioMixerGroupin code) to its group, so the group’s volume and effects apply to that signal. - Capture a snapshot per mood. Snapshots capture the values of every parameter in the mixer: volume, pitch, send level, wet mix and effect settings. Create one per mood with + in the Snapshots panel, select it, and set the mix. For example, Explore has
Music_Combatat −80 dB, and Combat reverses it. Right-click a snapshot and choose Set as Start Snapshot to pick the mix the mixer loads with. - Keep both cues playing (looping, in sync if possible), then move between snapshots from code:
using UnityEngine;
using UnityEngine.Audio;
public class MusicMood : MonoBehaviour
{
[SerializeField] private AudioMixer musicMixer;
[SerializeField] private AudioMixerSnapshot exploreSnapshot;
[SerializeField] private AudioMixerSnapshot combatSnapshot;
public void EnterCombat() => combatSnapshot.TransitionTo(1.0f);
public void LeaveCombat() => exploreSnapshot.TransitionTo(3.0f);
// Blend by a 0-1 danger value instead of switching outright.
public void SetDanger(float danger)
{
var snapshots = new[] { exploreSnapshot, combatSnapshot };
var weights = new[] { 1f - danger, danger };
musicMixer.TransitionToSnapshots(snapshots, weights, 0.5f);
}
}TransitionTo(timeToReach) performs an interpolated transition towards the snapshot over that many seconds. TransitionToSnapshots(snapshots, weights, timeToReach) moves to a weighted mixture of snapshots, which the Scripting API suggests for games that treat state as a continuum (Unity Technologies, Scripting API: AudioMixerSnapshot.TransitionTo; AudioMixer.TransitionToSnapshots). AudioMixer.FindSnapshot("Combat") fetches a snapshot by its exact name if you prefer not to assign it in the Inspector.
Mixer gotchas (Unity Technologies, Scripting API: AudioMixer.SetFloat; AudioMixer.updateMode; Unity Manual: Audio Mixer):
SetFloattakes a parameter away from snapshots. Once you callAudioMixer.SetFloaton an exposed parameter (for example a “MusicVolume” options slider), snapshots no longer control it. UseClearFloatto hand it back. Keep player volume settings on a different group (for example a parentMusicgroup) from the groups your snapshots move.- Don’t call
SetFloatinAwakeorOnEnable. The Scripting API advisesStartor later. - Pausing. Snapshot transitions follow scaled game time by default. Set the mixer’s
updateModetoAudioMixerUpdateMode.UnscaledTimeif music must keep changing whileTime.timeScaleis 0. - Mixer group volumes are in decibels, not 0–1 like
AudioSource.volume. The bottom of the fader, −80 dB, is used as “off” in practice. Fade a group by moving between snapshots rather than by writing a linear 0–1 value into a dB parameter.
4. Bar-aligned switching with PlayScheduled and dspTime (horizontal re-sequencing)
A crossfade that starts mid-phrase can sound like a radio being retuned. To change cue on the next bar line, as horizontal re-sequencing does, use the audio system’s own clock:
AudioSettings.dspTime(adouble, in seconds) is the current time of the audio system. It is based on the number of samples actually processed, so it is far more precise thanTime.time.AudioSource.PlayScheduled(double time)plays the clip at an absolute time on thatdspTimetimeline. It does not depend on frame rate and is accurate to the sample. Unity calls it the preferred way to stitch clips together in a music player. Schedule about 100–200 ms ahead so streamed audio has time to load. If the time has already passed, playback starts as soon as possible and the missed audio is skipped.AudioSource.SetScheduledEndTime(double time)changes when an already scheduled sound stops, on the same timeline.
(Unity Technologies, Scripting API: AudioSource.PlayScheduled; AudioSettings.dspTime; AudioSource.SetScheduledEndTime)
public class BarClock : MonoBehaviour
{
[SerializeField] private float bpm = 120f;
[SerializeField] private int beatsPerBar = 4;
private double songStartDsp; // set when bar 1 of the current cue was scheduled
public double NextBarDspTime()
{
double secondsPerBar = 60.0 / bpm * beatsPerBar;
double now = AudioSettings.dspTime;
double bars = System.Math.Floor((now - songStartDsp) / secondsPerBar) + 1.0;
double nextBar = songStartDsp + bars * secondsPerBar;
return (nextBar - now < 0.1) ? nextBar + secondsPerBar : nextBar; // leave load time
}
public void SwitchOnBar(AudioSource from, AudioSource to, AudioClip cue)
{
double barTime = NextBarDspTime();
to.clip = cue;
to.PlayScheduled(barTime); // new cue starts exactly on the bar line
from.SetScheduledEndTime(barTime); // old cue stops on the same sample
songStartDsp = barTime;
}
}This is a hard cut on the bar. Composers often write cues so that a cut on a bar line sounds intentional. For a softer change, keep the old cue playing and run the equal-power fade from section 1, starting when dspTime reaches barTime. The full example AdaptiveMusicPlayer.cs does both: CrossfadeNow and SwitchOnNextBar.
Requirements for the bar maths to stay correct:
- Shared grid. Every cue shares one tempo and time signature, or you store BPM per cue.
- Bar-length loops. Each looping clip is exactly a whole number of bars long, with no silence at the start or end of the file. Otherwise the loop drifts off the grid.
- Use doubles. Do the timing arithmetic in
double, notfloat:dspTimekeeps growing for the whole session. - Schedule ahead. The scheduling call must happen at least a frame or two before the bar line, hence the 0.1 s safety margin.
- Scheduled start (tentative).
SetScheduledEndTimeis documented for a sound that has already been scheduled to play, so start your cues withPlayScheduledrather thanPlayif you intend to end them this way. This has not been tested against aPlay()-started source.
5. Vertical layering (stems) in plain Unity
For continuous intensity, such as more drums as enemies close in, use vertical layering. Ask the composer for stems of one piece (pad, bass, percussion, lead), all exported at the same length and tempo. Give each stem its own looping AudioSource and start them all on the same dspTime with PlayScheduled, so they stay locked together. Never stop and restart an individual stem; only change its volume:
double start = AudioSettings.dspTime + 0.2;
foreach (AudioSource stem in stems) { stem.loop = true; stem.volume = 0f; stem.PlayScheduled(start); }
// Each frame: move each stem towards its target (1 if i <= intensity, else 0).
stems[i].volume = Mathf.MoveTowards(stems[i].volume, target[i], fadeSpeed * Time.unscaledDeltaTime);The complete component is MusicStemLayers.cs. Alternatively, route each stem to its own mixer group and let snapshots (section 3) do the fading.
Choosing between vertical and horizontal
| Vertical layering (stems) | Horizontal re-sequencing (cue switching) | |
|---|---|---|
| Unity tools | Synchronised PlayScheduled start + volume fades or snapshots | Two decks + PlayScheduled on the next bar, or a crossfade |
| Response | Immediate | Waits for the next bar or phrase |
| Musical change possible | Same harmony and tempo, more or less density | New key, tempo, instrumentation |
| Best for | Rising and falling tension (stealth → alert → combat) | Distinct states (town → dungeon → boss) |
These trade-offs follow Sweet’s account of the two techniques (Sweet, Writing Interactive Music for Video Games, see source-writing-interactive-music). Many games combine them: vertical layers inside each state, and horizontal switches between states.
6. Changing tempo when combat starts
AudioSource.pitch is not a tempo control. The Manual describes the Pitch property as the “Amount of change in pitch due to slowdown/speed up of the Audio Clip. Value 1 is normal playback speed” (Unity Technologies, Unity Manual: Audio Source component reference). It works like a tape or record played faster: pitch = 1.1f makes the music 10% faster and about a semitone and a half higher, so every instrument shifts key and timbre. That can be a deliberate effect (a “hurry up” timer, a slowed-down death moment), but it is not what a composer means by a tempo change.
For a real tempo change on entering combat:
- Ask for a combat cue written at the new tempo (or a version of the exploration cue re-rendered at that tempo). This is horizontal re-sequencing: the table above lists a new tempo as something only a cue switch can deliver.
- Switch on a bar line with
PlayScheduled(section 4), as a hard cut or with a short equal-power fade. Because the two cues have different tempi, store the BPM and beats per bar per cue and updateBarClock’s values when the new cue starts, otherwise the next bar calculation uses the old grid. - Optionally add a transition cue: a one-bar fill or riser the composer wrote to bridge the two tempi, scheduled on the bar before the combat cue.
C# or FMOD/Wwise? For one or two tempo changes (explore → combat → explore), the C# above is enough. Middleware earns its place when the composer wants to author these transitions themselves: in FMOD Studio, tempo markers on the timeline define the beat grid that quantised transitions use, and transition timelines provide the bridging passage, all without code changes (Firelight Technologies, FMOD Studio User Manual: Authoring Events). Either way, the game code’s job is the same: report “combat started”, and leave the musical timing to the music system.
(Tentative, not tested for this page: Unity’s mixer has an Audio Pitch Shifter effect, so in principle you could raise pitch and shift the mixer group back down to keep the key. Expect audible artefacts on music; a cue written at the right tempo sounds better.)
Trade-offs
Plain Unity is enough when:
- there are a handful of music states, and changes can wait for a fade or the next bar
- one programmer owns both the music logic and the C#
- vertical layering uses a few stems of a single piece
- the project is a jam, prototype, coursework or small release, where adding another toolchain costs more than it saves
FMOD or Wwise is worth it when (the more boxes you tick, the stronger the case):
- The composer or sound designer needs to author and audition transitions, layers and mixes themselves, without waiting for code changes. Composer-facing state tools are one of the main things middleware provides (Sweet, Writing Interactive Music for Video Games, see source-writing-interactive-music).
- Music should respond to continuous game parameters (health, threat, speed). In FMOD, game code sets one value with
EventInstance.setParameterByName("Intensity", value)and the event’s own automation curves decide what happens (Firelight Technologies, FMOD Studio API: EventInstance::setParameterByName; FMOD Studio User Manual: Authoring Events). - You need quantised transitions (jump only on a chosen beat or bar), transition timelines (a bridge passage played during the jump) or several conditional destinations. FMOD Studio provides these as transition regions, tempo markers and transition timelines. Without tempo markers it assumes 120 BPM in 4/4 (Firelight Technologies, FMOD Studio User Manual: Authoring Events). In Unity you would have to hand-build each of these from
dspTimemaths. - There are many states, stingers and variations, and the hand-written C# state logic is becoming the hard part.
- You need runtime DSP, randomised variation and per-platform audio optimisation at scale (Sweet, Writing Interactive Music for Video Games, see source-writing-interactive-music).
- The team can absorb another toolchain: the integration package, building banks, licensing, and keeping both tools in sync. See audio-middleware-overview.
With middleware, the division of labour changes. C# only reports what state the game is in, for example by setting a parameter or a state. The timing of the musical change (wait for the bar, play a bridge, crossfade over two beats) is authored in the middleware project. In plain Unity, your C# owns both.
Examples
- Explore → combat in a top-down shooter: two decks, with
SwitchOnNextBar(combatCue)when the first enemy notices the player. When the last enemy dies, a stinger plays (PlayOneShot) followed byCrossfadeNow(exploreCue, 3f). - Stealth tension: a
MusicStemLayerswith four stems. Intensity rises from 0 (pad only) to 3 (full percussion) as guards move from unaware to searching to alerted, matching the AI states in ai-state-machine-pattern. - Underwater or pause muffling: a mixer snapshot that adds a low-pass filter to the
MusicandAmbiencegroups, reached withTransitionTo(0.3f), with the mixer’supdateModeset toUnscaledTime. - Red Dead Redemption (2010) is Sweet’s example of vertical layering responding to place and threat. Uncharted 2 (2009) triggers explore → suspense transitions from spatial volumes (Sweet, see interactive-music-techniques).
Related
- interactive-music-techniques: the design side, covering horizontal re-sequencing, vertical remixing, stingers and timeouts
- music-in-games: why music must adapt (the repetition problem, cues, motifs)
- unity-audiosource:
Play,PlayOneShot, looping and volume basics - audio-middleware-overview: FMOD and Wwise as the next step up
- observer-pattern: raising “state changed” events that the music system listens to
- ai-state-machine-pattern: AI alert states as a driver for music intensity
- monobehaviour-lifecycle: coroutines,
StartandUpdatetiming - game-feel: audio as part of moment-to-moment feedback
- source-writing-interactive-music