Blog, code, iPhone, openAL

alBufferDataStatic: why you should avoid it20 Sep

OK, So, I just got an email from Ken, a dude who surfed across this OpenAL stuff on my blog, had some troubles, and asked me for help. He is having trouble unloading buffers and then re-using them again without getting some esoteric OpenAL errors.

So I went through the snippets he sent me very quickly and threw out some advice. Ken came back with a few more questions and I went over the code again and noticed this:

 
          // use the static buffer data API
          alBufferDataStaticProc(buffer, format, data, size, freq);
 

Ah-HA! I said, that is probably your problem right there! alBufferDataStatic should be avoided unless you need it. When do you know if you will need it? Here is a rule of thumb: If you dont know the answer to that question then you dont need it.

(I don't want to seem to be picking on Ken, this problem is so common that I decided I needed to do a post about it (similar to my rant about never using the sample code from Apple in a shipping app))

OK, so why do I think you should avoid it? I am sure you have heard that if you want the best performance then you should be using alBufferDataStatic() right?! No.

Well, what is alBufferDataStatic() anyway? alBufferDataStatic puts the onus of memory management for all your buffer data onto your code.

Let me repeat that: When using alBufferDataStatic() YOU are responsible for all the memory management.

That is it. That is your big performance boost.

Wait! Really? that is all it does?

Yes.

The iPhone's shared memory space and the way it handles memory and the way your app gets jettisoned if you use too much is a very unique situation. A situation that requires that you often have to really closely handle your memory. So! That means that if you are trying to squeeze every single last byte of memory out of the iPhone so that your fully immersive 3d first-person-shooter can keep a consistent 30FPS then you will probably want to spend a few weeks tweaking your sound code so that you have very close control of your memory at all times.

For the other %99.9999 percent of apps that might want to use OpenAL (even if you are using lots of sounds) you are just adding extra headaches to your code by using alBufferDataStatic(). Here is another rule of thumb: If you are not employing a developer whose sole job is to handle the sound code in your app, then you don't need alBufferDataStatic().

Have a quick read of the technote that mentions alBufferDataStatic().

What does this really mean?

It means this: when you use alBufferData() you load your sound data into a local buffer (one that you have to malloc) then when you call alBufferData(), OpenAL copies all those bytes into it's own buffer that it deals with. Then you free your local storage and OpenAL now owns that buffer. The downside: for a brief moment, you have 2 copies of your sound in memory. The upside: very very easy memory management.

If you are getting jettisoned right here, during this buffer copy, then you may need alBufferDataStatic(). But more likely you have done a crap job of handling your memory elsewhere. (also it is good to note that if you are loading big sounds into memory you are doing it wrong anyway, those big sounds should be streamed).

Ok, you are absolutely, positively sure that you need to manage your own sound memory? Maybe you should be using alBufferDataStatic(), but first: here is yet another rule of thumb: if you are reading this blog post to see if you need to use it; you don't. go to your code right now and take it out. Just take it out.

Obviously I am being a bit hyperbolic in an attempt to make a point. I dont know about your code, but here is a small list of the most common bugs I find in my code:

  • Forgot to release an object properly and it leaks and eventually my app is jettisoned
  • Accidentally released an autoreleased object, causing a crash
  • Accidentally released an object owned by some other object, causing it to crash much much later
  • malloced some space and forgot to free it, causing a leak and eventual jettison
  • freed some malloced space and then tried to use it, causing my app to go batshit crazy

The list goes on. But can you see a common thread? Memory. Why would I want to add yet more memory management responsibilities when I dont need it? Why do developers always insist on making their own lives so much more difficult than they have to?

Here are a few platitudes:

  • Less code == Less bugs. More code == More bugs. Therefore Less Code is always better.
  • Do not add any code you dont need right now. (this is very common, you think to yourself: Hmm I am already here in this object, and I could see that I might need a method that does X in the future so I will just write it now so I dont have to write it then. Wrong. In that 'future' when you do need it, it will need to do Y, not X and you will have to re-write it. Or, more likely you will never need a method that does X, and you just wasted a bunch of time and added more code
  • Do not prematurely optimize. Get the code working first. Then go back and identify the places you need to smooth out. If you try to optimize as you go then you will end up spending all your time making that one method totally awesome (only to realize later that method only gets called once every few seconds, so really it could be the least efficient bit of code ever and nobody would ever notice.)

So, by using alBufferDataStatic() you are breaking all three of my lovely platitudes.

Dont do it.

You are only causing yourself grief and making your code buggy and shitty. Use alBufferData(). It works. It is easy. The highly experienced engineers who work on OpenAL have made sure that there are no memory issues with alBufferData(). If you decide to port your code, alBufferData() will still work! alBufferData() makes your breath smell minty! alBufferData() will totally pull chicks.

I have put OpenAL code into about 6 apps now. A few games and a few utilities. And one 'sound board' style app that played 32 sounds simultaneously (which was the max at the time, probably still is) and was able to handle gigantic sounds files (via streaming) without ever using alBufferDataStatic(). Was this some herculean task of super-awesome mad coding skillzzzz?

No. Just the opposite actually. I keep all my code as absolutely bare-minimum simple as I can.

Keep your code simple, don't use stuff you don't need.

Cheers!
-B

code, iPhone, multitouch, openAL, openSoundControl

WWDC 200918 May

I will be there.

If you are also going to be there, and you want to meet up and get a beer, or talk about anything on my blog or anything else really, then drop me a line at support@benbritten.com, or follow my rarely updated twitter: http://twitter.com/benbritten. I plan to use it during WWDC to help people find me and to help me find people. Otherwise I rarely use it, so dont go looking for profound wisdom from my twitter feed.

code, iPhone, openAL

Lots and Lots of sounds in openAL02 May

There has been a flurry of openAL related stuff going on in my life in the last few days, and that reminded me that I have a few half-finished posts that I keep meaning to finish..

Also, I have found at least one other blog that has copied my code (and even my blog posts!) with out even rewording the text!, so that is kinda cool i guess :-) (although I would have liked at least a courtesy link :-)

OK, today: lots and lots of sounds.

In my older posts I mentioned that there is an upper limit to the number of simultaneous sounds you can play in openAL. (in other words the most concurrent sources). At some point (around 32) you can no longer get any more sources via:

 
alGenSources(1, &sourceID);
 

the downside is that this just quietly fails, and does not seem to raise an error. The moral of this story: don't ask for more than 32 sources! (note: this is on iPhone OS 2.2, might be different on other OS versions and platforms)

So what does this mean? it means that no matter what, you cannot have more than 32 sounds playing in the same instant. This really should never be a problem. If your goal is to simulate and entire orchestra by playing each instrument individually, then you are probably not going to want to be doing it on the iPhone. (although you can actually get past this limit by building the buffers yourself, but really, more than 32 sounds? come on!)

That said, you may have more than 32 sounds in your app, and if you follow my simple example from past posts, i suggested that you assign each sound buffer to a single source and just call that source when you need to play the sound.

This works well for lots of scenarios. However, not so much if you have more than 32 sounds in your app. So how do we deal with that? I mentioned it briefly in an older post: you need to have a way to hand out the sources you are not using to the buffers that need to be played.

This is actually pretty simple and I will show you how!

First, you may want to preload your sources. This is not strictly necessary, but I like to keep the processing to a minimum at the point I want to play the sounds, so I like to have everything ready to go up front.

 
// note: MAX_SOURCES is how many source you want
// to preload.  should keep it below 32
-(void)preloadSources
{
        // lazy init of my data structure
	if (sources == nil) sources = [[NSMutableArray alloc] init];
 
	// we want to allocate all the sources we will need up front
	NSUInteger sourceCount = MAX_SOURCES;
	NSInteger sourceIndex;
	NSUInteger sourceID;
	// build a bunch of sources and load them into our array.
	for (sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) {
		alGenSources(1, &sourceID);
		[sources addObject:[NSNumber numberWithUnsignedInt:sourceID]];
	}
}
 

pretty simple really, just have openAL build a bunch of sources and store those IDs somewhere. You can actually have openAL build all the sources at once by calling alGenSources with a number besides 1, and store them in a C-style array, but I like to keep my stuff obj-c because I am lazy and dont like to malloc/free if I dont have to.

Now, the old play sound method looked like so:

 
// the main method: grab the sound ID from the library
// and start the source playing
- (void)playSound:(NSString*)soundKey
{
	NSNumber * numVal = [soundDictionary objectForKey:soundKey];
	if (numVal == nil) return;
	NSUInteger sourceID = [numVal unsignedIntValue];
	alSourcePlay(sourceID);
}
 

But that was back in the olden days when we assigned a single source to a single buffer. Now we dont have any source pre-assigned, we need to attach them at play-time.

so we want something like this:

 
- (NSUInteger)playSound:(NSString*)soundKey gain:(ALfloat)gain pitch:(ALfloat)pitch loops:(BOOL)loops
{
	ALenum err = alGetError(); // clear error code 
 
	// first, find the buffer we want to play
	NSNumber * numVal = [soundLibrary objectForKey:soundKey];
	if (numVal == nil) return 0;
	NSUInteger bufferID = [numVal unsignedIntValue];	
 
	// now find an available source
	NSUInteger sourceID = [self _nextAvailableSource];	
 
	// make sure it is clean by resetting the source buffer to 0
	alSourcei(sourceID, AL_BUFFER, 0);
	// attach the buffer to the source
	alSourcei(sourceID, AL_BUFFER, bufferID); 
 
	// set the pitch and gain of the source
	alSourcef(sourceID, AL_PITCH, pitch);
	alSourcef(sourceID, AL_GAIN, gain);
 
	// set the looping value
	if (loops) {
		alSourcei(sourceID, AL_LOOPING, AL_TRUE);
	} else {
		alSourcei(sourceID, AL_LOOPING, AL_FALSE);
	}
	// check to see if there are any errors
	err = alGetError();
	if (err != 0) {
		[self _error:err note:@"Error Playing Sound!"];
		return 0;
	}
        // now play!
	alSourcePlay(sourceID);
	return sourceID; // return the sourceID so I can stop loops easily
}
 

Ok, it is important to note in this new method that we are no longer getting a sourceID out of our soundLibrary, but a buffer ID, so you will need to store the buffers instead of the sources. (I will leave that as an exercise for the reader, but if this doesnt make sense, I can post my buffer-loading code as well. However it is really just exactly like the stuff in the earlier posts)

OK, so that method looks pretty easy! wait! what is this line:

 
	// now find an available source
	NSUInteger sourceID = [self _nextAvailableSource];
 

Ahh yes, this is where the magic happens. It is pretty simple tho: we just step through our list of sources, find one that is not being used, and return it.

 
-(NSUInteger)_nextAvailableSource
{
	NSInteger sourceState; // a holder for the state of the current source
 
	// first check: find a source that is not being used at the moment.
	for (NSNumber * sourceNumber in sources) {
		alGetSourcei([sourceNumber unsignedIntValue], AL_SOURCE_STATE, &sourceState);
		// great! we found one! return it and shunt
		if (sourceState != AL_PLAYING) return [sourceNumber unsignedIntValue];
	}
 
	// in the case that all our sources are being used, we will find the first non-looping source
	// and return that.
	// first kick out an error
       NSLog(@"available source overrun, increase MAX_SOURCES");
 
	NSInteger looping;
	for (NSNumber * sourceNumber in sources) {
		alGetSourcei([sourceNumber unsignedIntValue], AL_LOOPING, &looping);
		if (!looping) {
			// we found one that is not looping, cut it short and return it
			NSUInteger sourceID = [sourceNumber unsignedIntValue];
			alSourceStop(sourceID);
			return sourceID;
		}
	}
 
	// what if they are all loops? arbitrarily grab the first one and cut it short
	// kick out another error
       NSLog(@"available source overrun, all used sources looping");
 
	NSUInteger sourceID = [[sources objectAtIndex:0] unsignedIntegerValue];
	alSourceStop(sourceID);
	return sourceID;
}
 

So, what is happening here? Really, 99% of the cases will be caught in the first loop. You just run through the array, find one that is not playing and return it. In my experience, even in a pretty sound-intense app (like a beat-box where you are mashing on keys and playing samples) you rarely get more than a couple of sounds that are actually playing at once (unless your samples are very long) so this will return very quickly.

But what if everything is already playing? You have a few options: Easy: increase you max_sources until this problem goes away, harder: (but still pretty easy) generate new sources on the fly when you run out.

Now if you manage to use up all 32 sources and still find yourself calling this method for another source, then the last 2 chunks of code in the above method will take care of it. The way you handle a source overrun is really application specific. but in the above case I am presuming that new sounds are more important than old sounds, and loops are more important than effects: so we have a 2 step process to find a sound to kill.

first we look to find the first non-looping sound and we kill it (cutting it off) and return that source. In the second case we have MAX loops running, so we just grab the first source, cut it short and return it.

Now, your app might have different requirements if you run out of sources. so you could handle that in lots of different ways. This was probably the easiest and it works for all the apps I have done (I tend to 'tune' the MAX number until I never get an overrun, even during heavy sound usage, so these are really just worst case 'graceful failing')

OK, that is about it for today!
Happy sound playing :-)

Oh! One last thing: the code above will work fine, but it is not my production code :-) I tend to remove all the extra error checking and stuff like that to keep the code clean and easy to read for the examples, so be sure to put error checks back into the code if you plan to use it !

code, iPhone, openAL

restarting openAL after application interruption on the iPhone02 Feb

Hey All,

I have been fiddling with openAL on the iPhone for awhile now, and have discovered a few things. If you havent already read it, (and want a basic overview of openAL on the iPhone) have a look at the openAL tutorial I posted a while back.

Today I want to talk a bit about how the Audio Sessions system interacts with the openAL stuff on the iPhone, and how to get it all working in your app. It took me a looong time to figure this out and get all the little bits in the right place at the right time. Many thanks to the contributors at the apple iphone dev forums, couldn't have done it without them. In any case, hopefully this will save someone out there some time trying to figure this all out.

First off: WTF are Audio Sessions? From the docs:

"Audio Session Services is a C interface for managing an application’s audio behavior in the context of other applications."

So basically, if you want your app to play nice with other sound making things on the iPhone, you will need to deal with Audio Sessions. The biggest reason to do this is so that you can play your iPod music and have it mix with the sound that your app is generating.

So, lets get right into it.

First off, you need to let the Audio Session Service know that your app exists and give it a way to talk to you via a method callback:

 
OSStatus result = AudioSessionInitialize(NULL, NULL, interruptionListenerCallback, self);
 

The first two params that I am passing as NULL specify which run loop and what run loop mode you want to use when calling the callback method. passing NULL basically says: "Use the main run loop, and the default run mode", which for our purposes here is going to be fine.

the third param is the callback method pointer, more on that in a moment.

and lastly, the final param is the object that is the target of the callback message.

OK, now we have let the Audio Sessions know that our app exists, the next thing to do is tell it what kind of sound we will be generating. We do this with an audio category.

There are a handful of valid categories:
kAudioSessionCategory_UserInterfaceSoundEffects
kAudioSessionCategory_AmbientSound
kAudioSessionCategory_MediaPlayback
kAudioSessionCategory_LiveAudio
kAudioSessionCategory_RecordAudio
kAudioSessionCategory_PlayAndRecord

If any of these sound like your app, then look em up in the docs and double check. I am going to talk about the two that I use for most everything: kAudioSessionCategory_AmbientSound and kAudioSessionCategory_MediaPlayback.

kAudioSessionCategory_AmbientSound: This one is for 'longer' sound playback, but works fine for short sounds as well. It is what i refer to as the 'game category'; good for games. good to play longer looping background sounds (like a rockin soundtrack, or atmospheric noises) as well as short effects (like gunshots or interface clicks) This category will also allow the iPod music to mix with your app sounds.

kAudioSessionCategory_MediaPlayback: this is good if you have your own music and do NOT want to allow the ipod to play while your app is active. Basically this one tells the iPhone that you want your app to be the only source of sound. Good for things that generate music, or where it doesnt make sense to allow for iPod sound.

OK, so how do we tell the audio session about our category?

 
UInt32 category = kAudioSessionCategory_AmbientSound;
OSStatus result = AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(category), &category);
 

Pretty easy really, just set the property and you are done.

OK, with just those few lines of code your app will play nice with other apps and allow (or not) iPod music to mix with your sound.

But there is one more thing we need to do: handle sound interruptions. Like an alarm or phone call. (note: if you get a phone call and answer it, then your app will be dismissed. However if you get a phone call an ignore it, then your app will get the interruption call. Of course, on the Touch you dont get calls but you can still get alarm clock interruptions etc..)

OK, so we need to define a callback method. this needs to be some ugly C code, so all you Cocoa-only types, just hold your nose and cut and paste :-)

 
void interruptionListenerCallback (void   *inUserData, UInt32    interruptionState ) {
 
        // you could do this with a cast below, but I will keep it here to make it clearer
	YourSoundControlObject *controller = (YourSoundControlObject *) inUserData;
 
	if (interruptionState == kAudioSessionBeginInterruption) {
		[controller _haltOpenALSession];
	} else if (interruptionState == kAudioSessionEndInterruption) {
		[controller _resumeOpenALSession];
	}
}
 

What is going on here? Pretty simple really; the inUserData is a pointer to your object (or whatever obejct you put in for the last param above when you called AudioSessionInitialize()), and interruption state is an enum so you know if you are being interrupted or resumed.

I like to get right out of the ugly C code and right back into the lovely land of Cocoa by calling some cocoa methods on my controller object. (though you could just call the openAL and audio session functions from right there, but it is ugly)

So, how do we halt and resume openAL?

first thing, let the audio sessions know that you are shutting down

 
// Deactivate the current audio session
AudioSessionSetActive(NO);
 

Next, shut down openAL in a nice way so you can keep your context intact:

// set the current context to NULL will 'shutdown' openAL
alcMakeContextCurrent(NULL);
// now suspend your context to 'pause' your sound world
alcSuspendContext(mContext);

easy.

And to come back from the above halting?

Basically the same thing in reverse, but with one added twist: you have to re-register your audio session category:

 
// Reset audio session
UInt32 category = kAudioSessionCategory_AmbientSound;
AudioSessionSetProperty ( kAudioSessionProperty_AudioCategory, sizeof (category), &category );
 
// Reactivate the current audio session
AudioSessionSetActive(YES);
 
// Restore open al context
alcMakeContextCurrent(mContext);
// 'unpause' my context
alcProcessContext(mContext);
 

And that is it. That will allow your openAL app to play nice with the rest of the system and recover from interruptions.

edit: renato reminds me in the comments that you should always be checking your error codes. Otherwise openAL can choke on some small issue and it wont work until that code has been cleared.

I tend to put one of these after every call to openAL: like so:

 
alSourceQueueBuffers(sourceID, 1, &bufferID);
ALEnum err = alGetError();
if (err != 0) NSlog(@"Error Calling alSourceQueueBuffers: %d",err);
 
alSourcef(sourceID, AL_PITCH, pitch);
err = alGetError();
if (err != 0) NSlog(@"Error Calling alSourcef with AL_PITCH: %d",err);
 

and so on.

Cheers!
-ben

code, iPhone, openAL

More openAL tidbits for iPhone24 Jan

Hey All,

my last post (which was a zillion years ago it seems) was about openAL. It has gotten quite a bit of traffic in the past few months, so apparently openAL on the iPhone is something people are interested in. So i am going to post this which is a few more tidbits that I have discovered over the intervening months about openAL.

First off, i have noticed that the vast majority of people are taking apple's sample code 'SoundEngine' and plopping it directly into their code, and then spending countless hours and days trying to get it to work just right for them.

Here is my advice:

Dont.

Look, I love the apple engineers, I love the work they do, but sample code is just that; a sample. It is meant to show you all the various different ways you can do something. the SoundEngine code does way more than you should ever really need. Your best bet is to start from scratch, figure out what your sound needs are and build you own sound controller object. If you do this you will be in a much better place to debug (because you will know how it all works) and you will be able to add only the code your app needs, and less code means less bugs.

What you say? What about code re-use?! I shouldnt reinvent the wheel, I dont fall into the 'not invented here' trap!! Heresy!

to that I say: you are re-using code. you are re-using the openAL codebase. but it turns out at the level that you are gluing openAL into your app, you should strive to make your own implementation geared towards what you need.

This is especially true for the iPhone. If there is any code in your app that you can get rid of by pre-formatting your sounds then you should be doing that. There is absolutely no reason that your sounds should be in more than 2 formats (ie an uncompressed format for your short sounds and a compressed format for anything longer) if you are checking the type and format of your sound files at runtime you are doing it wrong. Pick a sample frequency and format (stereo/mono) that works with all your sounds and make them all the same. This will simplify your code immensely and simpler code == less bugs == better performance.

Anyway, that was a bit of a rant. But I see so many posts to the apple dev forums that are like: "I am using SoundEngine from the crashLanding app and i am having this problem..."

Really, openAL is not that hard. I spent about 2 hours with the programmer guide and I got my simple sound controller working and wrote a tutorial since i was such an expert by then. That and the resulting code was about a tenth the size and complexity as soundEngine and I have had very few problems with it (and the problems I have had were easy to track down and debug because there just is not that much code there.

Anyhow, I really did mean to have some technical advice in here, and not just rant, however this is getting long, so it may have to wait until another post. :-)

-b

About

meMy full name is Ben Britten Smith.

I go by Ben Britten because Ben Smith is a bit too common and using my full name is a mouthful.

I live in Melbourne, Australia and service clients all over the globe.

Contact

Have some questions?

Feel free to contact me directly at support@benbritten.com with any questions you might have about any of the applications I support.

Thanks!

PHVsPjxsaT48c3Ryb25nPndvb19hYm91dDwvc3Ryb25nPiAtIGFib3V0LXdpZGdldDwvbGk+PGxpPjxzdHJvbmc+d29vX2FkX2JlbG93X2ltYWdlPC9zdHJvbmc+IC0gaHR0cDovL2JlbmJyaXR0ZW4uY29tL3dwLWNvbnRlbnQvdGhlbWVzL3ZpYnJhbnRjbXMvaW1hZ2VzL2FkNDY4LmpwZzwvbGk+PGxpPjxzdHJvbmc+d29vX2FkX2JlbG93X3VybDwvc3Ryb25nPiAtIGh0dHA6Ly93d3cud29vdGhlbWVzLmNvbTwvbGk+PGxpPjxzdHJvbmc+d29vX2FsdF9zdHlsZXNoZWV0PC9zdHJvbmc+IC0gYmVuYnJpdHRlbi5jc3M8L2xpPjxsaT48c3Ryb25nPndvb19ibG9ja19pbWFnZTwvc3Ryb25nPiAtIGh0dHA6Ly9iZW5icml0dGVuLmNvbS93cC1jb250ZW50L3RoZW1lcy92aWJyYW50Y21zL2ltYWdlcy9hZDMzNi5qcGc8L2xpPjxsaT48c3Ryb25nPndvb19ibG9ja191cmw8L3N0cm9uZz4gLSBodHRwOi8vd3d3Lndvb3RoZW1lcy5jb208L2xpPjxsaT48c3Ryb25nPndvb19ibG9nPC9zdHJvbmc+IC0gdHJ1ZTwvbGk+PGxpPjxzdHJvbmc+d29vX2Jsb2djYXQ8L3N0cm9uZz4gLSAvY2F0ZWdvcnkvYmxvZy88L2xpPjxsaT48c3Ryb25nPndvb19jYXRfbWVudTwvc3Ryb25nPiAtIGZhbHNlPC9saT48bGk+PHN0cm9uZz53b29fY29udGFjdDwvc3Ryb25nPiAtIGNvbnRhY3Q8L2xpPjxsaT48c3Ryb25nPndvb19jdXN0b21fY3NzPC9zdHJvbmc+IC0gPC9saT48bGk+PHN0cm9uZz53b29fY3VzdG9tX2Zhdmljb248L3N0cm9uZz4gLSBodHRwOi8vYmVuYnJpdHRlbi5jb20vZmF2aWNvbi5pY288L2xpPjxsaT48c3Ryb25nPndvb19mZWF0cGFnZXM8L3N0cm9uZz4gLSA1NDk8L2xpPjxsaT48c3Ryb25nPndvb19mZWVkYnVybmVyX3VybDwvc3Ryb25nPiAtIDwvbGk+PGxpPjxzdHJvbmc+d29vX2dvb2dsZV9hbmFseXRpY3M8L3N0cm9uZz4gLSA8L2xpPjxsaT48c3Ryb25nPndvb19ncmF2YXRhcjwvc3Ryb25nPiAtIHRydWU8L2xpPjxsaT48c3Ryb25nPndvb19sYXlvdXQ8L3N0cm9uZz4gLSBkZWZhdWx0LnBocDwvbGk+PGxpPjxzdHJvbmc+d29vX2xvZ288L3N0cm9uZz4gLSA8L2xpPjxsaT48c3Ryb25nPndvb19tYW51YWw8L3N0cm9uZz4gLSBodHRwOi8vd3d3Lndvb3RoZW1lcy5jb20vc3VwcG9ydC90aGVtZS1kb2N1bWVudGF0aW9uL3ZpYnJhbnRjbXMvPC9saT48bGk+PHN0cm9uZz53b29fbmF2X2V4Y2x1ZGU8L3N0cm9uZz4gLSAyLDgyLDU0OSw1NTMsNTY3LDUzMiw1MzQsNTM3LDgzMjwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3J0bmFtZTwvc3Ryb25nPiAtIHdvbzwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3dfYWQ8L3N0cm9uZz4gLSBmYWxzZTwvbGk+PGxpPjxzdHJvbmc+d29vX3Nob3dfbXB1PC9zdHJvbmc+IC0gZmFsc2U8L2xpPjxsaT48c3Ryb25nPndvb19zdGVwczwvc3Ryb25nPiAtIDEuLCAyLiwgMy48L2xpPjxsaT48c3Ryb25nPndvb190YWJiZXI8L3N0cm9uZz4gLSBmYWxzZTwvbGk+PGxpPjxzdHJvbmc+d29vX3RoZW1lbmFtZTwvc3Ryb25nPiAtIFZpYnJhbnRDTVM8L2xpPjwvdWw+