Recursive AI

Getting started with oneaudio

All oneaudio docs

Install oneaudio for .NET 10, decode MP3, Ogg, Opus, FLAC and WAV files, then resample, mix, encode, normalize loudness, and play and record on audio devices, all in C# with no native binaries.

oneaudio is an audio library written in C# with no dependency beyond .NET itself. Every sample on this page was compiled and run against RecursiveAI.OneAudio 1.2.1 from NuGet (the device sample with RecursiveAI.OneAudio.Devices 1.2.1), and the output shown is from that run.

Install

oneaudio targets .NET 10 (net10.0), so you need the .NET 10 SDK.

Terminal
dotnet new console -o AudioStart
cd AudioStart
dotnet add package RecursiveAI.OneAudio --version 1.2.1

To play or record on an audio device, add the device package too:

Terminal
dotnet add package RecursiveAI.OneAudio.Devices --version 1.2.1

Everything is in the OneAudio namespace, with the codec-specific types in OneAudio.Flac, OneAudio.Opus, OneAudio.Vorbis and OneAudio.Mp3, and the meter in OneAudio.Loudness. The device types are in OneAudio.Devices.

The samples read and write files in the current directory. The ones used here are a 32-second stereo test piece (music.wav, 44.1 kHz 16-bit, with the same audio as music.mp3, music.ogg, music.opus and music.flac), a six-second spoken line (voice.wav, 16 kHz mono) and a short click (hit.wav, 48 kHz mono). Put your own files there under those names.

Decode a file

Audio.Load decodes a whole file to 32-bit float samples:

C#
using OneAudio;
 
// Decode a whole file to 32-bit floats. The format is found from the bytes,
// not the extension.
AudioClip clip = Audio.Load("music.mp3");
AudioFormat format = clip.Format;
double seconds = clip.Frames / (double)format.SampleRate;
Console.WriteLine($"{format.SampleRate} Hz, {format.Channels} channels, " +
                  $"{clip.Frames} frames, {seconds:F3} s");
 
// Samples are interleaved: frame i of channel c is at [i * channels + c].
ReadOnlySpan<float> samples = clip.Samples.Span;
float peak = 0;
foreach (float s in samples)
    peak = Math.Max(peak, Math.Abs(s));
Console.WriteLine($"{samples.Length} samples, sample peak {20 * Math.Log10(peak):F1} dBFS");
 
// Convert on the way in: 48 kHz mono, whatever the source was.
var options = new LoadOptions { SampleRate = 48000, Channels = 1 };
AudioClip mono48 = Audio.Load("music.ogg", options);
Console.WriteLine($"music.ogg as {mono48.Format.SampleRate} Hz, " +
                  $"{mono48.Format.Channels} channel: {mono48.Frames} frames");
Text
44100 Hz, 2 channels, 1411200 frames, 32.000 s
2822400 samples, sample peak -12.1 dBFS
music.ogg as 48000 Hz, 1 channel: 1536000 frames
  • The format is detected from the first bytes of the file, so the extension doesn’t matter. WAV and RF64, Ogg Vorbis, Ogg Opus, FLAC (native and in Ogg) and MP3 (MPEG-1, 2 and 2.5 Layer III) all come back the same way.
  • A frame is one sample per channel. Samples is interleaved, so a stereo clip of N frames holds 2N floats. Full scale is ±1, and decoded samples are never clipped.
  • The MP3 is exactly 32 seconds, the length of the source. A LAME or Xing/Info header tells the decoder how much padding the encoder added, and it’s trimmed. See Decode MP3 in C#.
  • LoadOptions converts the sample rate and channel count on the way. Load refuses more than 46,080,000 frames (16 minutes at 48 kHz) unless you raise MaxFrames. Longer files belong in a stream.

To keep memory flat, open the file as a stream instead and read it in blocks:

C#
using OneAudio;
 
// Stream a file block by block instead of holding it in memory.
using IAudioDecoder decoder = Audio.Open("music.flac");
AudioFormat format = decoder.Format;
Console.WriteLine($"{format.SampleRate} Hz, {format.Channels} channels, " +
                  $"{format.NativeType}, {decoder.LengthFrames} frames");
 
float[] buffer = new float[4096 * format.Channels];   // room for 4096 frames
long frames = 0;
int read;
while ((read = decoder.Read(buffer)) > 0)   // frames, not samples; 0 is the end
    frames += read;
Console.WriteLine($"read {frames} frames");
 
// Seeking is sample-accurate.
decoder.Seek(format.SampleRate * 10L);                // 10 seconds in
read = decoder.Read(buffer);
Console.WriteLine($"after the seek: position {decoder.PositionFrames}, {read} frames read");
Text
44100 Hz, 2 channels, PcmSigned16, 1411200 frames
read 1411200 frames
after the seek: position 445096, 4096 frames read

Audio.Open returns an IAudioDecoder. Read fills the span you give it and returns the number of frames, which can be fewer than asked for; 0 means the end. Decoders allocate nothing per read, and every one of them seeks to the exact frame when the underlying stream can seek. Dispose the decoder when you’re done. More in Decode audio from a stream.

Convert and resample

C#
using OneAudio;
using OneAudio.Opus;
 
// Convert between formats. The output format comes from the extension:
// .wav, .flac, or .opus/.ogg (Ogg Opus).
TranscodeResult flac = Audio.Transcode("music.wav", "music-copy.flac");
long flacBytes = new FileInfo("music-copy.flac").Length;
Console.WriteLine($"FLAC: {flac.Frames} frames at {flac.Input.SampleRate} Hz, {flacBytes:N0} bytes");
 
var opusOptions = new TranscodeOptions
{
    Opus = new OpusFileWriterSettings
    {
        Encoder = new OpusEncoderSettings { BitrateBps = 96_000 },
    },
};
TranscodeResult opus = Audio.Transcode("music.wav", "music-96k.opus", opusOptions);
long opusBytes = new FileInfo("music-96k.opus").Length;
Console.WriteLine($"Opus: {opus.Frames} frames, {opusBytes:N0} bytes");
 
// Change the rate and channel count while converting.
var toMono48 = new TranscodeOptions { SampleRate = 48000, Channels = 1 };
Audio.Transcode("music.mp3", "music-48k-mono.wav", toMono48);
 
// Resample in memory. The result has exactly round(frames x ratio) frames,
// with the filter delay removed.
AudioClip clip = Audio.Load("music.wav");
AudioClip at48 = Audio.Resample(clip, 48000);
Console.WriteLine($"resampled {clip.Frames} frames at {clip.Format.SampleRate} Hz " +
                  $"to {at48.Frames} at {at48.Format.SampleRate} Hz");
 
float[] voice = Audio.Load("voice.wav").Samples.ToArray();       // 16 kHz mono
float[] voice44 = Audio.Resample(voice, channels: 1, fromRate: 16000, toRate: 44100,
                                 ResamplerQuality.PolyphaseFast);
Console.WriteLine($"voice: {voice.Length} samples at 16 kHz, {voice44.Length} at 44.1 kHz");
Text
FLAC: 1411200 frames at 44100 Hz, 813,193 bytes
Opus: 1411200 frames, 392,386 bytes
resampled 1411200 frames at 44100 Hz to 1536000 at 48000 Hz
voice: 96400 samples at 16 kHz, 265703 at 44.1 kHz
  • Audio.Transcode streams block by block, so memory use doesn’t depend on the length. A lossless source written to WAV or FLAC with no conversion is bit-exact: the FLAC above decodes to the same samples as music.wav.
  • .ogg as an output extension writes Ogg Opus, the only Ogg format oneaudio encodes. There is no MP3, Vorbis or AAC encoder. A path with another extension throws ArgumentException.
  • Audio.Resample returns exactly round(frames × ratio) frames with the filter’s delay removed, so the result lines up with the source. The default quality is Polyphase (144 taps, at least 100 dB of alias rejection). PolyphaseFast and Linear are cheaper. See Resample audio.

Mix

AudioMixer mixes any number of voices on buses into a buffer you supply. In a game, the buffer goes to your audio output. Here it goes into a WAV file:

C#
using OneAudio;
 
// The mixer renders into a buffer you supply. Here the buffer goes into a
// WAV file; in a game it goes to your audio output instead.
AudioMixer mixer = Audio.CreateMixer(sampleRate: 48000, channels: 2);
 
const int MusicBus = 0, VoiceBus = 1;
mixer.SetDucking(MusicBus, VoiceBus);   // music down while the voice bus is loud
 
// A file at 44.1 kHz: the voice converts it to the mixer's rate.
mixer.Play("music.wav", new PlayOptions { Bus = MusicBus, Gain = 1.5f });
 
// PlayParams can start a voice at an exact frame of the mixer's timeline.
var voice = Audio.Load("voice.wav");
var hit = Audio.Load("hit.wav");
mixer.Play(new PlayParams(voice) { Bus = VoiceBus, StartAtFrame = 48000 * 2 });
mixer.Play(new PlayParams(hit) { Bus = VoiceBus, StartAtFrame = 48000 * 10, Pan = 0.5f });
 
var format = new AudioFormat(48000, 2, AudioFormat.DefaultMask(2),
                             NativeSampleType.PcmSigned16, 16);
using var wav = new WavEncoder(File.Create("mix.wav"), format,
                               new WavEncoderSettings(), leaveOpen: false);
 
float[] block = new float[1024 * 2];    // 1024 frames, stereo, interleaved
for (int i = 0; i < 48000 * 12 / 1024; i++)
{
    mixer.Render(block);                // allocates nothing, takes no locks
    wav.Write(block);
}
wav.Finish();
Console.WriteLine($"mix.wav: {wav.FramesWritten} frames, {mixer.ActiveVoices} voice still playing");
Text
mix.wav: 575488 frames, 1 voice still playing
  • Audio.CreateMixer builds a mixer with 64 voices and a limiter on the master output at −1 dB. Every voice feeds a bus, and each bus feeds the master output or another bus.
  • SetDucking(MusicBus, VoiceBus) turns the music bus down while the voice bus is loud: by 12 dB by default, once the voice bus passes −30 dB.
  • Sources at another sample rate are converted per voice. StartAtFrame places a voice on the mixer’s timeline to the frame.
  • Render allocates nothing, takes no locks and never blocks, so it can run on an audio thread. In a real-time app, call Play and the other control methods from another thread. They go through a lock-free queue that Render drains. Offline, as here, one thread does both.
  • The voice still playing is the music, which is 32 seconds long.

To hear the mix instead, hand the mixer to an audio device (next section), or pass Render’s buffer to an output API you already use. Buses, filters, reverb and meters are in Mix audio files.

Measure and normalize loudness

C#
using OneAudio;
using OneAudio.Loudness;
 
// Integrated loudness, loudness range and true peak (BS.1770-5, EBU R 128).
LoudnessResult before = Audio.Measure("music.flac");
Console.WriteLine($"music.flac: {before.IntegratedLufs:F1} LUFS, " +
                  $"range {before.LoudnessRange:F1} LU, true peak {before.TruePeakDbtp:F1} dBTP");
 
// A copy at -14 LUFS with the true peak at most -1 dBTP (the defaults).
NormalizeResult result = Audio.Normalize("music.flac", "music-14.flac");
Console.WriteLine($"gain {result.GainDb:+0.0;-0.0} dB, " +
                  $"limited by the peak ceiling: {result.LimitedByPeak}");
 
LoudnessResult after = Audio.Measure("music-14.flac");
Console.WriteLine($"music-14.flac: {after.IntegratedLufs:F1} LUFS, " +
                  $"true peak {after.TruePeakDbtp:F1} dBTP");
Text
music.flac: -23.0 LUFS, range 0.2 LU, true peak -11.8 dBTP
gain +9.0 dB, limited by the peak ceiling: False
music-14.flac: -14.0 LUFS, true peak -2.8 dBTP
  • Loudness follows ITU-R BS.1770-5 and EBU R 128: integrated loudness in LUFS, loudness range in LU, and the true peak in dBTP, found with 4× oversampling.
  • Audio.Normalize writes a copy at −14 LUFS by default, with the true peak held to −1 dBTP. If the ceiling would be crossed, the gain stops short of the target and LimitedByPeak is true. It decodes the file twice instead of holding it in memory. Silence is left alone.
  • ffmpeg’s ebur128 filter reads the normalized file as −14.0 LUFS too.

For broadcast at −23 LUFS, or to normalize audio in memory, see Normalize loudness. For a live meter, see Measure loudness.

Play and record

The RecursiveAI.OneAudio.Devices package opens the sound card. Audio.Play hands a mixer to the default output device and starts it:

C#
using OneAudio;
using OneAudio.Devices;
 
var mixer = Audio.CreateMixer();          // 48 kHz stereo
using var output = Audio.Play(mixer);     // default device, follows changes
mixer.Play("hit.wav");                    // every voice the mixer plays is heard
 
Thread.Sleep(1000);
DeviceCounters c = output.Counters;       // what the hardware really did
Console.WriteLine($"{output.BackendName} '{output.Device?.Id}' {output.State}: " +
                  $"{c.FramesPlayed / (double)mixer.SampleRate:F1} s played, " +
                  $"peak {20 * Math.Log10(c.PeakPlayed):F1} dBFS, " +
                  $"{c.Underruns} underruns");
Text
alsa 'default' Running: 1.0 s played, peak -5.0 dBFS, 0 underruns
  • Audio.Play(mixer), Audio.CreateOutput(...) and Audio.CreateInput(...) come with the package, as extension members on Audio. Each opens the device and starts it.
  • The backend is the operating system’s own: WASAPI on Windows, Core Audio on macOS and iOS, ALSA on Linux (PipeWire and PulseAudio through their ALSA plug-ins). This run was on Linux through PipeWire.
  • Counters come from the device’s own clock: frames written and played, the played peak, underruns, lost devices and reopens.
  • A device that can’t be opened isn’t an exception. The output keeps rendering silently at real-time pace, retries twice a second and says why in LastError.
  • The output follows the system default when it changes, and reopens after the device is lost.

Play a file, record from the microphone and pick a device in Play an audio file, Record from the microphone and List audio devices. The changelog lists which backends have been checked on real hardware.

The packages

  • Packages: RecursiveAI.OneAudio 1.2.1, namespace OneAudio, for .NET 10 (net10.0). RecursiveAI.OneAudio.Devices 1.2.1 adds playback and recording, namespace OneAudio.Devices, and versions in step with the core package.
  • Dependencies: none. No native binaries and no other NuGet packages. The device package calls the operating system’s own audio libraries.
  • License: MIT or Apache-2.0, at your choice.
  • NativeAOT and trimming: supported, with no warnings.
FormatSupport
WAV, RF64Read and write
FLACRead (bit-exact) and write, levels 0–8
Ogg FLACRead
Ogg VorbisRead
Ogg OpusRead and write
MP3Read, gapless

WAV covers PCM at 8, 16, 24 and 32 bits, float at 32 and 64 bits, and WAVE_FORMAT_EXTENSIBLE. FLAC covers 4 to 32 bits and 1 to 8 channels. MP3 means MPEG-1, 2 and 2.5 Layer III. Chained Ogg files play as one stream.

The one-call Audio class registers every codec, so an app that uses it carries all of them. To keep only the codecs you need in a published app, see Keep only the codecs you use.

What oneaudio doesn’t do

  • MP3, Vorbis and AAC encoding. It writes WAV, FLAC and Ogg Opus only.
  • AAC and M4A, MP3 Layers I and II, MIDI, spatial audio. None of these are in 1.2.1. Layer I and II files throw UnsupportedAudioFormatException.

Where next

Cookbook recipes, each a complete program:

The reference: the API overview and the changelog.

Guides: Decode MP3 in C#, Resample audio in C#, Loudness normalization in C#, Choosing a C# audio library and Migrate from NAudio.