This project has moved and is read-only. For the latest updates, please go here.

[Solved] How to decode OGG to Wav from byte array properly?

Jan 8, 2015 at 9:12 AM
I am creating a custom file format for my game, the file contains some game assets such as the sprite and the ogg file as the bgm of the game, and i want to load the ogg file without writing it into the disk.
byte[] oggData = GetSoundFromAsset("TestData.customfileformat"); // Will return OGG file in byte array

byte[] wavData = // how to decode the 'oggData' into wav byte array?
oggData = null;

// Do something with 'wavData'
Any kind of help would be appreciated :D
Jan 8, 2015 at 1:56 PM
Three options:
  1. Wrap the byte data in a MemoryStream: var oggStream = new MemoryStream(oggData);
  2. Change the custom file format reader to return a stream that reads data as needed (preferred)
  3. Implement NVorbis.IPacketProvider in the custom file format reader (advanced)
In all cases, you pass the stream / instance into the NVorbis.VorbisReader constructor and call ReadSamples to decode from there. The decoded data is 32-bit float PCM, and can be played by most audio libraries without further processing.

If you want to try option # 3, I have further comments. It is probably unnecessary, but since you are writing your own file format there is the possibility.
Jan 9, 2015 at 12:13 PM
Edited Jan 9, 2015 at 12:14 PM
Seem i figured out how to decode it with 1st option (not sure tho, it uses NVorbis.VorbisReader), but the decoding process somehow is so heavy, its seem due to loop on ReadSamples
Here my code:
public void Decode(byte[] oggdata, out byte[] result)
        {
            int wavLength = 0;

            using (var vorbis = new NVorbis.VorbisReader(new MemoryStream(oggdata), true))
            using (var wavStream = new MemoryStream())
            using (var writer = new System.IO.BinaryWriter(wavStream))
            {
                writer.Write(Encoding.ASCII.GetBytes("RIFF"));
                writer.Write(0);
                writer.Write(Encoding.ASCII.GetBytes("WAVE"));
                writer.Write(Encoding.ASCII.GetBytes("fmt "));
                writer.Write(18);
                writer.Write((short)1); // PCM format
                writer.Write((short)vorbis.Channels);
                writer.Write(vorbis.SampleRate);
                writer.Write(vorbis.SampleRate * vorbis.Channels * 2); // avg bytes per second
                writer.Write((short)(2 * vorbis.Channels)); // block align
                writer.Write((short)16); // bits per sample
                writer.Write((short)0); // extra size
                writer.Write(Encoding.ASCII.GetBytes("data"));
                
                var dataPos = wavStream.Position;
                writer.Write(0);

                var buf = new float[vorbis.SampleRate / 10 * vorbis.Channels];
                int count;
                while ((count = vorbis.ReadSamples(buf, 0, buf.Length)) > 0)
                {
                    for (int i = 0; i < count; i++)
                    {
                        var temp = (int)(32767f * buf[i]);
                        if (temp > short.MaxValue) temp = short.MaxValue;
                        else if (temp < short.MinValue) temp = short.MinValue;
                        writer.Write((short)temp);
                    }
                }

                writer.Seek(4, System.IO.SeekOrigin.Begin);
                writer.Write((int)(wavStream.Length - 8L));

                writer.Seek((int)dataPos, System.IO.SeekOrigin.Begin);
                writer.Write((int)(wavStream.Length - dataPos - 4L));

                result = wavStream.GetBuffer();
                wavLength = (int)wavStream.Length;
            }

            // Trim Wave data (file-size optimization)
            byte[] data = new byte[wavLength];
            Array.Copy(result, data, wavLength);
            result = data;
            data = null;
        }
am i doing it right? if so, how can i make the decoding process little bit faster?
Jan 9, 2015 at 8:26 PM
To be honest, no. Your problem here is massive memory allocations. I'm seeing 1) the ogg file, 2) the write buffer (about 11x the ogg file size), and 3) the result buffer (same as # 2). That means for every minute of 44.1kHz stereo 128kbit audio you are using 21MB of RAM!

A better solution:
  • Use an audio library that supports PCM buffers instead of WAV. Make sure it supports partial buffers (it asks for small amounts of audio data at a time)
  • Make your custom file format reader support long-lived Stream instances with seeking, then pass that to VorbisReader...
  • Decode the ogg file as needed (only decode as much as you need to fill the requested buffer size)
I think you'll find that your memory requirements will be reduced drastically, and your performance increase significantly, if you make these changes.
Marked as answer by CXO2 on 1/12/2015 at 7:58 AM
Jan 9, 2015 at 8:49 PM
BTW, the performance bottleneck you see in the loop on ReadSamples is due to MemoryStream having to expand its capacity pretty often.

If you don't want to (or can't) change the logic:
  • Set the MemoryStream's initial capacity to (int)Math.Ceiling(vorbis.TotalDuration.TotalSeconds * vorbis.SampleRate * vorbis.Channels * 2 + 47)
  • Just return wavStream.GetBuffer()... Your playback logic should be smart enough to ignore the extra bytes in the array
Those together should improve performance some and should reduce the memory requirement by about 10MB per minute of audio... I still recommend my "better solution" above if you can do it, though.
Jan 10, 2015 at 7:56 AM
I see
To be honest, i am trying to create my own player, my game is based on OpenTK and the sound is based OpenAL that provided by OpenTK.
I can't find cross-platform audio library that based on OpenTK that could play WAV and OGG, so i decide to write my own player.

if you can provide me a code example to to play ogg from byte array / stream with your solution, it would be appreciated.
Otherwise, can you suggest me what audio library that i should use to achieve my goal?

Thanks
Jan 10, 2015 at 5:39 PM
There is an OpenTK project in the NVorbis repository... You might consider looking at it to understand how to use NVorbis with OpenTK/OpenAL.
Jan 11, 2015 at 5:57 PM
ioctlLR wrote:
There is an OpenTK project in the NVorbis repository... You might consider looking at it to understand how to use NVorbis with OpenTK/OpenAL.
Thank you!
Finally I figured out how to play it properly with "Queue Buffer" technique

Last questions:
Somehow VorbisReader.ReadSamples return 0 when I am modifying FillBuffer(OggStream, int) to fit my engine and the sound is looping (after loop, the sound is not played again), it seem happened because the offset is at the end of stream and its not back to beginning position, this could be solved by recreating VorbisReader upon the music is finished.

Question is.. do you know how to solve this issue with better solution? the simplest solution in my mind is set the offset of VorbisReader to 0, but VorbisReader doesn't have Seek method like Stream classes.
Jan 12, 2015 at 1:28 PM
VorbisReader exposes a DecodedTime property that can be set to "seek" the stream. Just set it to TimeSpan.Zero to loop the stream. Please note that your underlying Stream instance has to support seeking (not a problem if you are still using a MemoryStream).
Jan 12, 2015 at 3:57 PM
That make everything clear now
Thank you so much!
I deleted those code because i thought it something to do with Logger (duh dumb me XD)

Once again, thanks!