Recursive AI

Migrating from NAudio

Move NAudio code to OneAudio: AudioFileReader, WdlResamplingSampleProvider, WaveFileWriter, FLAC and Opus encoding, and AlsaOut or WasapiPlayer playback.

This guide moves an NAudio app to OneAudio: reading audio files, resampling, writing WAV, FLAC and Opus, and playing sound on a device. Each NAudio sample follows NAudio’s own documentation, and each pair was compiled and run on Linux against NAudio 3.3.0 and OneAudio 1.2.1. OneAudio decodes MP3, Ogg Vorbis, Opus and FLAC in managed code, so this part of an app no longer depends on Media Foundation, ACM or a system libsndfile. Playback and recording can move as well, to the companion package RecursiveAI.OneAudio.Devices (WASAPI, Core Audio and ALSA), or stay with NAudio through a small adapter. The last section shows both.

When moving makes sense

  • You decode MP3, FLAC, Ogg Vorbis or Opus on Linux or macOS. In NAudio 3.3.0, AudioFileReader reads WAV and AIFF everywhere but compressed formats only on Windows, through Media Foundation and ACM, per its package README. Off Windows it points you to NAudio.SoundFile, which calls libsndfile installed on the machine.
  • You want one managed assembly that publishes with NativeAOT and trimming, with nothing to install on servers or containers.
  • You need FLAC or Opus encoding, EBU R 128 loudness, or a mixer whose render call allocates nothing.
  • You want one playback and recording API on Windows, Linux and macOS, instead of a different player and reader per platform.

Stay with NAudio for ASIO and WinMM devices, MIDI, VST 3 hosting, the SoundFont sampler, AIFF files, and MP3, AAC or WMA encoding through Media Foundation. None of these are in OneAudio 1.2.1.

API map

NAudio 3OneAudio 1.2.1
new AudioFileReader(path), MediaFoundationReader, Mp3FileReader, SoundFileReaderAudio.Open(path) to stream, Audio.Load(path) to decode it all
ISampleProvider.Read(Span<float>), returns samplesIAudioDecoder.Read(Span<float>), returns frames (samples ÷ channels)
reader.WaveFormatdecoder.Format (SampleRate, Channels, NativeType)
reader.TotalTimedecoder.LengthFrames (−1 when unknown)
Setting reader.Position or CurrentTimedecoder.Seek(frame), sample-accurate
WdlResamplingSampleProvider, MediaFoundationResamplerAudio.Resample, AudioResampler.Create, or TranscodeOptions.SampleRate
WaveFileWriter.CreateWaveFile16(path, provider)Audio.Transcode(input, "out.wav"), or WavEncoder for samples you produce
SoundFileWriter (FLAC, Opus), MediaFoundationEncoder (FLAC)Audio.Transcode(input, "out.flac") or "out.opus"; FlacEncoder, OpusFileWriter
MediaFoundationEncoder.EncodeToMp3, EncodeToAac, EncodeToWmaNo equivalent
WasapiPlayer, WasapiOut, AlsaOut: Init(provider), Play()Audio.Play(mixer), or Audio.CreateOutput(source) for any IAudioSource or render callback (RecursiveAI.OneAudio.Devices, see below)
PlaybackState, PlaybackStoppedThe mixer’s VoiceEnded event (mixer.TryGetEvent); output.Counters for what the device played
WasapiRecorder, AlsaIn with DataAvailableAudio.CreateInput() and Read(Span<float>), or a capture callback
AsioOut, WaveOut (WinMM)No equivalent; keep NAudio

Reading an audio file

The NAudio version reads a WAV file, which AudioFileReader handles on every OS:

Program.csNAudio 3.3.0
using NAudio.Wave;
 
using var reader = new AudioFileReader("music.wav");
WaveFormat format = reader.WaveFormat;
Console.WriteLine($"{format.SampleRate} Hz, {format.Channels} channels, {reader.TotalTime}");
 
float[] buffer = new float[4096 * format.Channels];
long samples = 0;
int read;
while ((read = reader.Read(buffer.AsSpan())) > 0)   // samples, not frames
    samples += read;
Console.WriteLine($"{samples / format.Channels} frames");
OutputConsole
44100 Hz, 2 channels, 00:00:30
1323000 frames

The OneAudio version reads the MP3 of the same audio, on any OS:

Program.csOneAudio 1.2.1
using OneAudio;
 
using IAudioDecoder decoder = Audio.Open("music.mp3");   // WAV, MP3, Ogg Vorbis, Opus or FLAC
AudioFormat format = decoder.Format;
Console.WriteLine($"{format.SampleRate} Hz, {format.Channels} channels, {decoder.LengthFrames} frames");
 
float[] buffer = new float[4096 * format.Channels];
long frames = 0;
int read;
while ((read = decoder.Read(buffer)) > 0)   // frames, not samples
    frames += read;
Console.WriteLine($"{frames} frames");
OutputConsole
44100 Hz, 2 channels, 1323000 frames
1323000 frames
  • The one change that bites: NAudio’s Read returns a count of samples, OneAudio’s a count of frames. Multiply by Channels to get samples. Both fill an interleaved float buffer at ±1 full scale.
  • OneAudio finds the format from the file’s bytes. A WAV, FLAC, Ogg Vorbis or Opus file goes through the same code.
  • The MP3 decodes to exactly the source’s 1,323,000 frames because OneAudio trims the encoder delay and padding recorded in the LAME header. More in decode MP3 in C#.

Resampling

NAudio’s resampling guide recommends WdlResamplingSampleProvider where Media Foundation isn’t available. From the guide, with a WAV input:

Program.csNAudio 3.3.0
using NAudio.Wave;
using NAudio.Wave.SampleProviders;
 
int outRate = 48000;
var inFile = "music.wav";
var outFile = "music-48k-wdl.wav";
using (var reader = new AudioFileReader(inFile))
{
    var resampler = new WdlResamplingSampleProvider(reader, outRate);
    WaveFileWriter.CreateWaveFile16(outFile, resampler);
}
 
using var check = new WaveFileReader(outFile);
Console.WriteLine($"{check.WaveFormat}, {check.SampleCount} frames");
OutputConsole
16 bit PCM: 48000Hz 2 channels, 1440000 frames
Program.csOneAudio 1.2.1
using OneAudio;
 
// A file to 48 kHz: decoded, resampled and written block by block.
Audio.Transcode("music.wav", "music-48k.wav", new TranscodeOptions { SampleRate = 48000 });
 
// Samples in memory: interleaved floats, any channel count.
AudioClip clip = Audio.Load("music.wav");
float[] at48 = Audio.Resample(clip.Samples.ToArray(), clip.Format.Channels,
                              fromRate: 44100, toRate: 48000, ResamplerQuality.Polyphase);
Console.WriteLine($"{clip.Frames} frames -> {at48.Length / clip.Format.Channels} frames");
OutputConsole
1323000 frames -> 1440000 frames

Audio.Resample returns exactly round(frames × ratio) frames with the filter delay removed. The default quality is a 144-tap polyphase filter; PolyphaseFast and Linear are cheaper. For a resampler you feed block by block, as WdlResamplingSampleProvider is fed, use AudioResampler.Create, shown in resample audio in C#.

Writing WAV, FLAC and Opus

Off Windows, NAudio encodes FLAC through NAudio.SoundFile, as in its cross-platform guide. This needs libsndfile installed (apt install libsndfile1 on Debian and Ubuntu):

Program.csNAudio.SoundFile 3.3.0
using NAudio.SoundFile;
using NAudio.Wave;
 
using (var reader = new SoundFileReader("music.wav"))
{
    SoundFileWriter.CreateSoundFile("music.flac", reader,
        SoundFileMajorFormat.Flac,
        new SoundFileWriterOptions { CompressionLevel = 0.8 });
}
Console.WriteLine($"music.flac: {new FileInfo("music.flac").Length:N0} bytes");
OutputConsole
music.flac: 2,157,949 bytes

OneAudio writes FLAC, Ogg Opus and WAV from any input it reads, with nothing to install:

Program.csOneAudio 1.2.1
using OneAudio;
using OneAudio.Flac;
using OneAudio.Opus;
 
// FLAC, lossless. Levels 0 to 8, like the flac command line (default 5).
Audio.Transcode("music.wav", "music.flac", new TranscodeOptions
{
    Flac = new FlacEncoderSettings { CompressionLevel = 8 },
});
 
// Ogg Opus at 128 kbit/s. Input that isn't 48 kHz is resampled on the way.
Audio.Transcode("music.wav", "music.opus", new TranscodeOptions
{
    Opus = new OpusFileWriterSettings { Encoder = new OpusEncoderSettings { BitrateBps = 128_000 } },
});
 
// 24-bit WAV from an MP3.
Audio.Transcode("music.mp3", "music-24bit.wav", new TranscodeOptions
{
    Wav = new WavEncoderSettings { SampleType = NativeSampleType.PcmSigned24 },
});
 
foreach (string f in new[] { "music.flac", "music.opus", "music-24bit.wav" })
    Console.WriteLine($"{f}: {new FileInfo(f).Length:N0} bytes");
OutputConsole
music.flac: 2,079,705 bytes
music.opus: 479,795 bytes
music-24bit.wav: 7,938,104 bytes
  • The FLAC file decodes to exactly the samples of music.wav: we compared the decoded PCM with ffmpeg. Levels 0 to 8 trade encoding time for size, as with the flac tool.
  • Opus input that isn’t 48 kHz is resampled for you, and the file records the original rate.
  • For samples your code produces rather than a file, write them with WavEncoder or FlacEncoder: Write(ReadOnlySpan<float>) as often as you like, then Finish(). The same input and settings always give the same bytes on a platform.
  • There is no MP3, AAC or Vorbis encoder. If you deliver MP3 or AAC, keep MediaFoundationEncoder on Windows or use FFmpeg.

Playback and recording

NAudio plays through an IWavePlayer: WasapiPlayer or WasapiOut on Windows, AlsaOut from the NAudio.Alsa package on Linux. This is the Linux example from NAudio’s ALSA playback guide, run as written with a WAV file:

Program.csNAudio.Alsa 3.3.0 · Linux
using NAudio.Wave;
using NAudio.Wave.Alsa;
 
using (var audioFile = new WaveFileReader("intro.wav"))
using (var outputDevice = new AlsaOut())          // default device
{
    outputDevice.Init(audioFile);
    outputDevice.Play();
    while (outputDevice.PlaybackState == PlaybackState.Playing)
    {
        Thread.Sleep(200);
    }
}
Console.WriteLine("done");

In OneAudio, a device plays a mixer, and files become voices on it. One program covers every OS: the RecursiveAI.OneAudio.Devices package picks WASAPI, Core Audio or ALSA itself, and the file can be any format OneAudio decodes, MP3 included, with no separate reader for each platform:

Program.csOneAudio.Devices 1.2.1 · .NET 10
using OneAudio;
using OneAudio.Devices;
 
AudioMixer mixer = Audio.CreateMixer();                        // what the device plays
using AudioOutputDevice output = Audio.Play(mixer);            // default device; WASAPI, Core Audio or ALSA
VoiceHandle voice = mixer.Play("intro.wav");                   // any format OneAudio reads
 
// NAudio's PlaybackState loop becomes a wait for the mixer's VoiceEnded event.
bool ended = false;
while (!ended)
{
    Thread.Sleep(50);
    while (mixer.TryGetEvent(out MixerEvent e))
        ended |= e.Kind == MixerEventKind.VoiceEnded && e.VoiceId == voice.Id;
}
Console.WriteLine($"done: {output.Counters.FramesPlayed} frames played");
OutputConsole · Linux
done: 242881 frames played
  • dotnet add package RecursiveAI.OneAudio.Devices brings in the core package too. Both are managed assemblies; on Linux the device package loads the system’s libasound, the same library NAudio.Alsa uses.
  • NAudio’s player pulls from one provider. OneAudio’s output pulls from the mixer, so starting a second sound is another mixer.Play call, not a MixingSampleProvider you build first.
  • A missing device isn’t an exception: the output keeps its clock running, retries twice a second and says why in output.LastError. It also follows the system default device when the user changes it.
  • Recording maps the same way: Audio.CreateInput() replaces AlsaIn or WasapiRecorder and their DataAvailable event, and you read float samples instead of bytes. Both are shown side by side in record audio in C#.

Or keep NAudio’s player

You don’t have to move playback at all. A few lines turn a OneAudio decoder (or the OneAudio mixer’s output) into an NAudio ISampleProvider, which every NAudio player accepts. NAudio 3’s providers read into a Span<float>, per its migration guide. This sample writes the provider to a file so it runs anywhere; with a device, pass the same provider to player.Init:

Program.csOneAudio 1.2.1 + NAudio.Core 3.3.0
using NAudio.Wave;
using OneAudio;
 
// Decode with OneAudio, hand the samples to anything in NAudio that takes an ISampleProvider:
// a player such as WasapiPlayer or AlsaOut (player.Init(provider)), a mixer, or a file writer.
using IAudioDecoder decoder = Audio.Open("music.mp3");
var provider = new OneAudioSampleProvider(decoder);
 
WaveFileWriter.CreateWaveFile16("bridged.wav", provider);
Console.WriteLine($"{provider.WaveFormat} -> bridged.wav, {new FileInfo("bridged.wav").Length:N0} bytes");
 
sealed class OneAudioSampleProvider(IAudioDecoder decoder) : ISampleProvider
{
    public WaveFormat WaveFormat { get; } =
        WaveFormat.CreateIeeeFloatWaveFormat(decoder.Format.SampleRate, decoder.Format.Channels);
 
    // NAudio counts samples; OneAudio counts frames (one sample per channel).
    public int Read(Span<float> buffer)
    {
        int channels = decoder.Format.Channels;
        int total = 0;
        while (total < buffer.Length)
        {
            int frames = decoder.Read(buffer[total..].Slice(0, (buffer.Length - total) / channels * channels));
            if (frames == 0) break;
            total += frames * channels;
        }
        return total;
    }
}
OutputConsole
32 bit IEEEFloat: 44100Hz 2 channels -> bridged.wav, 5,292,044 bytes

The adapter converts between the two counting conventions: NAudio asks for samples, the decoder returns frames. It reads until the buffer is full, because NAudio expects a provider to fill every read until the end of the stream, as its resampling guide explains. For game audio, wrap AudioMixer.Render the same way and let NAudio’s player pull the mix.

Where next