---
title: "Play audio in C#"
url: https://recursiveai.org/oneaudio/articles/play-audio-csharp/
description: "Play MP3, WAV, Ogg, Opus and FLAC files in C# on Windows, Linux and macOS with one API, pick an output device and play samples you generate."
date: 2026-10-27
author: OneAudio team
---

# Play audio in C#

Play MP3, WAV, Ogg, Opus and FLAC files in C# on Windows, Linux and macOS with one API, pick an output device and play samples you generate.

October 27, 2026 · OneAudio team

To play audio in C# on Windows, Linux and macOS, add the `RecursiveAI.OneAudio.Devices` package, create a mixer with `Audio.CreateMixer()`, start the default output device with `Audio.Play(mixer)`, and play a file on it with `mixer.PlayStream("song.mp3")`. The same code plays MP3, WAV, Ogg Vorbis, Opus and FLAC through WASAPI on Windows, Core Audio on macOS and iOS, and ALSA on Linux, which also reaches PipeWire and PulseAudio. Both packages are 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 real output device, and the outputs shown are from those runs.

## Before you start

*Terminal · NuGet*

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

The device package brings in `RecursiveAI.OneAudio`, which does the decoding and mixing. Both target .NET 10 and are licensed MIT OR Apache-2.0. On Linux the package loads the system’s `libasound`, which desktop distributions install with their sound stack. The samples use `intro.mp3`, a five-second stereo clip at 44.1 kHz; use any file of your own.

## Play an MP3 file

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

```
using OneAudio;
using OneAudio.Devices;
 
// The mixer is what the device plays: 48 kHz stereo, with a limiter on the master.
AudioMixer mixer = Audio.CreateMixer();
using AudioOutputDevice output = Audio.Play(mixer);   // opens the default output device and starts it
Console.WriteLine($"{output.BackendName}: {output.Device?.Name ?? "no device"}, {output.DeviceFormat.SampleRate} Hz");
 
// A long file streams: it is decoded ahead on a background thread and converted to 48 kHz.
using StreamingVoice song = mixer.PlayStream("intro.mp3");
Console.WriteLine($"playing {song.Source.LengthFrames} frames");
 
// The mixer reports the end of a voice as an event.
while (!HasEnded(mixer, song.Voice))
    Thread.Sleep(50);
Thread.Sleep(200);   // let the device play what is still queued
 
DeviceCounters c = output.Counters;
Console.WriteLine($"written {c.FramesWritten}, played {c.FramesPlayed} frames, peak {c.PeakPlayed:F3}, underruns {c.Underruns}");
 
static bool HasEnded(AudioMixer mixer, VoiceHandle voice)
{
    while (mixer.TryGetEvent(out MixerEvent e))
        if (e.Kind == MixerEventKind.VoiceEnded && e.VoiceId == voice.Id)
            return true;
    return false;
}
```

*Output · Console · Linux*

```
alsa: default, 48000 Hz
playing 220500 frames
written 252720, played 251203 frames, peak 0.055, underruns 0
```

- `Audio.Play(mixer)` opens the default output in shared mode with about 20 ms of buffer, starts it and returns the running `AudioOutputDevice`. Dispose it to stop.
- `PlayStream` suits music and other long files: a background thread decodes ahead, so the file is never fully in memory. The 44.1 kHz MP3 is converted to the mixer’s 48 kHz as it plays, and `LengthFrames` is counted in the file’s own frames, so 220,500 is five seconds at 44.1 kHz. For short sounds, `mixer.Play("click.wav")` loads the file into memory and starts it at once.
- The counters come from the device’s own position, not from what the program handed over: `FramesPlayed` is what the hardware has played, `PeakPlayed` the loudest sample among those frames, and `Underruns` how often it ran dry. Here the device kept playing silence for a moment after the clip ended, so it played more frames than the clip holds; the exact counts change a little from run to run.
- The format is detected from the bytes: change the file name to a `.wav`, `.ogg`, `.opus` or `.flac` file and nothing else changes.

## Play several sounds at once

Everything you play is a voice on the mixer, so overlapping sounds need no extra code: call `mixer.Play` again while the first is still playing. `PlayOptions` sets `Gain`, `Pan`, `Pitch`, `Loop`, a fade-in and the bus the voice feeds, and the mixer’s limiter on the master output keeps the sum from clipping. The mixer’s render call allocates nothing and takes no locks, so it can run on the device’s real-time thread. Buses, ducking and effects for games are covered in [a zero-allocation audio mixer for C# games](https://recursiveai.org/oneaudio/articles/game-audio-mixer-csharp/).

## Choose a device and play samples you generate

To play audio that your own code computes, such as a synthesizer, a tone or a stream from the network, pass a callback instead of a mixer. This sample lists the outputs, opens the default one by id, and plays a 440 Hz tone for one second:

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

```
using OneAudio;
using OneAudio.Devices;
 
// What the system offers. On Linux the ids are ALSA PCM names; "default" follows the desktop's choice.
IReadOnlyList<AudioDeviceInfo> outputs = AudioDevices.List(DeviceDirection.Output);
AudioDeviceInfo? fallback = AudioDevices.Default(DeviceDirection.Output);
Console.WriteLine($"{outputs.Count} outputs, default: {fallback?.Id} ({fallback?.Name})");
 
// Play samples you compute yourself: a 440 Hz tone for one second.
// The callback runs on the device thread, so it must not allocate, lock or block.
double phase = 0, step = 2 * Math.PI * 440 / 48000;
var options = new DeviceStreamOptions { DeviceId = fallback?.Id, SampleRate = 48000, Channels = 2 };
using AudioOutputDevice output = Audio.CreateOutput((Span<float> buffer, in RenderContext ctx) =>
{
    for (int i = 0; i < buffer.Length; i += 2)            // interleaved stereo: left, right
    {
        float s = 0.1f * (float)Math.Sin(phase);
        buffer[i] = s;
        buffer[i + 1] = s;
        phase = (phase + step) % (2 * Math.PI);
    }
}, options);
 
Thread.Sleep(1000);
DeviceCounters c = output.Counters;
Console.WriteLine($"{c.FramesPlayed} frames played, peak {c.PeakPlayed:F2}, buffer {c.BufferFrames} frames, {c.LatencyMilliseconds:F1} ms");
```

*Output · Console · Linux*

```
90 outputs, default: default (Default)
47681 frames played, peak 0.10, buffer 960 frames, 26.6 ms
```

- The callback fills an interleaved float buffer at the stream’s rate and channel count. It runs on the device thread, so it must not allocate, lock or block; an exception it throws is caught, counted in `CallbackFaults`, and that block plays as silence.
- On Linux the device ids are ALSA PCM names, which is why this machine reports 90: the list includes every card, every plug-in and every route. `default` follows the output the user picked on a PipeWire or PulseAudio desktop and converts rate and channels for you. On Windows the ids are WASAPI endpoint ids with their friendly names, and on macOS Core Audio devices.
- Pass `DeviceId = null` (the default) to use the system default and follow it when the user switches, for example to headphones. `output.SelectDevice(id)` moves a running stream to another device.
- Any type that implements `IAudioSource`, a sample rate, a channel count and `Render(Span<float>)`, can be played with `Audio.CreateOutput(source)`. The mixer is one of them.

## When there is no device

A device that can’t be opened isn’t an exception. The output keeps calling your mixer or callback at real-time pace and discards the result, so a game clock keeps moving, retries the device twice a second, and says why in `output.LastError`; `output.State` tells you whether a device is open. A lost device, such as unplugged USB headphones, is reopened, and the `DeviceLost`, `DeviceRestored` and `DefaultChanged` events say what happened. That is also why a server or a CI machine without a sound card runs the same code without crashing.

Per the package’s [changelog](https://github.com/recursive-ai-oss/oneaudio-csharp/blob/v1.2.1/CHANGELOG.md), the backends have been checked on real hardware on Linux (ALSA through PipeWire, playback and capture), on macOS (Core Audio, including following a change of default output) and on an iPad; the WASAPI backend for Windows is checked by unit tests and on Windows CI. On Windows, shared mode (the default) accepts any rate and channel count, and `ShareMode.Exclusive` asks for the device to itself with a lower latency, as the [package README](https://www.nuget.org/packages/RecursiveAI.OneAudio.Devices/1.2.1) describes.

## Other ways to play sound in C#

### SoundPlayer

Most answers for “c# play sound” use `System.Media.SoundPlayer`. Microsoft’s [documentation](https://learn.microsoft.com/en-us/dotnet/api/system.media.soundplayer) describes it as playback “of a sound from a .wav file” and notes that it “cannot play other file types, such as .wma or .mp3”. On .NET it comes from the `System.Windows.Extensions` package, and the compiler marks it Windows-only (warning CA1416). On Linux it fails at run time:

*Program.cs · System.Windows.Extensions 10.0.14 · Linux*

```
using System.Media;
 
try
{
    using var player = new SoundPlayer("intro.wav");
    player.PlaySync();
}
catch (Exception ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
```

*Output · Console*

```
PlatformNotSupportedException: System.Windows.Extensions types are not supported on this platform.
```

### NAudio

NAudio 3 plays through `WasapiPlayer` or `WasapiOut` on Windows and through `AlsaOut`, in the separate `NAudio.Alsa` package, on Linux. Which files it can play off Windows depends on the reader: its [ALSA playback guide](https://github.com/naudio/NAudio/blob/main/Docs/PlayAudioFileLinuxAlsa.md) says `AudioFileReader`, `Mp3FileReader` and `MediaFoundationReader` are Windows-only, and offers `NAudio.SoundFile` (a system libsndfile) or the managed NLayer decoder for MP3. The NLayer route from that guide runs on Linux:

*Program.cs · NAudio.Alsa 3.3.0 + NLayer.NAudioSupport 3.0.0*

```
using NAudio.Wave;
using NAudio.Wave.Alsa;
using NLayer.NAudioSupport;
 
// MP3 on Linux: NLayer's managed frame decompressor in Mp3FileReaderBase, played through ALSA.
using var mp3 = new Mp3FileReaderBase("intro.mp3", waveFormat => new Mp3FrameDecompressor(waveFormat));
using var outputDevice = new AlsaOut();                    // default device
outputDevice.Init(mp3);
outputDevice.Play();
while (outputDevice.PlaybackState == PlaybackState.Playing)
    Thread.Sleep(200);
Console.WriteLine($"played {mp3.TotalTime.TotalSeconds:F2} s of {mp3.WaveFormat}");
```

*Output · Console · Linux*

```
played 5.04 s of 32 bit IEEEFloat: 44100Hz 2 channels
```

NAudio is a good choice when you need ASIO, MIDI or VST 3 hosting. If you only need to play files and sounds, OneAudio does it with one program for every OS, every format it decodes and no reader to choose per platform. Its MP3 decoder trims the encoder’s padding, so the five-second clip plays as exactly five seconds; the 5.04 s above includes the padding. Moving an NAudio app across is covered in [migrating from NAudio](https://recursiveai.org/oneaudio/articles/migrate-from-naudio/#playback).

## Where next

- [Record audio in C#](https://recursiveai.org/oneaudio/articles/record-audio-csharp/), from the microphone to a WAV file.
- The cookbook recipes: [play an audio file](https://recursiveai.org/oneaudio/cookbook/play-audio-file/), [list audio devices](https://recursiveai.org/oneaudio/cookbook/list-audio-devices/) and [mix audio](https://recursiveai.org/oneaudio/cookbook/mix-audio/).
- The [device API overview](https://recursiveai.org/oneaudio/docs/api/#devices-recursiveaioneaudiodevices): options, counters, events and the backends.
- [Choosing a C# audio library](https://recursiveai.org/oneaudio/articles/csharp-audio-library/).
