Dealing with variable block size and dsp in FL Studio

Hi,
So, I’m kinda stumped on this one and any leads would be greatly appreciated. I’m having trouble with getting my plugin working in FL Studio. I’m pretty new to dsp and juce in general, but I’m making a pretty simple plugin with stereoswap, delays and couple of filters (one LowPass and one HighPass).
Everything is nice and good when testing in REAPER or juce’s AudioPluginHost, but FL studio makes the filters pop like crazy. The popping goes away when ticking on “use fixed buffer size”, so this issue seems to be related to the variable block sizes FL Studio gives for some reason.

I’m using dsp filters with ProcessDuplicators (dsp::ProcessorDuplicator<dsp::IIR::Filter<float>, dsp::IIR::Coefficients<float>> lowPassFilter;) and simply setting their states every processBlock like this
lowPassFilter.state = *dsp::IIR::COefficients<float>::makeFirstOrderLowPass(sampleRate, lowPassFreq); and then using process on the audio buffer. I thought this implementation should be independent of block size, but I seem to be missing something.

So, I’ve read several forum posts, mainly this one, and tried to make the buffer size constant on my own by using a circular AudioBuffer.

I save the desiredBlockSize given in prepareToPlay(), then in processBlock I keep filling the circular buffer until I have enough, then move that amount of buffers to the output buffer and do my processing in processBlockInternal. avbsBuffer is of type CircularAudioBuffer, which you can see below in its entirety. It just has an AudioBuffer and an AbstractFifo to keep track of writes and reads.

void JucePluginAppAudioProcessor::processBlock(AudioBuffer<float> &buffer, MidiBuffer &midiMessages)
{
    avbsBuffer.addSamples(buffer);
    if (avbsBuffer.getSize() >= desiredBlockSize)
    {
        avbsBuffer.popSamples(desiredBlockSize, buffer);
        processBlockInternal(buffer, midiMessages);
    }
    else
    {
        // fill buffer wth zeros
        buffer.applyGain(0);
    }
}

The CircularAudioBuffer class in its entirety looks like this

class CircularAudioBuffer
{
public:
    CircularAudioBuffer(int numChannels, int maxNumSamples): internalBuf(numChannels, maxNumSamples * 2),
                                                             fifo(maxNumSamples*2)
    {}
    void addSamples(AudioBuffer<float>& buf);
    void popSamples(int numSamples, AudioBuffer<float>& bufToOverwrite);
    int getSize() { return fifo.getNumReady() }
    void setSize(int numChannels, int numSamples)
    {
        fifo.setTotalSize(numSamples*2);
        fifo.reset();
        internalBuf.setSize(numChannels, numSamples * 2);
        internalBuf.clear();
    }
    void clear()
    {
        fifo.reset();
        internalBuf.clear();
    }
private:
    AudioBuffer<float> internalBuf;
    AbstractFifo fifo;
};

In the addSamples and popSamples methods I just copy over the samples using AbstractFifo to keep track of all operations.

void CircularAudioBuffer::addSamples(AudioBuffer<float> &buf)
{
    int start1, size1, start2, size2;
    int numSamples = buf.getNumSamples();
    fifo.prepareToWrite(numSamples, start1, size1, start2, size2);
    if (size1 > 0)
    {
        for (int c = 0; c < buf.getNumChannels(); ++c)
        {
            internalBuf.copyFrom(c, 0, buf.getReadPointer(c) + start1, size1);
        }
    }
    if (size2 > 0)
    {
        for (int c = 0; c < buf.getNumChannels(); ++c)
        {
            internalBuf.copyFrom(c, size1, buf.getReadPointer(c) + start2, size2);
        }
    }
    fifo.finishedWrite(size1+size2);
}

void CircularAudioBuffer::popSamples(int numSamples, AudioBuffer<float> &bufToOverwrite)
{
    int start1, size1, start2, size2;
    fifo.prepareToRead (numSamples, start1, size1, start2, size2);

    if (size1 > 0)
    {
        for (int c = 0; c < bufToOverwrite.getNumChannels(); ++c)
        {
            bufToOverwrite.copyFrom(c, 0, internalBuf.getReadPointer(c) + start1, size1);
        }
    }
    if (size2 > 0)
    {
        for (int c = 0; c < bufToOverwrite.getNumChannels(); ++c)
        {
            bufToOverwrite.copyFrom(c, size1, internalBuf.getReadPointer(c) + start2, size2);
        }
    }

    fifo.finishedRead (size1 + size2);
}

With this implementation though the audio is heavily distorted even on “normal” hosts.
I really can’t seem to grasp what the problem is here, but that could also be because I’ve misunderstood something fundamental. Any and all help is greatly appreciated!

you resize your buffers in prepareToPlay and basically only use them in processBlock. prepare’s 2nd arg is the max block size. numSamples is always <= max block size.

Thanks for a quick reply.

Yeah, I call avbsBuffer.setSize(2, samplesPerBlock) in prepareToPlay so the buffer size is 2*samplesPerBlock and I think that would be sufficient.

I also realized that my question may well be way too broad. So, to sum it up neatly, my problem is that my dsp filters (highPass and lowPass) really don’t like FL Studio’s variable block size. Is this a problem in the filters or is it more likely that I implemented them wrong?

All of the Circular Buffer stuff can be trashed if I get the filters working. The CircularAudioBuffer stuff is just my try of fixing this issue but nothing I do seems to help much.

ok i thought you were trying to write a delay or smth. variable block sizes are in every daw, except cubase. you deal with them by not using the whole buffers everytime, but only numSamples of them

I use contexts and the process functions like this:

// Update filter states.
*lowPassFilter.state = *getLowPassCoefficients(settings, (float)currSampleRate);
*highPassFilter.state = *getHighPassCoefficients(settings, (float)currSampleRate);

// Process stuff
dsp::AudioBlock<float> block (tempBuffer);
auto context = dsp::ProcessContextReplacing<float>(block);
lowPassFilter.process(context);
highPassFilter.process(context);

If I’m not mistaken, this context only uses numSamples? This is the only code in processBlockInternal regarding these filters but from what I’ve read this should work regardless of buffer size. How wrong am I?

The getLowPassCoefficients and getHighPassCoefficients are pretty much just convenience functions that return updated coefficients. This is the correct way to do it right?

ReferenceCountedObjectPtr<dsp::IIR::Coefficients<float>> getLowPassCoefficients(const Settings &chainSettings,
                                                                                float sampleRate)
{
    return dsp::IIR::Coefficients<float>::makeFirstOrderLowPass(sampleRate, chainSettings.lowPassFreq);
}

What is block? Is it the AudioBuffer passed into processBlock? If it’s a different buffer, is there a chance that its size doesn’t match the size of the processBlock buffer?

it’s a newly created dsp::AudioBlock. It’s created right above context and used when creating context. I guess I should use curly brackets when constructing stuff like this :slight_smile:

Oops, sorry. I guess the question still stands for tempBuffer.

Hi, thanks for quick answers!
So, tempBuffer is an AudioBuffer initialized to samplesPerBlock in prepareToPlay(). My plugin involves delays, and I want to apply these filters only to the delayed samples while leaving the dry signal untouched. That’s why I pop samples from the DelayLines (which AFAIK I actually need two of because left and right channels have different delays) to tempBuffer and then do processing on it. At the end of processBlock I just do buffer.addFrom( … tempBuffer, buffer.getNumSamples());

In that case, the pops are probably because the filter is processing the full tempBuffer, but the actual buffer is shorter. This will leave the filter in the wrong state for the next process call.

You could try:

const auto block = dsp::AudioBlock<float> (tempBuffer).getSubBlock (0, buffer.getNumSamples());
1 Like

Dang, you were spot on!
This possibility completely slipped my mind while trying to debug. Thank you lots for your help!