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.
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");
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 aStreamingSource(a file decoded ahead on a background thread, for long music).mixer.Play(path)andmixer.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.MaxBusesis 8 by default. - Ducking:
SetDucking(target, source, settings)turns the target bus down while the source bus is aboveThresholdDb, byReductionDb, withAttackMsandReleaseMs. - Effects:
SetBusFilterandSetVoiceFilteradd a low-pass or high-pass filter, 12 dB per octave.SetBusReverbadds a reverb, up toMaxReverbsbuses at a time (2 by default). The master limiter keeps every sample under its ceiling. It limits sample peaks, not true peaks. - Meters:
MaxBusMetersreserves loudness meters, andTryGetBusLoudnessreads 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:
TryGetEventreports 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. 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.
Buses, ducking and a zero-allocation mix for games: Game audio in C#.