Skip to content

Scheduling Loop

passivist edited this page Jan 20, 2017 · 4 revisions

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.

Removing Grains

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);
    }
}
  1. Here we output the current size of grainStack to the console so we can check later if grainStack has the size we would expect it to have. Right now that size is 1.
  2. 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.
  3. 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.
  4. 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

Adding grains

changes to processBlock

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:

  1. We wrapped the code in another for loop iterating through all the grains currently on the grainStack.
  2. We changed the access to the grainStack so we process each grain.

changes to the run function

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);
    }
}
  1. First we check if a valid buffer exists.
  2. We add a new Grain object to the grainStack. 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.

<<< last Chapter next Chapter >>>

Clone this wiki locally