Pepper.SFX

Features

Music and timing

Intro-and-loop music, crossfades between tracks, tempo-driven one-shots and posts that land on an exact DSP time.

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 Post returns.
  • 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 exact AudioSettings.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 an AudioDuration (Shortest, Longest, Loops, IsKnown) before you post.
  • BeatScheduler. A component that posts a one-shot event every 60 / Bpm seconds, each beat on its DSP grid time. Changing Bpm at runtime changes the tempo, not the pitch: heartbeats, metronomes, sonar pings. OnBeat fires 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

  1. 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.
  2. Set Max Instances to 1 on music events.
  3. For a designer-driven change, add a CrossFade action, or use Window > Audio System > Sound Builder (Simple), which asks for the Fade From Container.
  4. 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 (OnBeat still 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.

Learn more