Wednesday, June 17, 2009

How to stream an ogg file using DirectSound in SlimDX.

With a nudge in the right direction from the SlimDX devs, I've finally got a working streaming solution for DirectSound in SlimDX. This will be a part of the 1.007 release of AI War, improving performance in the game, but I also wanted to share the code for this since it isn't out there anywhere else.

First of all, let me note that this entire solution is really just an adaptation of an MDX Directsound streaming solution by Dr Gary. Secondly, it uses the wonderful Ogg Vorbis Decoder (a C# wrapper for the standard ogg dlls) by Atachiants Roman. And, lastly, of course this uses the SlimDX library.

Here's the code, including a compiled test executable. Do with it what you will. If you want a deep explanation of what is going on there, you want to read Dr Gary's article above. This is basically his work, after all, and he explains it very nicely.

Here are the major things I added/changed:

1. SlimDX is used instead of MDX, which obviously necessitated a lot of changes.

2. Rather than use Dr Gary's wav stream class, I'm using the ogg stream class. This required even more changes, especially since the ogg calls are apparently not threadsafe (Dr. Gary was using his wav stream object from multiple threads, which the ogg library did not like one bit).

3. The way I am pulling data off of the ogg stream is a bit different from how the original code was pulling data off of the wav stream. In the original, the stream was seeking every time, to prevent data skips and loss. In my version, I'm using a loop to make sure that this isn't neccessary, and thus there's a minor performance gain.

4. I took out support for looping. This code was originally being adapted for AI War, which doesn't need looping in the same sense. If you want to loop the file, just do like I'm doing in the sample and start playing it again after it completes. My goal was to keep this simple.

5. There are a variety of other minor changes, just switching it to be more in line with the AI War codebase. This was for my own purposes, but since I wanted to make the code public these changes are also seen here.

That's it! Happy coding.

9 comments:

Michael Popoloski said...

This is cool. I have to remember to get a streaming sample into SlimDX at some point.

Christopher M. Park said...

Thanks, Mike! I know you're really busy with everything else you've got in the works, though -- at least there's something available out there, now.

And thanks for adding the SetNotificationPositions method, by the way. I had still been thinking there was something important missing in SlimDX that made streaming not easily possible, but Promit pointed out this method to me. Last time I had looked at this was before the March 2009 version, so I'd missed that you added that. Good stuff!

juern said...

Thanks for the great work. It saves me a lot time.
But I think I found a possible bug.
Is there any reason for calling UnPause in the Stop method? Under certain circumstances that UnPause causes an access violation.

Christopher M. Park said...

Thanks, juern -- glad it was useful to you. As for the exception on the Stop method with UnPause, thanks for letting me know about that. I think that I didn't find that error because my application rarely calls the stop method (usually it just pauses it, or waits until completion and then runs another track), and in the one case in which Stop is called (when music is disabled entirely), it is in a try/catch just to be safe.

Anyway, so I'm glad you found that and let me know. I'm out of town right now, but I'll update the sample after I get back. Thanks!

Rewasvat said...

Yeah thanks for the nice work Christopher, also saves me a lot of time to play ogg files (C#/SlimDX) without filling the memory with the entire sound file haha

But I'm getting some AccessViolations as well. Don't think it's in the UnPause method tho, still couldn't figure why they happen, they seem kinda random... VisualStudio says they're coming from the line currentAmount = oggStream.Read(TransferBuffer, dataBytesThisTime, SectorSize - dataBytesThisTime); in the TransferBlockToSecundaryBuffer method (inside that while block).
Any idea why that may be happening? I'm using pretty much your code (thanks for letting :)), at least the MusicPlayer part which does all the ogg streaming/playing, but on a different class representing a audio, from it I derive 2 classes, Music and Sound. While my project only allows 1 music to be played at one time, several Sounds can be played together...
However in the tests I saw the AccessViolations, a single music was all that was ever created and was running...

And another question, is it possible to "adapt" your class to work for WAV files as well? I want my project to be able to play wav files this easily (and memory friendly) too, so it seems better to just have a class that does both. If you could just point me in the right direction I would already appreciate.

Anyway, thanks again.

Christopher M. Park said...

Hey there, no problem, I'm glad the code is helpful. If you are getting access violations, usually that is going to be because of cross-thread calls to the same object (in this case, the ogg stream). The ogg stream methods are not threadsafe, so you need to be careful that you are only ever using them from one thread at a time.

As for a wav stream, by all means you can do that pretty easily. Really just using a basic StreamReader class in place of the OggStream class would probably do it. In the original MDX article that my SlimDX code is based on, there is a wav stream class, if you want to use that. I felt like it was a bit more complex than it needed to be, though, and a little too reliant on making many calls to Seek methods (which can be slow).

Hope that helps!

juern said...

At least the access violations I had in TransferBlockToSecondaryBuffer were because of the UnPause in the Stop method.
Since UnPause is there for no reason you can just remove it.
Calling UnPause and TransferBlockToSecondaryBuffer from different threads is the reason I guess.

Rewasvat said...

Yea that worked. Changed the "oggStream" attribute from OggVorbisFileStream to simply Stream, and in the Load method, creating it as a SlimDX.Multimedia.WaveStream(and getting the Format from it) in case of a WAV file. In the case of a OGG file it does the same thing than before.

Now about the AccessViolations, still having them randomly (and not always)... Took out the UnPause call from Stop method and while I didn't get a AccessViolation (could be because it simply didn't happen or because this fixed it), calling Pause and then Stop freezed the process... Simply froze it, VS didn't came to focus telling about an warning or exception... Nothing, it simply froze...

And I have another question: since I can have several objs of this Audio class created and playing, each one with her own DataTransferThread, I would like to know: is it safe/ok (code-wise, memory-wise, etc) to have multiple threads created like this? Or is it best to try and adapt the code to have a static DataTranfer thread for all Audio objs to use?

And thanks again guys.

Christopher M. Park said...

This is odd, I have yet to have any AccessViolations (do you get them with the sample player I have there). It must have to do with how you are calling the properties from across on other threads. That's the only thing I can think of that would cause that, because the ogg vorbis calls are definitely NOT threadsafe.

And yes, having multiple instances of this class running at once will be just fine. I do something similar with general directsound buffers, but that does not involve streaming. There's no reason I can think of that this wouldn't work with multiple versions of this running at once.