Stop using private variables in virtual functions please!

Hey there. Audio programmer here.

If you notice, there are specifically tagged access areas in the base class USoundWaveProcedural. For 4.13, for data that is access on the audio device thread, I intentionally chose to make that data private. In 4.12, there were a number of people who were using that data in an unsafe way and were generating crashes. The reason this needed to change was that 4.12 changed the way audio buffers are generated on PC (both for procedural waves but also real-time async decoding sound waves). Prior to 4.12, all generating code was called on the game thread. This caused significant buffer underruns (stuttering) any time the game thread was blocked or halted for any reason. It was particularly problematic during load screens. I switched to using XAudio2 callbacks (e.g. OnBufferEnd) to allow the voices themselves to notify when it needs more audio (on the hardware thread), rather than depending on a game-thread-dependent poll/pump. This allows audio to play without buffer underruns but does add greater complexity to the implementation as audio buffer data needs to be prepared in a thread-safe way.

For maximum simplicity in implementing procedural sound waves, I recommend implementing procedural sound waves by not overriding any virtual functions. I’d prefer to make all its overrides non-virtual (i.e. remove the word virtual from any functions in USoundWaveProcedural). Instead, for your base class, all you need to do is register a single delegate function.

For example, here’s what I used in some code I implemented recently (for our internal QA team to test procedural sound waves):

OnSoundWaveProceduralUnderflow = FOnSoundWaveProceduralUnderflow::CreateUObject(this, &UQASoundWaveProcedural::GenerateData);

The signature of my GenerateData delegate is:

void UQASoundWaveProcedural::GenerateData(USoundWaveProcedural* InProceduralWave, int32 SamplesRequested)

Then you put your generating code in that function. This function is still called on the audio device thread (on PC), so you’ll need to take care to ensure any data or events you want from the game thread are safely transferred to the audio device thread. This thread safety issue is only currently an issue on PC, but I’ve based the upcoming audio mixer code (new multi-platform backend) on this paradigm so it’ll be good to make sure your code is thread safe no matter what platform you’re targeting.

I’m planning on writing up a tutorial/blog series on real-time synthesis, filtering, DSP, for UE4 audio… as soon as the multi-platform backend is out and available for people to use.

EDIT: After some feedback from Epic legal, I removed the posted source since these forums are public and I can’t post long-form code. Writing only snippets would probably cause more confusion. So here’s a high-level implementation guide:

In general a good paradigm for implementing procedural sound waves is to first create a component container (inherited from UActorComponent etc) that has a handle to the instance of the procedural sound wave. Then, when you create the instance, you’ll be able to pass data to it from BP by writing a BP API for your component. You’ll also be able to use any the static gameplay functions we normally use for USoundBase* types (PlaySound/SpawnSound etc) as well as any other related audio types (Concurrency, Attenuation, sound classes, etc). Basically you can use your procedural sound wave like any other USoundBase type. The only thing you have to be careful of is to sure that any parameters passed to your USoundWaveProcedural* type is thread safe (use critical sections or a thread safe queue to pass data to your code).

EDIT 2:

I realized another bit of complexity I didn’t explain and that also resulted from the switch to calling the GeneratePCMData function from the audio hardware thread. Since the callback is generated from an XAudio2 OnBufferEnd callback, if no buffer is submitted to the XAudio2 voice, then no more OnBufferEnd callbacks will be made since it no longer has any enqueued buffers. This means the procedural sound wave will just mysteriously fall silent. Therefore, the old paradigm of not returning audio if none is available won’t work. That was what many overrides of GeneratePCMData in certain use-cases were doing, in particular VOIP implementations or other things that depended on systems that may or may not have audio to generate. My base-class implementation of USoundWaveProcedural attempts to handle that case and will always return audio buffers even if no audio has been queued. It also attempts to wait until a certain amount of audio buffers (to “build up” audio buffers) before starting. This is to support streaming systems or VOIP streams that may not have a enough audio ready at first. The amount to wait until feeding audio out is configurable by NumSamplesToGeneratePerCallback. This also determines the general cost of the procedural sound wave. Larger NumSamplesToGeneratePerCallback will reduce CPU cost but increase latency (for real-time synthesis that gets param data from the game thread, this will mean that your synthesizer will respond more slowly to parameters, etc). Also, the larger the NumSamplesToGeneratePerCallback, the fewer OnSoundWaveProceduralUnderflow delegate callbacks will be made per GeneratePCMData callback.

Also, the amount of silent audio to write out in the case of real buffer underrun is also configurable with NumBufferUnderrunSamples. This is to decouple the amount of silent audio written out from the number of samples we normally generate per callback (i.e. you may want to reduce the size of the buffer underrun and ensure that the xaudio2 voice performs an OnBufferEnd callback faster or shorter for silent buffers than for audio-filled buffers).