---
title: "Record audio in C#"
url: https://recursiveai.org/oneaudio/articles/record-audio-csharp/
description: "Record from the microphone in C# on Windows, Linux and macOS: write a WAV file while recording, show a live input level, and handle missing devices."
date: 2026-10-27
author: OneAudio team
---

# Record audio in C#

Record from the microphone in C# on Windows, Linux and macOS: write a WAV file while recording, show a live input level, and handle missing devices.

October 27, 2026 · OneAudio team

To record audio from the microphone in C#, add the `RecursiveAI.OneAudio.Devices` package, start the default input with `Audio.CreateInput()`, and drain it with `mic.Read(buffer)` into a `WavEncoder`, which writes the WAV file as you go. The same code records through WASAPI on Windows, Core Audio on macOS and iOS, and ALSA on Linux (including PipeWire and PulseAudio desktops), and it is managed .NET code with nothing native to ship. Every sample below was compiled and run against OneAudio 1.2.1 on .NET 10, on Linux with a USB webcam microphone, and the outputs shown are from those runs.

## Before you start

*Terminal · NuGet*

```
dotnet new console -o RecordAudio
cd RecordAudio
dotnet add package RecursiveAI.OneAudio.Devices --version 1.2.1
```

The device package brings in `RecursiveAI.OneAudio`, which has the WAV, FLAC and Opus encoders. Both target .NET 10 and are licensed MIT OR Apache-2.0.

## Record the microphone to a WAV file

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

```
using OneAudio;
using OneAudio.Devices;
 
const int Rate = 48000, Seconds = 3;
 
// Open and start the default input device: 48 kHz mono, converted for you if the microphone runs at another rate.
using AudioInputDevice mic = Audio.CreateInput(new DeviceStreamOptions { SampleRate = Rate, Channels = 1 });
Console.WriteLine($"{mic.BackendName}: recording {Seconds} s from {mic.Device?.Name ?? "no device"}");
 
// Write 16-bit PCM WAV while recording.
using (FileStream file = File.Create("take.wav"))
using (var wav = new WavEncoder(file, new AudioFormat(Rate, 1)))
{
    float[] block = new float[Rate / 10];                 // up to 100 ms per read
    long recorded = 0;
    while (recorded < Rate * Seconds)
    {
        int frames = mic.Read(block);                     // never blocks: 0 means nothing has arrived yet
        if (frames == 0) { Thread.Sleep(10); continue; }
        frames = (int)Math.Min(frames, Rate * Seconds - recorded);
        wav.Write(block.AsSpan(0, frames));
        recorded += frames;
    }
    wav.Finish();
}
 
AudioClip take = Audio.Load("take.wav");
float peak = 0;
foreach (float s in take.Samples.Span) peak = Math.Max(peak, Math.Abs(s));
Console.WriteLine($"take.wav: {take.Frames} frames at {take.Format.SampleRate} Hz, {take.Format.NativeType}, " +
                  $"peak {20 * Math.Log10(peak):F1} dBFS, overruns {mic.Counters.Overruns}");
```

*Output · Console · Linux*

```
alsa: recording 3 s from default
take.wav: 144000 frames at 48000 Hz, PcmSigned16, peak -31.6 dBFS, overruns 0
```

- `Audio.CreateInput` opens and starts the default input. You choose the rate and channel count you want; if the microphone runs at something else (this webcam runs at 32 kHz), the system’s audio layer converts, and you get 48 kHz mono floats.
- Captured audio goes into a ring buffer of one second by default (`CaptureBufferMilliseconds`). `Read` copies whatever has arrived and never blocks, returning 0 when nothing is waiting, so the loop sleeps briefly and tries again. If nobody reads for longer than the buffer holds, new audio is dropped and `Overruns` counts it.
- `WavEncoder` writes each block as it arrives, so a long recording doesn’t sit in memory. With a lossy-typed format such as `new AudioFormat(48000, 1)` it writes 16-bit PCM; set `WavEncoderSettings.SampleType` for 24-bit or float. `Finish` patches the sizes in the header, and the file becomes RF64 by itself if it grows past what a plain WAV can describe. More in [read and write WAV files in C#](https://recursiveai.org/oneaudio/articles/wav-files-csharp/).
- To record straight to a compressed file, swap the encoder: `FlacEncoder` for lossless FLAC or `OpusFileWriter` for Ogg Opus take the same `Write` and `Finish` calls.
- The peak of −31.6 dBFS is the room’s background: nobody spoke during this run, and your levels will differ.

## Show a live input level

For a level meter, or anything that reacts as sound arrives, give `CreateInput` a callback. It receives each block on the device thread, with the same rules as a playback callback: no allocation, no locks, no blocking, no I/O. Here it keeps only the block’s peak, and the main thread reads it twice a second:

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

```
using OneAudio;
using OneAudio.Devices;
 
// A live input level: the callback gets each block on the device thread as it arrives.
// Like a render callback, it must not allocate, lock, block or do I/O, so it only stores the block's peak.
float[] lastPeak = new float[1];
using AudioInputDevice mic = Audio.CreateInput((ReadOnlySpan<float> block, in RenderContext ctx) =>
{
    float peak = 0;
    foreach (float s in block)
        peak = MathF.Max(peak, MathF.Abs(s));
    Volatile.Write(ref lastPeak[0], peak);
}, new DeviceStreamOptions { SampleRate = 48000, Channels = 1 });
 
if (mic.State == DeviceState.NoDevice)
    Console.WriteLine($"no input: {mic.LastError}");
 
for (int i = 0; i < 4; i++)
{
    Thread.Sleep(500);
    float db = 20 * MathF.Log10(Math.Max(Volatile.Read(ref lastPeak[0]), 1e-6f));
    Console.WriteLine($"{db,6:F1} dBFS {new string('#', (int)Math.Max(0, (db + 60) / 2))}");
}
Console.WriteLine($"{mic.Counters.FramesPlayed} frames delivered, overruns {mic.Counters.Overruns}");
```

*Output · Console · Linux*

```
 -46.7 dBFS ######
 -38.9 dBFS ##########
 -37.4 dBFS ###########
 -42.4 dBFS ########
82560 frames delivered, overruns 0
```

- An input opened with a callback has no read buffer: `Read` throws, because the callback already got every block.
- `FramesPlayed` on an input counts the frames delivered to you (82,560 frames is 1.72 s at 48 kHz), and `Overruns` counts capture overruns, when audio arrived faster than it could be taken.
- For a loudness reading in LUFS rather than a peak, feed the blocks to a `LoudnessMeter` from the core package, whose `Process` call also allocates nothing. See [loudness normalization in C#](https://recursiveai.org/oneaudio/articles/loudness-normalization-csharp/).

## Permissions and missing microphones

A microphone that can’t be opened isn’t an exception: `mic.State` is `DeviceState.NoDevice`, `mic.LastError` says why, and the input retries twice a second, as the second sample checks. The common reasons, per the device package’s [API documentation](https://github.com/recursive-ai-oss/oneaudio-csharp/blob/v1.2.1/docs/api.md#devices-recursiveaioneaudiodevices):

- **Windows:** the microphone privacy setting must allow desktop apps; a denied open says so in `LastError`.
- **macOS:** if the user denied microphone access, opening fails with a message saying so. A command-line program without an app bundle and a microphone usage description may be unable to ask and receive silence.
- **iOS:** the app needs a microphone usage description in its Info.plist, and a capture stream uses the play-and-record audio session.
- **Linux:** the `default` input is the sound server’s default source. `AudioDevices.List` with `DeviceDirection.Input` lists the others, and `DeviceId` picks one.

## Recording with NAudio

NAudio 3 records with `WasapiRecorder` or `WaveInEvent` on Windows and with `AlsaIn`, in the `NAudio.Alsa` package, on Linux. Its [ALSA recording guide](https://github.com/naudio/NAudio/blob/main/Docs/RecordAudioFileLinuxAlsa.md) shows the pattern: set a `WaveFormat`, write each `DataAvailable` buffer to a `WaveFileWriter`, and dispose the writer in `RecordingStopped`. Following it, with a wait for that event added so the file is complete before it is read:

*Program.cs · NAudio.Alsa 3.3.0 · Linux*

```
using NAudio.Wave;
using NAudio.Wave.Alsa;
 
var waveFormat = new WaveFormat(48000, 16, 1);
var writer = new WaveFileWriter("recorded.wav", waveFormat);
var stopped = new ManualResetEventSlim();
 
using var input = new AlsaIn();                          // default capture device
input.WaveFormat = waveFormat;
input.DataAvailable += (sender, a) => writer.Write(a.Buffer, 0, a.BytesRecorded);
input.RecordingStopped += (sender, a) =>
{
    writer.Dispose();                                    // safe once the capture thread has finished
    stopped.Set();
};
 
input.StartRecording();
Thread.Sleep(3000);
input.StopRecording();
stopped.Wait();
 
using var check = new WaveFileReader("recorded.wav");
Console.WriteLine($"recorded.wav: {check.SampleCount} frames, {check.WaveFormat}");
```

*Output · Console · Linux*

```
recorded.wav: 130048 frames, 16 bit PCM: 48000Hz 1 channels
```

The two models differ in where the work happens. NAudio raises an event with bytes in the device’s sample format, and its guide notes that the buffer is reused, so you copy or write it before the handler returns. OneAudio hands you float samples, either through a buffer you drain on your own thread or through a callback with real-time rules, and the counters (`Overruns`, `FramesPlayed`, `DeviceLosses`) say what the device did. If your app already records with NAudio, the API map in [migrating from NAudio](https://recursiveai.org/oneaudio/articles/migrate-from-naudio/) lists the equivalents.

## Where next

- [Play audio in C#](https://recursiveai.org/oneaudio/articles/play-audio-csharp/): files, sound effects and generated samples.
- [Record from the microphone](https://recursiveai.org/oneaudio/cookbook/record-microphone/) and [list audio devices](https://recursiveai.org/oneaudio/cookbook/list-audio-devices/), the cookbook recipes.
- [Read and write FLAC in C#](https://recursiveai.org/oneaudio/articles/flac-csharp/), to keep recordings lossless in smaller files.
