Skip to content

Thread Safe Sample Management

passivist edited this page Jan 16, 2017 · 6 revisions

ReferenceCountedBuffer class

To implement the checkForPathToOpen function we first have to define another class. The class will get its own file named ReferenceCountedBuffer.h. We'll add the file to the project in the same way we added Grain.h. This is the class:

#ifndef REFERENCECOUNTEDBUFFER_H_INCLUDED
#define REFERENCECOUNTEDBUFFER_H_INCLUDED

class ReferenceCountedBuffer : public ReferenceCountedObject
{
public:
    typedef ReferenceCountedObjectPtr<ReferenceCountedBuffer> Ptr; // [1]
    
    ReferenceCountedBuffer (const String& nameToUse,
                            int numChannels,
                            int numSamples) :   name (nameToUse),
                                                buffer (numChannels, numSamples) // [2]
    {
        DBG (
             String ("Buffer named '") + name +
             "' constructed. numChannels = " + String (numChannels) +
             ", numSamples = " + String (numSamples) );
    }
    
    ~ReferenceCountedBuffer()
    {
        DBG (String ("Buffer named '") + name + "' destroyed");
    }
    
    AudioSampleBuffer* getAudioSampleBuffer() // [3]
    {
        return &buffer;
    }
    
private:
    String name;
    AudioSampleBuffer buffer;
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ReferenceCountedBuffer)
};

#endif  // REFERENCECOUNTEDBUFFER_H_INCLUDED

ReferenceCountedBuffer is a subclass of ReferenceCountedObject. ReferenceCountedObject is a base class that contains methods that manage the automatic deletion of objects once they are no longer needed. This employs the technique of 'reference counting'1 to test if an object is still in use.

  1. A class inheriting from ReferenceCountedObject has to be made accessible with a ReferenceCountedObjectPtr with the type of the class. In our case this we would need to access the pointer like this: ReferenceCountedBuffer::ReferenceCountedObjectPtr<ReferenceCountedBuffer>. This is a quite unwieldy construction that we can simplify with a typedef. A typedef allows us to give a type a different name. Here we give the type ReferenceCountedObjectPtr<ReferenceCountedBuffer> the name Ptr and can access the type now with ReferenceCountedBuffer::Ptr.
  2. In the constructor we set defaults with the : operator. We supply a name for the buffer for debugging and also supply the buffer itself. In the constructors function body we print the name of the buffer to the console.
  3. We also define a method for accessing the buffer.

1: see also https://mortoray.com/2012/01/08/what-is-reference-counting/

Thread-Safe loading of the sample

Now we can implement checkForPathToOpen. First we declare a String variable named chosenPath in the header for the AudioProcessorEditor class:

String chosenPath;

And implement the function:

void GrrnlrrAudioProcessorEditor::checkForPathToOpen()
{
    String pathToOpen;
    swapVariables(pathToOpen, chosenPath);
    
    if(pathToOpen.isNotEmpty()){
        std::cout << "We have a file!" << std::endl;
        loadSample(pathToOpen);
    }
}

In the function we declare a variable String pathToOpen and swap the values of pathToOpen and chosenPath it with the JUCE function swap. pathToOpen open will be an empty string. With this we assure that chosenPath will always be empty after we execute checkForPathToOpen. This prevents that once we supply a path for chosenPath the program will try to load the sample multiple times. Next we check if pathToOpen is still empty. If that is the case that means that we have no sample to load. If we have a valid path we first post a short message for debugging and then call the function loadSample with pathToOpen.

To test if everything is working we can temporarily define chosenPath in the constructor of the AudioProcessorEditor class:

[...]
startThread();
    
chosenPath = "/Users/raffaelseyfried/dev/eigene/GRRNLRR/Resources/Piano_D11_High.wav";
    
setSize (400, 300);
[...]

If we compile and test the plugin now, it should load and play the audio-file at the specified path.

Next we should rewrite the loadSample function with the ReferenceCountedBuffer. First we change the type of fileBuffer from AudioSampleBuffer to ReferenceCountedBuffer::Ptr:

ReferenceCountedBuffer::Ptr fileBuffer;

Next we should comment out the whole function body of processBlock. This renders the code inert. We will worry about getting it to run again later incorporating the ReferenceCountedBuffer. We comment out by simply placing start (/*) and end comments (*/) around the function body like this:

void GrrnlrrAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    /*
    const int numSamplesInBlock = buffer.getNumSamples();
    const int numSamplesInFile  = fileBuffer.getNumSamples();
    
    for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
        buffer.clear (i, 0, buffer.getNumSamples());
    
    // return from the function if there's nothing in our buffer
    if(fileBuffer.getNumSamples() == 0) return;
    
    for (int i=0; i<numSamplesInBlock; ++i){
        grain.process(buffer, fileBuffer, buffer.getNumChannels(), numSamplesInBlock, numSamplesInFile, time); // [1]
        ++time; // [2]
    }
    */
}

Afterwards we can rewrite the loadSample function:

void GrrnlrrAudioProcessorEditor::loadSample(String path)
{
    const File file (path);
    
    // we create the right kind of AudioFormatReader for our File
    ScopedPointer<AudioFormatReader> reader(formatManager.createReaderFor(file));
    ReferenceCountedBuffer::Ptr newBuffer = new ReferenceCountedBuffer(file.getFileName(),
                                                                       reader->numChannels,
                                                                       reader->lengthInSamples);
    
    if(reader != nullptr){
        // stream the contents of the Audio File into the Buffer
        // args: AudioSampleBuffer*, startSample, endSample, readerStartSample, useLeftChan, useRightChan
        // [1]
        reader->read (newBuffer->getAudioSampleBuffer(), 0, reader->lengthInSamples, 0, true, true);
        // [2]
        std::cout << "Samples in Buffer: " << newBuffer->getAudioSampleBuffer()->getNumSamples() << std::endl;
        
        // processor.fileBuffer = newBuffer;
    }
}
  1. Rather than supplying the AudioSampleBuffer directly to the read method of the AudioFormatReader we here have to first make that buffer available through the ReferenceCountedBuffer.
  2. We also have to rewrite the text output accessing the ReferenceCountedBuffer.

Here we should once again test if everything is still working. Note that the buffer shouldn't play at this point because we 'disabled' the processBlock function by commenting out the code. We should however see text output in the terminal that the buffer has been loaded correctly.

Now we can also rewrite the processBlock function like this:

void GrrnlrrAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    // make a copy of the Pointer to the fileBuffer so we are completely seperated
    // from the other thread
    ReferenceCountedBuffer::Ptr retainedBuffer (fileBuffer);
    
    // return from the function if there's no valid buffer
    if(retainedBuffer == nullptr) return;
    
    // finally get a AudioSampleBuffer we can use for processing
    AudioSampleBuffer* currentBuffer = retainedBuffer->getAudioSampleBuffer();
    
    const int numSamplesInBlock = buffer.getNumSamples();
    const int numSamplesInFile  = currentBuffer->getNumSamples();
    
    for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
        buffer.clear (i, 0, buffer.getNumSamples());
    
    for (int i=0; i<numSamplesInBlock; ++i){
        grain.process(buffer, *currentBuffer, buffer.getNumChannels(), numSamplesInBlock, numSamplesInFile, time);
        ++time; // increment time
    }
}

A few things are happening at once here. First we make a local (to the scope of this function) copy of the pointer to our buffer. Doing this the buffer inside processBlock is totally independent of the messaging-thread where a buffer could be deleted or added while the function is running. Next we see if retainedBuffer is pointing to a valid buffer. If not we return from the function. Then finally we can make the buffer accessible and proceed. In the following code all mentions of fileBuffer have been renamed to currentBuffer.

Large changes like this affecting multiple places in the program are always more error prone. We have to change many things before the program can compile again and we can test it. A strategy that has worked well for me in these situations was when something went wrong to first pin-point exactly where the failure was happening. I started by backtracking my steps and commenting out the code I had just written. When the program would compile and run again (with limited functionality) I would add the functions back in one by one until the error would start to happen again. Knowing where the error is happening is often (more than) half the battle here.

Next we will create a button for choosing an arbitrary file to load.

<<< last Chapter next Chapter >>>

Clone this wiki locally