Occasional Issues With GatherNextPage/FindNextPageHeader

Feb 4, 2013 at 11:03 PM
Hiya,

Thanks for your help over at NAudio. I'm finding that sometimes if I manage to load an ogg file and play it too quickly after, or if I try to load the same ogg file twice then it throws an exception for not being able find the next page.

I need to be able to load the same file multiple times so that I can play it back as many times laid over each other as required (gunshots, etc). Am I going about it the wrong way?

Any assistance would be greatly appreciated.

Thanks,

Steve
Coordinator
Feb 5, 2013 at 12:08 AM
Hrm... What is the exception text (message, stack trace, etc.)?
Feb 5, 2013 at 8:12 AM
Hah, yes of course, all of that information would be useful!

It boils down to one of the crc checks failing and the method FindNextPageHeader() returning null because of this. It never appears to be the first CRC check either.

Here's the real kicker, it appears that in Debug it just stutters the track endlessly, and only in Release does it throw the exception. The error is thrown by GatherNextPage because hdr == null. Error is "Could not find next page."

Stack trace:
NAudio.dll!NAudio.OggVorbis.ContainerReader.GatherNextPage(string noPageErrorMessage) Line 329 C#
NAudio.dll!NAudio.OggVorbis.ContainerReader.GatherNextPage(int streamSerial, NAudio.OggVorbis.ContainerReader.PageReaderLock pageLock) Line 351 + 0xe bytes C#
NAudio.dll!NAudio.OggVorbis.PacketReader.ReadAllPages() Line 157    C#
NAudio.dll!NAudio.OggVorbis.PacketReader.GetLastPacket() Line 168   C#
NAudio.dll!NAudio.OggVorbis.ContainerReader.NAudio.OggVorbis.IPacketProvider.GetLastGranulePos(int streamSerial) Line 361 + 0x1e bytes  C#
NAudio.dll!NAudio.OggVorbis.VorbisReader.TotalTime.get() Line 307 + 0x2b bytes  C#
NAudio.dll!NAudio.Wave.VorbisFileReader.Length.get() Line 52 + 0x11 bytes   C#
NAudio.dll!NAudio.Wave.AudioFileReader.Initialize(string filename) Line 47 + 0x1a bytes C#
AudioFileReader.Initialize() is just my alternative for new AudioFileReader() that contains the same code. VorbisFileReader is just renamed VorbisStreamReader for consistency with the NAudio library.

Let me know if any more info would be useful.

Steve
Feb 5, 2013 at 8:26 AM
Edited Feb 5, 2013 at 8:34 AM
Right, ok, it appears to only happen when I'm loading an OGG on a different thread at the same time.

Here's my set up currently:

I have a thread that I tell "this is your next track", and it loads that ogg then fades it in while fading the current one out.

Also:

At the same time, pretty much straight after telling the music thread to play the battle music, I play the character battle cry as a test. Then I get the error I've posted above.

Is there something not thread safe about the loading process? Should I be handling it differently?

Thanks,

Steve

P.S. I would like to apologise for my misleading, non-threading related analysis previously. It seems entirely down to my threaded audio. Tbh, I'm a great high-level games programmer, but when it gets to the low level stuff I'm still a bit of a newbie lol.
Coordinator
Feb 5, 2013 at 12:17 PM
I don't immediately see where the threading issue is, but you might try moving line 47 in AudioFileReader.Initialize(string) to the getter for Length. That way your loading function doesn't read through the entire file before returning, and the length is only read when actually needed. This will also speed up file loading tremendously.

I've seen an issue with threading Length while decoding, but it should have been fixed by the multi-threaded stream wrapper (Ogg.ThreadSafeStream)...
Feb 5, 2013 at 12:42 PM
Hiya,

Well I've noticed that sometimes when it crashes out, this method isn't in the call stack:
NAudio.OggVorbis.ContainerReader.PageReaderLock pageLock) Line 351 + 0xe bytes C#

I don't know whether that's pertinent to the issue or not.

That's a great suggestion about the length. Currently the AudioFileReader sets its internal length for later use, I'll shift it around so that AudioFileReader only gets length the first time its asked for.

I'll have a nose around and if need be, I'm sure I can be smarter with my threading so that Audio is all handled by one thread.

Thanks very much :)

Steve
Coordinator
Feb 5, 2013 at 2:40 PM
Cool. I'd like to know the cause of this threading issue, but I think it would be better in your case to change the way your audio engine works...

My recommendations:

1) Decode sound effects into a cache. You may consider decoding on a separate thread for each file requested.
2) Decode background music into a WaveBuffer on a dedicated thread. Include all logic for changing tracks here.
3) Put your mixing and your sound effect playback logic in your implementation of IWaveProvider.Read(...).
4) Add a "command queue" between your game engine and the audio engine. If using a global counter for your engine, put a field in the commands to mark when the audio engine should start the command (vs. the global counter).
5) Have the command queue immediately trigger #1 when the cache does not have a sound effect decoded yet.
6) Have the background music thread watch the command queue and execute the commands as needed.
7) Have the sound effect playback logic watch the command queue and start playback of sound effects at the correct time (from the cache).
8) Don't let the cache get too big... Include pruning logic somewhere. I'd probably put it in #5.

For #4, you might even consider two command queues: One for the music (and a "stop everything" command), and one for the sound effects.
Feb 5, 2013 at 5:24 PM
Edited Feb 5, 2013 at 5:25 PM
Hiya,

As far as I can see, the NAudio+NVorbis combination just doesn't enjoy being on a separate thread. Even implementing the most basic of a threaded audio system causes it to either immediately or eventually start to stutter/lock up a sound effect/music track.

If you can excuse the code dump, this is my threaded audio class (only if you're interested), I've strimmed out the error checking stuff for ease of viewing:
namespace Punchbag.Core
{
    public class ThreadedAudio
    {
        readonly Object thisLock = new Object();
        readonly Thread audioThread;
        PrecisionTimer timer;
        bool quittingThread = false;
        string next = null;
        bool trackStopped = false;
        readonly List<Audio> tracks = new List<Audio>(4);
        long lastTime;
        readonly Dictionary<string, Audio> soundEffects = new Dictionary<string, Audio>(16);

        public ThreadedAudio()
            : base()
        {
            audioThread = new Thread(AudioThread);
            audioThread.Priority = ThreadPriority.AboveNormal;
            audioThread.Start();
        }

        void AudioThread()
        {
            timer = new PrecisionTimer(false);
            timer.Start();

            while (!quittingThread)
            {
                var currentTime = timer.GetElapsedTime();
                var elapsedTime = (currentTime - lastTime) * 0.000004f;
                for (int i = 0; i < this.tracks.Count; )
                {
                    this.tracks[i].Volume = MathHelper.Clamp(this.tracks[i].Volume + ((this.trackStopped || (i < this.tracks.Count - 1) ? -1f : 1f) * elapsedTime), 0.0f, 1.0f);
                    if (this.tracks[i].Volume <= 0.0f && (this.trackStopped || i < this.tracks.Count - 1))
                    {
                        this.tracks[i].Stop();
                        this.tracks[i].Dispose();
                        this.tracks.RemoveAt(i);
                    }
                    else
                    {
                        ++i;
                    }
                }
                lastTime = currentTime;

                string sfx = null;
                if (this.queuedSFX.Count > 0)
                {
                    lock (thisLock)
                    {
                        if (this.queuedSFX.Count > 0)
                        {
                            sfx = this.queuedSFX[0];
                            this.queuedSFX.RemoveAt(0);
                        }
                    }
                }

                if (sfx != null)
                {
                    if (this.soundEffects.ContainsKey(sfx))
                    {
                        this.soundEffects[sfx].Play();
                    }
                    else
                    {
                        var a = Audio.FromFile(sfx);
                        if (a != null)
                        {
                            a.Play();
                            this.soundEffects.Add(sfx, a);
                        }
                    }
                }

                if (next != null)
                {
                    lock (thisLock)
                    {
                        if (this.next != null)
                        {
                            Audio a = null;
                            for (int i = 0; i < this.tracks.Count; ++i)
                            {
                                if (this.tracks[i].Filename == this.next)
                                {
                                    a = this.tracks[i];
                                    this.tracks.RemoveAt(i);
                                    break;
                                }
                            }

                            if (a == null)
                            {
                                a = Audio.FromFile(this.next);
                                if (a != null)
                                {
                                    a.Volume = 0f;
                                    a.Play();
                                    lastTime = timer.GetElapsedTime();  // To stop fade breaking because of slow file load time
                                }
                            }

                            if (a != null)
                            {
                                this.tracks.Add(a);
                                this.trackStopped = false;
                            }

                            this.next = null;
                        }
                    }
                }
            }

            foreach (var track in this.tracks)
            {
                track.Stop();
                track.Dispose();
            }

            this.next = null;
        }

        public void QueueMusic(string filename)
        {
            lock (thisLock)
            {
                if (!this.quittingThread)
                {
                    this.next = filename;
                }
            }
        }

        List<string> queuedSFX = new List<string>();
        public void QueueSoundEffect(string filename)
        {
            lock (thisLock)
            {
                queuedSFX.Add(filename);
            }
        }

        public void StopTrack()
        {
            lock (thisLock)
            {
                this.next = null;
                this.trackStopped = true;
            }
        }

        public void QuitThread()
        {
            this.quittingThread = true;
        }
    }
}
And here's the Audio class it uses:
namespace Punchbag.Core
{
    public class Audio : IDisposable
    {
        // Todo: support looping
        public float Volume
        {
            get { return this.stream.Volume; }
            set { this.stream.Volume = value; }
        }

        public string Filename { get; private set; }

        IWavePlayer player;
        AudioFileReader stream;

        protected void Init(AudioFileReader stream)
        {
            this.player = new WaveOut();
            this.player.Init(stream);
            this.stream = stream;
        }

        public void Play()
        {
            if (player.PlaybackState != PlaybackState.Playing)
            {
                player.Stop();
                this.stream.Seek(0, System.IO.SeekOrigin.Begin);
                player.Play();
            }
        }

        public void Stop()
        {
            player.Stop();
        }

        public static Audio FromFile(string filename)
        {
            AudioFileReader inputStream = null;
                inputStream = new AudioFileReader();
            var result = new Audio();
                result.Init(inputStream);
            result.Filename = filename;
            return result;
        }

        public void Dispose()
        {
            if (this.player != null)
            {
                this.player.Stop();
            }

            if (this.stream != null)
            {
                this.stream.Close();
                this.stream.Dispose();
                this.stream = null;
            }

            if (this.player != null)
            {
                this.player.Dispose();
                this.player = null;
            }
        }
    }
}
Coordinator
Feb 5, 2013 at 8:02 PM
Hmmm.... Decoding in the playback thread is "twitchy", since the playback thread is latency sensitive and the decoder may be delayed by I/O or memory allocation...

You might be in a situation where I/O is taking too long and is causing the decode logic to block for too long. I've also seen heavy memory allocation cause the decode logic to block.

Also, I wonder if something is causing the multiple stream instances to use the same file handle somehow (when decoding the same file multiple times).

Finally, I strongly recommend that the decode and playback logic be separated to different threads, since decoding is not a "free" operation and memory will usually be cheaper than processor cycles...
Feb 5, 2013 at 8:13 PM
Thanks for the explanation. I get where you're coming from. Just so I'm clear, decoding is the stage where the file is initialised? Can I get away with decoding on multiple threads and playing on one single separate thread or should I be decoding all on one thread and playing all on another?

I appreciate all your help, definitely gives me an understanding that I wouldnt have otherwise have had :)

Steve
Feb 5, 2013 at 10:26 PM
Something else I thought worth adding:

Even if no new instances of audio are decoded during the running of the threaded playback occasionally it starts to stutter. I don't suppose you have a working, proven example of threaded OGG audio that I could adapt? I feel like I must be doing something inherently wrong (beyond not decoding on a separate thread) for nobody else to have encountered the issues that I have.

Cheers

Steve
Coordinator
Feb 6, 2013 at 12:29 PM
Decoding in NVorbis happens as needed, specifically in the call to ReadSamples(...). Initialization just finds the stream headers and unpacks the decode parameters (no audio data, though).

I would highly recommend that the playback thread only have to deal with previously-decoded data. It is very latency-sensitive, so there's probably not time to wait for the decoder in most cases. Separate threads makes this much simpler. Decoding on multiple threads will work great; Just make sure you manage your threads correctly (normal multi-threading rules / guidelines apply here).

The stuttering is likely related to the above point: Playback is very latency-sensitive and really can't afford to wait for the decoder. It "can" be done on a fast enough machine, but I wouldn't even bother. Also, be aware that NVorbis will have performance issues if the disk takes too long to return data. I have some ideas for making it less sensitive, but won't be able to mess with them for a while.
Feb 6, 2013 at 3:54 PM
Ah yes, of course. That makes great sense thanks. I'll develop it to have the decoding work as fast as it can to buffer a decent amount on separate threads, then have the playing of that buffered data be on the main audio thread, then use the main gameplay thread for everything else.

I appreciate all of the depth you've gone into with me here, its really helped me understand your library.

Cheers,

Steve