A JUCE-ier way of handling this


#1

I have a plugin with a spectrum analyser… the spectrum runs on its own thread and asks for a FIFO from the audio processor ~50 times a second.
In my audio processor I have the following code to add to the FIFO:

void processBlock(AudioBuffer<float>& buffer, MidiBuffer& midiMessages)
{
    // get number of samples, channel data, etc...
    // ...

    for (int i = 0; i < numSamples; i++)
    {
        float mono = 0.5f * (leftChannel[i] + rightChannel[i]);

        // fifo is a std::vector and private member of the processor class
        fifo.push_back(mono);
    }
}

Also in the processor:

// size parameter is the number of samples/bins the FFT (in the Spectrum class) requires
std::vector<float> getFIFO(int size)
{
    if (fifo.size() < size)
        // not enough samples in fifo yet... return empty vector
        return {};
    
    // calculate the difference in size between the fifo size and the required size
    int difference = fifo.size() - size;

    // rotate the vector so the most recent <size> samples are at the front
    std::rotate(fifo.begin(), fifo.begin() + difference, fifo.end());
    
    // resize the vector to the required size
    fifo.resize(size); // [1]

    return fifo; // [2]
}

The above method is called repeatedly by the Spectrum class which then processes the FIFO to produce frequency bins etc.
However, using this method an issue can arise where, during the execution of getFIFO, more samples are added to the FIFO after resizing it, and before returning it (between points [1] and [2]) because the audio thread is still running in the background…
To solve this issue I simply took a copy of the current FIFO at the start of getFIFO, like so:

// size parameter is the number of samples/bins the FFT (in the Spectrum class) requires
std::vector<float> getFIFO(int size)
{
    // get a copy of the fifo vector
    std::vector<float> fifoCopy = fifo;

    if (fifoCopy.size() < size)
        // not enough samples in fifo yet... return empty vector
        return {};
    
    // calculate the difference in size between the fifo size and the required size
    int difference = fifoCopy.size() - size;

    // rotate the vector so the most recent <size> samples are at the front
    std::rotate(fifoCopy.begin(), fifoCopy.begin() + difference, fifoCopy.end());
    
    // resize the vector to the required size
    fifoCopy.resize(size); // [1]

    return fifoCopy; // [2]
}

This does solve the issue and guarantees that the returned vector always has the correct size. However I feel there may be a better, more JUCE-y way of achieving this. Would anyone be able to suggest a better method?


#2

I rely on Timur’s AbstractFIFO class:

I am not sure, if I would go down the rout of resizing the buffer… it adds quite some complexity for IMHO little benefit. Just make it big enough initially? In realtime from the envisioned frame rate you can do a good estimate on how big the buffer has to be. And in non realtime it doesn’t matter, you will have to block there anyway, otherwise it will keep growing until you have almost all audio in the buffer…

N.B. while reading your post I realise, I will have to add a flag, when the processing keeps falling behind, since each under-run adds a discontinuity… It will probably be gone without noticing, but once you use it for analytics, like keeping a max value, it will show up.


#3

Thanks, I’ll have a look at the AbstractFIFO class…

About resizing the buffer… I want to make sure that when I’m updating the spectrum I always get the N most recent samples. For example if I have a sample rate of 48000 for each frame 960 audio samples have come in (obviously this isn’t going to be consistent frame-to-frame since the audio is coming in blocks… but 960 would be the average). If I have an FFT size of 512 samples, 448 samples are going to waste and I’d rather throw away older samples than the newer ones. However with a large FFT size like 8192, I want to be able to reuse samples used in previous frames to make sure I always have 8192 samples and 50 fps.

I have just realised an issue with making a copy of the fifo vector which is that the original will keep growing and growing without ever throwing out old samples since only the copy is resized down to the required size.

Maybe I’ll have to rethink how I do all this… thanks for your input!


#4

Maybe you want to use a ring buffer of a fixed size which can then be filled with however many samples are coming in each block? That way you will always have access to the N most recent samples independent of block size.

So you would fill it in the audio thread, and then read it in the thread that does the FFT every N ms.


#5

I had considered a ring buffer before… might go back to that idea. Thanks!