Audio playback failing - BMax 1.41

BlitzMax Forums/BlitzMax Programming/Audio playback failing - BMax 1.41

jtfrench(Posted 2011) [#1]
Hi,

We're having some trouble getting audio to playback properly in our basic in-game radio. The scenario we're trying to perform is this:

• Use LoadBank() to download an .ogg file from an HTTP server [works]
• LoadSound() the TBank into a TSound
• Playback the TSound with PlaySound
• Download a new .ogg file from same HTTP server, LoadBank() it, LoadSound() it, stop playing the last sound, and then start playing the new sound

This works fine for when the code fetches the first song from the server, converts the TBank to a TSound, and plays back the song fine. However, when clicking the "skip" button to advance to the next song, on Win7 64-bit it crashes on LoadSound( via DirectSound it crashes in 'CreateSoundBuffer' / DSASS , and via FreeAudio it crashes in fa_CreateSound). On Mac OS X v10.6 instead of crashing, it gets to the PlaySound command, but never starts playing the song. Calls to ChannelPlaying() indicate that there is indeed nothing playing.

Things of note:

• I hypothesized that perhaps the audio wouldn't play the second time (Mac) because I had called StopChannel. I tried re-allocating an audio channel between song plays (using AllocChannel()) and the same problem persists
• I would like to debug where exactly the program is dying, but once it enters C++ code, it looks like MaxIDE stops being able to follow it in the debugger. I'm not sure how to get around this ---I can run a separate debugger, but then putting breakpoints in (BlitzMax & C++ side) gets complicated (or at least I haven't figured out how to successfully debug BlitzMax apps w/ a 3rd party, non-IDE bound debugger)
• We're using multithreading. I was afraid calling loadSound on the non-main thread would cause drama, so all LoadSound/PlaySound calls happen on the main thread
• We're using the BlitzMax audio drivers that ship with BlitzMax (e.g. OpenAL, FreeAudio, and DirectSound). No MaxMod2 here.
• We're using the twrc.rjson JSON parsing module

Here are some images of the crash:

The FreeAudio crash (Win 7/64-bit)


The DirectSound crash (Win 7/64-bit)



Any help would be greatly appreciated. Thanks!

-Jason

Last edited 2011

Last edited 2011


GfK(Posted 2011) [#2]
Are you using OpenAL?

I had the same problem on my Mac and got around it by... I can't quite remember! But I think I posted about it here. I'll look.

[edit] http://www.blitzbasic.com/Community/posts.php?topic=91048#1036418

Looks like I switched to FreeAudio which also had problems, but there's the work-around.

Last edited 2011


jtfrench(Posted 2011) [#3]
Thanks for the response GfK,

On the Mac we're using OpenAL, but on Windows we've opted to not even bother seeing as we've read a bunch of issues about the inavailability of 64-bit OpenAL .DLLs.

I'll check out the link you posted.

Thanks,
Jason


jtfrench(Posted 2011) [#4]
On the PC though, we unfortunately don't even make it past loadSound ..... :(


GfK(Posted 2011) [#5]
Are you downloading the second lot of data into the same bank as the first? Could be data left over from the first sound that's messing it up, or something. I'm just randomly guessing.


BlitzSupport(Posted 2011) [#6]
I've followed the scenario you listed, and this works here -- does it work for you?


Local path:String = "http::www.hi-toro.com/blitz/testaudio/"

Local file1:String = "test1.ogg" ' Two 10-second
Local file2:String = "test2.ogg" ' samples...

Local bank:TBank = LoadBank (path$ + file1)

If bank

	Local sound1:TSound = LoadSound (bank)
	
	If sound1
	
		Local channel1:TChannel = PlaySound (sound1)
		
		Delay 5000 ' Play for 5 seconds, then load new sound...
		
		bank = LoadBank (path + file2)
		
		If bank
		
			Local sound2:TSound = LoadSound (bank)
			
			If sound2

				Local channel2:TChannel = PlaySound (sound2)

				StopChannel channel1

				If channel2
					Delay 5000
					StopChannel channel2
				EndIf

			EndIf
			
		EndIf
		
	EndIf
	
EndIf

If the above works, I think we'd need some isolated sample code.


jtfrench(Posted 2011) [#7]
This code sample seems to work for us. I'm very confused about what's making our code not work under Windows. Our game project at this point is very large, but below I've pasted in the key parts of our audio loading.

This is the function that we use to download the song data. We currently do this on a child thread:

(note: variables preceded with "m" indicate that they are member variables/fields within the Type. The below code is from our sound-managing type/framework called "SoundKit" (often abbreviated "mSK"). It is the audio subsystem in our game engine. RUNTIME is a global class which maintains a reference to all the sub-systems/kits, so RUNTIME.mSK is refering to our instance of SoundKit which keeps references to all the audio channels.)
Method cacheAudio:Int()
		Local ms:Long = MilliSecs();
		mBank = LoadBank(mUrl);
		toLog mName + " took " + ( MilliSecs() - ms );
		If(mBank <> Null)
			Return True;
		EndIf
		Return False;
	EndMethod


When it comes time to play the cached audio, from the Main thread we execute the following:
Method playCachedAudio:Int()
		
		If(CurrentThread() = MainThread())
			If (RUNTIME.mSk.isRadioPlaying())	'this just does a ChannelPlaying() on mRadioChannel
				RUNTIME.mSk.stopRadioChannel();	'this does StopChannel() on mRadioChannel
			EndIf
			mSound = LoadSound(mBank, SOUND_HARDWARE);
			RUNTIME.Log("Loaded sound from bank");
			If(mSound <> Null)
				RUNTIME.mSK.mRadioChannel = AllocChannel(); 'since the radio may have been stopped earlier, allocating a new one
				RUNTIME.mSK.mRadioChannel = PlaySound(mSound, RUNTIME.mSk.mRadioChannel); 'Start the new song
				
                    
				If(ChannelPlaying(RUNTIME.mSK.mRadioChannel))  'checking to see if it actually started playing...purely for diagnostics
					RUNTIME.Log("Started playing sound for (" + mName + ") on radio channel");
				Else
					RUNTIME.Log("Radio channel not playing...trying again...");
					RUNTIME.mSK.mRadioChannel = PlaySound(mSound, RUNTIME.mSk.mRadioChannel); 'a desperate attempt to try again..
					If(ChannelPlaying(RUNTIME.mSK.mRadioChannel))
						RUNTIME.Log("Yay its playing");
					Else
						RUNTIME.Log("still no luck playing song"); 'this is always the case...if it doesn't work the first time, it usually doesn't the second
					EndIf
				EndIf
				RUNTIME.mSk.mCurrentSong = Self;								'Update the current song
				RUNTIME.Log("SoundKit now refers to this new SKSong as its current song");
				
				RUNTIME.mSk.mSongToBePlayed = Null;
				If(RUNTIME.mSk.isMainMenuPlaying())
					RUNTIME.Log("Main menu music is still playing...telling to hush.");
					RUNTIME.mSk.stopPlayingMainMenu();
				EndIf
				Return True;
			EndIf
		Else
			toLog("current thread isnt main thread");
		EndIf
	EndMethod


A few questions:

• Are there any major caveats for LoadSound()/PlaySound() when used in a multi-threaded environment?
• Does (multi-threaded) garbage collection affect this in any way? (I think as of our last build we were manually GC-ing every 5 seconds)

We're getting pretty desperate since not being able to play audio w/o crashing has indefinitely halted development. We're building an angel-backed MMO in BlitzMax/MiniB3D that requires multi-threading and dynamic audio. So far the debugging limitations of MaxIDE are hurting our productivity (we're getting crashes in places that we can't step into w/ MaxIDE's debugger). I'm not sure if this is that type of thing that can be diagnosed without reviewing all the code. Makes me more excited to switch to pure C++ for the next version so that I can at least debug things more effectively, but getting things to work in BlitzMax is our goal for now (and what I'd prefer).

If there's anyone who would want to take a deeper look at our project and help out, we would love to hear from you. We pretty much just need to get a solution for this and fast.

Thanks,
Jason

Last edited 2011


jtfrench(Posted 2011) [#8]
Any reasons why LoadSound would fail? We've even tried LoadBank-ing the .ogg into memory, and then SaveBank()ing it to disk and then having LoadSound load that. STILL crashes (on the second song, not first). Then when double clicking on the saved file that's on disk, it's a perfectly normal and playable .ogg.

Are there any known failure cases for LoadSound? Thread related? GC related? etc?


BlitzSupport(Posted 2011) [#9]
Are there mutexes or anything else in place to control access to mBank between the threads? I don't see anything in the cacheAudio method or playCachedAudio, though maybe you're doing it outside of this code?


jtfrench(Posted 2011) [#10]
Yes, we've been using mutexes to control access. Our first response to odd crashes is just to mutex the hell out of stuff until it works (heh heh) but in this case it doesn't seem to be solving the problem....any other ideas?

(I'll re-visit our code and make sure that we are indeed doing proper mutex-ing)


BlitzSupport(Posted 2011) [#11]
I'm pretty sure it's going to be related to that sort of thing. This (quick hack!) appears to work, loading the bank in a background thread (hit SPACE for new song), then playing it in the main thread without problem:



Unfortunately, it sounds like your sound manager code may be a bit more complex! Is it just the bank loading that needs to happen in a thread, so music downloads in the background?

It sounds like you're kinda "winging it" with mutexes, etc, which is possibly/probably a recipe for disaster with multithreading! If the mutexes were to happen within the methods above, for example, I'd expect to be seeing something like this (and this is only accounting for mBank, ignoring any shared access your sound manager might give to TChannels, TSounds, etc):

Global CONTROLLER:TMutex = CreateMutex () ' Start of program...		' *****************

Method cacheAudio:Int()
		Local ms:Long = MilliSecs();
		LockMutex CONTROLLER					' *****************
		mBank = LoadBank(mUrl);
		toLog mName + " took " + ( MilliSecs() - ms );
		If(mBank <> Null)
			UnlockMutex CONTROLLER				' *****************
			Return True;
		EndIf
		UnlockMutex CONTROLLER					' *****************
		Return False;
EndMethod

Method playCachedAudio:Int()
		
		If(CurrentThread() = MainThread())
			If (RUNTIME.mSk.isRadioPlaying())	'this just does a ChannelPlaying() on mRadioChannel
				RUNTIME.mSk.stopRadioChannel();	'this does StopChannel() on mRadioChannel
			EndIf
			LockMutex CONTROLLER				' *****************
			mSound = LoadSound(mBank, SOUND_HARDWARE);
			UnlockMutex CONTROLLER				' *****************
			RUNTIME.Log("Loaded sound from bank");
			If(mSound <> Null)


Finally, I noticed your log may be telling you porkies:

			RUNTIME.Log("Loaded sound from bank");
			If(mSound <> Null)


Anyway, gotta hit the hay now... it's 0630 here! Don't ask...


jtfrench(Posted 2011) [#12]
I hear what you're saying about using proper thread synchronization, but I believe I am doing so (I just failed to include that portion of the code in my previous example). In reality, I'm using playSong() which calls cacheAudio() [ to LoadBank the audio ] --- all within mutexes.

You can see that code here:

Method playSong(s:SKSong)
		'DebugStop;
		If(s = Null)
			toLog "SoundKit::playSong() received null song", 1;
			End;
		EndIf
		toLog "SoundKit::playSong(" + s.mName + ")";
	
		LockMutex(mMutex2) ;
			
			RUNTIME.Log("About to download audio data for " +s.mName );
			If(s.cacheAudio())
				RUNTIME.Log("Successfully downloaded audio for " + s.mName);
				RUNTIME.mSK.mSongToBePlayed = s;
			EndIf
			
		UnlockMutex(mMutex2) ;
		


As for playCachedAudio(), this function performs the actual LoadSound()s and PlaySound(). It only happens on the main thread, and there shouldn't be anything else calling LoadSound at the time which made me think it wasn't the thread synchronization culprit. However, just to be sure, I even tried re-factoring playCachedAudio() to use LoadSound_safe()/PlaySound_safe() ---> two wrapper functions I wrote to ensure these calls are wrapped in the absolute strictest of synchronization constraints.....however the crash still occurs.

Here's the updated playCachedAudio():

	Method playCachedAudio:Int()
		RUNTIME.Log("SKSong::playCachedAudio()", 2) ;
		'DebugStop();
		RUNTIME.Log("SKSong::playCachedAudio() for " + mName, 2) ;
		If(CurrentThread() = MainThread())
			RUNTIME.Log("Executing from main thread", 2) ;
			If (RUNTIME.mSk.isRadioPlaying())							'If a song is already playing...
				RUNTIME.Log(RUNTIME.mSK.mCurrentSong.mName + " is already playing on radio. Going to stop it to make room for " + mName);
				RUNTIME.mSk.stopRadioChannel();							'Stop it
			EndIf
			mSound = LoadSound_safe(mBank, SOUND_HARDWARE);
			RUNTIME.Log("Loaded sound from bank");
			If(mSound <> Null)
				RUNTIME.Log "playing (" + mName + ")...now"
				RUNTIME.mSK.mRadioChannel = AllocChannel();
				RUNTIME.mSK.mRadioChannel = PlaySound_safe(mSound, RUNTIME.mSk.mRadioChannel);				'Start the new song
				
				If(ChannelPlaying_safe(RUNTIME.mSK.mRadioChannel))
					RUNTIME.Log("Started playing sound for (" + mName + ") on radio channel");
				Else
					RUNTIME.Log("Radio channel not playing...trying again...");
					RUNTIME.mSK.mRadioChannel = PlaySound_safe(mSound, RUNTIME.mSk.mRadioChannel);
					If(ChannelPlaying_safe(RUNTIME.mSK.mRadioChannel))
						RUNTIME.Log("Yay its playing");
					Else
						RUNTIME.Log("still no luck playing song");
					EndIf
				EndIf
				RUNTIME.mSk.mCurrentSong = Self;								'Update the current song
				RUNTIME.Log("SoundKit now refers to this new SKSong as its current song");
				'RUNTIME.mSk.mFirstSongPlayedDebug = False;
				RUNTIME.mSk.mSongToBePlayed = Null;
				If(RUNTIME.mSk.isMainMenuPlaying())
					RUNTIME.Log("Main menu music is still playing...telling to hush.");
					RUNTIME.mSk.stopPlayingMainMenu();
				EndIf
				RUNTIME.Log("Yay. All done here.");
				Return True;
			EndIf
		Else
			toLog("current thread isnt main thread");
		EndIf
	EndMethod



Any other ideas?

Last edited 2011


jtfrench(Posted 2011) [#13]
The wrapper functions. Excessively strict/fine-grain and will negatively impact performance. The same goal can be achieved w/ coarser granularity, but I just want to ensure that this isn't the problem.

Global LoadSoundMutex:TMutex = CreateMutex();
Global ChannelMutex:TMutex = CreateMutex() ;

Function LoadSound_safe:TSound(url:Object, flags:Int)
	LockMutex(LoadSoundMutex);
	Local sound:TSound = LoadSound(url, flags);
	UnlockMutex(LoadSoundMutex);
	Return sound;
EndFunction

Function PlaySound_safe:TChannel(sound:TSound,channel:TChannel=Null)
	LockMutex(LoadSoundMutex);
	Local retval:TChannel = PlaySound(sound, channel);
	UnlockMutex(LoadSoundMutex);
	Return retval;
EndFunction

Function ChannelPlaying_safe:Int(channel:TChannel)
	LockMutex(ChannelMutex) ;
	Local retval:Int = ChannelPlaying(channel) ;
	UnlockMutex(ChannelMutex) ;
	Return retval;
EndFunction


Function StopChannel_safe(channel:TChannel)
	LockMutex(ChannelMutex) ;
	StopChannel(channel) ;
	UnlockMutex(ChannelMutex) ;
EndFunction



ziggy(Posted 2011) [#14]
Are you re-using shound channels?


jtfrench(Posted 2011) [#15]
Was at first, but have since re-implemented it to re-allocate the channels and let go of the old one each time. Not exactly sure which is better --- you have any recommendations on that?

As for fixing the issue, I've had some success with doing garbage collection. I'm thinking I might have had a massive memory leak. If I null out my TBank and TSound after playing back, and the GCCollect() it seems to not crash anymore the Windows machine (desktop) it was crashing on. However on a different Windows machine (laptop) with the same Windows 7 OS, it doesn't crash period — with manual garbage collection or not!

Not sure what's up with that. I doubt it's that the laptop has loads more memory --- I left it running for over an hour while constantly downloading and caching new songs into memory and it seemed not to crash (and this is without the garbage collection). Meanwhile on the desktop machine it would crash after 3 or 4 song loads.

Weird.


skidracer(Posted 2011) [#16]
Do you get different behavior when running in RELEASE mode?

I've had a quick look and the following *may* help.

Your samples are 10's of megabytes and you are using LoadBank which calls LoadByteArray which has a fault where the longer the file the more fragmented your memory will become.

Try changing brl.mod/stream.mod/stream.bmx[1045] to:

		If size=data.length data=data[..size*3]


and rebuild. If anything this should speed things up and make things a little easier on GC.

Last edited 2011

On second thoughts I think you are unwise to use multi threading in BlitzMax for commercial endeavors. I would consider a second .exe (or a process can clone itself) that pulls and stores the sound files onto a temporary storage device. You are better protected when your stream throws exceptions and you aren't handing BlitzMax such complex tasks.

Last edited 2011


jtfrench(Posted 2011) [#17]
Thanks, skidracer ----> can you tell me a bit more about it being unwise to use multi-threading BlitzMax commercially?

I've noticed that while BlitzMax seems to state that there is complete threaded support in these later versions of BlitzMax, I've found only a subset of the functions work. For example, I can lock and unlock a mutex fine, but I can't use any of the semaphore/signal broadcasting functions (none of them work properly, at least in my experience). We've managed to work our way around this though, as we need to do HTTP requests in the game and can't do those on the main thread without slowing down gameplay.

I'd be curious to here more why you'd recommend against it, and what workarounds have you found that are a) high-performance, and b) cross-platform (we're targeting Macs and Windows)

Thanks,
Jason


jtfrench(Posted 2011) [#18]
We're still getting the same error (DirectSound, E_OUTOFMEMORY, -2147024882) on one of our PCs. It crashes the entire app.

Any ideas how to get around this?