Transition of working JUCE 5.4.3 project to JUCE 6.0.8 crashes

For reasons outside the scope of this discussion I am upgrading an inherited JUCE project from JUCE 5.4.3 to JUCE 6.0.8. I’m getting an assertion warning and eventual crash that did not occur with older version.
The project analyzes a 6-channel audio input, and among other things uses convolution; so just to get the build to succeed, I had to update the call to loadImpulseResponse() from:

loadImpulseResponse(File, bool, bool, int, bool)

to

loadImpulseResponse(File,dsp::Convolution::Stereo,dsp::Convolution::Trim,int,dsp::Convolution::Normalise)

that’s the only change I made, and it was required just to get it to compile.

When I build in Debug mode, the updated version generates a repetitive assertion error here:

I’ve modified jassertfalse to just crash, and using gdb/bt, its not a consistent nesting to the point of the assertion failure. If I just analyze the core without causing jassertfalse to crash, it is also inconsistent.
This hints of a stack corruption but I’m really at a loss at this point.

Keeping in mind that this code has worked without glitch for about a year when running on JUCE 5.4.3, can anyone suggest what may be causing a crash (and/or the assertion warning) when updating to JUCE 6? Note that I am not the originator of this code, and I am pretty new to JUCE so I’m climbing a pretty steep curve at the moment…

It looks like something is expecting a larger buffer than is actually available. It’s difficult to be more specific without seeing the code and the stack-trace.

In a debugger, you should be able to work out which buffer is being written-to, and where the write is coming from. After that, it should be possible to resolve the issue, e.g. by increasing the size of the buffer, or by initialising the writer so that it doesn’t try to write beyond the limits of the buffer.

The crash and the assertion failure are deep in the bowels of JUCE (not implying its a JUCE problem, just that it is really tough to debug as a JUCE newbie).

Regarding a larger buffer size, and keeping in mind that this has been working in 5.4.5, is there anything you can think of that should be done differently between JUCE 5.4.3 vs JUCE 6?

The code makes a few calls to loadImpulseResponse() (one for each filter) at startup, and never again. It was being done in the MainComponent constructor, so I moved that to MainComponent::prepareToPlay(), and also tried putting it into the first call to MainComponent::getNextAudioBlock().
That didn’t make any difference.

You mentioned that loadImpulseResponse() is done asynchronously. I assume that implies that it doesn’t block the caller until completed, correct?

No, the most relevant change I can think of is the update to the Convolution. I can’t think of any potential pitfalls other than the change to the Convolution threading guidelines.

The assertion is telling you that the destination buffer is too short to store a sample at the requested index. This means that the function will write the new value at an invalid location in memory, which is probably causing your crash. If you have a backtrace from the assertion, this should tell you where the buffer is being written. If you share the backtrace here, it’s possible someone will be able to see what’s going wrong, or suggest another place to look.

Well, I sure wish I didn’t have to do this, but here it is…
Sure would be less ugly if the window scrolled horizontally…

(gdb) bt
#0 0x0000000000470f70 in juce::AudioBuffer::setSample (this=0x1bfdb00, destChannel=0, destSample=512, newValue=0.786234915)
at /opt/JUCE/modules/juce_audio_basics/buffers/juce_AudioSampleBuffer.h:626
#1 0x00000000006ca617 in juce::dsp::CrossoverMixer::processSamples<juce::dsp::Convolution::Impl::processSamples(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)::{lambda(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)#1}, juce::dsp::Convolution::Impl::processSamples(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)::{lambda(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)#2}, juce::dsp::Convolution::Impl::processSamples(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)::{lambda()#3}>(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&, juce::dsp::Convolution::Impl::processSamples(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)::{lambda(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)#1}&&, juce::dsp::Convolution::Impl::processSamples(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)::{lambda(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)#2}&&, juce::dsp::Convolution::Impl::processSamples(juce::dsp::AudioBlock const&, juce::dsp::AudioBlock&)::{lambda()#3}&&) (this=0x1bfdae0, input=…, output=…,
current=<unknown type in ~myapp/Builds/LinuxMakefile/build/chirpAoA, CU 0x60004e, DIE 0x6808e9>,
previous=<unknown type in ~myapp/Builds/LinuxMakefile/build/chirpAoA, CU 0x60004e, DIE 0x6808ff>,
notifyDone=<unknown type in ~myapp/Builds/LinuxMakefile/build/chirpAoA, CU 0x60004e, DIE 0x680915>)
at /opt/JUCE/modules/juce_dsp/frequency/juce_Convolution.cpp:962
#2 0x00000000006b43b4 in juce::dsp::Convolution::Impl::processSamples (this=0x1bfdab0, input=…, output=…)
at /opt/JUCE/modules/juce_dsp/frequency/juce_Convolution.cpp:1068
#3 0x0000000000695ce3 in juce::dsp::Convolution::<lambda(const auto:11&, auto:12&)>::operator()<juce::dsp::AudioBlock, juce::dsp::AudioBlock >(const juce::dsp::AudioBlock &, juce::dsp::AudioBlock &) const (__closure=0x7f97332966b0, in=…, out=…)
at /opt/JUCE/modules/juce_dsp/frequency/juce_Convolution.cpp:1288
#4 0x0000000000695ef4 in juce::dsp::Convolution::Mixer::processSamples<juce::dsp::Convolution::processSamples(const juce::dsp::AudioBlock&, juce::dsp::AudioBlock&, bool)::<lambda(const auto:11&, auto:12&)> >(const juce::dsp::AudioBlock &, juce::dsp::AudioBlock &, bool, <unknown type in ~myapp/Builds/LinuxMakefile/build/chirpAoA, CU 0x60004e, DIE 0x6814f0>) (this=0x1befd28, input=…, output=…,
isBypassed=false, processWet=<unknown type in ~myapp/Builds/LinuxMakefile/build/chirpAoA, CU 0x60004e, DIE 0x6814f0>)
at /opt/JUCE/modules/juce_dsp/frequency/juce_Convolution.cpp:1176
#5 0x0000000000695a11 in juce::dsp::Convolution::processSamples (this=0x1befd20, input=…, output=…, isBypassed=false)
at /opt/JUCE/modules/juce_dsp/frequency/juce_Convolution.cpp:1289
#6 0x0000000000427880 in juce::dsp::Convolution::process<juce::dsp::ProcessContextReplacing, 0> (this=0x1befd20, context=…)
at /opt/JUCE/modules/juce_dsp/frequency/juce_Convolution.h:190
#7 0x000000000041ba4a in bandlimitedAoA::processChirpEnergy (this=0x1bed5d8)
at …/…/Source/bandlimitedAoA.cpp:125
#8 0x000000000040e481 in MainComponent::getNextAudioBlock (this=0x1bec400, bufferToFill=…)
at …/…/Source/MainComponent.cpp:378
#9 0x00000000004912ff in juce::AudioSourcePlayer::audioDeviceIOCallback (this=0x1bec810, inputChannelData=0x20e4e30, totalNumInputChannels=6,outputChannelData=0x0, totalNumOutputChannels=0, numSamples=1024)
at /opt/JUCE/modules/juce_audio_devices/sources/juce_AudioSourcePlayer.cpp:138
#10 0x000000000048db11 in juce::AudioDeviceManager::audioDeviceIOCallbackInt (this=0x1bec4c8, inputChannelData=0x20e4e30, numInputChannels=6,outputChannelData=0x0, numOutputChannels=0, numSamples=1024)
at /opt/JUCE/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp:825
#11 0x00000000004970cd in juce::AudioDeviceManager::CallbackHandler::audioDeviceIOCallback (this=0x1be6a70, ins=0x20e4e30, numIns=6, outs=0x0, numOuts=0,numSamples=1024)
at /opt/JUCE/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp:54
#12 0x0000000000485014 in juce::(anonymous namespace)::ALSAThread::run (this=0x1c1f740)
at /opt/JUCE/modules/juce_audio_devices/native/juce_linux_ALSA.cpp:720
#13 0x00000000005ebfd9 in juce::thread::threadEntryPoint (this=0x1c1f740)
at /opt/JUCE/modules/juce_core/threads/juce_Thread.cpp:96
#14 0x00000000005ec0a8 in juce::juce_threadEntryPoint (userData=0x1c1f740)
at /opt/JUCE/modules/juce_core/threads/juce_Thread.cpp:118
#15 0x000000000060ef36 in juce::threadEntryProc (userData=0x1c1f740)
at /opt/JUCE/modules/juce_core/native/juce_posix_SharedCode.h:850
#16 0x00007f97bdd2e6ba in start_thread (arg=0x7f9733297700)
at pthread_create.c:333
#17 0x00007f97bcfbb51d in clone ()
at …/sysdeps/unix/sysv/linux/x86_64/clone.S:109

My best guess is that the maximum block size passed to Convolution::prepare is smaller than the audio block being passed to Convolution::process.

I’d recommend adding some logging to Convolution::processSamples around line 1281 to find the input block size (e.g. DBG ("processSamples: " << input.getNumSamples());) and something similar to print spec.maximumBlockSize in Convolution::prepare, line 1265. If the buffer passed to process is longer than the maximumBlockSize then you’ll get problems.

1 Like

Ok, interesting…
I added the logging you suggest to both versions.
The value in prepare is 512, but the value in processSamples is 8192.
In JUCE 6 this crashes, but in the older build it seems to be fine with it.
Does that make any sense?
UPDATE:
But the 512 used in prepare is the filter length, the value in processSamples (as best I can tell)
is the block size. Do they have to be the same?
UPDATE2:
Hmmm, wait, cancel that… I’m going crosseyed looking at this.
ONE MORE UPDATE:
Ok, sorry for the confusion… yes, when I increase the maxBlockSize in prepare(), things seem to be working!

You should not guess the maxBlockSize, you should know :slight_smile:

The value in prepare should be a maximum. You must not call processSamples() with greater buffers than maxBlockSize.

In a plugin that value is supplied by the host when it calls prepareToPlay().

In an audio app this is available in the AudioIODevice calling getCurrentBufferSizeSamples()

Hope that helps

@daniel,
Absolutely… I took a high-side “guess” just to see if the crash was resolved.
Seems the incoming size varies a bit over time. Gotta understand that.
Anyway, the good news is the crash is now understood (thank you @reuk).
The bad news is this just brings me back to my original question on running
this in a headless configuration. Having about a week’s worth of experience
with JUCE, I think that’s gonna be a challenge…
Much thanks to all.

There’s a few situations where you might get different blockSizes in my experience, it’s good to be sure of which exact situation you are facing.

The ones that I have seen have mostly been in a plugin context, for example when scanning a plugin, a host may scan it with a block size different than the block size that will actually run when the plugin loads for real.

Another situation is with hosts like FL Studio that have a variable block size, in which case it is necessary to regulate this with rebuffering.

It sounds to me that your situation is more similar to the first scenario, but in my experience you will catch every incoming block size change in prepareToPlay, I’ve never come across a situation where processBlock/getNextAudioBlock received a block size that was not first indicated in prepareToPlay.

So in this case you need to be sure to reset/initialize the convolution engine passing in the latest block size every time.

When this application runs (note, I didn’t write it, so I’m not sure of much of the logic), prepareToPlay() is called twice… once at startup, then again a few seconds later after a releaseResources() is called for some reason.
In the MainComponent constructor the audio device is set up with a call to getAudioDeviceSetup() followed by some setup changes then setAudioDeviceSetup(). I’m guessing that this probably causes the releaseResourses() callback to get hit; hence the second prepareToPlay() call; but that also implies to me that prepareToPlay was called the first time prior to the device being setup. Does that make any sense?
Is there any overview documentation that talks about all this?

I am guessing that the device was initialised first with a default (e.g. setAudioChannels (input, output)) and later the code might set it up with some bespoke settings (which could be done in one go if done correctly).

Unfortunately the JUCE documentation doesn’t document concepts, only a reference for each method is given (I think out of fear to be consumed maintaining the documentation).

I would take this statement with a pinch of salt. Hosts are allowed to send arbitrary buffer sizes. If your plugin doesn’t cope with those situations, it is your fault, not the hosts. And there are more hosts that a developer can test themselves.
The AudioIODevice though changes the blocksize only if you ask it to open with a specific buffer size.

Actually you are right, I contradicted myself from earlier, as there are definitely DAWs like FL Studio and a few music players that support plugins, that do not send a regular buffer size. It seems the universal solution is to either handle processing variable buffer sizes, or to regulate the buffer sizes by rebuffering. I think the latter solution is needed for the dsp::Convolution which definitely wants regular buffer sizes

Yeah, I’ve seen FL go down to 1 sample at a time occasionally.
I personally find the best way to look at the incoming buffer is as a continuous steam of single samples.
This stops me overloading the block processing start, and I have to do my own buffer filling if I want certain block sizes for FFT processing, etc. - not forgetting to set ‘latency’ of course. :slight_smile:
It’s not much of a pain really.