Dealing with non-multiple buffer sizes while performing FFT based operations

I am working on a filter which performs a FFT on 1024 samples, while pushing 256 new samples every time it iterates. This creates a 75% overlap and I am dealing with the overlap-add so it is working well for 256 and multiples of 256 buffer size. How to deal with other sample sizes, I want to have a consistent sounding results across different buffer sizes so I do not want to update it more frequently or something.

Here is the code that works for 256 samples and multiples of it :

const int numSamples = bufferToWritePointer->getNumSamples();

        accumulation += numSamples;
        int sampleIndex = 0;

        while ((accumulation - maxAccumulation) >= 0) {

            for (int channel = 0; channel < 2; ++channel) {
                float* channelData = bufferToWritePointer->getWritePointer(channel);
                const float* rawData = readBuff->getReadPointer(channel);
                float* fftDataChannel = fftData[channel].data();
                float* tempDataChannel = tempData[channel].data();

                float* overlapBuffer1 = overlapStore[0]->getWritePointer(channel);
                float* overlapBuffer2 = overlapStore[1]->getWritePointer(channel);
                float* overlapBuffer3 = overlapStore[2]->getWritePointer(channel);

                // Shift the previous samples forward by maxAccumulation.
                for (int i = 0; i < fftSize - maxAccumulation; ++i) {
                    fftDataChannel[i] = fftDataChannel[i + maxAccumulation];
                }

                // Append maxAccumulation new samples to the end of the fftData buffer.
                for (int i = 0; i < maxAccumulation; ++i) {
                    fftDataChannel[fftSize - maxAccumulation + i] = rawData[sampleIndex + i];
                }

                // Apply the windowing function to the entire fftSize buffer and copy it to temp buffer.
                for (int i = 0; i < fftSize; ++i) {
                    tempDataChannel[i] = fftDataChannel[i] * windowCoefficients[i];
                }

                // Perform an inplace FFT on the tempDataChannel.
                forwardFFT.performRealOnlyForwardTransform(tempDataChannel);

                // Multiply with the corresponding filter coefficients.

                // Perform the inverse FFT
                inverseFFT.performRealOnlyInverseTransform(tempDataChannel);

                // Overlap-add the processed samples into the output buffer
                for (int i = 0; i < maxAccumulation; ++i) {
                    channelData[sampleIndex + i] += (   tempDataChannel[i] +
                                                        overlapBuffer3[i + maxAccumulation] +
                                                        overlapBuffer2[i + 2 * maxAccumulation] +
                                                        overlapBuffer1[i + 3 * maxAccumulation]) / 4.0f;
                }

                // Store the new overlap samples
                std::copy(tempDataChannel, tempDataChannel + fftSize, overlapBuffer1);
            }

            // change the order of the pointers as
            // from     [ 1, 2, 3 ]
            // to       [ 2, 3, 1 ]
            juce::AudioBuffer<float>* temp = overlapStore[0];
            overlapStore[0] = overlapStore[1];
            overlapStore[1] = overlapStore[2];
            overlapStore[2] = temp;

            sampleIndex += maxAccumulation;
            accumulation -= maxAccumulation;
        }

You have to introduce latency. The following blog might be helpful.

2 Likes

After tearing my hair out to make this work for a couple of days finally settled on this :

here maxAccumulation is 256
and leftOverSamples is 256 at the beginning (this kind of introduces a latency as @zsliu98 mentioned) and because we are reading from partBufferOut make sure to clear it out before processing the first block :

void BExp() {
        const int numSamples = bufferToWritePointer->getNumSamples();
        int sampleIndex = 0;
        int sampleIndexRead = 0;

        // Handle leftover samples first
        if (leftOverSamples) {
            float* write_right_ch_data = bufferToWritePointer->getWritePointer(1);
            float* write_left_ch_data = bufferToWritePointer->getWritePointer(0);

            float* part_out_right = partStoreBufferOUT.getWritePointer(1);
            float* part_out_left = partStoreBufferOUT.getWritePointer(0);

            while (leftOverSamples > 0 && sampleIndex < numSamples) {
                write_right_ch_data[sampleIndex] = part_out_right[partBufferConsumeIndex];
                write_left_ch_data[sampleIndex] = part_out_left[partBufferConsumeIndex];

                sampleIndex++;
                partBufferConsumeIndex++;
                leftOverSamples--;
            }

        }


        while (sampleIndexRead < numSamples) {
            int remainingSamples = numSamples - sampleIndexRead;

            // if there are enough samples to process them and write them to the output.
            if (partBufferFillIndex + remainingSamples >= maxAccumulation) {

                float* part_in_right = partStoreBufferIN.getWritePointer(1);
                float* part_in_left = partStoreBufferIN.getWritePointer(0);
                const float* read_right = readBuff->getReadPointer(1);
                const float* read_left = readBuff->getReadPointer(0);

                int samplesToFill = maxAccumulation - partBufferFillIndex;

                for (int i = 0; i < samplesToFill; i++, sampleIndexRead++, partBufferFillIndex++) {
                    part_in_left[partBufferFillIndex] = read_left[sampleIndexRead];
                    part_in_right[partBufferFillIndex] = read_right[sampleIndexRead];
                }

                // PROCESS THE PARTIAL BUFFER
                processPartBuffer();

                partBufferFillIndex = 0;
                leftOverSamples = 256;
                partBufferConsumeIndex = 0;

                float* write_right_ch_data = bufferToWritePointer->getWritePointer(1);
                float* write_left_ch_data = bufferToWritePointer->getWritePointer(0);

                const float* part_out_right = partStoreBufferOUT.getReadPointer(1);
                const float* part_out_left = partStoreBufferOUT.getReadPointer(0);

                // copy the processed samples to the output buffer to write.
                for (; partBufferConsumeIndex < maxAccumulation && sampleIndex < numSamples; sampleIndex++, partBufferConsumeIndex++, leftOverSamples--) {
                    write_right_ch_data[sampleIndex] = part_out_right[partBufferConsumeIndex];
                    write_left_ch_data[sampleIndex] = part_out_left[partBufferConsumeIndex];
                }
            } else {
                // if the samples are not enough just copy them we will process the next time.
                float* part_in_right = partStoreBufferIN.getWritePointer(1);
                float* part_in_left = partStoreBufferIN.getWritePointer(0);

                const float* raw_in_right = readBuff->getWritePointer(1);
                const float* raw_in_left = readBuff->getWritePointer(0);

                for (int i = 0; i < remainingSamples; i++, sampleIndexRead++, partBufferFillIndex++) {
                    part_in_left[partBufferFillIndex] = raw_in_left[sampleIndexRead];
                    part_in_right[partBufferFillIndex] = raw_in_right[sampleIndexRead];
                }

            }
        }

    }

    void processPartBuffer() {
        for (int channel = 0; channel < 2; ++channel) {
            float* channelData = partStoreBufferOUT.getWritePointer(channel);
            const float* rawData = partStoreBufferIN.getReadPointer(channel);
            float* fftDataChannel = fftData[channel].data();
            float* tempDataChannel = tempData[channel].data();

            float* overlapBuffer1 = overlapStore[0]->getWritePointer(channel);
            const float* overlapBuffer2 = overlapStore[1]->getReadPointer(channel);
            const float* overlapBuffer3 = overlapStore[2]->getReadPointer(channel);

            // Shift the previous samples forward by maxAccumulation.
            for (int i = 0; i < fftSize - maxAccumulation; ++i) {
                fftDataChannel[i] = fftDataChannel[i + maxAccumulation];
            }

            // Append maxAccumulation new samples to the end of the fftData buffer.
            for (int i = 0; i < maxAccumulation; ++i) {
                fftDataChannel[fftSize - maxAccumulation + i] = rawData[i];
            }

            // Apply the windowing function to the entire fftSize buffer and copy it to temp buffer.
            for (int i = 0; i < fftSize; ++i) {
                tempDataChannel[i] = fftDataChannel[i] * windowCoefficients[i];
            }

            // Perform an inplace FFT on the tempDataChannel.
            forwardFFT.performRealOnlyForwardTransform(tempDataChannel);

            // DO WHAT YOU WANT WITH THE FREQUENCY DOMAIN SIGNAL HERE.
            /////////////////////////////


            // Perform the inverse FFT
            inverseFFT.performRealOnlyInverseTransform(tempDataChannel);

            for (int i = 0; i < fftSize; ++i) {
                tempDataChannel[i] = tempDataChannel[i] * windowCoefficients[i];
            }

            // Overlap-add the processed samples into the output buffer
            for (int i = 0; i < maxAccumulation; ++i) {
                channelData[i] = (float)(tempDataChannel[i] +
                                         overlapBuffer3[i + maxAccumulation] +
                                         overlapBuffer2[i + 2 * maxAccumulation] +
                                         overlapBuffer1[i + 3 * maxAccumulation]) * 0.25f;
            }

            // Store the new overlap samples(replace the oldest ones)
            std::copy(tempDataChannel, tempDataChannel + fftSize, overlapBuffer1);
        }

        // change the order of the pointers as
        // from     [ 1, 2, 3 ]
        // to       [ 2, 3, 1 ]
        juce::AudioBuffer<float>* temp = overlapStore[0];
        overlapStore[0] = overlapStore[1];
        overlapStore[1] = overlapStore[2];
        overlapStore[2] = temp;
    }

Hope this will be helpful to anyone.

1 Like