Addition to AudioSampleBuffer


#1

I’m creating a class AudioBufferPool, which manages a list of AudioSampleBuffers. This is used during audio processing whenever one or more temporary buffers are needed. By using a pool, it avoid having each AudioProcessor having to allocate its own buffer.

In order to be able to manage the lifetime and re-use of these buffers, I need this function added:

    int AudioSampleBuffer::getNumSamplesAllocated () const throw()
      { return allocatedBytes / sizeof(float); }

This way when I put an AudioSampleBuffer back into the pool I know what its maximum size is for subsequent re-use.

This is a really handy class, if anyone is interested I can publish it when I’m done.


#2

Here are the classes:

(Removed the old version which relied on a patch to Juce, see the new one below)


#3

Does this seem useful to anyone?


#4

I don’t really see the point of your getNumSamplesAllocated idea, and the calculation doesn’t really add up because the space may not all be used for samples. You should just use getNumSamples and getNumChannels instead.


#5

When the buffer goes into the pool, I need to know its allocated size, rather than the number of samples that were needed at the time. The algorithm for re-using a buffer tries to keep the total amount of memory used to a minimum. If there are multiple buffers in the pool that are large enough, it will use the smallest one. If the pool has buffers, but none of them are big enough, it will choose the biggest one and resize that.

Using this technique, the maximum amount of memory allocated at any single moment, is minimized.

That’s why I need the getNumSamplesAllocated(). You can see this in the function AudioBufferPool::acquireBuffer ():

        int numSamplesAvailable = cur->getNumSamplesAllocated();

        // Use the smallest buffer which is equal or bigger than what
        // we need. If no buffer is large enough, then we will use the
        // largest available and increase it, to minimize memory usage.
        if ( (numSamplesAvailable >= samplesNeeded &&
              numSamplesAvailable < buffer->getNumSamplesAllocated()) ||
             (numSamplesAvailable < samplesNeeded &&
              numSamplesAvailable > buffer->getNumSamplesAllocated()))
        {
          buffer = cur;
          index = i;
        }

I got it up and running and converted all of my code to use the pool instead of various static AudioSampleBuffer variables, and it works great!

Please don’t make me patch Juce again!


#6

you can’t avoid running your patched version of Juce. it’s a one man framework, not a community driven one !


#7

It isn’t a method that makes any sense in that class - it’s not clear what it means, or what the return value actually represents. It assumes that the buffer is allocated as a single block of memory, which isn’t true for buffers that refer to external arrays, and in the future I may choose to implement the buffers differently, perhaps with separate blocks for each channel, or shared reference-counted buffers, etc etc.


#8

Okay, that makes sense. I thought about it and rewrote my class so that I can keep the allocation information outside of the Juce class using a derivation. Here it is:

// Manages a collection of AudioSampleBuffer that
// persist in memory and never decrease in size.
class AudioBufferPool
{
public:
  class Buffer : public AudioSampleBuffer
  {
  public:
    Buffer (int numChannels,
            int numSamples);

    void resize (int newNumChannels, int newNumSamples);

    int getNumSamplesAllocated () const;

  private:
    int m_samplesAllocated;
  };

public:
  AudioBufferPool ();
  ~AudioBufferPool ();

  Buffer* acquireBuffer (int numChannels,
                         int numSamples);

  void releaseBuffer (Buffer* buffer);

private:
  CriticalSection m_mutex;
  Array<Buffer*> m_buffers;

  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioBufferPool);
};

//------------------------------------------------------------------------------

// scoped lifetime management for a temporary audio buffer
class ScopedAudioSampleBuffer
{
public:
  ScopedAudioSampleBuffer (AudioBufferPool& pool,
                           int numChannels,
                           int numSamples)
      : m_pool (pool)
      , m_buffer (pool.acquireBuffer (numChannels, numSamples))
  {
  }
  
  ~ScopedAudioSampleBuffer ()
  {
    m_pool.releaseBuffer (m_buffer);
  }

  AudioSampleBuffer* getBuffer ()
  {
    return m_buffer;
  }

  AudioSampleBuffer* operator-> ()
  {
    return getBuffer();
  }

  AudioSampleBuffer& operator* ()
  {
    return *getBuffer();
  }

  operator AudioSampleBuffer* ()
  {
    return getBuffer();
  }

private:
  AudioBufferPool& m_pool;
  AudioBufferPool::Buffer* m_buffer;

  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScopedAudioSampleBuffer);
};

AudioBufferPool::Buffer::Buffer (int numChannels,
                                 int numSamples)
  : AudioSampleBuffer (numChannels, numSamples)
{
}

void AudioBufferPool::Buffer::resize (int newNumChannels, int newNumSamples)
{
  int samplesAllocated = newNumChannels * newNumSamples;

  if (m_samplesAllocated < samplesAllocated)
    m_samplesAllocated = samplesAllocated;

  setSize (newNumChannels, newNumSamples, false, false, true);
}

int AudioBufferPool::Buffer::getNumSamplesAllocated () const
{
  return m_samplesAllocated;
}

//------------------------------------------------------------------------------

AudioBufferPool::AudioBufferPool()
{
  m_buffers.ensureStorageAllocated (10);
}

AudioBufferPool::~AudioBufferPool()
{
  for (int i = 0; i < m_buffers.size(); ++i)
    delete m_buffers[i];
}

AudioBufferPool::Buffer* AudioBufferPool::acquireBuffer (int numChannels, int numSamples)
{
  AudioBufferPool::Buffer* buffer = 0;
  int samplesNeeded = numChannels * numSamples;

  int index = -1;

  {
    ScopedLock lock (m_mutex);

    for (int i = 0; i < m_buffers.size(); ++i)
    {
      Buffer* cur = m_buffers[i];

      if (!buffer)
      {
        buffer = cur;
        index = i;
      }
      else
      {
        int numSamplesAvailable = cur->getNumSamplesAllocated();

        // Use the smallest buffer which is equal or bigger than what
        // we need. If no buffer is large enough, then we will use the
        // largest available and increase it, to minimize memory usage.
        if ( (numSamplesAvailable >= samplesNeeded &&
              numSamplesAvailable < buffer->getNumSamplesAllocated()) ||
             (numSamplesAvailable < samplesNeeded &&
              numSamplesAvailable > buffer->getNumSamplesAllocated()))
        {
          buffer = cur;
          index = i;
        }
      }
    }

    if (buffer)
      m_buffers.remove (index);
  }

  if (buffer)
    buffer->resize (numChannels, numSamples);
  else
    buffer = new Buffer (numChannels, numSamples);

  return buffer;
}

void AudioBufferPool::releaseBuffer (Buffer* buffer)
{
  if (buffer)
  {
    ScopedLock lock (m_mutex);

    m_buffers.add (buffer);
  }
}

Obviously, someone could abuse it. For example they might get a buffer and then resize it themselves instead of going through the API. But this works for my purposes and doesn’t require a change to Juce.


#9

I see that you have solved a similar problem in AudioProcessorGraph, calculating the optimal number of buffers.