-
Notifications
You must be signed in to change notification settings - Fork 12
Scheduling Loop
In this chapter we will add a background thread to the AudioProcessor
object and implement its run
function to dynamically add and remove grains.
To add the background thread, which we will call schedulingThread
to our PluginAudioProcessor
we follow the same procedure when adding it to the PluginAudioProcessorEditor
(see the Threads chapter). That means inheriting from the Thread
class, defining the run
function and finally starting the thread in the constructor of PluginAudioProcessor
and stopping it in the deconstructor.
Now we will implement a method of deleting finished grains from the grainStack
before adding new ones. This is a tricky part of the program because it can lead to a lot of obscure and hard to debug errors.
Let's have a look at the run
function:
void GrrnlrrAudioProcessor::run()
{
while (! threadShouldExit()) {
int dur = 250;
std::cout << "stack size: " << grainStack.size() << std::endl; // [1]
if( grainStack.size() > 0){ // [2]
for (int i=grainStack.size() - 1; i >= 0; --i) {
// check if the grain has ended
int grainEnd = grainStack[i].onset + grainStack[i].length; // [3]
bool hasEnded = grainEnd < time;
if(hasEnded) grainStack.remove(i);
// [4]
std::cout << "hasEnded: " << hasEnded
<< " grainEnd: " << grainEnd
<< " time: " << time
<< std::endl;
}
}
wait(dur);
}
}
- Here we output the current size of
grainStack
to the console so we can check later ifgrainStack
has the size we would expect it to have. Right now that size is 1. - For safety we check if the size is greater than 1. If it is we iterate over the array backwards. Note that we have to subtract by 1 because the number of elements in the array counts from 1 and the index from 0. When changing an array it is safer to iterate backwards over it. When we remove an element from the array the index of all the elements following it gets reduced by one. If we were to iterate forwards we could produce an out of bounds error if we delete the next to last element. The last element would then have the index of the next to last element and when we increment our index we would try to access an element that is not part of the array anymore, resulting in a crash.
- Here we check if the grain at the current index has already ended by comparing the time the grain ends with the current time. If the grain is found to have ended we remove it from the array.
- Finally we output some data to the console to compare our expectation of how the program should behave to its actual behavior.
If we compile and test the plugin now the output in the console after loading a sound file looks something like this:
We have a file!
Buffer named 'Piano_D11_High.wav' constructed. numChannels = 2, numSamples = 444641
Samples in Buffer: 444641
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 6656
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 18944
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 30720
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 43008
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 54784
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 67072
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 78848
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 90624
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 102912
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 114688
stack size: 1
hasEnded: 0 grainEnd: 132300 time: 126976
stack size: 1
hasEnded: 1 grainEnd: 132300 time: 138752
stack size: 0
stack size: 0
stack size: 0
First we will rewrite the processBlock
function a bit:
void GrrnlrrAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
// clear the buffer so we don't get any noise
for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
buffer.clear (i, 0, buffer.getNumSamples());
// 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();
const Array<Grain> localStack = grainStack;
for (int s = 0; s < numSamplesInBlock; ++s) {
for(int i=0; i < localStack.size(); ++i) {
if (localStack[i].onset < time) {
if (time < (localStack[i].onset + localStack[i].length)) {
localStack[i].process(buffer, *currentBuffer, buffer.getNumChannels(), numSamplesInBlock, numSamplesInFile, time);
}
}
}
++time; // increment time
}
}
Two things are changed here:
- We wrapped the code in another
for
loop iterating through all the grains currently on thegrainStack
. - We changed the access to the
grainStack
so we process each grain.
The position our grain starts to play is a parameter that has to be quite robust. We want to randomize it later so we must assure that it will always be inside the permitted value-range for the sample.
This becomes especially important if we start at the "edges" of the sample: the beginning or the end. At the end we have the problem that randomized values may surpass the number of samples in the buffer. At the beginning values may become negative. Here we will write a small utility function that will wrap the values so no matter what values we supply they will always lay in the legal range for the buffer:
int GrrnlrrAudioProcessor::wrap(int val, const int low, const int high)
{
int range_size = high - low + 1;
if (val < low)
val += range_size * ((low - val) / range_size + 1);
return low + (val - low) % range_size;
}
Now we remove the initial grain we add to the grainStack
in the constructor instead adding one every call of the run
function:
void GrrnlrrAudioProcessor::run()
{
while (! threadShouldExit()) {
int dur = 1000;
// delete grains
std::cout << "stack size: " << grainStack.size() << std::endl;
if( grainStack.size() > 0){
for (int i=grainStack.size() - 1; i >= 0; --i) {
// check if the grain has ended
int grainEnd = grainStack[i].onset + grainStack[i].length;
bool hasEnded = grainEnd < time;
if(hasEnded) grainStack.remove(i);
std::cout << "hasEnded: " << hasEnded
<< " grainEnd: " << grainEnd
<< " time: " << time
<< std::endl;
}
}
// add grains
if(fileBuffer != nullptr){ [1]
int numSamples = fileBuffer->getAudioSampleBuffer()->getNumSamples();
int onset = 1000;
int length = 44100 * 0.5;
int startPosition = -1000;
// [2]
grainStack.add( Grain(time + onset, length, wrap(startPosition, 0, numSamples)) );
}
wait(dur);
}
}
- First we check if a valid buffer exists.
- We add a new
Grain
object to thegrainStack
. Note that we should schedule a bit into the future so our grain always starts from the beginning. We also give the grain a length (in samples) and a startPosition (also in samples). I recommend playing around with these values a bit to test edge cases: do very small grains still work? (they should!) How about very long grains? What happens if we supply different values for the start position, for example very near to the end of the sample or after the end of the sample? What happens with negative values for the sample position? The plugin shouldn't crash in any of those cases.