Recursive AI

Play an audio file

All oneaudio recipes

Play a WAV, MP3, Ogg or FLAC file on the default sound card in C#, with oneaudio’s mixer and the RecursiveAI.OneAudio.Devices package, and check from the device’s own counters that it played.

Terminal
dotnet add package RecursiveAI.OneAudio --version 1.2.1
dotnet add package RecursiveAI.OneAudio.Devices --version 1.2.1
C#
using OneAudio;
using OneAudio.Devices;
 
// A mixer is the source; Audio.Play opens the default output and starts it.
AudioMixer mixer = Audio.CreateMixer();           // 48 kHz stereo
using AudioOutputDevice output = Audio.Play(mixer);
 
AudioClip clip = Audio.Load("voice.wav");         // 16 kHz mono; the mixer converts
mixer.Play(clip);
long frames = clip.Frames * mixer.SampleRate / clip.Format.SampleRate;
 
// Wait until the device's own clock says the whole clip has played.
var timeout = DateTime.UtcNow.AddSeconds(15);
while (output.Counters.FramesPlayed < frames && DateTime.UtcNow < timeout)
    Thread.Sleep(50);
 
DeviceCounters c = output.Counters;
AudioDeviceFormat device = output.DeviceFormat;
double rate = mixer.SampleRate;
Console.WriteLine($"{output.BackendName}: {output.Device?.Id ?? "no device"}, " +
                  $"{output.State}");
Console.WriteLine($"device format {device.SampleRate} Hz, {device.Channels} ch, " +
                  $"latency {c.LatencyMilliseconds:F0} ms");
Console.WriteLine($"clip {frames / rate:F2} s, " +
                  $"device played {c.FramesPlayed / rate:F2} s, " +
                  $"peak {20 * Math.Log10(c.PeakPlayed):F1} dBFS, " +
                  $"underruns {c.Underruns}");
if (output.LastError is not null)
    Console.WriteLine($"last error: {output.LastError}");
Text
alsa: default, Running
device format 48000 Hz, 2 ch, latency 25 ms
clip 6.03 s, device played 6.06 s, peak -7.5 dBFS, underruns 0

This ran on Linux, through ALSA’s default device to PipeWire, with the PipeWire sink muted so the run made no sound. The stream itself ran as usual.

  • Audio.Play(mixer) opens the default output at the mixer’s rate and channel count and starts pulling from Render on its own thread. Any voice the mixer plays is heard, so a game keeps one output and calls mixer.Play for each sound.
  • FramesPlayed comes from the device’s clock, not from what was handed over, so it’s the right thing to wait on. It runs a little past the clip because the device keeps playing silence. PeakPlayed is the loudest sample among frames the device has finished: 0 means it only played silence.
  • DeviceFormat is what the hardware runs at. When it differs from the mixer’s rate, the package converts with the library’s polyphase resampler.
  • The output limiter leaves samples up to 0.9 alone and bends anything louder smoothly under 1, so a hot mix doesn’t clip.
  • When no device can be opened, State is NoDevice, the output renders silently at real-time pace, retries twice a second, and LastError says why. The program above then stops at its timeout and prints the error.
  • using disposes the output, which stops and closes the device. AudioMixer isn’t disposable.

For a long file, stream it instead of loading it: mixer.PlayStream("music.ogg") decodes ahead on a background thread. To play on a device other than the default, see List audio devices.

MP3, WAV and Ogg playback on Windows, Linux and macOS, compared with SoundPlayer and NAudio: Play audio in C#.