---
title: "Mix audio files"
url: https://recursiveai.org/oneaudio/cookbook/mix-audio/
description: "Mix music, voice and sound effects in C# with oneaudio’s real-time mixer: buses, ducking, reverb, pitch, a limiter and loudness meters, rendered to a WAV…"
version: "1.2.1"
---

# Mix audio files

Mix music, voice and sound effects in C# with oneaudio’s real-time mixer: buses, ducking, reverb, pitch, a limiter and loudness meters, rendered to a WAV file or to your own audio output.

```csharp
using OneAudio;
using OneAudio.Loudness;

// Two buses, a meter on the master output and a limiter in front of it.
var mixer = new AudioMixer(new MixerOptions
{
    SampleRate = 48000,
    Channels = 2,
    MaxBusMeters = 1,
    LimiterLookaheadMilliseconds = 1.0,
});
mixer.SetLimiter(new LimiterSettings { CeilingDb = -1f });

const int Music = 0, Voice = 1;
mixer.SetBusGain(Music, 1.5f);
var ducking = new DuckingSettings { ReductionDb = 12f, ReleaseMs = 400f };
mixer.SetDucking(Music, Voice, ducking);
var reverb = new ReverbSettings { RoomSize = 0.4f, DecaySeconds = 0.8f, Wet = 0.15f };
mixer.SetBusReverb(Voice, reverb);
mixer.EnableBusMeter(AudioMixer.MasterBus);

// Clips are decoded once and can be shared by any number of voices.
AudioClip music = Audio.Load("music.flac");
AudioClip voice = Audio.Load("voice.wav");
AudioClip hit = Audio.Load("hit.wav");
mixer.Play(new PlayParams(music) { Bus = Music, Loop = true });
mixer.Play(new PlayParams(voice) { Bus = Voice, StartAtFrame = 48000 * 4 });
for (int i = 0; i < 4; i++)
{
    mixer.Play(new PlayParams(hit)
    {
        Bus = Voice,
        StartAtFrame = 48000 * 12 + i * 12000,      // frame-exact
        Pan = i % 2 == 0 ? -0.6f : 0.6f,
        Pitch = 1f + i * 0.12f,
    });
}

// Render 16 seconds into a WAV file, reading the master meter once a second.
var format = new AudioFormat(48000, 2, AudioFormat.DefaultMask(2),
                             NativeSampleType.PcmSigned16, 16);
using var wav = new WavEncoder(File.Create("game-mix.wav"), format,
                               new WavEncoderSettings(), leaveOpen: false);
float[] block = new float[480 * 2];                 // 10 ms blocks
for (int b = 1; b <= 1600; b++)
{
    mixer.Render(block);
    wav.Write(block);
    if (b % 100 != 0)
        continue;
    if (mixer.TryGetBusLoudness(AudioMixer.MasterBus, out LoudnessSnapshot m))
        Console.Write($"{b / 100,2}s {m.MomentaryLufs,6:F1}{(b % 400 == 0 ? "\n" : "   ")}");
}

mixer.TryGetBusLoudness(AudioMixer.MasterBus, out LoudnessSnapshot total);
Console.WriteLine($"master: {total.IntegratedLufs:F1} LUFS integrated, " +
                  $"true peak {total.TruePeakDbtp:F1} dBTP");
while (mixer.TryGetEvent(out MixerEvent e))
    Console.WriteLine($"{e.Kind} voice {e.VoiceId}");
Console.WriteLine($"limiter: {mixer.LimiterGainReductionDb:F1} dB in the last pass, " +
                  $"{mixer.ActiveVoices} voice active");
```

```text
 1s  -20.7    2s  -20.7    3s  -20.9    4s  -20.8
 5s  -18.1    6s  -16.6    7s  -17.7    8s  -18.3
 9s  -17.9   10s  -23.6   11s  -21.1   12s  -20.8
13s  -19.6   14s  -22.0   15s  -21.0   16s  -21.0
master: -18.7 LUFS integrated, true peak -1.8 dBTP
VoiceEnded voice 2
VoiceEnded voice 3
VoiceEnded voice 4
VoiceEnded voice 5
VoiceEnded voice 6
limiter: 0.0 dB in the last pass, 1 voice active
```

`game-mix.wav` is 16 seconds of looping music, an announcement from 4 seconds with reverb, and four clicks from 12 seconds, alternating left and right and rising in pitch. Rendering the same mix with and without the announcement showed the music 10 to 12 dB lower while the announcement played, and back to full level within a second of it ending.

## The mixer

- **Voices** play an `AudioClip` (decoded audio in memory, shareable between voices) or a `StreamingSource` (a file decoded ahead on a background thread, for long music). `mixer.Play(path)` and `mixer.PlayStream(path, loop: true)` do the loading for you.
- **Buses** sum voices. Each bus feeds the master output or another bus (`SetBusOutput`), and has its own gain, mute, filters and reverb. `MaxBuses` is 8 by default.
- **Ducking**: `SetDucking(target, source, settings)` turns the target bus down while the source bus is above `ThresholdDb`, by `ReductionDb`, with `AttackMs` and `ReleaseMs`.
- **Effects**: `SetBusFilter` and `SetVoiceFilter` add a low-pass or high-pass filter, 12 dB per octave. `SetBusReverb` adds a reverb, up to `MaxReverbs` buses at a time (2 by default). The master limiter keeps every sample under its ceiling. It limits sample peaks, not true peaks.
- **Meters**: `MaxBusMeters` reserves loudness meters, and `TryGetBusLoudness` reads one from any thread without locking. A bus is metered after its filters, reverb and gain, but before it’s ducked into its parent, so meter the master output to see the effect of ducking.
- **Events**: `TryGetEvent` reports voices that ended or were stolen, plays that were dropped and underruns. Nothing is called back on the render thread.

## In a real-time app

`Render` allocates nothing, takes no locks, never blocks and never throws, so it’s safe on the audio thread. Call `Play`, `SetGain`, `SetDucking` and the rest from another thread. They go through a lock-free queue, and `Render` applies them between its processing passes of at most `MaxBlockFrames` (1,024 by default). If the queue is full, a call returns false and the command is dropped.

To play the mix on the sound card, add `RecursiveAI.OneAudio.Devices` and call `Audio.Play(mixer)`: the device pulls from `Render` on its own thread. See [Play an audio file](https://recursiveai.org/oneaudio/cookbook/play-audio-file/). With an output API you already use, call `mixer.Render(buffer)` from its callback and copy the interleaved floats it fills.

Every mixer option is in the [API overview](https://recursiveai.org/oneaudio/docs/api/#mixing).

Buses, ducking and a zero-allocation mix for games: [Game audio in C#](https://recursiveai.org/oneaudio/articles/game-audio-mixer-csharp/).
