Friday 19 February 2010

OpenALSoft/Pulseaudio: it's not me, it's you.

I learnt a valuable lesson this week. If you have rewritten your code 3 times, and your tests all pass and you understand every part of it and yet it still doesn't work ... it's probably something not to do with you.

Last weekend I decided it was time my game gained some sound. I'd just finished the game menu screen and figured that some music would make it seem a little more complete. In an attempt to follow the KISS principle I decided to keep it simple, I'd write a class that just plays an OGG file in 2D. No multiple sources, no bells and whistles, nothing fancy, just load an OGG and play it. A quick Google search turned up this gem of a tutorial, so I figured it was a 10 minute job. When the time came to do more, I'd look for a decent high-level library, or just steadily build on my simple base code but for now, I figured, following this tutorial would teach me a little OpenAL then I could move on.

And for a little while I was right. I wrote my class, loaded an OGG file and played it with OpenAL by loading it into a single buffer and binding the buffer to the source before calling alSourcePlay(). It worked!


...


For about 2 seconds. Then it crackled and went silent.

I assumed that the problem was the track I was playing was too long to be bound in a single buffer, and instead I figured I should break it into smaller buffers and feed it into the source to play. Possibly a bad assumption, I guess I should've tried playing a smaller sound to see if that was actually the case - but I was having fun, and queueing the buffers looked simple enough.

So I got to work, I decoded the OGG stream into a vector of vector<char>'s the idea being that as room became available in the queue I could grab the next chunk of data and queue it up ready for playing. Again, it only sort of worked. The track would buzz for 10 seconds, then play a second or 2, then go back to buzzing, then play some more. Sometimes it would play a whole 30-40 seconds perfectly, then the buzzing would start again. I fiddled with buffer sizes, buffer counts, filled my code with logging statements (the buffer queuing just seemed to stall while the buzzing took place). I tried to simplify it, removing lines of code, reorganizing until the code was slim and streamlined, I switched from vectors, to filling out static char* buffers. It made no difference.

I downloaded the source code of various apps, SuperTuxKart uses OpenAL and apparently the followed the same tutorial as me! In fact all the examples I could find had very similar code to the same tutorial, and my code looked pretty much the same.

This is the point I should have realized something else was at work here, in fact I did try upgrading ALSA but that changed nothing, so what did I do? I came up with a new design, one that had many benefits and might, just might have the side effect of solving the problem.

The problem with the previous attempts was I was loading and decoding the entire OGG file in one go. A decompressed, high quality OGG takes up a lot of memory, it's REALLY wasteful to decode the whole thing and keep it in memory. The other thing I had to be careful of was that multiple sources could play the same stream without an issue. I decided instead to load and store the compressed file data. Then create an OggDecoder class which each source gets an instance of that decompresses the data from memory on the fly. The benefits of this approach are huge, you don't hit the disk while playing, you keep memory usage down, and multiple sources can play from the same compressed data, and the code is pretty small too. So I finished, stood back and admired my work and pressed "compile and run". It worked! I was relieved. But just in case, I ran it a second time (it had *occasionally* worked before, it just wasn't consistent). You can guess what happened, the buzzing was back. Repeated runs left me with the same problem.

I was about to give up, about half way through this process I discovered cAudio which was basically where unintentional feature creep was pushing my code anyway (while trying to load an play a sound, I'd come up with a whole multiple song streaming solution with pluggable decoders!), so I figured I'd dump my code into the "deprecated" folder that I keep, and just use cAudio. Just as I was about to give up I found a forum post where someone had similar problems and they solved it by upgrading the OpenALSoft libraries. I gave it a try and lo and behold, it works!

I don't know why it works and didn't before, perhaps an OpenALSoft conflict with PA? Perhaps a bug in OpenALSoft? I dunno, and I don't care, if the problem occurs on another PC at least I know how to fix it.

The only regret is that I didn't realize it wasn't me earlier. I'd literally rewritten the code several times in different ways while comparing to code in other projects. Alarm bells should have rung earlier. Oh well.