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.
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:
dotnet add package RecursiveAI.OneAudio.Devices --version 1.2.1Everything 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:
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");
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.
Samplesis 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#.
LoadOptionsconverts the sample rate and channel count on the way.Loadrefuses more than 46,080,000 frames (16 minutes at 48 kHz) unless you raiseMaxFrames. Longer files belong in a stream.
To keep memory flat, open the file as a stream instead and read it in blocks:
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");
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
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");
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.Transcodestreams 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 asmusic.wav..oggas 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 throwsArgumentException.Audio.Resamplereturns exactly round(frames × ratio) frames with the filter’s delay removed, so the result lines up with the source. The default quality isPolyphase(144 taps, at least 100 dB of alias rejection).PolyphaseFastandLinearare 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:
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");
mix.wav: 575488 frames, 1 voice still playingAudio.CreateMixerbuilds 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.
StartAtFrameplaces a voice on the mixer’s timeline to the frame. Renderallocates nothing, takes no locks and never blocks, so it can run on an audio thread. In a real-time app, callPlayand the other control methods from another thread. They go through a lock-free queue thatRenderdrains. 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
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");
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.Normalizewrites 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 andLimitedByPeakis true. It decodes the file twice instead of holding it in memory. Silence is left alone.- ffmpeg’s
ebur128filter 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:
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");
alsa 'default' Running: 1.0 s played, peak -5.0 dBFS, 0 underrunsAudio.Play(mixer),Audio.CreateOutput(...)andAudio.CreateInput(...)come with the package, as extension members onAudio. 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.
Counterscome 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.OneAudio1.2.1, namespaceOneAudio, for .NET 10 (net10.0).RecursiveAI.OneAudio.Devices1.2.1 adds playback and recording, namespaceOneAudio.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.
| Format | Support |
|---|---|
| WAV, RF64 | Read and write |
| FLAC | Read (bit-exact) and write, levels 0–8 |
| Ogg FLAC | Read |
| Ogg Vorbis | Read |
| Ogg Opus | Read and write |
| MP3 | Read, 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:
- Decode: MP3, Ogg Vorbis, Opus, from a stream or untrusted input, and read format and tags
- Read and write FLAC and WAV, encode Opus, convert between formats
- Resample audio and mix audio files
- Play an audio file, record from the microphone and list audio devices
- Measure loudness and normalize loudness
- Keep only the codecs you use, for small NativeAOT apps
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.