Android - Crash in SoundWaveStreaming

Hi, when streaming realtime audio in android, I randomly have a crash after few seconds.

After some investigation in engine source code, I’ve found that this is likely a concurrency problem in SoundWaveStreaming.cpp, where method void USoundWaveStreaming::QueueAudio gets interrupted by a callback which in end empties an array used, causing an exception when invoking: &QueuedAudio[ Position ] (Array index out of bounds)

Basically, I’ve added some more logs, and every crash shows following pattern:

05-07 00:04:43.568: D/UE4(22601): [2015.05.06-22.04.43:571][  0]LogSoundWaveStreaming:Warning: USoundWaveStreaming::QueueAudio Position:28160
05-07 00:04:43.568: D/UE4(22601): [2015.05.06-22.04.43:572][  0]LogSoundWaveStreaming:Warning: USoundWaveStreaming::QueueAudio2 Position:28160
05-07 00:04:43.628: D/UE4(22601): [2015.05.06-22.04.43:636][  0]LogSoundWaveStreaming:Warning: USoundWaveStreaming::QueueAudio Position:30080
05-07 00:04:43.628: D/UE4(22601): [2015.05.06-22.04.43:638][  0]LogSoundWaveStreaming:Warning: USoundWaveStreaming::GeneratePCMData SamplesNeeded 8192 > SamplesAvailable 16000
05-07 00:04:43.628: D/UE4(22601): Array index out of bounds: 30080 from an array of size 15616

code affected in SoundWaveStreaming.cpp is following:

void USoundWaveStreaming::QueueAudio( const uint8* AudioData, const int32 BufferSize )
{
	if (BufferSize == 0 || !ensure( ( BufferSize % sizeof( int16 ) ) == 0 ))
	{
		return;
	}
	
	const int32 Position = QueuedAudio.AddUninitialized( BufferSize );
	UE_LOG( LogSoundWaveStreaming, Warning, TEXT("USoundWaveStreaming::QueueAudio Position:%d"), Position);
	FMemory::Memcpy( &QueuedAudio[ Position ], AudioData, BufferSize );
	UE_LOG( LogSoundWaveStreaming, Warning, TEXT("USoundWaveStreaming::QueueAudio2 Position:%d"), Position);
}

int32 USoundWaveStreaming::GeneratePCMData( uint8* PCMData, const int32 SamplesNeeded )
{
	int32 SamplesAvailable = QueuedAudio.Num() / sizeof( int16 );

UE_LOG( LogSoundWaveStreaming, Warning, TEXT("USoundWaveStreaming::GeneratePCMData SamplesNeeded %d > SamplesAvailable %d"), SamplesNeeded, SamplesAvailable);
	// if delegate is bound and we don't have enough samples, call it so system can supply more
 	
	if (SamplesNeeded > SamplesAvailable && OnSoundWaveStreamingUnderflow.IsBound())
	{
		OnSoundWaveStreamingUnderflow.Execute(this, SamplesNeeded);
		// Update available samples
		SamplesAvailable = QueuedAudio.Num() / sizeof( int16 );
	}

	if (SamplesAvailable > 0 && SamplesNeeded > 0)
	{
		const int32 SamplesToCopy = FMath::Min<int32>( SamplesNeeded, SamplesAvailable );
		const int32 BytesToCopy = SamplesToCopy * sizeof( int16 );

		FMemory::Memcpy( ( void* )PCMData,  &QueuedAudio[ 0 ], BytesToCopy );
		QueuedAudio.RemoveAt( 0, BytesToCopy );
		return BytesToCopy;
	}
	return 0;
}

What happen is:

  • While void USoundWaveStreaming::QueueAudio is running, thread gets preempted by callback void FSLESSoundSource::OnRequeueBufferCallback in AndroidAudioSource.cpp, which in turn calls int32 USoundWaveStreaming::GeneratePCMData. (you can see that USoundWaveStreaming::QueueAudio2 never gets printed)
  • In int32 USoundWaveStreaming::GeneratePCMData command QueuedAudio.RemoveAt( 0, BytesToCopy ); empties part of queue, so when thread gets back to FMemory::Memcpy( &QueuedAudio[ Position ], AudioData, BufferSize ); in void USoundWaveStreaming::QueueAudio array &QueuedAudio[ Position ] causes exception discussed.

Synchronising two methods above (e.g. making void USoundWaveStreaming::QueueAudio call “monolithic” and not preemptable would probably solve issue, unless there is some better lower level solution in AndroidAudioSource.cpp.

How can I achieve this in unreal engine code? FScopeLock? Any fix?

Fixed issue using thread locks, don’t know if solution is acceptable on any platform, anyway:

void USoundWaveStreaming::QueueAudio( const uint8* AudioData, const int32 BufferSize )
{
	
	if (BufferSize == 0 || !ensure( ( BufferSize % sizeof( int16 ) ) == 0 ))
	{
		return;
	}
	
	if(lock==nullptr){
		lock = createThreadLock();
		notifyThreadLock(lock);
	} else {
		waitThreadLock(lock);
	}
	
	const int32 Position = QueuedAudio.AddUninitialized( BufferSize );
	FMemory::Memcpy( &QueuedAudio[ Position ], AudioData, BufferSize );

	notifyThreadLock(lock);
}

int32 USoundWaveStreaming::GeneratePCMData( uint8* PCMData, const int32 SamplesNeeded )
{

	
	int32 SamplesAvailable = QueuedAudio.Num() / sizeof( int16 );

UE_LOG( LogSoundWaveStreaming, Warning, TEXT("USoundWaveStreaming::GeneratePCMData SamplesNeeded %d > SamplesAvailable %d"), SamplesNeeded, SamplesAvailable);
	// if delegate is bound and we don't have enough samples, call it so system can supply more
	
	if (SamplesNeeded > SamplesAvailable && OnSoundWaveStreamingUnderflow.IsBound())
	{
		OnSoundWaveStreamingUnderflow.Execute(this, SamplesNeeded);
		// Update available samples
		SamplesAvailable = QueuedAudio.Num() / sizeof( int16 );
	}
	
	if(lock==nullptr){
		lock = createThreadLock();
		notifyThreadLock(lock);
	 } else {
		waitThreadLock(lock);
	 }
	
	if (SamplesAvailable > 0 && SamplesNeeded > 0)
	{
		const int32 SamplesToCopy = FMath::Min<int32>( SamplesNeeded, SamplesAvailable );
		const int32 BytesToCopy = SamplesToCopy * sizeof( int16 );

		FMemory::Memcpy( ( void* )PCMData,  &QueuedAudio[ 0 ], BytesToCopy );
		QueuedAudio.RemoveAt( 0, BytesToCopy );
		notifyThreadLock(lock);
		return BytesToCopy;
	}
	UE_LOG( LogSoundWaveStreaming, Warning, TEXT("USoundWaveStreaming::GeneratePCMData NoSamples"));
	notifyThreadLock(lock);
	
	return 0;
}

Is there a better solution for this? this solution may or may not work according what OnSoundWaveStreamingUnderflow callback does…
Is this class actually meant to be used concurrently as current Android implementation does?

Good to see you again !

Yeah, so I was worried when you started your ambitious feature you’d run into some serious issues. Getting real-time streaming to work right is definitely tricky. guy who wrote streaming code is currently unable to respond right now to your questions and I (new dedicated audio programmer) don’t have bandwidth right now to give you a proper response and dig into this problem. It sounds like you are on right-ish path here and I admire your tenacity.

So, some points to consider (again, without digging into this myself):

  1. Adding new thread locks without fully undestanding threading behavior of how this voice streaming interacts with other threads in android is potentially really dangerous and will most likely cause performance issues.

  2. audio engine is currently not thread safe and currently operates on main thread. If you’ve created any new threads that interact with streamed sound wave on android (which it sounds like you did), you’ll have to be super careful about making sure streaming operations are thread safe. A common pattern to implement streaming without requiring lots of locks is to use a double-buffering technique or a ring-buffer. Basically a mechanism to be able to read/write from same buffer (or array of buffers) without having to worry about reading/writing from same location.

  3. Doing my suggestion of using double-buffer or a ring buffer is likely going to be super-tricky as it’ll probably involve a rewrite/refactor of USoundWaveStreaming class, which is something that I really don’t recommend at this point.

You may be finding out first hand why nobody has done this yet in UE4!

Again, I’m sorry I can’t be more help – I’m currently up to my neck in high-priority issues (various platform crashes, oculus SDK support, etc) while trying to maintain forward progress on a new audio engine that will hopefully make what you’re trying to do much easier to do! One of main goals of new audio engine is to remove as much as possible platform issues you’re currently running into. We’ll be doing our own low-level audio mixing and effects processing in platform-independent code so something like this can be done once and for all platforms.

Hi aaronmcleran! Glad to hear you again, too :slight_smile: Yes, I’ve been digging a bit it mac and android code and it’s kind of though to fully understand it. I have been working on voice capture module for android up to now, and I have a mostly working module. You can check this other question about it for some pending issues.

Thank you for your feedback, here are some comments:

  1. I don’t like this solution, but a better solution would probably require a rewrite of AndroidAudioSource.cpp (which by way, would probably be necessary to fix another blocking issue I fixed by implementing OnSoundWaveStreamingUnderflow callback of SoundWaveStreaming class). I’m fairly confident this fix will work with my configuration, but if you are not scrapping out this code really soon,I really would like that somebody with a deeper knowledge of audio subsystem could look into this.

  2. Please note that both OnSoundWaveStreamingUnderflow issue and this crash are not referred to my code, they are affecting previously existing UE code and would probably show up to anybody trying to stream continuously real time audio programmatically. In particular, two thread accessing this code are network code receiving voice packages from other clients and android audio source requesting new data in queue (both asynchronously).

I’ve already implemented ring buffer in my VoiceModuleAndroid and it works pretty well, but at this time I really don’t want to refactor myself low level classes like SoundWaveStreaming and AndroidAudioSource, that may impact a lot of other areas in engine.

[continues…]

…in any case: at this point my patches are good enough for me to proceed developing (and experimenting with spatialisation in future), but I would really advice to try find a better solution to these issues in engine, sooner or later. I’m really looking forward for your work on new audio engine :slight_smile: as you say, most of these issue will not be relevant anymore then. If you want, I can open anyway pull requests with my code in meantime. :slight_smile:

About oculus SDK: what are your plans for a cross platform integration?
Do you think that FMOD plugin (with oculus plugin) would be usable for streaming somehow voip audio?

Thanks again for your time and your help, in any case :slight_smile:

No problem, wish I had more bandwidth to help you more.

Yeah, I think you’re right that stream threading problem is probably an issue in Android platform in general and not you’re code. I’m not sure that it’s been tested much there.

I did see that other thread about underflow and repeating test asset with VOIP stream. I chatted with Josh about it and I meant to reply to your thread. I think your solution to writing out zeros on buffer underflow made sense to me to prevent repeated buffers and is pretty common to continuous audio streams. It’s possible to make low-level audio voice callbacks stop calling for more data if none is available, but output device expects audio for that voice every audio device callback, so it’s reasonable and simple to just write out zeros way you did it.

As for Opus stuff you linked to other thread, I myself am still learning intricacies of UBT systems myself so won’t be able to help you there.

It’s possible to make low-level audio voice callbacks stop calling for more data if none is available

There’s something wrong going on with this solution, especially on OSX. What I see is that randomly I get sessions where no underflow happen (and then audio is really smooth and fine) and sessions where I keep getting underflow to every and each callback (and in this case I can hear small gaps introduced by zeros)

LogSoundWaveStreaming:Warning: USoundWaveStreaming::GeneratePCMData SamplesNeeded 8192 > SamplesAvailable 6720
LogVoice:Warning: FVoiceEngineImpl::StreamingWaveUnderflow
LogSoundWaveStreaming:Warning: USoundWaveStreaming::GeneratePCMData SamplesNeeded 8192 > SamplesAvailable 5440

This in exactly same context. e.g same LAN environment. Shouldn’t be possible when I get first underflow to stop and buffer just enough data to prevent this?

but output device expects audio for that voice every audio device callback, so it’s reasonable and simple to just write out zeros way you did it.

Correct, but when FSLESSoundSource::OnRequeueBufferCallback is called stack is probably still playing previous samples, so waiting a little bit for “real data” could be a better solution (and would probably lead to no gaps) than injecting a silence. If waiting in this thread is not an option, maybe we could just avoid calling SLresult result = (*SL_PlayerBufferQueue)->Enqueue here and call it again when more data is available. Does this make any sense?

No problem for opus stuff, as I said, I’m not stuck, this is just preventing proper engine integration and pull requests at moment.

Again, about oculus SDK: what are your plans for a cross platform integration? Do you think that FMOD plugin (with oculus plugin) would be usable for streaming somehow voip audio?

Thank you for your help and for this info in any case, I really appreciate!

AndrewHurley, if possible I’d like to keep this question open until problem is actually addressed to keep tracking issue, thanks.

I’m not 100% certain you are seeing what you think you are seeing.

SoundWaveStreaming::QueueAudio is called from GeneratePCMData when it needs more samples (likely via callback) as GeneratePCMData uses AudioBuffer to update sound source buffer.

Based on your log output above you are seeing QueueAudio being called BEFORE GeneratePCMData.

This means that QueuedAudio.RemoveAt(…) function being called shouldn’t be related to QueueAudio(…) function where crash is happening.

call graph would be;
GeneratePCMData → Callback → QueueAudio

And that only happens when needed sample count is greater than avaliable which means there is no way that GeneratePCMData() call is related to QueueAudio crash directly nor do I think it is a threading problem as all this should be happening on one thread and that is Audio callback thread on Android.

There clearly is a problem here but without more details it’ll be pretty hard to pin it down.
Including the ‘this’ pointer or some other ID which would help identify location log output is coming from might be helpful at this point.

Umh now that’s fun, I don’t really understand what’s going on.

SoundWaveStreaming::QueueAudio is called from GeneratePCMData when it needs more samples (likely via callback) as GeneratePCMData uses AudioBuffer to update sound source buffer.

How is this supposed to work? only way GeneratePCMData could call SoundWaveStreaming::QueueAudio is possibly through OnSoundWaveStreamingUnderflow.Execute(this, SamplesNeeded);
which in Android was unbounded, as far as I can tell. SoundWaveStreaming::QueueAudio was repeatedly called nevertheless, though

Based on your log output above you are seeing QueueAudio being called BEFORE GeneratePCMData.
This means that QueuedAudio.RemoveAt(…) function being called shouldn’t be related to QueueAudio(…) function where crash is happening.

If you check listing in beginning part of post, you can see where logs are in code:
const int32 Position = QueuedAudio.AddUninitialized( BufferSize );
UE_LOG( LogSoundWaveStreaming, Warning, TEXT(“USoundWaveStreaming::QueueAudio Position:%d”), Position);
FMemory::Memcpy( &QueuedAudio[ Position ], AudioData, BufferSize );
UE_LOG( LogSoundWaveStreaming, Warning, TEXT(“USoundWaveStreaming::QueueAudio2 Position:%d”), Position);

strange thing is USoundWaveStreaming::QueueAudio2 log never gets printed, and log from USoundWaveStreaming::GeneratePCMData is instead. How can that be possible?

And that only happens when needed sample count is greater than avaliable which means there is no way that GeneratePCMData() call is related to QueueAudio crash directly nor do I think it is a threading problem as all this should be happening on one thread and that is Audio callback thread on Android.

Again, thread locks above fix crash (even if probably not in best way), so GeneratePCMData really seems to be cause of crash…

There clearly is a problem here but without more details it’ll be pretty hard to pin it down. Including the ‘this’ pointer or some other ID which would help identify location log output is coming from might be helpful at this point.

location of logs is visible from snippet of code in top post, but I’m available for any other detail if needed.

As said StreamingWaveUnderflow was originally unbounded in Android, but SoundWaveStreaming::QueueAudio was called nevertheless.

StreamingWaveUnderflow I added in VoiceEngineImpl, which actually calls QueueAudio but is not invoked at moment of crash, is following:

void FVoiceEngineImpl::StreamingWaveUnderflow(USoundWaveStreaming* InStreamingWave, int32 SamplesRequired)
{
	UE_LOG(LogVoice, Warning, TEXT("FVoiceEngineImpl::StreamingWaveUnderflow"));

	const int32 QueuedSamples = InStreamingWave->GetAvailableAudioByteCount()/sizeof(uint16);
	const int32 SamplesNeeded = SamplesRequired - QueuedSamples;
	const int32 BlocksNeeded = FMath::CeilToInt(SamplesNeeded/1);
	
	//UE_LOG(LogTemp, Log, TEXT("Creating %d blocks for %s"), BlocksNeeded, *GetWorld()->GetPathName());
	
	TArray<int16> SampleData;
	SampleData.AddUninitialized(BlocksNeeded * 1);
	int32 CurrSample = 0;
	
	for(int32 BlockIdx=0; BlockIdx<BlocksNeeded; BlockIdx++)
	{		
		for(int32 SampleIdx=0; SampleIdx<1; SampleIdx++)
		{
			SampleData[CurrSample] = 0;
			CurrSample++;
		}
	}
	
	InStreamingWave->QueueAudio((uint8*)SampleData.GetData(), SampleData.Num() * sizeof(int16));
}

AnswerHub will sometimes auto accept answers at random times if you comment on an answer.

Again, about oculus SDK: what are your plans for a cross platform integration? Do you think that FMOD plugin (with oculus plugin) would be usable for streaming somehow voip audio?

For Oculus SDK, I’m not currently planning on doing integration to every platform since my focus right now is making progress on new audio framework. Any work I do in existing system will likely become wasted work.

I posted a brief overview of my plans for new audio engine here:

And in this thread I talked about Oculus SDK integration:
Native 3D audio and VR performance improvements coming in 4.8! - XR Development - Epic Developer Community Forums!

As for FMOD, I’m quite familiar with FMOD in general, however, I haven’t had time to deeply inspect their integration into UE4. However, knowing FMOD, it should be straightforward to connect work you’ve already done with an FMOD streaming voice callback.

Great, really interesting stuff, thanks!

Fixed all my pending issues on voice module for mac :wink:

Opened up a pull request here, it also includes StreamingWaveUnderflow callbacks discussed here:
https://github.com/EpicGames/UnrealEngine/pull/1162

Does not include thread lock solution of in SoundWaveStreaming.cpp though.

I’ll reply to PR thread on github. Would you mind if we close this UDN thread out?

Thanks for your work on this :slight_smile:

Hi , fix is related to thread about underflow and repeating test asset with VOIP stream you mentioned above, but does not include patch to crash mentioned in this thread. If you think solution posted below (thread locks in SoundWaveStreaming) is acceptable I can open a separate pull request for it, otherwise I would suggest to keep this open until a proper solution is identified.

Hi ,

Please submit solution you made below as a separate pull request, and then it can be discussed further there if needed. I’ll go ahead and resolve this post. Thanks!

Hi , ok thanks.

Here you go: