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
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
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}");
alsa: recording 3 s from default take.wav: 144000 frames at 48000 Hz, PcmSigned16, peak -31.6 dBFS, overruns 0
Audio.CreateInputopens 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).Readcopies 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 andOverrunscounts it. WavEncoderwrites each block as it arrives, so a long recording doesn’t sit in memory. With a lossy-typed format such asnew AudioFormat(48000, 1)it writes 16-bit PCM; setWavEncoderSettings.SampleTypefor 24-bit or float.Finishpatches 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#.- To record straight to a compressed file, swap the encoder:
FlacEncoderfor lossless FLAC orOpusFileWriterfor Ogg Opus take the sameWriteandFinishcalls. - 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:
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}");
-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:
Readthrows, because the callback already got every block. FramesPlayedon an input counts the frames delivered to you (82,560 frames is 1.72 s at 48 kHz), andOverrunscounts 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
LoudnessMeterfrom the core package, whoseProcesscall also allocates nothing. See loudness normalization in C#.
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:
- 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
defaultinput is the sound server’s default source.AudioDevices.ListwithDeviceDirection.Inputlists the others, andDeviceIdpicks 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 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:
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}");
recorded.wav: 130048 frames, 16 bit PCM: 48000Hz 1 channelsThe 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 lists the equivalents.
Where next
- Play audio in C#: files, sound effects and generated samples.
- Record from the microphone and list audio devices, the cookbook recipes.
- Read and write FLAC in C#, to keep recordings lossless in smaller files.