Pepper.SFX

Features

Multi-position audio

One sound from up to 16 places at once (a river, a row of torches, club speakers), posted with one call and controlled with one handle.

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

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.

Learn more