Documentation

Unity Engine


User Manual

Script Reference

Unity Engine


Example: Create an effect

Create an effect and apply it to an audio source.
Read time 5 minutesLast updated 11 days ago

This example walks through how to set up a gain effect and apply it to an
AudioSource
. The effect scales every sample of the audio source's signal by a gain value.
It's organized into two parts:
  • Part I (Minimal): Uses a fixed gain value.
  • Part II (Parameterized): Extends the minimal example with a gain control.

Required includes

Some of the code in this section requires the following includes:
using Unity.Burst;using UnityEngine;using UnityEngine.Audio;using static UnityEngine.Audio.ProcessorInstance;
Make sure to add these includes at the start of your code for them to compile.

Part I: Minimal example

Implement
EffectInstance.IRealtime
to process audio in
Process
. The method reads from the input buffer and writes to the output buffer. Write every output sample, because the output buffer starts undefined.
[BurstCompile(CompileSynchronously = true)] struct Realtime : EffectInstance.IRealtime { // Fixed attenuation applied to every sample. private const float k_Gain = 0.5f; // Called when the real-time side of the graph updates (e.g., new control data available). // Keep this method allocation-free and exception-free. public void Update(UpdatedDataContext context, Pipe pipe) { } public EffectInstance.Result Process( in RealtimeContext context, ChannelBuffer inputBuffer, ChannelBuffer outputBuffer, EffectInstance.Arguments args) { // Scale every input sample and write it to the output buffer. // Always write every output sample: the output buffer starts undefined. for (int frame = 0; frame < inputBuffer.frameCount; frame++) { for (int ch = 0; ch < inputBuffer.channelCount; ch++) outputBuffer[ch, frame] = inputBuffer[ch, frame] * k_Gain; } return default; } }
Next, implement
EffectInstance.IControl<Realtime>
. This minimal effect doesn't depend on the audio configuration, so
Configure
only assigns the reserved
Setup
value.
struct Control : EffectInstance.IControl<Realtime> { // Dispose is called when the effect instance is destroyed. public void Dispose(ControlContext context, ref Realtime realtime) { } // Control-side tick; e.g., poll external state or schedule events. public void Update(ControlContext context, Pipe pipe) { } // Optional message hook; return `Unhandled` for messages you don't consume. public Response OnMessage(ControlContext context, Pipe pipe, Message message) => Response.Unhandled; // Called initially when constructed and additionally when the system changes configuration. public void Configure( ControlContext context, ref Realtime realtime, in AudioConfiguration configuration, out EffectInstance.Setup setup) { // Setup is reserved for configure-time output; assign default for now. setup = default; } }
Finally, tie everything together in a
MonoBehaviour
that implements
IAudioEffect
:
public class Driver : MonoBehaviour, IAudioEffect { public EffectInstance CreateInstance( ControlContext context, AudioFormat? nestedFormat, EffectInstance.CreationParameters creationParameters) { // Allocate a new effect instance pairing the realtime and control structs. return context.AllocateEffect(new Realtime(), new Control(), nestedFormat, creationParameters); } }
Add the
Driver
component to a GameObject with an
AudioSource
, and enter Play Mode. Unity discovers the component automatically, and everything the audio source plays comes through at half level. You don't assign the component to a field in the Inspector.

Part II: Configure the gain

To parameterize the gain of the effect, start by defining a value type to represent gain change messages:
// Small value-type message for the pipe. readonly struct GainEvent { public readonly float value; public GainEvent(float value) => this.value = value; }
Secondly, update the real-time struct to store the gain. In the
Update
method, read any pending
GainEvent
messages from the pipe, and update the gain. This approach ensures the audio thread safely handles any number of pending events without throwing errors on the audio thread.
[BurstCompile(CompileSynchronously = true)] struct Realtime : EffectInstance.IRealtime { internal float gain; // Linear gain, set from control messages. public void Update(UpdatedDataContext context, Pipe pipe) { // Iterate over all available events (newer overwrite older). foreach (var element in pipe.GetAvailableData(context)) { if (element.TryGetData(out GainEvent evt)) { gain = evt.value; } // Ignore other message types gracefully. } } public EffectInstance.Result Process( in RealtimeContext context, ChannelBuffer inputBuffer, ChannelBuffer outputBuffer, EffectInstance.Arguments args) { for (int frame = 0; frame < inputBuffer.frameCount; frame++) { for (int ch = 0; ch < inputBuffer.channelCount; ch++) outputBuffer[ch, frame] = inputBuffer[ch, frame] * gain; } return default; } }
In the control struct, handle messages sent via the instance and forward them to the real-time part:
struct Control : EffectInstance.IControl<Realtime> { public void Dispose(ControlContext context, ref Realtime realtime) { } public void Update(ControlContext context, Pipe pipe) { } public Response OnMessage(ControlContext context, Pipe pipe, Message message) { if (message.Is<GainEvent>()) { // Forward the gain change to the real-time part. pipe.SendData(context, message.Get<GainEvent>()); return Response.Handled; } return Response.Unhandled; } public void Configure( ControlContext context, ref Realtime realtime, in AudioConfiguration configuration, out EffectInstance.Setup setup) { setup = default; } }
Finally, add a gain slider to the driver and update the audio instance only when the value changes to avoid spamming the control part. Use
AudioSource.GetEffectInstance
to access the instance for the effect component.
Note
Always guard accesses to the instance. It doesn't exist until the audio source first plays, and it's destroyed if the effect component is removed, or if the audio source is disabled or destroyed.
public class Driver : MonoBehaviour, IAudioEffect { private AudioSource m_AudioSource; [Range(0f, 2f)] public float gain = 0.5f; private float m_PreviousGain; public EffectInstance CreateInstance( ControlContext context, AudioFormat? nestedFormat, EffectInstance.CreationParameters creationParameters) { // Seed the real-time part with the serialized gain value. return context.AllocateEffect(new Realtime { gain = gain }, new Control(), nestedFormat, creationParameters); } private void Awake() { // Expects an AudioSource on the same GameObject. m_AudioSource = GetComponent<AudioSource>(); m_PreviousGain = gain; } private void Update() { // Early out if unchanged (use Approximately to avoid redundant updates). if (Mathf.Approximately(gain, m_PreviousGain)) return; // Access the instance via the AudioSource. var instance = m_AudioSource.GetEffectInstance(this); // Guard the handle: the instance may be missing or have been destroyed. if (!ControlContext.builtIn.Exists(instance)) return; var message = new GainEvent(gain); // Send the gain change to the control side. ControlContext.builtIn.SendMessage(instance, ref message); m_PreviousGain = gain; } }

Additional resources