Basic questions: process block, dynamic resizing of things, etc

Hello everyone,

I’m still fairly new to JUCE and I have some basic questions about the operation of the process block. All answers are greatly appreciated!

So – I know that the processBlock() needs to run as quickly as possible, so we must avoid doing any memory reallocation in this function. But what about subroutines that are called within the flow of processBlock()? If I have a helper function that needs to dynamically resize an array on the fly, and I want to call this function within processBlock(), is that a no-no? Does everything called/declared within the body of processBlock() run on a single thread? (sorry if this is a stupid question)

Would it be less problematic to instead declare a new float[desiredSize] within the subroutine and then delete it every time, or does declaring a new array also run into the memory allocation problem?

Specifically, I’m working on implementing a pitch shifting algorithm, and for my resynthesis phase’s OLA step I need to calculate and store the sample values of a Hanning window function whose length is proportional to the fundamental frequency (I’ve already dealt with detecting the fundamental frequency w/ the YIN algorithm). So theoretically, I could either have a global float array that gets resized & filled with values for each signal vector, or I could create a new array with each call of processblock() and delete it before the end of processBlock().

If anyone’s curious and wants to sweep an eye over my code for glaring bugs, it’s on GitHub here. (my plugin doesn’t have a GUI yet, I’m just working on my implementations of all the back-end code for now)

Thanks everyone!

Yes that is a no-no. Figure out the maximum size you need and preallocate in prepareToPlay or even better in the constructor. Note that many arrays allow to reserve space and then resizing doesn’t reallocate unless the reserve is exceeded.

Yes.

Yes it is, because processBlock() will have to wait for that subroutine before it can continue.
You could only trigger a resize asynchronously, that would then be available a few calls later.

The better approach is
a) reserve bigger buffers (and you need to use a different value for numSamples that fits the actual available number of samples from the input)
b) wrap the processBlock in a private function that you call in a loop of smaller chunks:

void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi) override
{
    auto left = buffer.getNumSamples();
    while (left > 0)
    {
        auto numSamples = std::max (left, maxBufferSize);

        juce::AudioBuffer<float> proxy (buffer, buffer.getNumSamples(), buffer.getNumSamples() - left, numSamples);
        // TODO: fix midi timestamps...
        processBlockPrivate (proxy, midi);

        left -= numSamples;
    }
}

EDIT: @pflugshaupt was quicker, same advise…

I think we clicked at the exact same time :wink:

for your specific example i’d say that the others are definitely right and it’s good to just use the maximum size by default, which in turn means deciding for a minimum possible frequency. gladly the difference between buffer sizes varies massively on low frequencies. the most extreme example is 0hz and 1hz, where 0hz is theoretically infinite, while 1hz is sampleRate. there are also examples where reallocating a dynamic error within processBlock is ok, like when the user drags the plugin to a track with a different channel count, or when the user changes the sampleRate of the project, because that doesn’t happen very often. A related issue that I ran into lately was, when i allocated a lot of vectors to buffer.getNumSamples() in my current project, because I mainly use cubase, where processBlock has a constant buffer size. My plugin happened to break massively in FL Studio and a lot of other DAWs apparently because they have a variable buffer size and it changes almost all the time, so all my buffers reallocated like hell. The fix was to use the 2nd argument from prepareToPlay instead, but make sure to add a check wether or not the value has changed, because some DAWs might call prepareToPlay more often than just on init.

Thanks for the advice! I have a few questions, which may just give away my inexperience with C++…

first, when @pflugshaupt said to preallocate a maximum size for the buffer and then “resizing doesn’t reallocate” – my question with this is, is RESIZING an array/buffer/vector a distinct operation from REALLOCATING the amount of memory for the array/buffer/vector? I was assuming that the amount of elements actually stored in the array/buffer/vector object at any given moment would always be equal to the object’s capacity in number of elements… is this not the case? And if you have an array with max size 512 and only 256 elements exist inside it, would you have 256 “0” or “null” elements at the end of your array, or would those element indices not exist, even though they are within the range of the array’s max size?

As far as your example code with the wrapped processBlockPrivate – I think I understand what’s going on here… so the while (left > 0) loop would only execute more than once if the incoming buffer contains more samples than the previously defined maxBufferSize, right? And the proxy buffer processes slices of the input buffer no larger than maxBufferSize, but possibly much smaller, correct?

I think I understand the logistics here as far as audio buffers are concerned: declare a global maximum frame size, and slice incoming buffers into slices of size <= framesize if they’re larger. But why doesn’t the line
juce::AudioBuffer<float> proxy (buffer, buffer.getNumSamples(), buffer.getNumSamples() - left, numSamples);
violate the rule about reallocating memory in the process block? I get that this constructor “creates a buffer using a pre-allocated block of memory”, but even though the BLOCK is preallocated, what if the actual audioBuffer object you’re creating contains fewer items than the preallocated default size? That wouldn’t trigger a resizing/reallocating operation?

If I can get my brain around this, I will definitely need to use the same or a similar approach for several arrays & vectors containing float & integer data that need to correspond to the length of the input data or the fundamental frequency of the input audio… I’m guessing it’s the same thing, where I can declare a maximum/default size and then they would possibly be smaller… but again, is this not resizing/reallocating in the audio thread…?

Take a look at std::vector docs. Size and capacity are indeed different properties. resize(), push_back(), etc only allocate if the current size has reached the current capacity. You can increase the capacity of a vector with reserve(), without resizing it. This allows to allocate a known maximum once and avoid further allocations.

Because this constructor doesn’t hold sample data. Instead it references the original buffer and appears like a subset of the samples.

AudioBuffer has a flag called “avoid reallocation”, but you cannot rely on that. If the size is bigger, it has to allocate.

OK, I think I understand. @daniel in your example with the proxy audioBuffer, what exactly would the code look like for preallocating the memory for the proxy buffer? Is it declared as a Juce audioBuffer object named proxy and the max size set with proxy.setSize(2, maxSize) in the pluginProcessor constructor?

Also, in this example, is there a specific reason why the midi buffer is also being passed to the processBlockPrivate? If I have a separate class/functions to handle processing midi input, could that be run at the beginning of the top-level processBlock() regardless of the size of the incoming audio buffer?

I think I understand the concepts here… so any array/vector that needs to hold a certain number of data items corresponding to either the length of the input buffer or the fundamental frequency, I can simply declare a maximum possible size, preallocate this memory, and then if the incoming data contains fewer items, resize to a size smaller than the maximum, and all of this should be fine to do in functions called on the audio thread. Is that correct?

For sequences of float data, is it still true that a std::vector is the fastest container? Or is the difference between that and a juce Array fairly negligible?

@daniel would you need any additional code to write the output of processBlockPrivate() from the proxy buffer to the I/O buffer of processBlock() ? or would this happen within processBlockPrivate?

@daniel I tried implementing this proxy buffer trick in my processBlock(), and on the line juce::AudioBuffer<float> proxy (buffer, buffer.getNumSamples(), buffer.getNumSamples - left, numSamples); I’m getting an error “no matching constructor for initialization of AudioBuffer”. Do I need to declare proxy elsewhere first or something?

Apologies, was written from memory in a rush. Should have double checked the docs.

This is how it should look like:

juce::AudioBuffer<float> proxy (buffer.getArrayOfWritePointers(),  // array of raw float*
                                buffer.getNumChannels(), 
                                buffer.getNumSamples() - left,     // startSample
                                numSamples);                       // numSamples in this sub block

performance-wise it is negligible. But the way resizing is handled differs. std::vector has the reserve functionality and by definition never shrinks its allocation unless you call shrink_to_fit(). Juce::Array does change its allocation when resizing.

yes, except for:

you set it to max size so that you never have to change its size again anymore then. the idea is to just not use the whole data if it’s not needed. but it would still be there and wait for another low note to appear. maybe it will never appear but doesn’t matter

@daniel got it, I got the proxy buffer into processBlockPrivate() trick to work! Thank you!

My remaining question about this is, is there any benefit / reason to also pass the midiBuffer into processBlockPrivate() as opposed to just calling a midi processing function first in the top level processBlock() before the private function is called?

Thanks for your help!

@pflugshaupt so are you saying that in the audio-thread, a std::vector would be the better choice, memory-management-wise?

with a Juce Array, what if you call array.ensureStorageAllocated(MAX_BUFFERSIZE) in the constructor, then for every iteration of the processBlock() you call array.clearQuick() and then build the new array by calling array.add() for each element – wouldn’t this have the same effect of preallocating a block of memory of a maximum size, and then dynamically resizing to a possibly smaller size without reallocating? Or is a std::vector still a better choice across the board?

@hdlAudio ohh interesting… so if you’re doing iterative operations on this array, you could just stop at numSamples - 1, and then even if there are still values at higher indexes in the array, they essentially wouldn’t exist for the purposes of that iteration of the function…

interesting! I think this may end up being the best approach, because memory reallocation would never be required. Arrays are created with MAX_BUFFERSIZE elements, and always remain the same size, and your functions may be told to ignore a subset of the array if the input buffer has fewer samples. correct?

Yes and yes. As long as you don’t call juce::Array::resize() they behave the same way. For audio data I would use juce::AudioBuffer however because it can hold multiple channels.

1 Like

@pflugshaupt got it. thanks!