To read a WAV file in C#, add the RecursiveAI.OneAudio package and call Audio.Open("in.wav"): Read gives you float samples, ReadInteger the exact integers stored in the file, and Format says how they are stored. To write one, create a WavEncoder over a stream, call Write with float samples as often as you like, and Finish. Both handle 8-, 16-, 24- and 32-bit PCM, 32- and 64-bit float, WAVE_FORMAT_EXTENSIBLE and RF64, in managed code on any OS. Every sample below was compiled and run against OneAudio 1.2.1 on .NET 10.
Before you start
dotnet new console -o WavFiles cd WavFiles dotnet add package RecursiveAI.OneAudio --version 1.2.1
OneAudio targets .NET 10 and is licensed MIT OR Apache-2.0. The samples use music-24bit-48k.wav, 30 seconds of 24-bit stereo at 48 kHz.
Read a WAV file
using OneAudio; // Open a WAV file: the format says how the samples are stored. using IAudioDecoder wav = Audio.Open("music-24bit-48k.wav"); AudioFormat f = wav.Format; Console.WriteLine($"{f.SampleRate} Hz, {f.Channels} ch, {f.NativeType}, {f.BitsPerSample} bits, {wav.LengthFrames} frames"); // As floats (full scale is -1 to +1) ... wav.Seek(48000); // 1 second in float[] floats = new float[1024 * f.Channels]; int frames = wav.Read(floats); Console.WriteLine($"frame 48000 as float: {floats[0]:F6}, {floats[1]:F6}"); // ... or as the exact integers in the file, for lossless PCM. wav.Seek(48000); int[] ints = new int[1024 * f.Channels]; frames = wav.ReadInteger(ints); Console.WriteLine($"frame 48000 as 24-bit integers: {ints[0]}, {ints[1]}"); // The whole file in memory, as one array. AudioClip clip = Audio.Load("music-24bit-48k.wav"); Console.WriteLine($"loaded {clip.Samples.Length} samples ({clip.Frames} frames)");
48000 Hz, 2 ch, PcmSigned24, 24 bits, 1440000 frames frame 48000 as float: 0.066345, 0.102448 frame 48000 as 24-bit integers: 556544, 859392 loaded 2880000 samples (1440000 frames)
Formatholds the sample rate, channel count, channel layout (ChannelMask, fromWAVE_FORMAT_EXTENSIBLEwhen present),NativeTypeandBitsPerSample. You don’t parse the header yourself, and you don’t need to know where the data chunk starts.- Samples are interleaved: frame i of channel c is at
[i * channels + c].ReadandReadIntegerreturn the number of frames, not samples. Readscales to ±1 full scale whatever the depth: 556,544 / 223 is 0.066345.ReadIntegerreturns the stored values; 8-bit files, which are unsigned on disk, come back re-centered to −128…127.Audio.Openstreams, so a file of any length reads in constant memory, andSeekis exact.Audio.Loadreads it all into one array.- The format is detected from the bytes, so the same code reads FLAC, MP3, Ogg Vorbis and Opus. Exact integers exist only for lossless PCM, from WAV and FLAC files;
HasNativeIntegerstells you whether a decoder has them.
Write a WAV file
using OneAudio; // Samples your code produces: one second of a 440 Hz tone, 48 kHz stereo. float[] tone = new float[48000 * 2]; for (int i = 0; i < 48000; i++) tone[2 * i] = tone[2 * i + 1] = 0.25f * MathF.Sin(2 * MathF.PI * 440 * i / 48000f); var format = new AudioFormat(48000, 2); Write("tone-16.wav", new WavEncoderSettings()); // 16-bit PCM (default) Write("tone-16-dither.wav", new WavEncoderSettings { Dither = true }); // 16-bit with TPDF dither Write("tone-24.wav", new WavEncoderSettings { SampleType = NativeSampleType.PcmSigned24 }); // 24-bit PCM Write("tone-float.wav", new WavEncoderSettings { SampleType = NativeSampleType.IeeeFloat32 }); // 32-bit float void Write(string path, WavEncoderSettings settings) { using (FileStream file = File.Create(path)) using (var wav = new WavEncoder(file, format, settings)) { wav.Write(tone); // call as often as you like wav.Finish(); // patches the sizes in the header } using IAudioDecoder check = Audio.Open(path); Console.WriteLine($"{path}: {new FileInfo(path).Length:N0} bytes, {check.Format.NativeType}, {check.LengthFrames} frames"); }
tone-16.wav: 192,080 bytes, PcmSigned16, 48000 frames tone-16-dither.wav: 192,080 bytes, PcmSigned16, 48000 frames tone-24.wav: 288,104 bytes, PcmSigned24, 48000 frames tone-float.wav: 384,116 bytes, IeeeFloat32, 48000 frames
- Float input is scaled, rounded to nearest and clipped to the integer type; NaN becomes 0.
Dither = trueadds one LSB of triangular noise from a seeded generator first, which you want when reducing a mix to 16 bits. The same input and settings always give the same bytes. - Files with more than two channels or more than 16 bits are written as
WAVE_FORMAT_EXTENSIBLEwith the channel mask, as the format expects; that is why the 24-bit header is 24 bytes longer. Float files carry afactchunk. WriteIntegerwrites exact integers, and throws if a value doesn’t fit the stored depth.- To convert a file rather than write samples,
Audio.Transcode("in.flac", "out.wav")does it in one call, withTranscodeOptionsfor the sample type, rate and channels.
Files over 4 GB and streams that can’t seek
A RIFF WAV file stores its sizes in 32 bits, so it can’t describe more than 4 GB of audio, about 4.1 hours of 24-bit stereo at 48 kHz. WavEncoder reserves room in the header, and when it finishes a seekable file that has grown past that limit it rewrites the header as RF64 (EBU Tech 3306); ForceRf64 = true writes RF64 from the start. The reader handles both.
On a stream that can’t seek, such as an HTTP response or a pipe, the encoder can’t go back to patch sizes. Pass TotalFrames when you know the length; otherwise the sizes are written as “unknown”, which OneAudio’s reader takes to mean “to the end of the stream”.
WAV files with NAudio
NAudio’s cross-platform NAudio.Core package has WAV and AIFF readers and writers, per its package README: WaveFileReader reads a file in its own format, AudioFileReader reads WAV as floats on every OS, and WaveFileWriter.CreateWaveFile16 writes a provider to a 16-bit file, as in its mixing guide:
using NAudio.Wave; // NAudio: WaveFileReader for the file's own format, AudioFileReader for floats, WaveFileWriter to write. using (var reader = new WaveFileReader("music-24bit-48k.wav")) Console.WriteLine($"{reader.WaveFormat}, {reader.SampleCount} frames"); using (var floats = new AudioFileReader("music-24bit-48k.wav")) WaveFileWriter.CreateWaveFile16("music-16bit-48k.wav", floats); using var check = new WaveFileReader("music-16bit-48k.wav"); Console.WriteLine($"{check.WaveFormat}, {check.SampleCount} frames");
24 bit PCM: 48000Hz 2 channels, 1440000 frames 16 bit PCM: 48000Hz 2 channels, 1440000 frames
If WAV is all you read and NAudio already plays your audio, that works. OneAudio gives you the exact integers next to the floats, writes 24-bit and float files with optional dither, switches to RF64 for long recordings by itself, and reads compressed formats through the same call on every OS. In the project’s published benchmarks, decoding 60 seconds of 16-bit stereo at 48 kHz took 3.48 ms and allocated 8 KB, and writing 60 seconds as 16-bit PCM took 2.8 ms (BenchmarkDotNet 0.15.8, AMD Ryzen 9 7950X3D, Linux, .NET 10.0.13).
Where next
- WAV files, the cookbook recipe.
- Record audio in C#, writing WAV while recording.
- Read and write FLAC in C#, for lossless files at a fraction of the size.
- Resample audio in C#, for example 44.1 kHz to 48 kHz.