This thread has inspired me to rewrite my circular buffer methods so I can easily swap both approaches to check for performance gains. Feel free to comment if you find something that can still be optimized! Here’s my last version with two classes: CircularAudioBuffer and P2CircularAudioBuffer with the mask trick which may give it a little edge when getting samples individually. Use the methods push/pushBuffer to add samples and get/getBuffer to get them.
// Author: Qfactor (2023). Released with the MIT license.
// If you change something please add a note here.
/**
Circular buffer where you can push samples and get the last samples written
individually or in blocks
*/
template<class Type = float>
class CircularAudioBuffer : public AudioBuffer<Type>
{
protected:
int ptr; // writing position
int lastPosition; // numOfSamples-1
public:
CircularAudioBuffer(int numChannels, int numOfSamples)
:AudioBuffer<Type>(numChannels, numOfSamples)
, ptr(0)
, lastPosition(numOfSamples-1)
{
}
virtual void setSize(int newNumChannels,
int numOfSamples,
bool keepExistingContent = false,
bool clearExtraSpace = false,
bool avoidReallocating = true)
{
ptr=0;
lastPosition = numOfSamples - 1;
AudioBuffer<Type>::setSize(newNumChannels, numOfSamples, keepExistingContent, clearExtraSpace, avoidReallocating);
}
virtual void push(const Type source)
{
jassert(AudioBuffer<Type>::getNumChannels() == 1);
AudioBuffer<Type>::setSample(0, ptr, source);
if (ptr==lastPosition)
ptr=0;
else
ptr++;
}
virtual void push(const Type sourceLeft, const Type sourceRight)
{
jassert(AudioBuffer<Type>::getNumChannels() == 2);
AudioBuffer<Type>::setSample(0, ptr, sourceLeft);
AudioBuffer<Type>::setSample(1, ptr, sourceRight);
if (ptr==lastPosition)
ptr=0;
else
ptr++;
}
template<class SourceType>
void pushBuffer(const SourceType* sourceBuffer, const int sourceStartSample, const int numSamples)
{
jassert(numSamples < AudioBuffer<Type>::getNumSamples());
jassert(sourceStartSample >= 0);
jassert(AudioBuffer<Type>::getNumChannels() == 1);
sourceBuffer+=sourceStartSample;
int ptrFinal = ptr + numSamples;
if (ptrFinal<AudioBuffer<Type>::getNumSamples())
{
AudioBuffer<Type>::copyFrom(0,ptr,sourceBuffer,numSamples);
}
else
{
ptrFinal-=AudioBuffer<Type>::getNumSamples();
const auto samplesToEnd=AudioBuffer<Type>::getNumSamples()-ptr;
AudioBuffer<Type>::copyFrom(0,ptr,sourceBuffer,samplesToEnd);
AudioBuffer<Type>::copyFrom(0,0,sourceBuffer+samplesToEnd,ptrFinal);
}
ptr=ptrFinal;
}
template<class SourceType>
void pushBuffer(SourceType* sourceBufferLeft, SourceType* sourceBufferRight, const int sourceStartSample, const int numSamples)
{
jassert(numSamples < AudioBuffer<Type>::getNumSamples());
jassert(sourceStartSample >= 0);
jassert(AudioBuffer<Type>::getNumChannels() == 2);
sourceBufferLeft+=sourceStartSample;
sourceBufferRight+=sourceStartSample;
int ptrFinal = ptr + numSamples;
if (ptrFinal<AudioBuffer<Type>::getNumSamples())
{
AudioBuffer<Type>::copyFrom(0,ptr,sourceBufferLeft,numSamples);
AudioBuffer<Type>::copyFrom(1,ptr,sourceBufferRight,numSamples);
}
else
{
ptrFinal-=AudioBuffer<Type>::getNumSamples();
const auto samplesToEnd=AudioBuffer<Type>::getNumSamples()-ptr;
AudioBuffer<Type>::copyFrom(0,ptr,sourceBufferLeft,samplesToEnd);
AudioBuffer<Type>::copyFrom(1,ptr,sourceBufferRight,samplesToEnd);
AudioBuffer<Type>::copyFrom(0,0,sourceBufferLeft+samplesToEnd,ptrFinal);
AudioBuffer<Type>::copyFrom(1,0,sourceBufferRight+samplesToEnd,ptrFinal);
}
ptr=ptrFinal;
}
template<class SourceType>
void pushBuffer(AudioBuffer<SourceType> &source, const int numChannels, const int sourceStartSample, const int numSamples)
{
jassert(numChannels <= source.getNumChannels()
&& numChannels <= AudioBuffer<Type>::getNumChannels());
jassert(sourceStartSample < source.getNumSamples());
jassert(sourceStartSample + numSamples <= source.getNumSamples()
&& numSamples <= AudioBuffer<Type>::getNumSamples());
switch (AudioBuffer<Type>::getNumChannels()) {
case 1: {
jassert(source.getNumChannels()==1);
pushBuffer(source.getReadPointer(0), sourceStartSample, numSamples);
break;
}
case 2: {
jassert(source.getNumChannels()<=2);
pushBuffer(source.getReadPointer(0), source.getReadPointer(jmin(1, numChannels - 1)), sourceStartSample, numSamples);
break;
}
default: {
jassertfalse; //only mono and stereo signals are supported
break;
}
}
}
template<class SourceType>
void pushBuffer(AudioBuffer<SourceType> &source)
{
pushBuffer<SourceType>(source, source.getNumChannels(), 0, source.getNumSamples());
}
template<class SourceType>
void pushBuffer(AudioBuffer<SourceType> &source,const int sourceStartSample, const int numSamples)
{
pushBuffer<SourceType>(source, source.getNumChannels(), sourceStartSample, numSamples);
}
virtual Type get(int channel, int delay) const
{
jassert(delay<AudioBuffer<Type>::getNumSamples());
jassert(channel<AudioBuffer<Type>::getNumChannels());
int pos=ptr - delay - 1;
if (pos<0) pos+=AudioBuffer<Type>::getNumSamples();
return AudioBuffer<Type>::getSample(channel,pos);
}
virtual Type get(int delay) const
{
jassert(AudioBuffer<Type>::getNumChannels() == 1);
return get(0, delay);
}
// extract numSamples with a certain delay from the CircularBuffer
// and start writing them at destinationStartSample
template<class DestinationType>
void getBuffer(int delay, AudioBuffer<DestinationType> &destination, int destinationStartSample, int numSamples)
{
jassert(destinationStartSample+numSamples<=destination.getNumSamples());
jassert(destination.getNumSamples()<=AudioBuffer<Type>::getNumSamples());
jassert(delay+numSamples<=AudioBuffer<Type>::getNumSamples());
int endPtr=ptr-delay;
if (delay && endPtr<0)
endPtr+=AudioBuffer<Type>::getNumSamples();
int ptrFinal = endPtr - numSamples;
if (ptrFinal >= 0)
{
destination.copyFrom(0,destinationStartSample,*this,0,ptrFinal,numSamples);
if (destination.getNumChannels() > 1)
{
destination.copyFrom(1,destinationStartSample,*this,jmin(1,AudioBuffer<Type>::getNumChannels()-1),ptrFinal,numSamples);
}
}
else
{
ptrFinal += AudioBuffer<Type>::getNumSamples();
// samples from ptrFinal to the end of the circular buffer
numSamples-=endPtr;
destination.copyFrom(0,destinationStartSample,*this,0,ptrFinal,numSamples);
if (destination.getNumChannels() > 1)
{
destination.copyFrom(1,destinationStartSample,*this,jmin(1,AudioBuffer<Type>::getNumChannels()-1),ptrFinal,numSamples);
}
// samples continuing at the start of the circular buffer until endPtr
destinationStartSample+=numSamples;
destination.copyFrom(0,destinationStartSample,*this,0,0,endPtr);
if (destination.getNumChannels() > 1)
{
destination.copyFrom(1,destinationStartSample,*this,jmin(1,AudioBuffer<Type>::getNumChannels()-1),0,endPtr);
}
}
}
// extract the last destination.getNumSamples() of the CircularBuffer
template<class DestinationType>
void getBuffer(AudioBuffer<DestinationType> &destination)
{
getBuffer(0,destination,0,destination.getNumSamples());
}
// extract destination.getNumSamples() with a certain delay from the CircularBuffer
template<class DestinationType>
void getBuffer(int delay, AudioBuffer<DestinationType> &destination)
{
getBuffer(delay,destination,0,destination.getNumSamples());
}
};
/**
Circular buffer optimized to push samples and get the last pushed samples
*individually* (it is created with a power of 2 size bigger than numOfSamples
to be able to use a binary mask for the modulo).
It supports numOfSamples < 2^30 = 1073741824.
*/
template<class Type = float>
class P2CircularAudioBuffer : public CircularAudioBuffer<Type> {
// we can use lastPosition = nextPowerOfTwo(numOfSamples) - 1 as the mask to do the modulo
public:
P2CircularAudioBuffer(int numChannels, int numOfSamples)
:CircularAudioBuffer<Type>(numChannels, nextPowerOfTwo(numOfSamples))
{
// nextPowerOfTwo supports numOfSamples < 2^30 = 1073741824
jassert(numOfSamples <= 1073741824);
}
void setSize(int newNumChannels,
int numOfSamples,
bool keepExistingContent = false,
bool clearExtraSpace = false,
bool avoidReallocating = true) override
{
const auto binaryNumOfSamples = nextPowerOfTwo(numOfSamples);
// nextPowerOfTwo supports numOfSamples < 2^30 = 1073741824
jassert(numOfSamples < 1073741824);
CircularAudioBuffer<Type>::setSize(newNumChannels, binaryNumOfSamples, keepExistingContent, clearExtraSpace, avoidReallocating);
}
void push(const Type source) override
{
jassert(AudioBuffer<Type>::getNumChannels() == 1);
AudioBuffer<Type>::setSample(0, CircularAudioBuffer<Type>::ptr, source);
CircularAudioBuffer<Type>::ptr=CircularAudioBuffer<Type>::lastPosition & ++CircularAudioBuffer<Type>::ptr;
}
void push(const Type sourceLeft, const Type sourceRight) override
{
jassert(AudioBuffer<Type>::getNumChannels() == 2);
AudioBuffer<Type>::setSample(0, CircularAudioBuffer<Type>::lastPosition & CircularAudioBuffer<Type>::ptr, sourceLeft);
AudioBuffer<Type>::setSample(1, CircularAudioBuffer<Type>::lastPosition & CircularAudioBuffer<Type>::ptr, sourceRight);
CircularAudioBuffer<Type>::ptr=CircularAudioBuffer<Type>::lastPosition & ++CircularAudioBuffer<Type>::ptr;
}
Type get(int channel, int delay) const override
{
jassert(channel<AudioBuffer<Type>::getNumChannels());
return AudioBuffer<Type>::getSample(channel, (CircularAudioBuffer<Type>::ptr - delay - 1) & CircularAudioBuffer<Type>::lastPosition);
}
Type get(int delay) const override
{
jassert(AudioBuffer<Type>::getNumChannels() == 1);
return get(0, delay);
}
};