-
Notifications
You must be signed in to change notification settings - Fork 12
Thread Safe Sample Management
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.
- A class inheriting from
ReferenceCountedObject
has to be made accessible with aReferenceCountedObjectPtr
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 atypedef
. Atypedef
allows us to give a type a different name. Here we give the typeReferenceCountedObjectPtr<ReferenceCountedBuffer>
the namePtr
and can access the type now withReferenceCountedBuffer::Ptr
. - 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. - We also define a method for accessing the buffer.
1: see also https://mortoray.com/2012/01/08/what-is-reference-counting/
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;
}
}
- Rather than supplying the
AudioSampleBuffer
directly to theread
method of theAudioFormatReader
we here have to first make that buffer available through theReferenceCountedBuffer
. - 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.