Game music needs an intro that runs into a loop, a clean change of track when combat starts, and hits that land on the beat. Pepper.SFX has no separate music engine: it does these with the same events and containers as every other sound, plus a few timing tools.
How it works
- Intro and loop. Either give an event two Play actions, with the loop’s Delay set to the intro’s length, or use a Sequence Container with an intro entry and a looping entry and Auto Advance on: it plays the intro once, then stays on the loop. Stop it with a fade on the handle
Postreturns. - Handle crossfade.
AudioManager.Instance.CrossFade(handle, nextEvent, 2f)fades out everything that handle plays, cancels what it was still going to start (the loop waiting behind an intro), posts the next event, fades it in and returns its handle, on an Equal Power curve by default. - CrossFade action. An event’s CrossFade action fades its Fade From Container out while its Container fades in, over its Fade Duration, through its Target Bus.
- Timed posts.
Post(source, position, dspTime)starts a sound on an exactAudioSettings.dspTime.Post(emitter, startDsp, startOffsetSeconds)makes a chosen moment inside the sound land on that time: a whoosh peaking on a pass-by. - Lengths.
evt.EstimateDuration()returns anAudioDuration(Shortest, Longest, Loops, IsKnown) before you post. - BeatScheduler. A component that posts a one-shot event every
60 / Bpmseconds, each beat on its DSP grid time. ChangingBpmat runtime changes the tempo, not the pitch: heartbeats, metronomes, sonar pings.OnBeatfires on the main thread for game logic.
Example
using UnityEngine;
using AudioSystem;
public class LevelMusic : MonoBehaviour
{
[SerializeField] private AudioEvent explorationMusic; // a loop at 120 BPM, whole bars long
[SerializeField] private AudioEvent combatMusic;
[SerializeField] private AudioEvent stinger; // a short one-shot hit
[SerializeField] private float bpm = 120f;
private AudioHandle music;
private double musicStart;
void Start()
{
musicStart = AudioSettings.dspTime + 0.1; // slightly ahead, to start exactly then
music = explorationMusic.Post(gameObject, transform.position, musicStart);
}
public void PlayStingerOnNextBar()
{
double bar = 4 * 60.0 / bpm;
double barsDone = System.Math.Ceiling((AudioSettings.dspTime - musicStart) / bar);
stinger.Post(gameObject, transform.position, musicStart + barsDone * bar);
}
public void EnterCombat() => music = AudioManager.Instance.CrossFade(music, combatMusic, 2f);
}
Starting the track on a known DSP time lets a stinger land on its next bar, and entering combat swaps tracks with a two-second crossfade.
Setting it up
- Make a Routing Container with Loop ticked, or a Sequence Container for intro and loop, and an event that plays it through a Music bus.
- Set Max Instances to 1 on music events.
- For a designer-driven change, add a CrossFade action, or use Window > Audio System > Sound Builder (Simple), which asks for the Fade From Container.
- For beats, Add Component > Audio > Beat Scheduler, assign its Audio Event (a one-shot) and set Bpm.
Good to know
- A delayed Play action and a Sequence’s next clip start in Pepper’s per-frame step, not on a DSP time, so the join can land up to about a frame late. For a sample-exact join, post the next part on a DSP time.
- Delays, Sequence gaps and crossfades run on the event’s Time. Scaled (the default) slows with the time scale and stops at 0; Unscaled keeps them running in slow motion or a pause.
- Each Sequence Container asset keeps one position, shared by everyone playing it; duplicate it for independent sequences.
- A Stop action only reaches its own event’s sounds, so stop music with its handle.
- A container crossfade, including the CrossFade action, fades only the voices playing then: a delayed loop or a Sequence’s next clip still comes in. The action’s handle finishes at once, so stop that music with
container.Stop()(every play of that asset) or another crossfade. - BeatScheduler posts at most once per frame and drops beats missed during a hitch (
OnBeatstill catches up). Two schedulers at the same Bpm aren’t phase-aligned, and there are no bars or song structure. - Full version only: a Networked Beat Scheduler puts every player’s beats on one shared grid.