How To Read an Ogg Vorbis in MonoGame without Content Pipeline

This is a highly specific technical blog post for a highly specific technical problem. I couldn't find a straight answer for how to accomplish this on Google so hopefully this post will find the next person searching as I did.

Summary

We'll use NVorbis (which comes packaged with MonoGame) to read the samples from an ogg file, then convert those samples into a MonoGame SoundEffect object. We will accomplish all of this at runtime without using the Content Pipeline.

Code

using System; using Microsoft.Xna.Framework.Audio; using NVorbis; public static class ReadOgg { private static void ConvertFloatBufferToShortBuffer( float[] inBuffer, short[] outBuffer, int length) { // The float[] we get from NVorbis has the range [-1f, 1f], // we need to convert that to a short[] with a range of [-32768, 32767] for (var i = 0; i < length; i++) { var temp = (int) (short.MaxValue * inBuffer[i]); temp = Math.Clamp(temp, short.MinValue, short.MaxValue); outBuffer[i] = (short) temp; } } public static SoundEffect ReadSoundEffect(string fullFileName) { // VorbisReader comes from NVorbis. using var vorbis = new VorbisReader(fullFileName); // TotalSamples is actually in Frames, // so we need to multiply it by channels to get Samples. var frames = new float[vorbis.TotalSamples * vorbis.Channels]; // Read all frames (again, NVorbis calls them Samples), // starting at index 0 and reading to the end. var length = vorbis.ReadSamples(frames, 0, frames.Length); // frames is a float[], we need a short[]. var castBuffer = new short[length]; ConvertFloatBufferToShortBuffer(frames, castBuffer, castBuffer.Length); // Now that we have the sound represented as a short[], // we need to convert that to bytes. // Each short is 2 bytes long, so we need 2X as many bytes // as we have shorts. var bytes = new byte[castBuffer.Length * 2]; for (var i = 0; i < castBuffer.Length; i++) { var b = BitConverter.GetBytes(castBuffer[i]); bytes[i * 2] = b[0]; bytes[i * 2 + 1] = b[1]; } // Finally, we convert the vorbis.Channels count to the AudioChannels enum. // Casting like this: `(AudioChannels) vorbis.Channels` would also work. var channels = vorbis.Channels == 2 ? AudioChannels.Stereo : AudioChannels.Mono; // Put it all together! return new SoundEffect(bytes, vorbis.SampleRate, channels);; } }

Caveats

The above code is quite slow. Even loading a single sound will cause your game to hiccup. This is something you'd want to do during a loading screen or perhaps do asynchronously in the background.

There are probably ways to make this more efficient, but this is enough to get something working.

Anyway, I hope that helps.