# Pepper.SFX: Audio middleware for Unity > Pepper.SFX is audio middleware for Unity. Build your game’s sound as assets (events, containers, buses, states and parameters) and play any of it with one line of code. Unity 6000.0.48f1 or newer. Package `com.pepper.sfx`, namespace `AudioSystem`. Free version: https://github.com/IamAmirPepper/Pepper.SFX_System. Full version: coming to the Unity Asset Store. Docs version: 3.1. Site: https://peppersfx.com/ # Features ## Events and containers Source: https://peppersfx.com/features/events-and-containers/ Without middleware, every sound in a Unity game is an AudioSource and a clip wired up in code, and picking variations, stopping spam, fading and routing are all yours to write. Pepper.SFX moves that work into assets. You build an **Audio Event** and the containers under it, and your code plays it with one call that returns a handle. ### How it works - **Audio Event** is what your code posts. It holds a list of **actions** that run in order, each with an optional Delay: Play, Stop, Pause, Resume, SetSwitch, SetRTPC, SetState, TriggerDucking and CrossFade. One post can start a sound, change a state and duck the music. - **Voice Settings** keep an event in check: Priority, **Max Instances** with a Steal Behavior (Oldest, Quietest, Furthest, Lowest Priority), **Cooldown**, and the same limits per Audio Emitter. Tick **Persistent** for sounds that must never be stolen, such as an engine. **Time** picks game or real time, so pause-menu sounds work at time scale 0. - **`Post`** returns an `AudioHandle`. Stop it with a fade, pause it, change its volume or pitch, or wait for `OnFinished`. - **Containers** decide which clips play, and they nest inside each other: - **Random Container**: one clip per post, with weights and Avoid Repeat Last, remembered per Audio Emitter. Footsteps, gunshots, clicks. - **Sequence Container**: clips in order (Forward, Reverse, Ping Pong or Random), with Auto Advance, Loop Sequence and a Delay After per entry. - **Switch Container**: plays the child that matches a switch value, such as the surface under a foot. - **Blend Container**: layers that crossfade along a parameter, such as engine RPM or combat intensity. - **Routing Container**: plays all its clips at once, for layered sounds like an explosion's blast and debris. - Every container also has volume and pitch randomisation, 3D settings, a Mixer Group and Parameter Curves. ### Example ```csharp using UnityEngine; using AudioSystem; public class Generator : MonoBehaviour { [SerializeField] private AudioEvent switchOn; // Random Container: three clunks [SerializeField] private AudioEvent hum; // Routing Container with Loop ticked private AudioHandle humHandle; public void TurnOn() { switchOn.Post(gameObject, transform.position); // fire and forget if (humHandle == null) humHandle = hum.Post(gameObject, transform.position); // keep the loop's handle } public void SetLoad(float load01) { humHandle?.SetPitchOffset(load01 * 2f); // up to two semitones higher under load } public void TurnOff() { humHandle?.Stop(0.5f); // fade out over half a second humHandle = null; } } ``` The clunk is posted and forgotten. The hum loops, so the script keeps its handle to bend its pitch and fade it out. Posting by name (`AudioManager.Instance.PostEvent("Generator_Hum")`) needs the event in an Audio Event Registry or `Resources/Audio/Events`. ### Setting it up 1. Add the `AudioManager` component to a GameObject in your first scene. 2. Create a container with **Assets > Create > Audio System > Random Container** (or Sequence, Switch, Blend or Routing Container) and add your clips. 3. Create the event with **Assets > Create > Audio System > Audio Event**. Add a Play action and set its Container and Target Bus. 4. Set Priority, Max Instances and Cooldown under Voice Settings. 5. Drag the event into an `AudioEvent` field on your script and call `Post`. **Window > Audio System > Sound Builder (Simple)** makes a bus, container and event in one step, and **Window > Audio System > Audition** plays a container outside Play mode. ### Good to know - A Stop action only stops sounds that its own event started. To stop music started by another event, keep the handle from `Post` and call `Stop` on it. - Calling `Stop()` on a container stops every sound playing from that container asset. To stop one of them, stop its handle. - Each Sequence Container asset keeps a single position, so two objects playing the same one share it. Duplicate the asset if you need two independent sequences. - Posting a Blend Container event again plays a second copy alongside the first. Set the event's Max Instances to 1 if you want it to restart instead. - A post blocked by Cooldown returns a handle that has already finished, so `OnFinished` still fires. ## Parameters, states and switches Source: https://peppersfx.com/features/parameters-states-switches/ A car's engine follows its RPM, footsteps change with the ground, and the mix shifts when the player dives underwater. Pepper.SFX gives each a home: parameters for numbers, switches for choices and states for game modes, set from code or from an event's actions. ### How it works - **Parameter assets** (RTPCs) are named numbers with a range, a unit label, a default and **Smoothing**: None, Rate or Ease, with separate rise and fall. The string API (`SetRTPC("CombatIntensity", 0.7f)`) and an asset with that Parameter Name are the same parameter. - **Three scopes.** A value can be set on one sound's handle, on an **Audio Emitter** (every sound on one car), or globally. A sound reads the closest one that has a value, so two cars can play the same engine Blend at different RPMs. - **Built-in values.** Set a parameter's **Value From** to Distance, Approach Speed or Angle and Pepper works it out for each sound, from the listening point. - **Parameter Curves** on any container drive its volume (dB), pitch (cents), low-pass or high-pass (Hz) from a parameter, with no code. A Blend Container crossfades its layers along one. - **Values at post.** `PostValues` carries up to four parameter values and two switch values, so a one-shot's first sample hears them. - **Switch Group assets** list a switch's values (Asphalt, Gravel, Grass). Set them globally, per emitter or per handle, and a Switch Container plays the matching child. **When Switch Changes** (Next Play Only, Crossfade, Restart) decides what a playing loop does. A group can also be **Driven By** a parameter through ranges, with hysteresis. - **Audio State assets** belong to a **State Group**, and one state per group is active. A state sets bus volumes, switches, RTPCs and exposed mixer effect values, easing the levels over a transition time. ### Example ```csharp using UnityEngine; using AudioSystem; [RequireComponent(typeof(AudioEmitter))] public class CarAudio : MonoBehaviour { [SerializeField] private AudioEvent engineEvent; // a Blend Container on RPM [SerializeField] private AudioEvent impactEvent; [SerializeField] private AudioParameter rpm; // 800-7000, Smoothing: Ease [SerializeField] private AudioParameter impactStrength; // 0-1 [SerializeField] private AudioSwitchGroup surface; // Asphalt, Gravel, Grass private AudioEmitter emitter; void Start() { emitter = GetComponent(); emitter.Post(engineEvent); } public void UpdateEngine(float engineRpm) => emitter.SetParameter(rpm, engineRpm); // this car only public void OnSurfaceChanged(string value) => emitter.SetSwitch(surface, value); void OnCollisionEnter(Collision collision) { float strength = Mathf.Clamp01(collision.relativeVelocity.magnitude / 20f); emitter.Post(impactEvent, PostValues.Of(impactStrength, strength)); } } ``` Each car keeps its RPM and surface on its Audio Emitter, so many cars share one engine event, and the impact's strength travels with the post. A game mode is one global call: `AudioManager.Instance.SetState("Underwater", 1.5f)`. ### Setting it up 1. **Assets > Create > Audio System > Parameter**. Set Unit Label, Min Value, Max Value, Default Value and Smoothing. 2. **Assets > Create > Audio System > Switch Group**. List its Values and set a Default Value. 3. On a container, add a Parameter Curve, set a Blend Container's Blend Parameter, or set a Switch Container's Switch Group. 4. Add **Add Component > Audio System > Audio Emitter** to each object that needs its own values, and post its sounds on it. 5. **Assets > Create > Audio System > Audio State**. Set its State Name and State Group, then the bus volumes, switches and RTPCs it changes. Keep states in `Resources/Audio/States`, or list them in an Audio Event Registry. ### Good to know - Values set by name (`SetRTPC`, `TransitionRTPC`) are never smoothed. Smoothing applies to writes through the asset (`SetParameter`, `RampParameter`). - Code can't set a built-in value: `SetParameter` on one is ignored with a warning. - A value that isn't a number (NaN or infinity) is refused with a warning, and sounds keep the last good value. - Setting a state applies only its own values: nothing from the previous state is undone, so give each state in a group a value for everything its siblings change. The exception is Bus Volume Mode Offset: its offsets go back when the state ends. - Parameters update once a frame, after every LateUpdate, so a value written anywhere this frame is heard this frame. - In Play mode, **Window > Audio System > Live View** shows every Audio Emitter's values and sounds. ## Buses and mixing Source: https://peppersfx.com/features/buses-and-mixing/ Players expect a music slider, dialogue they can hear over explosions, and a pause menu that stops the world but not its own clicks. In Pepper.SFX each of those is a setting on an **Audio Bus** asset. Set it once, and every event routed to that bus follows it. ### How it works - **Hierarchy.** Each bus has a Parent Bus. Its level (Volume, plus any state's offset, plus the player's volume, in dB) is multiplied by its ducking, its curves and its parents' levels, so muting a bus mutes its children. A bus can also feed a Unity Audio Mixer Group for Unity's effects. - **Ducking.** A trigger bus lists Ducking Targets, each with a Duck Amount in dB, plus attack and release times. A TriggerDucking action or `bus.TriggerDucking(holdSeconds)` starts it. When ducks overlap, the deepest wins, so a short UI duck can't cut a dialogue duck short. - **Parameter Curves on a bus.** A bus's volume, and a low- or high-pass on its Mixer Group, can follow a parameter, such as nitro sweeping the other cars' low-pass. - **Pausing a bus.** `Pause()` fades the bus and its child buses out over about 25 ms and holds their place without keeping real voices. `Resume()` carries on, every layer in phase. **While Paused** decides whether one-shots posted meanwhile are dropped or held. - **Player volume.** `PlayerVolumeDb` is a separate level for a settings menu. It is never saved into the asset, and no state undoes it. - **Voice caps.** **Max Real Voices** caps a bus's real voices. Over it, the least important sounds go virtual, never ended, and come back in step. - **States.** An Audio State can move bus levels: **Set Level** writes the bus's Volume, **Offset** moves it and puts it back when the state ends. - **Mixer window** (full version only): **Window > Audio System > Mixer** shows every bus as a fader with mute and solo; changes are saved and can be undone. ### Example ```csharp using UnityEngine; using AudioSystem; public class MixControls : MonoBehaviour { [SerializeField] private AudioBus musicBus; [SerializeField] private AudioBus worldBus; // parent of Vehicles, Ambience... [SerializeField] private AudioBus dialogueBus; // ducks Music when triggered // Settings sliders, 0 to 1. public void SetMasterVolume(float slider) => AudioManager.Instance.SetMasterVolume(slider); public void SetMusicVolume(float slider) => musicBus.PlayerVolumeDb = AudioSystem.AudioExtensions.LinearToDb(slider); public void OpenPauseMenu() { Time.timeScale = 0f; worldBus.Pause(); // the world holds; the menu's own buses play on } public void ClosePauseMenu() { worldBus.Resume(); Time.timeScale = 1f; } // Duck the music while a voice line plays. public void OnLineStarted(float lineSeconds) => dialogueBus.TriggerDucking(lineSeconds); } ``` Sliders set the player's volume, so a mix state never fights the settings menu. Pausing the World bus holds the game while the UI and menu-music buses outside it keep playing. ### Setting it up 1. **Assets > Create > Audio System > Audio Bus** for Master, then Music, SFX, Dialogue and so on, each with its Parent Bus set. 2. On the `AudioManager`, assign the Master Bus and add every bus to Buses, so lookups by name find them. 3. Set the Target Bus on each event's Play action. 4. For ducking, tick Enable Ducking on the Dialogue bus, add Music to its Ducking Targets, and give the dialogue event a TriggerDucking action (Target Bus: Dialogue, Fade Duration: the hold time). 5. For a filter curve, give the bus a Mixer Group, add a Low Pass Parameter Curve, and click **Add Low Pass to Mixer Group** in the bus inspector. ### Good to know - Ducking is triggered, not detected from what the bus plays. A duck runs on the target bus's Ducking Time Mode: Scaled, the default, freezes at time scale 0, so set Unscaled on buses ducked during a pause. - A raise can't take a sound past its AudioSource's full volume, so mix with headroom. - Bus curves read only a parameter's global value, and pitch curves are ignored on a bus. If the inspector button can't edit the mixer in your Unity version, it logs the manual steps. - Unity's Web audio has no mixer effects, so bus filter curves don't work in Web builds. - On Unity 6.3, write `AudioSystem.AudioExtensions` in full: Unity added its own `AudioExtensions`. ## Occlusion Source: https://peppersfx.com/features/occlusion/ A gunshot on the other side of a wall should sound dull and distant, not as if it went off beside the player. Pepper.SFX muffles 3D sounds when walls, cover or props stand between them and the listener. You tick **Use Occlusion** on a sound's container, and Pepper casts the rays and drives the filter. ### How it works - Every **Occlusion Update Interval** (0.2 s by default), the Audio Manager casts rays from the listening point to each occluded sound, against the layers in **Occlusion Mask**. Trigger colliders are ignored, and so are the sound's own object and the player. - **Occlusion Ray Count** (3 by default) fans the rays across the source, **Occlusion Ray Spread Meters** wide. A partly hidden sound is partly muffled, so walking past the edge of a wall gives a gradient rather than a jump. Set it to 1 for a hard edge. - Fully blocked, a sound drops by about 10 dB and a low-pass filter takes off its top end. Level and cutoff glide to their targets every frame, so nothing steps. - The filter doesn't sit on the AudioSource: a filter component resets each time the voice pool reuses a source, and that clicks. Instead Pepper builds an **occlusion mixer slot pool**, a set of mixer groups under each bus (six by default), each with a Lowpass effect. A voice borrows a slot before it starts and returns it when it ends, and Pepper writes the cutoff to the slot's exposed mixer parameter. - The same slots carry per-zone reverb sends (**Allow Reverb Send**) and muffling through doors (**Use Propagation**), so this setup is the groundwork for both. ### Example Occlusion needs no code. This turret posts its shot as usual, and shows how many of the Weapons bus's slots are in use, to help you size **Slots Per Bus**. ```csharp using UnityEngine; using UnityEngine.Audio; using AudioSystem; public class Turret : MonoBehaviour { [SerializeField] private AudioEvent fireEvent; // its container has Use Occlusion ticked [SerializeField] private AudioMixerGroup weaponsBus; // the Mixer Group that container plays through public void Fire() { // Nothing occlusion-specific: if a wall is in the way, the shot comes out muffled. fireEvent.Post(gameObject, transform.position); } void OnGUI() { var manager = AudioManager.Instance; if (manager == null || !manager.OcclusionSlotPoolsReady) { GUILayout.Label("Occlusion slots aren't set up"); return; } int used = manager.GetOcclusionSlotsInUse(weaponsBus); int total = manager.GetOcclusionSlotCapacity(weaponsBus); // -1 if the bus isn't in the layout GUILayout.Label($"Weapons occlusion slots: {used} / {total}"); } } ``` ### Setting it up 1. In your container, under **Mixer Settings**, set a **Mixer Group** and tick **Use Occlusion**. Under **3D Settings**, tick **Is 3D**. 2. **Create > Audio System > Occlusion Layout**. One per project. 3. Open **Window > Audio System > Occlusion Layout** (or **Open Occlusion Layout Builder** on the Audio Manager). Assign your **Voice Mixer** and the layout asset, leave **Slots Per Bus** at 6 (8 to 12 for weapon-heavy scenes) and click **Scan, Auto-Create, Refresh**. Under each bus your occluded containers use, it builds a `_OcclusionLayer` group holding `_Slot_00`, `_Slot_01` and so on. 4. On the Audio Manager, assign the same **Voice Mixer** and **Occlusion Layout**. 5. Press Play. The Console reports "Occlusion slot pools ready" with the number of buses and slots. 6. Tune **Occlusion Ray Count**, **Occlusion Ray Spread Meters**, **Occlusion Update Interval** and **Occlusion Mask** on the Audio Manager. Run step 3 again when an occluded container plays through a bus that isn't in the layout yet. ### Good to know - **Web builds:** sounds get quieter behind walls but aren't muffled, because Unity's Web audio has no mixer effects. - **Automatic mixer setup** has only been tested on Unity 6000.0.48f1. If it fails on a newer version, click **Diagnose Reflection** in the window and send us the Console output. - **No slot, no filter.** A voice that gets no slot still gets quieter behind walls but isn't filtered. If the bus's pool ran out, the Console warns; raise Slots Per Bus and run the window again. A bus missing from the layout gives no warning. Occluded, reverb-sending and propagation voices share the same slots. - In a Blend or Switch Container, each child's own Use Occlusion applies, not the parent's. - **Mobile:** raycasts cost more on phones. If the Profiler shows it, raise Occlusion Update Interval or lower Occlusion Ray Count. - It isn't a diffraction model. Sound doesn't bend round corners; the fan of rays only softens edges. ## Ambient propagation Source: https://peppersfx.com/features/ambient-propagation/ Rain outside a house shouldn't sound the same in the hallway as on the porch. Indoors you hear it through the open window, quieter, duller and from the window's direction; shut the door to that room and it fades further. Pepper.SFX models this with zones and portals, and routes long-running ambient loops through them. ### How it works - An **Audio Zone** is a room: a GameObject with one or more trigger BoxColliders. An L-shaped room is one zone with two boxes. - An **Audio Portal** is an opening between two zones: a doorway, a window, a vent. It sets how much gets through open (**Base Transmission**, **Open Low Pass Hz**) and closed (**Closed Transmission**, **Closed Low Pass Hz**). - A portal can follow a door. Any MonoBehaviour that implements `IPortalDoorSource` (an `OpenProgress` from 0 to 1 and an `OnChanged` event) drives it, so Pepper doesn't need to know how your doors work. Half open gives values halfway between. - An **Ambient Source** declares a looping clip in a zone. The **Propagation Manager** finds the best path from that zone to the listener's, adding up the loss at each portal and keeping the lowest cutoff, and plays the loop from a pooled emitter at the portal you hear it through. Walk along the wall and the rain slides along the window. - In a doorway you hear a blend of both sides, so there's no snap at the threshold. - The manager creates itself in Play mode, solves 15 times a second (**Solve Rate Hz**) and smooths volume, cutoff and position every frame. - One-shot sounds can use the same rooms: tick **Use Propagation** on a container and a knock behind a closed door is muffled through the portals. This needs the occlusion slots. ### Example A door that swings open and shut and tells its portal as it moves. ```csharp using System; using UnityEngine; using AudioSystem.Propagation; public class AudioDoor : MonoBehaviour, IPortalDoorSource { [SerializeField] private float swingSeconds = 0.8f; [SerializeField] private float openAngle = 90f; private float progress; // 0 = closed, 1 = open private bool wantOpen; public float OpenProgress => progress; public event Action OnChanged; public void Open() => wantOpen = true; public void Close() => wantOpen = false; void Update() { float target = wantOpen ? 1f : 0f; if (Mathf.Approximately(progress, target)) return; progress = Mathf.MoveTowards(progress, target, Time.deltaTime / swingSeconds); transform.localRotation = Quaternion.Euler(0f, progress * openAngle, 0f); OnChanged?.Invoke(); // the portal re-solves now, not on the next tick } } ``` Without `OnChanged`, the portal still picks up the change on the next solve, up to about 67 ms later. ### Setting it up 1. For each room, make a GameObject and add **Audio > Propagation > Audio Zone**. It adds a trigger BoxCollider; size it to the room. For the outdoors, set **Activation Radius** to Infinity so it's never culled. 2. At each opening, add **Audio > Propagation > Audio Portal** to a GameObject. Size its box to span the opening and overlap both rooms' boxes by at least 5 cm. Set **Zone A** and **Zone B** (or tick **Auto Detect Zones**), and point the object's +Z through the opening. 3. For a door, drag a component that implements `IPortalDoorSource` into the field under the portal's **Door Source** heading. Leave it empty for openings that are always open. 4. Add **Audio > Propagation > Ambient Source** to an object in the zone the sound comes from. Set **Source Zone**, **Looping Clip** and **Ambience Mixer Group**. 5. Add **Audio > Propagation > Audio Listener Zone Tracker** to the player object that has the Collider, not to the camera with the AudioListener. 6. Press Play and walk through. Select the Propagation Manager in the Hierarchy to see the listener's zone and the active emitters. ### Good to know - **Web builds:** beds still route through zones, portals and doors, but aren't muffled and get no reverb: the emitters use Unity's Low Pass and Reverb filter components, which Web audio doesn't support. - Ambient emitters sit outside Pepper's voice pool, capped by **Max Ambient Emitters** (16); past the cap, the farthest goes silent. - A source is heard through its single best path unless you raise **Max Paths Per Source** (up to 6), at one emitter per path. - Clutter inside a room doesn't block ambient beds unless you tick **Enable Obstruction** on the Propagation Manager. - Beds take on the reverb of the room you're in, from that zone's Reverb Send Bus. - A bad loop point in the clip clicks every time round. Sound doesn't bend round corners, so split large open spaces into more zones. ## Reverb Source: https://peppersfx.com/features/reverb/ A gunshot in a tiled bathroom should ring like a bathroom, even when you hear it from the hallway. One global reverb makes every room sound alike; a reverb that follows the listener makes that shot sound like the hallway. Pepper.SFX splits the two: a sound sends to the reverb of the room it's played in, and each room's reverb keeps its own character wherever you stand. ### How it works - A **Reverb Send Bus** asset is one acoustic space: a small room, a hall, a cave. It owns a mixer group with a reverb on it and holds the character: **Room**, **Decay Time**, **Reverb Level** and Unity's other SFX Reverb settings, or an impulse response. - An **Audio Zone** (from ambient propagation) routes: its **Reverb Bus** field says which bus the sounds inside it send to. Zones can share a bus; for two rooms that sound different, use two bus assets. - A container opts in with **Allow Reverb Send** and sets **Reverb Send Level Db**: around 0 dB for a gunshot, nearer −12 dB for footsteps. The send is full when you're in the same room, lower through an open door, lower again through a closed one, and right from the first frame. - **Explicit Reverb Bus** ties a sound to one bus wherever it plays: a choir that always sounds like a cathedral, or a narrator. - Ambient beds take the reverb of the room you're standing in, through a Reverb filter on each ambient emitter. Its six values (an `AudioReverbProfile`) come from that zone's bus asset and crossfade through doorways. **Convolution reverb.** Tick **Use Convolution** on a bus and it convolves the send with a real impulse response, a recording of an actual space, instead of using Unity's parametric SFX Reverb. It runs in a native plugin built for 64-bit Windows only. ### Example A probe for checking your setup: which bus a spot sends to, and how long it rings. ```csharp using UnityEngine; using AudioSystem; using AudioSystem.Propagation; public class ReverbProbe : MonoBehaviour { private readonly AudioReverbProfile character = new AudioReverbProfile(); [ContextMenu("Which reverb is here?")] void Report() { var registry = AudioManager.Instance != null ? AudioManager.Instance.ReverbSendBusRegistry : null; var zone = PropagationManager.HasInstance ? PropagationManager.Instance.GetZoneContainingPoint(transform.position) : null; if (registry == null || zone == null || zone.ReverbBus == null) { Debug.Log("No reverb zone here"); return; } ReverbSendBus bus = registry.FindByReverbBus(zone.ReverbBus); if (bus == null) { Debug.Log($"{zone.name}'s bus isn't in the registry"); return; } if (bus.UseConvolution) { Debug.Log($"{zone.name} sends to {bus.name} (convolution)"); return; } bus.GetDefaultProfile(character); // the six values Pepper writes to the mixer Debug.Log($"{zone.name} sends to {bus.name}: decay {character.DecayTime:0.0} s"); } } ``` In Play mode, run it from the component's menu in the Inspector. ### Setting it up Set up occlusion first: reverb sends use the same slots. 1. **Create > Audio System > Reverb Send Bus**, named after the room (`Reverb_Bathroom`). Assign **Reverb Mixer** (your voice mixer) and set **Decay Time** and **Room**. 2. In its Inspector, click **Generate Mixer Group**, then **Validate Wiring**, which should say "Wiring OK." 3. **Create > Audio System > Reverb Send Bus Registry** and assign it to the Audio Manager's **Reverb Send Bus Registry**. In **Window > Audio System > Reverb Send Buses**, click **Refresh** to fill it. 4. In **Window > Audio System > Occlusion Layout**, assign the registry and click **Scan, Auto-Create, Refresh** so every slot gets a Send to each bus. Repeat after adding a bus. 5. On each Audio Zone, drag the bus asset's **Reverb Bus** mixer group into the zone's **Reverb Bus** field. 6. On each container, tick **Allow Reverb Send**, set its level, and tick **Static Emitter** if the sound doesn't move. For convolution, tick **Use Convolution**, assign an **Impulse Response** clip with Load Type set to Decompress On Load or PCM, and generate again. To tune by ear, **Apply Params → Mixer** pushes the asset's values to the mixer and **Pull Params ← Mixer** reads them back. ### Good to know - **Convolution is 64-bit Windows only:** the Windows editor and Windows x64 players. Everywhere else, including macOS, Linux, mobile and consoles, a convolution bus is silent and Unity may log an error at startup. Unless you only ship on 64-bit Windows, use parametric buses. - **Web builds have no reverb at all**, parametric or convolution: Unity's Web audio has no mixer effects or sends. - Convolution in this version: the impulse response is fixed once loaded, a stereo one is mixed to mono, and ambient beds use the bus's parametric values instead. - A container's bus needs slots in the Occlusion Layout, and the layout window only builds them for a bus with at least one **Use Occlusion** container. - **Automatic mixer setup** has only been tested on Unity 6000.0.48f1. ## Multi-position audio Source: https://peppersfx.com/features/multi-position-audio/ A river isn't one point. Nor is a row of torches down a corridor, or the speakers round a dance floor. A separate sound at every spot costs a voice each, and identical loops playing side by side comb-filter into a hollow, phasey mess. Pepper.SFX plays one event from many positions with one call, `PostMultiPosition`, and lets you choose how those positions are voiced. ### How it works Any event can be posted at several positions; there's nothing to switch on in the event. Pepper uses up to 16 positions. You pass either an array of positions or transforms, or an **Audio Multi-Position Emitter Parent**: a component whose children, each with an **Audio Multi-Position Emitter Child**, mark the points. The parent's **Multi Position Type** (the `MultiPositionType` enum) has two values: - **SingleEmitter** (the default): one voice, placed at the point nearest the listener. As you move, it glides to the new nearest point (**Position Lerp Time**). With **Use Spread Curve**, it gets less directional when you stand among the points, down to **Min Spatial Blend**. One voice covers the whole river. Best for sound you don't hear as separate points: rivers, wind, rain across a courtyard. - **Decorrelated**: a voice at every active point. Each starts at a random place in the loop (up to **Max Random Offset**, 2 s by default) and is nudged in pitch by a few cents (**Max Pitch Variation Cents**, 3 by default), so the copies don't line up and comb-filter. Best for sources you can see: torches, speakers, machines. Posting with a `Vector3[]` or `Transform[]` array plays a third way, synchronised: every copy starts on the same audio sample, scheduled 100 ms ahead. Use it when the copies must stay in step, such as a stadium announcer over a PA. `PostMultiPosition` returns an `AudioMultiHandle`. It stops, pauses or changes the volume and pitch of every copy at once, or of one emitter by its index (`SetVoiceVolumeScale`, `StopVoice` and the others). The index is the point's place in the array, or among the parent's active children when you posted. ### Example ```csharp using UnityEngine; using AudioSystem; public class CourtyardAmbience : MonoBehaviour { [SerializeField] private AudioEvent riverLoop; // a looping 3D event [SerializeField] private AudioMultiPositionEmitterParent riverBank; // Multi Position Type: SingleEmitter [SerializeField] private AudioEvent torchLoop; [SerializeField] private AudioMultiPositionEmitterParent torches; // Multi Position Type: Decorrelated private AudioMultiHandle river; private AudioMultiHandle flames; void Start() { river = riverLoop.PostMultiPosition(riverBank); // one voice that follows the nearest point flames = torchLoop.PostMultiPosition(torches); // a voice per torch, offset in time and pitch } // The index is the torch's place among the parent's active children when it was posted. public void DouseTorch(int index) => flames?.StopVoice(index, 0.5f); void OnDestroy() { river?.Stop(2f); flames?.Stop(1f); } } ``` The river plays as one voice that follows you along the bank; each torch crackles on its own voice, out of step with the others, and `DouseTorch` fades one of them out. ### Setting it up 1. Make a container with **Is 3D** and **Loop** ticked (a Random or Routing Container), and an event that plays it. 2. Make an empty GameObject and choose **Add Component > SFX System > Audio Multi-Position Emitter Parent**. 3. Add child GameObjects with **Audio Multi-Position Emitter Child** at each point, or use the parent component's menu: **Create 8 Child Emitters (Circle Pattern)** or **Create 4 Child Emitters (Square Pattern)**. Each child has **Is Active** and **Volume Multiplier**. 4. On the parent, set **Multi Position Type** and **Max Emitters** (1 to 16), then tune the SingleEmitter or Decorrelated settings for that mode. 5. From a script, call `PostMultiPosition` with the parent. ### Good to know - **Decorrelated playback may click on sharp sounds** such as gunshots. It's meant for loops. - Voice cost: SingleEmitter plays one copy however many points there are. Decorrelated and synchronised play one copy per point, and each counts towards **Max Real Voices**. - The event's **Max Instances** is multiplied by the number of positions, and its cooldown is checked once per post. - Pass a source GameObject as the second argument and each 3D copy plays at its own position, attached to the source, so they all move with it. - SingleEmitter follows points that move. In the other modes, call `RefreshPositions` on the handle each frame to follow moving transforms. - A point that isn't active when you post isn't played. ## Voices and performance Source: https://peppersfx.com/features/voices-and-performance/ A busy scene can ask for more sounds than the hardware can play: a dozen cars, rain, gunfire and the UI at once. Pepper.SFX plays everything from a fixed pool of voices, ranks each sound by how much it matters, and keeps the important ones audible without clicks or engines losing a layer. ### How it works - **A fixed pool.** The Audio Manager builds its AudioSources once, at start: Max Real Voices + Max Virtual Voices + Steal Headroom (32 + 64 + 4 by default). - **Importance** is how loud a sound is where it is heard, times its event's Priority weight: Low 1/8, Medium 1/4, High 1/2, Critical 1. A loud Low sound beside you can outrank a faint Medium one far away. - **Virtual voices.** Over Max Real Voices, the least important sounds go virtual: silent, using no real voice, but still counting their place in the clip at their pitch. When there is room they come back exactly where they would have been, faded in over about 25 ms. A whole sound goes virtual at once, so an engine's layers stay together. - **Stealing ends whole sounds.** When the pool is full, a new sound makes room by ending whole sounds, silent virtual ones first, then the least important: every layer of a Blend together, never one layer. If there still isn't room, the new sound is skipped with a warning. - **Persistent events are never stolen.** Tick Persistent on an engine's event. It can still go virtual. Critical is the strongest weight, not an exemption. - **Steals fade.** A stolen sound fades out over Steal Fade Seconds (0.025 s, at least three frames) instead of clicking. - **Per-bus caps.** A bus's Max Real Voices limits the real voices its sounds use, so other players' cars can't crowd out yours. Over the cap, sounds go virtual; it never ends one. - **Dedicated servers.** With `AudioManager.Inert = true` set at boot, every post is silent and free: no voices, no allocation, no logging. ### Example ```csharp using AudioSystem; using UnityEngine; public class VoiceBudgetReadout : MonoBehaviour { [SerializeField] private AudioBus otherCarsBus; // Max Real Voices set on the bus asset private void OnGUI() { if (AudioManager.Instance == null) return; var stats = AudioManager.Instance.GetStatistics(); GUILayout.Label($"Real voices: {stats.activeVoices} / {stats.realVoiceBudget}"); GUILayout.Label($"Free in the pool: {stats.availableVoices} of {stats.poolSize}"); GUILayout.Label($"Other cars: {otherCarsBus.RealVoiceCount} / {otherCarsBus.MaxRealVoices}"); } #if UNITY_SERVER // A dedicated server build plays nothing and builds no voices. [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void SilenceServer() => AudioManager.Inert = true; #endif } ``` The readout shows real voices against the budget, free voices in the pool and a capped bus's live count. In a dedicated server build, Pepper goes inert before the first scene loads. ### Setting it up 1. Select the Audio Manager and set **Max Real Voices** and **Max Virtual Voices**. The guide suggests 32–64 real voices on desktop, 16–24 on mobile. 2. On each Audio Event, set **Priority**, and tick **Persistent** on events that must never drop out. 3. On a bus that groups many similar sounds, set **Max Real Voices** in its Voices section (0 = no cap). 4. In Play mode, open **Window > Audio System > Voice Pool Debug** for the real and virtual voices, or **Window > Audio System > Live View** to see why a voice is virtual. ### Good to know - If a very large number of sounds start in the same frame, some can be skipped when the pool can't free voices fast enough. Raise Steal Headroom if it happens often. - If a full pool holds only persistent sounds, a new sound is skipped. Raise Max Virtual Voices: 8 cars of 14 layers need 112 voices. - A virtual voice can't follow Unity's own Doppler. On engine layers that must come back in step, set Doppler to Pepper or Doppler Level to 0. - Pepper warns at startup if its voice counts exceed Unity's (Project Settings > Audio). - Pepper doesn't detect a server by itself, and `Inert` can't change while an Audio Manager runs: your boot code decides. ## Spatial audio and listeners Source: https://peppersfx.com/features/spatial-audio-and-listeners/ A car's engine should fade, dull and narrow as it drives away, come from its exhaust rather than its centre, and still sound right when two players share one screen. Pepper.SFX gives you shared distance assets, listening points you register yourself, and its own panning and Doppler for what one AudioListener can't do. ### How it works - **Attenuation assets.** Curves over metres, shared between sounds: Volume in dB, and optional Low Pass, High Pass and Spread. Set in a container's 3D Settings, one replaces its Min Distance, Max Distance and Rolloff. Past Max Distance a sound goes virtual (silent, keeping its place). `handle.SetAttenuation` gives one sound a different asset. - **Listening points.** `AddListeningPoint(cameraTransform)` tells Pepper where you listen from, so it never searches the scene. Voice importance, occlusion, Doppler and the Distance, Approach Speed and Angle values all read the points. `DriveUnityListener` keeps Unity's AudioListener on the primary one. - **Split screen: Spatial Mode = Pepper.** Pepper then pans and attenuates 3D sounds itself, from every point. A sound's level is the loudest of its attenuation at each point times that point's Weight (0–1); its pan blends the points, narrowed by each one's Pan Width (0 = mono). At one point of Weight 1 and Pan Width 1 it sounds the same as Unity's 3D. `SetPrimaryListeningPoint` picks the primary point, and changes glide over about 100 ms. - **Pepper's Doppler.** Set a container's Doppler to Pepper and pitch follows the closing speed on the listening point, scaled by Doppler Exaggeration (×1 to ×3) and capped by Doppler Max Cents. Virtual voices come back in step with it. `AudioEmitter.Teleport()` and `AudioListeningPoint.ClearVelocity()` stop a respawn or camera cut reading as speed. - **Named points and offsets.** An Audio Emitter lists named Points (Exhaust, Intake, RearAxle). `car.Post(exhaustEvent, "Exhaust")` attaches the sound there. `car.Post(impactEvent, localOffset)` plays anywhere on the object with the emitter's values, and `handle.SetLocalOffset` moves a playing sound, such as a scrape following the contact. ### Example ```csharp using AudioSystem; using UnityEngine; public class SplitScreenCar : MonoBehaviour { [SerializeField] private Transform player1Camera; [SerializeField] private Transform player2Camera; [SerializeField] private AudioEmitter car; [SerializeField] private AudioEvent exhaustEvent; [SerializeField] private AudioEvent impactEvent; private void Start() { var manager = AudioManager.Instance; manager.SpatialMode = AudioSpatialMode.Pepper; // before the sounds start AudioListeningPoint p1 = manager.AddListeningPoint(player1Camera); AudioListeningPoint p2 = manager.AddListeningPoint(player2Camera); p1.PanWidth = 0f; // top and bottom halves: no left and right p2.PanWidth = 0f; car.Post(exhaustEvent, "Exhaust"); // attached at the car's Exhaust point } private void OnCollisionEnter(Collision hit) { Vector3 local = car.transform.InverseTransformPoint(hit.GetContact(0).point); car.Post(impactEvent, local); // at the contact, with the car's values } } ``` Both cameras become listening points, so a car near either player sounds near. The exhaust rides on the car's Exhaust point, and each impact plays where the car was hit. ### Setting it up 1. **Create > Audio System > Attenuation**, shape its curves and set Max Distance. 2. In a container's **3D Settings**, assign the asset and set **Doppler** to Pepper if wanted. 3. **Add Component > Audio System > Audio Emitter** on the moving object. Add Points and drag them in the Scene view. 4. For split screen, set the Audio Manager's **Spatial Mode** to Pepper. 5. Register listening points from code at start. In Play mode the Audio Manager inspector lists them. ### Good to know - Spatial Mode = Pepper has no spatializer plugins (HRTF) and ignores the project's Doppler Factor. It balances a stereo clip rather than placing it, so give 3D sounds mono clips. - Unity's own AudioSources, such as the ambient system's, still hear from the AudioListener: keep it on the primary point with `DriveUnityListener`. - Changing Spatial Mode affects sounds started after the change. - With no listening point registered, Pepper searches for an AudioListener and warns once. - With Spatial Mode = Unity, Attenuation curves are handed to Unity when a sound starts, so Play-mode edits reach a playing sound only when it is posted again. ## Vehicle engines Source: https://peppersfx.com/features/vehicle-engines/ The engine is the sound a racing game plays most. It has to follow the RPM without steps or warble, change on and off the throttle, and every car on the track needs its own. Pepper.SFX builds an engine from loops recorded at known RPMs, keeps them in tune with each other at every RPM, and lets each car drive its own copy. ### How it works An engine is a **Blend Container** whose layers are your loops. - **Regions.** Give each layer its **Root Value** (the RPM it was recorded at) and press **Auto Layout**. Each loop plays in its own band of RPM and crossfades with its neighbours at equal power. - **Pitch Follow.** Each layer plays at RPM ÷ its Root Value: a loop recorded at 4000 RPM plays 1.25 times faster at 5000. The layers stay in tune with each other. - **A Parameter asset for RPM** with **Smoothing** (separate rise and fall), the same at any frame rate. Your code writes the raw value; the engine hears the smoothed one. - **One engine per car.** Each car posts its engine on its own **Audio Emitter** and sets RPM and load there. Ten cars share one engine asset. - **On load and off load.** A Blend on load crossfades an on-load set of loops with an off-load set. - **Silent Layers = Virtualize.** A layer below −60 dB (the default threshold) gives up its real voice, keeps its place in the clip and comes back in step. An engine of 14 loops with 2–4 heard at a time uses 2–4 real voices. - **Persistent events** are never stolen to make room for other sounds. Two windows do the setup: - **Engine Builder** (full version) reads each selected loop's set and RPM from its name (`Eng_OffLoad_2510`) and builds the whole tree: a looping container per loop, on-load and off-load Regions Blends with Pitch Follow and Virtualize, a load Blend over them, and one Persistent event. It never overwrites an asset. - **Loop Analysis** (Window > Audio System > Loop Analysis) measures each loop's root pitch and the RPM it means, whether it clicks at the loop point, the level jump across it, and its loudness in LUFS. **Seam ▶** plays the join on its own. It reads **Decompress On Load** clips only. ### Example ```csharp using AudioSystem; using UnityEngine; [RequireComponent(typeof(AudioEmitter))] public class CarEngineAudio : MonoBehaviour { [SerializeField] private AudioEvent engineEvent; // the Persistent event the Engine Builder made [SerializeField] private AudioParameter rpm; // EngineRpm, 0–9000, with Smoothing [SerializeField] private AudioParameter load; // EngineLoad01, 0–1 public float engineRpm = 900f; // written by your drivetrain public float throttle; // 0–1 private AudioEmitter emitter; private AudioHandle engine; void Start() { emitter = GetComponent(); engine = emitter.Post(engineEvent); // reads this car's values } void Update() { emitter.SetParameter(rpm, engineRpm); // raw: Smoothing eases it emitter.SetParameter(load, throttle); } public void OnCarReset() => emitter.SnapAllParameters(); // no glide after a teleport public void StopEngine() => engine?.Stop(0.5f); } ``` ### Setting it up 1. Import your loops with the RPM in each file name. 2. Open **Window > Audio System > Engine Builder** (full version), select the loops and press **Use Selection**. In the free version, build the same Blends by hand as the cookbook's engine recipe describes. 3. Check each row's set and RPM, or press **Detect** to measure it. 4. Pick the RPM and Load parameters, or leave them empty to have EngineRpm and EngineLoad01 made. Pick the **Target Bus** and press **Build**. 5. Open **Window > Audio System > Audition**, select the new event, press **Play** and add a sweep on RPM. Tune regions and Root Values by ear. 6. Add an **Audio Emitter** to the car, set **On Disable** to Stop, and add a script like the one above. ### Generated engines (full version, experimental, Unity 6000.3 or later) The **Granular Engine** plays an engine from sweeps instead of loops. **Window > Audio System > Engine Grains** cuts an on-load and an off-load sweep into a **Grain Table**: one grain per engine cycle, tagged with its RPM and cut to join in phase. A fast rev passes through every RPM with no band crossfade, and your code doesn't change. The **Additive Engine** adds the engine's firing orders as sine tones under another engine, for weight. Both use Unity 6.3's audio generators. They are new: not yet measured on a Steam Deck (about 0.4% of a CPU core each on a desktop PC), and they hear the RPM about 25–75 ms late. They are in the full version only, like the Engine Builder. Everything else on this page, Loop Analysis included, is in both. ### Good to know - A virtual layer fades back in over at least three frames. If an off-load set must answer a fast rev-limiter cut, set that Blend's Silent Layers to **Keep Playing**. - Engine layers that go virtual need **Doppler Level 0** or Pepper's own Doppler to come back in step. The Engine Builder sets 0. - With loops from one set only there is no load Blend, so the engine sounds the same on and off the throttle. ## Multiplayer Source: https://peppersfx.com/features/multiplayer/ (full version only) A door one player opens should be heard by everyone, a footstep should pick the same variation on every machine, and an alarm already sounding should reach whoever joins later. Pepper.SFX's netcode layer sends a short description of each sound, never the audio, and every machine plays it through its own Audio Manager. ### How it works - **A core that needs no netcode package.** `SFXSystem.Netcode` references only Pepper's runtime. A small adapter per networking package (`FishNetAudioTransport`, `NgoAudioTransport`) is the only code that touches it, and compiles only when that package is installed. - **Intent, not audio.** A post sends about 36 bytes: the event, the position, an object to follow and a random seed. Each machine plays it locally, so occlusion, reverb and voice management work as in single player. - **Local echo.** `PostNetworkedEvent` plays at once for the player who caused it; the server relays it to the others. With no session it is a plain local post. - **Stable event IDs.** Each Audio Event has a Stable ID worked out from its asset GUID, so every machine resolves it to the same event. - **Seed parity.** By default a networked post makes the same random picks everywhere: the clip, the pitch and volume randomisation, a Sequence's later clips. Pass `independentVariation: true` for sounds such as a crowd. - **Late join.** `PostNetworkedLoop` returns a handle, and `StopNetworked` stops the loop everywhere. The server replays running loops to a player who joins later. The event's Loop Sync Tier sets the joiner's phase: 0 from the start, 1 aligned at join, 2 phase-locked (experimental). - **Server-authored sounds.** `PostServerEvent` and `PostServerLoop` for sounds the server decides, such as a round start. - **Proximity culling.** With culling on, the server sends a 3D sound only to clients within its audible range, which also closes an audio wallhack. 2D sounds are never culled. - **Leaving it out.** The `SFXSYSTEM_DISABLE_NETCODE` define leaves every multiplayer assembly out of your build. ### Example ```csharp using AudioSystem; using AudioSystem.Netcode; using UnityEngine; // On a door that has a NetworkObject. public class DoorAudio : MonoBehaviour { private NetworkedSoundHandle alarm; public void Open() { // Plays here now; the server relays it to the other players, attached to this door. NetworkedAudioManager.PostNetworkedEvent("door_open", transform.position, gameObject); } public void StartAlarm() { // A shared loop: players who join later hear it too. alarm = NetworkedAudioManager.PostNetworkedLoop("alarm_loop", transform.position, gameObject); } public void StopAlarm() { NetworkedAudioManager.StopNetworked(alarm); // stops it on every machine alarm = null; } } ``` The door sound plays at once for the player who opened it and follows the door on the other machines. Passing the alarm's handle to `StopNetworked` ends the loop for everyone. ### Setting it up 1. Install one networking package: FishNet v4 or Netcode for GameObjects 2.x. The matching adapter compiles by itself. 2. Open **Window > Audio System > Audio Event IDs** and click **Capture All Missing IDs**. 3. Make one persistent networked GameObject with a NetworkObject and `FishNetAudioTransport` or `NgoAudioTransport`: one per session, active in the scene, not on player objects. 4. Optionally add a `NetworkAudioSettings` component for proximity culling, and assign its `ListenerPositionProvider` from code. 5. Set Unreliable Delivery, Independent Variation and Loop Sync Tier in each Audio Event's **Multiplayer** foldout. 6. In Play mode, **Window > Audio System > Network Audio Debug** shows the session, clock and loops. ### Good to know - **Support:** FishNet v4 is the tested backend, on one PC and on two over a real network. Netcode for GameObjects 2.x compiles but is only lightly tested. NGO 1.x and FishNet v3 cause compile errors. Mirror is not supported. - Loop Sync Tier 2 is experimental: a late joiner may hear the loop out of sync for a cycle or two before it locks in. - Proximity culling can use the wrong range for events that play a Switch or Blend container, so a nearby player might miss the sound. - RTPCs, switches, states and ducking aren't replicated; each client's game logic drives them. - Built for co-op and competitive sessions of up to tens of players. - On a dedicated server, set `AudioManager.Inert = true` at boot so it plays nothing but can still relay. ## Addressables loading Source: https://peppersfx.com/features/addressables-loading/ (full version only) In a large game, keeping every zone's audio in memory wastes RAM on sounds the player can't hear. Pepper.SFX can load a zone's clips through Unity's Addressables when the listener walks in and release them when it leaves, without unloading a clip a voice is still playing. ### How it works - **The clip-source layer.** Containers ask the active clip source for each clip. By default it returns the clip you assigned; the Addressables backend swaps in one that loads by address. Each entry holds a direct clip or an address, never both. Random, Sequence and Routing container entries and Ambient Source loops can be addressable. - **The warmer.** `WarmContainer`, `WarmClip` and `WarmClips` on the Audio Manager load the audio data of directly referenced clips before their first play, so it doesn't hitch. No extra package needed. - **Banks per zone.** A bank is an Addressables label, named by a Zone Audio Bank component on an Audio Zone. The bank preloads when the listener enters and is released after a linger (5 s by default) when it leaves, so a border doesn't cause churn. - **Nothing unloads under a playing voice.** A clip stays loaded while a bank, a playing voice or a pin holds it. A loop started in one zone plays on in the next, keeping only its own clip loaded. - **Zones really unload.** Proved in a Windows player build with three zones: standing in one held 7.4–11.3 MB of audio against 26.1 MB for all three, 57–72% less. - **Addressable Audio Authoring window.** Select containers, pick a Bank Label and Address Style, and click Convert to Addressable. Each bank gets its own group with one bundle per clip; Revert to Direct undoes it. The Groups tab marks clips used or unused, adds clips that aren't in a container, and removes unused ones. - **Loading Validator window.** Flags an address with no backend, a mistyped address, or an entry with both a clip and an address, even without Addressables installed. ### Example ```csharp using AudioSystem; using AudioSystem.Propagation; using UnityEngine; public class LevelAudioLoading : MonoBehaviour { [SerializeField] private AudioContainer bossMusic; // direct clips, not addressable // Before the boss fight: load the music's audio data so its first play doesn't hitch. public void OnBossDoorOpened() { AudioManager.Instance.WarmContainer(bossMusic); } // After moving the player in one jump (a respawn or fast travel): // the zones left behind release their banks, and the zones landed in load theirs. public void OnPlayerTeleported() { if (PropagationManager.HasInstance) PropagationManager.Instance.ResyncListenerAfterTeleport(); } } ``` Zone banks need no code. The example shows the two jobs that do: a jump in position, and warming direct clips. ### Setting it up 1. Install **Addressables** (`com.unity.addressables`) from the Package Manager. Pepper's backend compiles by itself. 2. Select a zone's containers, open **Window > Audio System > Addressable Audio Authoring**, set a **Bank Label**, leave Group on **Bank's own group**, and click **Convert to Addressable**. 3. On the Audio Zone's GameObject, **Add Component > Audio > Loading > Zone Audio Bank**, with the same Bank Label. 4. In your first scene, **Add Component > Audio > Loading > Addressable Audio Runtime**. One is enough; it persists across scene loads. 5. Run **Window > Audio System > Loading Validator** and click **Validate**. 6. Check memory in a Development Build with Unity's Memory Profiler. ### Good to know - The clip-source layer and the warmer are in every version. The Addressables backend, the Addressable Audio Authoring window and the Loading Validator are full version only. - One group for every zone saves no memory: Unity unloads a bundle only as a whole. The Groups tab flags such a group and offers Pack Each Clip Separately. - A clip that isn't loaded yet is skipped, not played late. Keep a zone's bank loaded before its sounds are needed. - With the editor's default Play Mode Script (Use Asset Database), clips never really unload: measure in a player build. - A group left out of builds plays in the editor but not in a build. The Groups tab's Include in Builds button fixes it. - Banks follow Audio Zones, one label per zone. Events and containers still load up front; only clips load on demand. ## Music and timing Source: https://peppersfx.com/features/music-and-timing/ 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 ```csharp 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. ## Editor tools Source: https://peppersfx.com/features/editor-tools/ Audio problems are hard to find by ear: a sound that never plays, a layer that should be silent, a voice that went quiet for no clear reason. Pepper.SFX comes with editor windows for building sounds, hearing them while you edit, seeing what plays while the game runs, and checking the project for mistakes. ### How it works #### Building sounds - **Sound Builder (Simple)** creates a bus, a container and an event in one step, with the references filled in. Its five modes are Complete Setup, Container Only, Event Only, Bus Only and Batch Import (one sound per clip), for all five container types. - **Sound Builder (Advanced)** (full version only) has the same modes but shows every setting of the event, container and bus, with tooltips, before anything is written. It is brand new. - **Audio Explorer** shows your events, containers and buses as three tabs over one searchable tree, with the selected asset's Inspector and a preview beside it. - **Import Manager** applies import settings to clips in batches by category (UI, SFX, Music, Ambience, Dialogue). **Folder Rules** give clips first imported into a folder that folder's settings, so engine loops arrive mono and Decompress On Load. Existing clips keep theirs until you press **Apply to Existing Clips**. #### Hearing and tuning - **Audition** plays a container or an event outside Play mode. Every parameter the sound reads gets a slider in its own unit, passed through its Smoothing as in the game, and every switch gets a dropdown. **Add Sweep** moves a parameter across a range (Once, Repeat or Back And Forth), several at once. The **Layers** table shows each voice's level, pitch, low-pass and whether it is real or virtual. Inspector changes are heard straight away. - **Live View** shows, in Play mode, every Audio Emitter: its values as written and as heard after smoothing, its switches, and each sound on it, with a row per voice for level, pitch and Blend region. A virtual voice says why: over the cap, silent layer, past Max Distance, held by code, bus or emitter paused, or stopping. **Record** captures what the game wrote to one emitter during a drive, and the Audition replays it, so you can tune a mix against a real lap. #### Checking and debugging - **Container Validator** checks every container and event: empty or null entries, zero weights, gaps in a Regions Blend, layers pitched too far, stereo clips on 3D layers, a Play action with no Target Bus, a looping event that isn't Persistent. **Fix** and **Auto-Fix All** can be undone. - **Voice Pool Debug** lists real and virtual voices in Play mode, with a "Why virtual" column and a stop button per voice. It compares the pool's objects with its size; a number that keeps rising means voices are leaking. - **Mixer** (full version only) shows strips for buses, containers and events, each with a fader, mute, solo, peak meter and a small spectrum. Bus fader, mute and solo changes can be undone and are saved with the project, in Play mode too. - **Audio Profiler** (full version only) shows every voice with its priority, level and distance, every bus with fader, mute, solo, meter and ducking, and real voices against Max Real Voices. - **EQ Spectrum Visualizer** (full version only) draws the spectrum of up to two sources at once: a clip, event, container, bus or Audio Mixer Group. Only clips play outside Play mode. - **Loading Validator** (full version only) finds Addressables setups that would fail silently: an address with no backend or not registered, an entry with both a clip and an address, or addressable content in a scene with no AddressableAudioRuntime. ### Where to find them Every window is under **Window > Audio System**: - Sound Builder (Simple), also at Assets > Create > Audio System > Sound Builder (Simple) - Sound Builder (Advanced), full version - Audio Explorer - Import Manager - Audition - Live View - Container Validator - Voice Pool Debug - Mixer, full version - Audio Profiler, full version - EQ Spectrum Visualizer, full version - Loading Validator, full version Folder Rules live on an Audio Import Settings asset: **Create > Audio System > Import Settings**. ### Setting it up 1. Add an **Audio Manager** to your first scene. 2. Open **Sound Builder (Simple)**, drop in your clips, choose Complete Setup and create the sound. 3. Select the new container or event and open **Audition** to tune it. 4. Run **Container Validator** before you build. 5. Press Play and open **Live View** or **Voice Pool Debug** to see what is playing and why. ### Good to know - Outside Play mode the Audition plays 3D sounds as 2D, at full level, and a Sequence plays only its first clip. It runs on its own hidden Audio Manager and adds nothing to your scene. - Sound Builder (Simple) always warns that events must be in a Resources folder. Ignore it if you use an Audio Event Registry. Its preview button may do nothing. - The Audio Profiler's "Est. CPU" is estimated from the voice count, and "Managed heap" is your whole game's memory. Use Unity's Profiler for real figures. - More windows cover engines (Loop Analysis, and in the full version Engine Builder and Engine Grains: see [Vehicle engines](https://peppersfx.com/features/vehicle-engines/)), mixer setup (Occlusion Layout, Reverb Send Buses) and, in the full version, Addressable Audio Authoring and Network Audio Debug. # Frequently asked questions ## What is Pepper.SFX? Pepper.SFX is audio middleware for Unity. You author sounds as assets (events, containers, buses, states and parameters) and play them from code with one call, such as footstepEvent.Post(gameObject, transform.position). Its code is in the AudioSystem namespace. See Features for what it covers. ## Which Unity versions does Pepper.SFX support? Unity 6000.0.48f1 or newer. Package Manager will install it on older 6000.0 versions, but those aren't supported. The automated tests pass on 6000.0.48f1 and 6000.3.19f1. The experimental generated engines need Unity 6000.3 or later. ## What is the difference between the free and full versions? The free version is compiled DLLs, installed as a package from GitHub. The full version is the complete source, with every editor window, multiplayer, Addressables loading and the example scripts; it is coming soon to the Unity Asset Store but isn't out yet. The windows left out of the free version are the Mixer, Audio Profiler, EQ Spectrum Visualizer, Loading Validator, Sound Builder (Advanced) and Engine Builder, and the experimental generated engines are full only too. See how to get it. ## Can I move from the free version to the full version without losing my work? Yes. The two versions can't be in the same project, and swapping the free DLLs for source would break every script reference to them. A separate upgrade tool, released together with the full version, rewrites those references once, so your events, containers, buses, scenes and prefabs carry over. Back up your project before you run it. ## Which platforms does Pepper.SFX run on? Pepper.SFX is C# on top of Unity's own audio, so it runs wherever Unity's audio does: Windows, macOS, Linux, mobile, Meta Quest and consoles. There are two exceptions. Convolution reverb works only on 64-bit Windows (x64), and Web builds play sound without occlusion muffling or reverb, because Unity's Web audio has no mixer effects. See Build Compatibility. ## Does Pepper.SFX replace Unity's audio system? No. It plays through Unity's own audio: every sound is played by a pooled AudioSource, and buses route to your AudioMixer groups. Pepper decides what plays, at what level and pitch, and through which bus; Unity mixes it and sends it to the speakers. Your settings in Project Settings > Audio still apply. ## Will my existing AudioSources keep working? Yes. Pepper.SFX keeps its own pool of AudioSources and doesn't change yours, so you can move a project over one sound at a time. Both share Unity's voice limits (Project Settings > Audio), and Pepper warns at startup if its own limits are higher than Unity's. ## Does Pepper.SFX support multiplayer? Yes, in the full version: a networked sound sends a small message saying which event to play and where, never audio, and each machine plays it locally. FishNet v4 is tested, on one PC and on two over a real network. Netcode for GameObjects 2.x has had light testing only, and NGO 1.x and FishNet v3 aren't supported. See Multiplayer. ## Does it work in split-screen games? Yes. Register each player's camera as a listening point with AudioManager.Instance.AddListeningPoint(cameraTransform) and set the Audio Manager's Spatial Mode to Pepper. Pepper then pans and attenuates 3D sounds from every listening point, weighted by each point's Weight and Pan Width. In that mode spatializer (HRTF) plugins aren't used and stereo clips are balanced rather than placed, so give 3D sounds mono clips (more in Spatial audio and listeners). ## Can I use Pepper.SFX on a dedicated server? Yes. Set AudioManager.Inert = true at boot, before any Audio Manager starts. Every post then returns an already-finished handle without allocating or logging, and an Audio Manager that starts builds no voices or per-frame work. The server can run the same scenes and prefabs as the clients. ## How does Pepper.SFX perform? Sounds play from a pool of AudioSources made when the Audio Manager starts, so playing a sound doesn't create GameObjects. By default 32 voices are heard at once and up to 64 more are tracked as virtual: silent, keeping their place, and brought back when they matter again. Parameter writes allocate nothing after their first use. The Voice Pool Debug and Live View windows show what is playing and why; see Voices and performance. ## Can Pepper.SFX play car engines? Yes. A Blend Container with Regions and Pitch Follow keeps loops recorded at different RPMs in tune at every RPM, and in the full version the Engine Builder window builds a whole engine from your loops. Each car posts its engine on its own Audio Emitter, so every car follows its own RPM. See Vehicle engines. ## How do I get started? Add an Audio Manager component to a GameObject in your first scene, then open Window > Audio System > Sound Builder (Simple) to make a bus, a container and an event from your clips. Drag the event into a field on your script and call Post on it. The Quick Start walks through it. ## Where is the documentation? On this site, under Docs: a Quick Start, a cookbook of recipes, the full manual and the API reference. Problems we already know about are listed under Known issues. ## How do I report a bug? Email amir.pepper.work@gmail.com with your Unity version, your platform, whether you use the free or full version, the steps that cause the problem, and any console errors. Check Known issues first, in case it's already listed. Other ways to get in touch are on the Contact page. # Documentation ## Introduction Source: https://peppersfx.com/docs/introduction/ ### What is Pepper.SFX? Pepper.SFX is professional audio middleware for Unity. Instead of manually managing AudioSources and juggling clips, you work with high-level concepts: **Events**, **Containers**, and **Buses**. **In one sentence:** Play sounds through events, not AudioSources. ### The Problem It Solves **Before (Standard Unity):** ```csharp public AudioClip footstepClip; public AudioSource audioSource; void PlayFootstep() { audioSource.clip = footstepClip; audioSource.Play(); } ``` ❌ No variations • ❌ Manual management • ❌ No pooling • ❌ No volume control structure **After (Pepper.SFX):** ```csharp public AudioEvent footstepEvent; void PlayFootstep() { footstepEvent.Post(gameObject, transform.position); } ``` ✅ Built-in variations • ✅ Automatic pooling • ✅ Professional mixing • ✅ Easy to organize > 🛠️ **Building for anything other than 64-bit Windows?** Read [6. Build Compatibility](https://peppersfx.com/docs/build-compatibility/) first. Convolution reverb is 64-bit Windows only, and Web builds have no occlusion muffling or reverb. ## Quick Start Source: https://peppersfx.com/docs/quick-start/ ### Choose Your Path Pick the setup method that fits your workflow: #### Method 1: Quick Wizard ⭐ (2 minutes) **Best for:** Beginners, rapid prototyping **Time:** 2 minutes **Skill:** Easy [Jump to Quick Wizard](#method-1-quick-wizard) #### Method 2: Manual Setup (10 minutes) **Best for:** Learning the system, custom control **Time:** 10 minutes **Skill:** Medium [Jump to Manual Setup](#method-2-manual-setup) #### Method 3: Registry System (Advanced) **Best for:** Large projects, eliminating Resources folder **Time:** Setup + migration **Skill:** Advanced [See Migration](https://peppersfx.com/docs/migration/) for full details. --- ### Method 1: Quick Wizard **The absolute fastest way to add sounds - perfect for beginners!** #### Step 1: Add AudioManager to Scene (30 seconds) 1. Create empty GameObject in your scene 2. Add `AudioManager` component 3. Done! (Auto-configures itself) #### Step 2: Create Sound Assets (1 minute) 1. In Unity menu: `Window > Audio System > Sound Builder (Simple)` 2. **Choose Creation Mode:** - **Complete Setup** - Creates Bus + Container + Event all at once (recommended) - **Container Only** - Just create a container - **Event Only** - Just create an event (with existing container or blank) - **Bus Only** - Just create a bus - **Batch Import** - Import multiple clips at once 3. Enter sound name (e.g., "ButtonClick") 4. **Select Container Type:** - **Routing** - Direct playback (plays all clips simultaneously if multiple added) - **Random** - Random selection with weights - **Sequence** - Sequential playback (Forward/Reverse/PingPong) - **Switch** - Switch-based selection - **Blend** - RTPC-driven real-time blending 5. Add one or more AudioClips 6. Configure advanced options if needed (pitch/volume randomization, 3D settings) 7. Click **"Create Complete Sound Setup"** There are no presets: you set each option yourself, and the defaults suit a simple 2D sound effect. ✅ Done! The wizard creates: - AudioBus (with custom name) - Container (any type: Routing, Random, Sequence, Switch, or Blend) - AudioEvent (with selectable action type) - All references pre-filled 💡 **Pro Tips:** - The wizard auto-detects selected audio clips and suggests batch import for multiple selections - Use "Auto-Create Standard Folder Structure" to set up all required folders - Routing containers can layer multiple clips for complex sounds (e.g., gunshot with shell eject) #### Step 3: Play in Code (30 seconds) ```csharp using AudioSystem; public class MyScript : MonoBehaviour { public AudioEvent buttonClickSound; void OnButtonPressed() { buttonClickSound.Post(gameObject, transform.position); } } ``` Drag the created AudioEvent into the script field, and you're done! **What's Next?** See the [Cookbook](https://peppersfx.com/docs/cookbook/overview/) chapter for more recipes or jump to [Troubleshooting](#troubleshooting). --- ### Method 2: Manual Setup **Traditional workflow - full control over every step** #### Step 1: Add AudioManager (2 minutes) 1. In your scene, create a new empty GameObject 2. Name it "AudioManager" 3. Add the `AudioManager` component 4. **Leave all default settings** - they're optimized for most games **Inspector Settings (defaults are fine):** - Master Volume: 1.0 - Max Real Voices: 32 - Max Virtual Voices: 64 - Enable LOD: ✓ Occlusion is opted into per-container via the `Use Occlusion` toggle on AudioContainer assets, not via a global AudioManager switch — see the Occlusion chapter in the Manual. **Important:** AudioManager persists between scenes automatically via `DontDestroyOnLoad`, as long as **Persist Across Scenes** is on (the default) and its GameObject is at the top of the hierarchy: Unity keeps only root objects. To keep it under another object (a scene that is never unloaded, a client-only root), turn Persist Across Scenes off; it then belongs to its own scene. --- #### Step 2: Create Folder Structure (1 minute) Right-click in Project window → `Create > Audio System > Sound Builder (Simple)` → Click **"Auto-Create Standard Folder Structure"** Or manually create: ``` Assets/Audio/ ├── Resources/ │ └── Audio/ │ ├── Events/ ← AudioEvents MUST go here │ └── States/ ← AudioStates MUST go here ├── Containers/ ← Container assets ├── Buses/ ← Bus assets └── AudioClips/ ← Your .wav/.mp3 files ├── SFX/ ├── Music/ └── Ambience/ ``` **Why Resources folder?** AudioManager auto-loads Events and States from `Resources/Audio/Events` and `Resources/Audio/States` at startup. (See the [Migration](https://peppersfx.com/docs/migration/) chapter to skip this requirement via `AudioEventRegistry`.) --- #### Step 3: Create Your First Bus (2 minutes) 1. Right-click in `Assets/Audio/Buses/` 2. Select `Create > Audio System > Audio Bus` 3. Name the file: "SFX_Bus" 4. Select it and configure in the Inspector: - **Bus Name:** "SFX" - **Volume Db:** 0 - **Parent Bus:** (leave empty for now) - **Mixer Group:** (optional) **What just happened?** You created a volume control group for sound effects. Later you can adjust all SFX together. 💡 **Tip:** The Quick Wizard creates this automatically in its Complete Setup mode. --- #### Step 4: Create Your First Container (2 minutes) 1. Right-click in `Assets/Audio/Containers/` 2. Select `Create > Audio System > Routing Container` 3. Name it: "TestSound_RC" 4. Select it and configure in the Inspector: - **Container Name:** "TestSound" - **Audio Clips:** Drag in 1-3 AudioClips from your project - **Volume:** 1.0 - **Loop:** ☐ (unchecked) **Optional - Add Variation:** - **Enable Volume Randomization:** ☑ - Min: -1, Max: 1 - **Enable Pitch Randomization:** ☑ - Min: -50, Max: 50 **What just happened?** You created a simple container that plays your clip(s) with optional randomization. --- #### Step 5: Create Your First Event (2 minutes) 1. Right-click in `Assets/Audio/Resources/Audio/Events/` 2. Select `Create > Audio System > Audio Event` 3. Name it: "Play_TestSound" 4. Select it and configure in the Inspector: **Event Configuration:** - **Event Name:** "Play_TestSound" **Voice Settings:** - **Priority:** Medium - **Max Instances:** 5 - **Cooldown:** 0 **Actions → Size: 1** **Action [0]:** - **Type:** Play - **Container:** TestSound_RC (drag from Containers folder) - **Target Bus:** SFX_Bus (drag from Buses folder) - **Delay:** 0 - **Fade Duration:** 0 **What just happened?** You created an event that plays your container through the SFX bus when triggered. ⚠️ **Important:** The Inspector will show a warning if the event is not in the Resources folder. Click the "Move to Resources" button if you see this warning. --- #### Step 6: Play It From Code (1 minute) Create a new C# script called `TestAudio.cs`: ```csharp using UnityEngine; using AudioSystem; public class TestAudio : MonoBehaviour { [SerializeField] private AudioEvent testEvent; void Update() { if (Input.GetKeyDown(KeyCode.Space)) { testEvent.Post(gameObject, transform.position); Debug.Log("Playing test sound!"); } } } ``` **Setup:** 1. Create a new GameObject in your scene (or use an existing one) 2. Attach the `TestAudio` script to it 3. In the Inspector, drag `Play_TestSound` event into the `Test Event` field 4. Press **Play** in Unity 5. Press **Spacebar** to hear your sound! --- ### 🎉 Congratulations! You've successfully set up the complete audio pipeline: **Your Code** → **AudioEvent** → **Container** → **Voice** → **Bus** → **Speaker** --- ### Troubleshooting #### "AudioEvent not playing!" **Check Inspector warnings:** - Open your AudioEvent asset - Look for red/yellow warning boxes - If it says "NOT in Resources folder" → Click "Move to Resources/Audio/Events/" #### "AudioManager not found!" - Ensure AudioManager is in scene - Check it persists: with Persist Across Scenes on (the default) it is kept across scenes, but only if its GameObject is a root (Unity warns otherwise). Under a parent, turn Persist Across Scenes off and keep it in a scene that stays loaded. #### "No AudioEvents found in Resources/Audio/Events" - Events MUST be in `Assets/Audio/Resources/Audio/Events/` folder - OR use the [AudioEventRegistry system](https://peppersfx.com/docs/migration/) #### Sound Too Quiet/Loud? **Check:** 1. Container volume (0-1) 2. Bus volume (in dB, 0 = no change) 3. Master volume on AudioManager 4. Unity's AudioListener volume --- ### Quick Reference #### Minimal Code Example ```csharp using AudioSystem; public AudioEvent mySound; void Start() { mySound.Post(gameObject, transform.position); } ``` #### Common Methods **Play a sound:** ```csharp AudioHandle handle = myEvent.Post(); AudioHandle handle = myEvent.Post(gameObject, transform.position); ``` **Control playback:** ```csharp handle.Stop(fadeTime: 0.5f); handle.Pause(); handle.Resume(); handle.SetVolumeScale(0.75f); // relative to the authored volume handle.SetPitchOffset(2f); // semitones, on top of the authored pitch ``` **Global controls:** ```csharp AudioManager.Instance.StopAllSounds(); AudioManager.Instance.PauseAll(); AudioManager.Instance.UnpauseAll(); AudioManager.Instance.MuteAll(true); // mute AudioManager.Instance.MuteAll(false); // unmute worldBus.Pause(); // a game's pause: this bus and its children hold, a pause menu's buses play worldBus.Resume(); ``` **Bus control:** ```csharp AudioManager.Instance.SetBusVolume("Music", volumeDb: -6f, transitionTime: 1f); ``` **State changes:** ```csharp AudioManager.Instance.SetState("Underwater", transitionTime: 1.5f); ``` **RTPC with transitions:** ```csharp AudioManager.Instance.SetRTPC("CombatIntensity", 0.75f); AudioManager.Instance.TransitionRTPC("CombatIntensity", 1.0f, duration: 2f); ``` --- ### What's Next? #### Level 1 - You are here! ✓ - Basic sound playback - Simple events and containers - Bus volume control #### Level 2 - [Cookbook](https://peppersfx.com/docs/cookbook/overview/) - Footsteps with variations - UI sound systems - Weapon audio - Music systems - 3D spatial audio #### Level 3 - [Manual](https://peppersfx.com/docs/manual/overview/) - Deep system understanding - Advanced features - Performance optimization - Registry system #### Level 4 - [API Reference](https://peppersfx.com/docs/#api) - Complete API documentation - All classes and methods - Advanced use cases --- ### Comparison: Which Method Should I Use? | Method | Best For | Setup Time | Flexibility | Learning | |--------|----------|------------|-------------|----------| | **Quick Wizard** | Beginners, rapid prototyping | 2 min | Low | Easy | | **Manual Setup** | Learning system, custom control | 10 min | Medium | Best | | **Registry System** | Large projects, no Resources | 15 min | High | Advanced | **Recommendation:** - **New users:** Start with Quick Wizard - **Learning the system:** Use Manual Setup - **After 10+ sounds:** Consider Registry System (see [Migration](https://peppersfx.com/docs/migration/)) --- ### Key Concepts to Remember 1. **Events are in Resources** - Must be in `Assets/Audio/Resources/Audio/Events/` to auto-load (or use registry) 2. **States are in Resources** - Must be in `Assets/Audio/Resources/Audio/States/` to auto-load (or use registry) 3. **One AudioManager per project** - It persists between scenes 4. **Buses use dB** - 0 dB = no change, -6 dB ≈ half as loud, -80 dB = silence 5. **Voice limit** - 32 real voices by default (configurable) --- ### 🆕 Ambient Propagation (v2.3.0) Pepper.SFX now ships with an **optional** zone/portal-based ambient propagation subsystem. Use it when you want ambient beds (rain, wind, machinery) to route *through geometry* — muffled through a closed door, swelling as it opens, heard *from the window* when the listener is in another room. Propagation is **additive** — it doesn't touch the SFX event pipeline you just learned. Skip this section if your project only plays discrete SFX. #### 2-Minute Setup ``` 1. Create empty GameObject → BoxCollider (isTrigger forced on) → Add "Audio/Propagation/Audio Zone" → Size collider to fit the room. Repeat for each acoustic space. 2. Create empty GameObject at a doorway → BoxCollider → Add "Audio/Propagation/Audio Portal" → Drag the two adjacent AudioZones into ZoneA and ZoneB. 3. Create empty GameObject inside the source zone → Add "Audio/Propagation/Ambient Source" → Assign Source Zone, Looping Clip, and Ambience Mixer Group. 4. On your player object, the one with the Collider (e.g. the player root's Capsule Collider) → Add "Audio/Propagation/Audio Listener Zone Tracker". Not on the camera that holds the AudioListener: trigger events only reach a GameObject that has a Collider. (A kinematic Rigidbody is auto-added if needed for trigger events.) ``` That's it. The `PropagationManager` auto-instantiates on first reference. Walk through the doorway and you'll hear the bed transition smoothly between zones. #### Door-Driven Transmission To let a door swing affect audio, implement `IPortalDoorSource` on any MonoBehaviour: ```csharp using AudioSystem.Propagation; public class MyDoor : MonoBehaviour, IPortalDoorSource { public float OpenProgress => animator.GetFloat("openAmount"); // 0..1 public event System.Action OnChanged; } ``` Drag it into the portal's `Door Source` field. Closed → `closedTransmission` amplitude + `closedLowPassHz` filtering; open → the base values. Interpolation is continuous — no discrete snap. #### Where to Learn More - **Cookbook** → [Ambient Propagation recipes](https://peppersfx.com/docs/cookbook/ambient-propagation/) - **Manual** → [Chapter 13: Ambient Propagation Subsystem](https://peppersfx.com/docs/manual/ambient-propagation-subsystem/) - **API Reference** → [Propagation](https://peppersfx.com/docs/api/propagation/) ## Build Compatibility Source: https://peppersfx.com/docs/build-compatibility/ What works on which platform. Pepper.SFX is C# on top of Unity's own audio (AudioSource and AudioMixer) and physics raycasts. Apart from the convolution reverb plugin it has no platform-specific code, so it behaves the same wherever Unity's audio does. The two exceptions are **convolution reverb** and **Web builds**. ### At a Glance | Platform | What works | |---|---| | Windows, 64-bit (x64) | Everything, including convolution reverb | | macOS, Linux | Everything except convolution reverb | | iOS, Android, Meta Quest | Everything except convolution reverb | | Consoles | Everything except convolution reverb | | Windows on ARM, 32-bit Windows | Everything except convolution reverb | | Web | Sound plays, but without occlusion muffling, reverb, or propagation filtering. See [Web Builds](#web-builds). | The free version has the same platform support as the full version. ### Convolution Reverb: 64-bit Windows Only Convolution reverb ("Use Convolution" on a Reverb Send Bus) runs in a native plugin, `SFXConvolutionReverb.dll`, built for 64-bit Windows only: the Windows editor and Windows x64 player builds. Everywhere else (macOS, Linux, mobile, consoles, Web, Windows on ARM and 32-bit Windows): - **A convolution bus is silent.** The dry sound still plays; only that bus's reverb is missing. - **Unity may log an error** about the missing mixer effect when the game starts. - **The impulse response isn't uploaded**, and a warning says so. - **The macOS and Linux editor** don't load the plugin either, so convolution buses are silent there while you work. Parametric reverb (a Reverb Send Bus without "Use Convolution") works on every platform except Web. If your game ships anywhere other than 64-bit Windows, use parametric buses. ### Web Builds Unity's Web platform uses its own, smaller audio engine built on the browser's audio. On the AudioMixer it supports groups and their volume, and nothing else: no mixer effects and no sends. Audio filter components (Low Pass, Reverb) aren't on its supported list either. Unity's manual lists the details on its "Audio in Web" page. What that means for Pepper.SFX: | Feature | On Web | |---|---| | Events, containers, switches, states, RTPCs | Work | | Buses: volume, mute, solo, ducking | Work | | Voice pool, virtual voices, priorities, steals | Work | | Scheduled and beat-synced playback | Work | | Occlusion | Sounds get quieter behind walls, but aren't muffled. The muffling is a low-pass on mixer effects. | | Reverb Send Buses, parametric and convolution | No reverb. Sends and reverb are mixer effects. | | Ambient propagation | Beds still route through zones and portals and follow doors and distance, but aren't muffled and get no reverb. The emitters use Low Pass and Reverb filter components. | | Multiplayer | Works if your netcode library's transport runs in a browser (WebSockets or WebRTC). The SFX netcode layer sends through that transport and has no network code of its own. | The browser adds its own rules: - **No sound until the player interacts.** Browsers block audio until the first click, tap or key press on the page. Sounds posted before that, such as menu music started at scene load, can start late or be missed. Start audio after the first input, or open with a "click to start" screen. - **No streaming.** Browsers can't stream audio from disk. The Music and Ambience import presets use the Streaming load type, so give those clips a Web override in their import settings (Compressed In Memory). - **iPhone and iPad.** In Safari on iOS, clips set to Decompress On Load can be silent when the device is in Silent Mode. Use Compressed In Memory for Web. If Web is a main target, design the mix so it doesn't depend on muffling or reverb, and test in a Web build early. ### Mobile Every feature works. Occlusion raycasts and the per-voice mixer effects cost more CPU on phones than on desktop. If Unity's Profiler shows audio or physics time climbing, lower **Max Real Voices**, raise **Occlusion Update Interval**, or lower **Occlusion Ray Count** on the AudioManager. ### Requirements - Unity 6000.0.48f1 or newer. --- *For API signatures and full type listings, see [API_REFERENCE.md](https://peppersfx.com/docs/#api).* *Pepper.SFX v3.0.0 — Professional Audio Middleware for Unity* ## Cookbook: overview Source: https://peppersfx.com/docs/cookbook/overview/ This section is organized around tasks, not features. Each recipe walks one outcome from asset creation through code. **Three ways to read this section — every recipe serves all three:** - 🏃 **Skimmer:** Read the recipe titles and the bolded step labels. Skip the prose. The ordered lists at the top of each topic are the map. - 📖 **Learner:** Read each recipe top to bottom. Each one is self-contained and explains *why* alongside *how*. - 🔍 **Reference reader:** Jump to the recipe whose title matches your task. Use the Table of Contents. > ## ⚠️ Read this before doing anything spatial > > **Occlusion, Per-Zone Reverb, and container-level Ambient Propagation all share one piece of infrastructure: the Occlusion Slot Pool.** > > On every play, the system checks: > > ``` > wantsSlot = container.UseOcclusion || container.AllowReverbSend || container.UsePropagation > ``` > > If *any* of those three flags is on and the slot pool has not been authored, the voice silently falls back to direct-to-bus routing. No low-pass muffling. No reverb sends. **No error.** Only the volume part works without a slot: an occluded or propagated sound still gets quieter behind walls and doors, it just keeps its treble, so the setup can sound half right. > > **You must author the Occlusion Slot Pool first**, before touching reverb buses, zones, or container flags. Do it here: [Occluding SFX Through Geometry → One-Time Project Setup](https://peppersfx.com/docs/cookbook/occluding-sfx-through-geometry/#recipe-one-time-project-setup-5-minutes). > > Exception: the `AmbientSource` propagation path (rain, wind, ambient beds) uses a separate emitter pool and does *not* require slots. Only container-level `UsePropagation` does. ## Setup & Organization Source: https://peppersfx.com/docs/cookbook/setup-organization/ ### Recipe: First-Time Project Setup (10 minutes) 📋 **What:** Complete setup from scratch to playing first sound #### Step 1: Create AudioManager (2 min) 1. Create empty GameObject in scene: "AudioManager" 2. Add component: `AudioManager` (Component → Audio System → Audio Manager) 3. Configure Inspector: - Master Volume: 1.0 - Max Real Voices: 32 - Max Virtual Voices: 64 - Enable LOD: ✓ Occlusion is opted into per-container (see the `Use Occlusion` toggle on AudioContainer assets), not via a global AudioManager switch. #### Step 2: Create Folder Structure (2 min) Right-click in Project, create folders: ``` Assets/ ├── Resources/ │ └── Audio/ │ ├── Events/ ← AudioEvents MUST go here │ └── States/ ← AudioStates MUST go here └── Audio/ ├── Containers/ ← Container assets ├── Buses/ ← Bus assets └── AudioClips/ ← Your .wav/.mp3 files ├── SFX/ ├── Music/ └── Ambience/ ``` #### Step 3: Create Master Bus (1 min) 1. Right-click `Buses/` → Create → Audio System → Audio Bus 2. Name: "Master_Bus" 3. Inspector: - Bus Name: "Master" - Volume Db: -3 - Parent Bus: (none) #### Step 4: Create SFX Bus (1 min) 1. Right-click `Buses/` → Create → Audio System → Audio Bus 2. Name: "SFX_Bus" 3. Inspector: - Bus Name: "SFX" - Volume Db: 0 - Parent Bus: Master_Bus #### Step 5: Create First Container (2 min) 1. Right-click `Containers/` → Create → Audio System → Routing Container 2. Name: "Test_Click_RC" 3. Inspector: - Container Name: "TestClick" - Audio Clips: Drag in a sound - Volume: 1.0 - Enable Volume Randomization: ✓ (Min: -1, Max: 1) - Enable Pitch Randomization: ✓ (Min: -50, Max: 50) #### Step 6: Create First Event (2 min) 1. Right-click `Assets/Resources/Audio/Events/` → Create → Audio System → Audio Event 2. Name: "Play_TestClick" 3. Inspector: - Event Name: "Play_TestClick" - Priority: High - Max Instances: 3 - Actions → Size: 1 - Action [0]: - Type: Play - Container: Test_Click_RC - Target Bus: SFX_Bus ✅ **Result:** Complete audio pipeline ready to use! --- ### Recipe: Recommended Bus Hierarchy 📋 **What:** Professional bus structure for games ⚙️ **Setup:** **Create Master Bus:** - Name: Master_Bus - Bus Name: "Master" - Volume Db: -3 - Parent: (none) **Create SFX Bus:** - Name: SFX_Bus - Bus Name: "SFX" - Volume Db: 0 - Parent: Master_Bus **Create SFX Sub-Buses:** 1. **Player_Bus** - Bus Name: "Player" - Parent: SFX_Bus - Volume Db: 0 2. **Weapons_Bus** - Bus Name: "Weapons" - Parent: Player_Bus - Volume Db: 0 3. **Footsteps_Bus** - Bus Name: "Footsteps" - Parent: Player_Bus - Volume Db: -6 4. **Enemies_Bus** - Bus Name: "Enemies" - Parent: SFX_Bus - Volume Db: -3 5. **UI_Bus** - Bus Name: "UI" - Parent: SFX_Bus - Volume Db: 0 6. **Ambience_Bus** - Bus Name: "Ambience" - Parent: SFX_Bus - Volume Db: -6 **Create Music Bus:** - Name: Music_Bus - Bus Name: "Music" - Parent: Master_Bus - Volume Db: -6 **Create Dialogue Bus:** - Name: Dialogue_Bus - Bus Name: "Dialogue" - Parent: Master_Bus - Volume Db: 0 - Enable Ducking: ✓ - Ducking Targets: - Music_Bus, Amount: 0.7 - SFX_Bus, Amount: 0.5 **Final Hierarchy:** ``` Master (-3dB) ├── SFX (0dB) │ ├── Player (0dB) │ │ ├── Weapons (0dB) │ │ └── Footsteps (-6dB) │ ├── Enemies (-3dB) │ ├── UI (0dB) │ └── Ambience (-6dB) ├── Music (-6dB) └── Dialogue (0dB) [Ducks Music & SFX] ``` ✅ **Result:** Professional mixing structure with ducking! ## UI Sounds Source: https://peppersfx.com/docs/cookbook/ui-sounds/ ### Recipe: Button Click Sound 📋 **What:** Button with click sound and subtle variations 🎯 **Use Case:** UI buttons, menu navigation #### Asset Setup **1. Create Container:** - Right-click `Containers/UI/` → Create → Random Container - Name: "UI_ButtonClick_RnC" - Inspector: - Audio Clips: Add 2-3 click sound variations - Avoid Repeat Last: 1 - Use Weighting: ✓ (all weights = 1.0) - Volume: 0.8 - Enable Volume Randomization: ✓ (Min: -1, Max: 1) - Enable Pitch Randomization: ✓ (Min: -50, Max: 50) **2. Create Event:** - Right-click `Assets/Resources/Audio/Events/UI/` → Create → Audio Event - Name: "Play_UI_ButtonClick" - Inspector: - Event Name: "Play_UI_ButtonClick" - Priority: Critical - Max Instances: 2 - Steal Behavior: Oldest - Cooldown: 0.05 - Actions → Size: 1 - Action [0]: - Type: Play - Container: UI_ButtonClick_RnC - Target Bus: UI_Bus #### Code ```csharp using UnityEngine; using UnityEngine.UI; using AudioSystem; public class UIButtonSound : MonoBehaviour { [SerializeField] private AudioEvent clickEvent; private Button button; void Awake() { button = GetComponent