---
title: "Read and write WAV files in C#"
url: https://recursiveai.org/oneaudio/articles/wav-files-csharp/
description: "Read WAV files in C# as floats or exact integers and write 16-bit, 24-bit and float WAV with optional dither, including RF64 for files over 4 GB, on any OS."
date: 2026-10-27
author: OneAudio team
---

# Read and write WAV files in C#

Read WAV files in C# as floats or exact integers and write 16-bit, 24-bit and float WAV with optional dither, including RF64 for files over 4 GB, on any OS.

October 27, 2026 · OneAudio team

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

*Terminal · NuGet*

```
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

*Program.cs · OneAudio 1.2.1 · .NET 10*

```
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)");
```

*Output · Console*

```
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)
```

- `Format` holds the sample rate, channel count, channel layout (`ChannelMask`, from `WAVE_FORMAT_EXTENSIBLE` when present), `NativeType` and `BitsPerSample`. 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]`. `Read` and `ReadInteger` return the number of frames, not samples.
- `Read` scales to ±1 full scale whatever the depth: 556,544 / 223 is 0.066345. `ReadInteger` returns the stored values; 8-bit files, which are unsigned on disk, come back re-centered to −128…127.
- `Audio.Open` streams, so a file of any length reads in constant memory, and `Seek` is exact. `Audio.Load` reads 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; `HasNativeIntegers` tells you whether a decoder has them.

## Write a WAV file

*Program.cs · OneAudio 1.2.1 · .NET 10*

```
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");
}
```

*Output · Console*

```
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 = true` adds 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_EXTENSIBLE` with the channel mask, as the format expects; that is why the 24-bit header is 24 bytes longer. Float files carry a `fact` chunk.
- `WriteInteger` writes 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, with `TranscodeOptions` for 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](https://www.nuget.org/packages/NAudio/3.3.0): `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](https://github.com/naudio/NAudio/blob/main/Docs/MixTwoAudioFilesToWav.md):

*Program.cs · NAudio 3.3.0 · Linux*

```
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");
```

*Output · Console*

```
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](https://github.com/recursive-ai-oss/oneaudio-csharp/blob/v1.2.1/README.md#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](https://recursiveai.org/oneaudio/cookbook/wav/), the cookbook recipe.
- [Record audio in C#](https://recursiveai.org/oneaudio/articles/record-audio-csharp/), writing WAV while recording.
- [Read and write FLAC in C#](https://recursiveai.org/oneaudio/articles/flac-csharp/), for lossless files at a fraction of the size.
- [Resample audio in C#](https://recursiveai.org/oneaudio/articles/resample-audio-csharp/), for example 44.1 kHz to 48 kHz.
