Pepper.SFX
All Guidance pages

Guidance / Cookbook

Music Systems

Recipe: Background Music (Intro + Loop)

📋 What: Music with intro that transitions to seamless loop

🎯 Use Case: Level music, menu music

Asset Setup

1. Create Containers:

Intro:

  • Routing Container: “Music_MainTheme_Intro_RC”
  • Audio Clips: [Intro clip]
  • Loop: ☐ (no loop)
  • Volume: 1.0
  • Is3D: ☐ (2D)

Loop:

  • Routing Container: “Music_MainTheme_Loop_RC”
  • Audio Clips: [Loop clip]
  • Loop: ✓ (loop enabled)
  • Volume: 1.0
  • Is3D: ☐ (2D)

2. Create Event:

  • Event: “Music_Start_MainTheme”
  • Priority: Medium
  • Max Instances: 1
  • Actions → Size: 2

Action [0]:

  • Type: Play
  • Container: Music_MainTheme_Intro_RC
  • Target Bus: Music_Bus
  • Delay: 0

Action [1]:

  • Type: Play
  • Container: Music_MainTheme_Loop_RC
  • Target Bus: Music_Bus
  • Delay: 8.5 ← SET TO YOUR INTRO LENGTH IN SECONDS

3. Stopping it: keep the handle Post returns and call Stop on it (see the code below). Don’t make a separate “Music_Stop” event with a Stop action: a Stop action only stops sounds its own event started, so it can’t stop this music.

Code

using UnityEngine;
using AudioSystem;

public class MusicManager : MonoBehaviour
{
    [SerializeField] private AudioEvent startMusicEvent;
    [SerializeField] private bool playOnStart = true;

    private AudioHandle musicHandle;

    void Start()
    {
        if (playOnStart)
        {
            StartMusic();
        }
    }

    public void StartMusic()
    {
        if (startMusicEvent != null)
        {
            musicHandle = startMusicEvent.Post();
        }
    }

    public void StopMusic(float fadeTime = 2f)
    {
        if (musicHandle != null)
        {
            // Also cancels the loop if the intro is still playing.
            musicHandle.Stop(fadeTime);
            musicHandle = null;
        }
    }

    void OnDestroy()
    {
        StopMusic(1f);
    }
}

Setup:

  1. Create empty GameObject: “MusicManager”
  2. Attach MusicManager script
  3. Assign the start event
  4. Mark as DontDestroyOnLoad if needed

✅ Result: Intro plays once, then the loop takes over and repeats.

💡 How close is the join? A delayed action waits on a frame timer, so the loop starts within about one frame after the intro ends (≈16 ms at 60 fps; more if that frame is slow). With the event’s Time on Scaled (the default), slow motion or a pause also stretches the wait, so set Time = Unscaled (§5.4) for music. On a hard musical cut that small gap can be heard. For a sample-accurate join, make two events (intro and loop, each with Delay 0) and start both on the audio clock yourself:

[SerializeField] private AudioEvent introEvent, loopEvent;
[SerializeField] private AudioClip introClip;   // the intro's clip, for its exact length
private AudioHandle introHandle, loopHandle;

public void StartMusic()
{
    double start = AudioSettings.dspTime + 0.1;                           // a little ahead, so both are scheduled in time
    double introLength = (double)introClip.samples / introClip.frequency;  // exact, in seconds
    introHandle = introEvent.Post(gameObject, transform.position, start);
    loopHandle  = loopEvent.Post(gameObject, transform.position, start + introLength);
}
// To stop, Stop both handles.

This needs the intro’s container to have no pitch randomization (a pitch change changes its length).

💡 Switching tracks: keep the handle Post returns and crossfade from it: musicHandle = AudioManager.Instance.CrossFade(musicHandle, nextTrackEvent, 2f);. That also cancels the delayed loop if the intro is still playing. A container crossfade from the intro would let the loop start anyway (see §10.3).


Recipe: Dynamic Combat Music (3 Layers)

📋 What: Music that responds to combat intensity with layered crossfading

🎯 Use Case: Action games, dynamic gameplay music

Asset Setup

1. Create Layer Containers:

Ambient Layer:

  • Routing Container: “Music_Ambient_RC”
  • Clip: Calm ambient music loop
  • Loop: ✓
  • Volume: 1.0

Tension Layer:

  • Routing Container: “Music_Tension_RC”
  • Clip: Tense percussion/strings loop
  • Loop: ✓
  • Volume: 1.0

Combat Layer:

  • Routing Container: “Music_Combat_RC”
  • Clip: Full combat music loop
  • Loop: ✓
  • Volume: 1.0

2. Create Blend Container:

  • Blend Container: “Music_Dynamic_Combat_BC”
  • Blend Parameter Name: “CombatIntensity”
  • Volume: 1.0
  • Loop: ✓

Blend Entries:

Entry [0] - Ambient:

  • Container: Music_Ambient_RC
  • Volume Curve:
    • Key (0.0, 1.0) ← Full volume at calm
    • Key (1.0, 0.0) ← Silent at combat

Entry [1] - Tension:

  • Container: Music_Tension_RC
  • Volume Curve:
    • Key (0.0, 0.0) ← Silent at calm
    • Key (0.5, 1.0) ← Peak at medium intensity
    • Key (1.0, 0.0) ← Silent at full combat

Entry [2] - Combat:

  • Container: Music_Combat_RC
  • Volume Curve:
    • Key (0.0, 0.0) ← Silent at calm
    • Key (1.0, 1.0) ← Full at combat

3. Create Event:

  • Event: “Music_Start_DynamicCombat”
  • Actions: Play Music_Dynamic_Combat_BC → Music_Bus

Code

using UnityEngine;
using AudioSystem;

public class DynamicCombatMusic : MonoBehaviour
{
    [Header("Audio")]
    [SerializeField] private AudioEvent startMusicEvent;

    [Header("Settings")]
    [SerializeField] private float transitionSpeed = 0.5f;
    [SerializeField] private float enemyDetectionRadius = 20f;
    [SerializeField] private LayerMask enemyLayer;

    [Header("Intensity Thresholds")]
    [SerializeField] private int calmEnemyCount = 0;
    [SerializeField] private int tensionEnemyCount = 2;
    [SerializeField] private int combatEnemyCount = 4;

    private float currentIntensity = 0f;
    private float targetIntensity = 0f;

    void Start()
    {
        StartMusic();
    }

    void StartMusic()
    {
        if (startMusicEvent != null)
        {
            startMusicEvent.Post();
        }
        AudioManager.Instance.SetRTPC("CombatIntensity", 0f);
    }

    void Update()
    {
        UpdateMusicIntensity();
    }

    void UpdateMusicIntensity()
    {
        // Calculate target intensity based on enemy count
        int enemyCount = CountNearbyEnemies();
        targetIntensity = CalculateIntensity(enemyCount);

        // Smooth transition
        if (Mathf.Abs(currentIntensity - targetIntensity) > 0.01f)
        {
            currentIntensity = Mathf.MoveTowards(
                currentIntensity,
                targetIntensity,
                transitionSpeed * Time.deltaTime
            );

            AudioManager.Instance.SetRTPC("CombatIntensity", currentIntensity);
        }
    }

    int CountNearbyEnemies()
    {
        Collider[] enemies = Physics.OverlapSphere(
            transform.position,
            enemyDetectionRadius,
            enemyLayer
        );
        return enemies.Length;
    }

    float CalculateIntensity(int enemyCount)
    {
        if (enemyCount <= calmEnemyCount)
            return 0f; // Ambient only

        if (enemyCount <= tensionEnemyCount)
            return 0.5f; // Tension layer

        if (enemyCount <= combatEnemyCount)
            return 0.75f; // Ramping up

        return 1f; // Full combat
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, enemyDetectionRadius);

        // Show intensity
        #if UNITY_EDITOR
        UnityEditor.Handles.Label(transform.position + Vector3.up * 2f,
            $"Music Intensity: {currentIntensity:F2}");
        #endif
    }
}

Setup:

  1. Attach to player or game manager
  2. Configure detection radius and thresholds
  3. Set enemy layer mask
  4. Tune transition speed (0.3-0.8 recommended)

✅ Result: Music dynamically responds to combat!

From the Pepper.SFX 3.1 guides (USER_GUIDE.md). Something wrong or unclear? Tell me.