Recursive AI

Decode Ogg Vorbis in C#

Decode Ogg Vorbis in C# with managed code on any OS: stream floats, seek to an exact frame, play chained files, convert OGG to WAV, and compare with NVorbis.

To decode an Ogg Vorbis file in C#, add the RecursiveAI.OneAudio package and call Audio.Open("music.ogg"): you get a streaming decoder that fills a float buffer block by block, with sample-accurate Seek. Audio.Load decodes the whole file into memory, and Audio.Transcode("music.ogg", "music.wav") converts OGG to WAV. The decoder is managed C# checked against libvorbis on the Xiph test vectors, so the same code runs on Windows, Linux and macOS. Every sample below was compiled and run against OneAudio 1.2.1 on .NET 10.

Before you start

TerminalNuGet
dotnet new console -o OggVorbis
cd OggVorbis
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.ogg, 30 seconds of stereo music at 44.1 kHz encoded with libvorbis at quality 5, and chained.ogg, that file followed by a second, 10-second Vorbis stream.

Decode, seek and convert an OGG file

Program.csOneAudio 1.2.1 · .NET 10
using OneAudio;
 
// Stream an Ogg Vorbis file: interleaved floats, block by block.
using IAudioDecoder vorbis = Audio.Open("music.ogg");
Console.WriteLine($"{vorbis.Format.SampleRate} Hz, {vorbis.Format.Channels} ch, {vorbis.LengthFrames} frames " +
                  $"({vorbis.LengthFrames / (double)vorbis.Format.SampleRate:F3} s)");
 
float[] block = new float[4096 * vorbis.Format.Channels];
long frames = 0;
int n;
while ((n = vorbis.Read(block)) > 0)
    frames += n;
Console.WriteLine($"decoded {frames} frames");
 
// Seek to an exact frame, e.g. a loop point at 10 s.
vorbis.Seek(441000);
n = vorbis.Read(block);
Console.WriteLine($"after seek: position {vorbis.PositionFrames}");
 
// Two Ogg Vorbis streams joined end to end (a chained file, here 30 s + 10 s) play as one.
using IAudioDecoder chained = Audio.Open("chained.ogg");
Console.WriteLine($"chained.ogg: {chained.LengthFrames} frames");
 
// OGG to WAV, or to FLAC or Opus by changing the extension.
Audio.Transcode("music.ogg", "music-from-ogg.wav");
Console.WriteLine($"music-from-ogg.wav: {new FileInfo("music-from-ogg.wav").Length:N0} bytes");
OutputConsole
44100 Hz, 2 ch, 1323000 frames (30.000 s)
decoded 1323000 frames
after seek: position 445096
chained.ogg: 1764000 frames
music-from-ogg.wav: 5,292,080 bytes
  • Read fills the span you pass with interleaved floats and returns a count of frames (one sample per channel), which can be less than you asked for; 0 means the end. Samples are never clipped, so a loud Vorbis file can exceed ±1 slightly and nothing is lost.
  • The length is exact before you decode: 1,323,000 frames is exactly 30 seconds, because the decoder applies the stream’s granule positions to trim the first and last packets.
  • Seek lands on the frame you ask for. Reading a block of 4,096 frames from frame 441,000 leaves the position at 445,096. That makes music loops with a loop point in the middle of a file sample-exact.
  • A chained file, two or more Vorbis streams one after another in one file, plays as one stream: 30 s + 10 s is 1,764,000 frames, and seeking works across the join.
  • Decoding allocates nothing per read. For a game, play the file with mixer.PlayStream("music.ogg", loop: true) and it is decoded ahead on a background thread. See the game audio mixer article.

NVorbis

NVorbis is the managed Vorbis decoder most C# answers point to, MIT licensed. Its latest stable release is 0.10.5, from October 16, 2022; 1.0.0-rc.2 is a prerelease. Following its README:

Program.csNVorbis 0.10.5
using NVorbis;
 
using (var vorbis = new VorbisReader("music.ogg"))
{
    var channels = vorbis.Channels;
    var sampleRate = vorbis.SampleRate;
    Console.WriteLine($"{sampleRate} Hz, {channels} ch, {vorbis.TotalTime}");
 
    var readBuffer = new float[channels * sampleRate / 5];   // 200 ms
    long samples = 0;
    int cnt;
    while ((cnt = vorbis.ReadSamples(readBuffer, 0, readBuffer.Length)) > 0)
        samples += cnt;                                       // samples, not frames
    Console.WriteLine($"decoded {samples / channels} frames");
}
OutputConsole
44100 Hz, 2 ch, 00:00:30
decoded 1323000 frames
  • Both decoders give the same 1,323,000 frames. NVorbis’s ReadSamples counts samples, OneAudio’s Read counts frames: divide or multiply by the channel count when you move code across.
  • The NVorbis README says the “sample value range is -0.99999994f to 0.99999994f unless vorbis.ClipSamples == false”, so it clips by default. OneAudio never clips decoded samples.
  • NVorbis decodes Vorbis only. OneAudio reads Vorbis, Opus, FLAC, MP3 and WAV through the same call, writes WAV, FLAC and Opus, and adds resampling, a mixer and loudness measurement.

In the project’s published decode benchmark, OneAudio decoded three Xiph test files 1.20 to 1.25 times as fast as NVorbis 0.10.5. On the 186-second stereo file it took 196 ms against 240 ms and allocated 686 KB against NVorbis’s 77.9 MB. Measured with BenchmarkDotNet 0.15.8 on an AMD Ryzen 9 7950X3D under Linux with .NET 10.0.13. The decoder’s accuracy is checked against libvorbis 1.3.7: the same frame counts on every test vector, and floor-1 streams within 2e-6 of libvorbis’s float output.

Writing a compressed file

OneAudio decodes Vorbis; for lossy output it encodes Opus, which is also stored in Ogg files. Audio.Transcode("music.wav", "music.opus") writes an Ogg Opus file, covered in encode and decode Opus in C#. For lossless output, write FLAC (read and write FLAC in C#).

Where next