Skip to content

Make Grains End

passivist edited this page Jan 20, 2017 · 4 revisions

Make Grains End

As it is now we create a single Grain object at the start of the program execution. That grain loops forever. The first thing we can do now is make that grain stop at some point.

The first thing to do here is adding member variables for the length and the onset time of the grain to the class. We will also be adding a startPosition member but we won't be using it until the next chapter. For that we declare the member variables and initialize them in the constructor:

public:
    int onset;
    int length;
    int startPosition;

    Grain(int onset, int length, int startPos) : onset(onset), length(length), startPosition(startPos)  // [1]
    {

    };

    Grain()
    {
        onset = 0;
        length = 0;
        startPosition = 0;
    }

    ~Grain(){};

Note that we now have two constructors. The first is the one we'll be actually using. The second one is the default constructor that gets called whenever we don't supply arguments for the creation of a new grain.

We also need to adjust the Grainclasses process member to take the onset value into account:

void process (AudioSampleBuffer& currentBlock, AudioSampleBuffer& fileBuffer, int numChannels, int blockNumSamples, int fileNumSamples, int time)
{
    for(int channel=0; channel<numChannels; ++channel){
        float* channelData = currentBlock.getWritePointer(channel);
        const float* fileData = fileBuffer.getReadPointer(channel%fileBuffer.getNumChannels());

        int position = (time - onset) + startPosition;

        channelData[time % blockNumSamples] = fileData[position % fileNumSamples];
    }
}

For this we define an integer position that offsets the global time by the value in samples. We now use this 'position' variable to index into our buffer. This will assure that the grain will start playing the buffer from startPosition no matter how far we schedule it into the future.

Then we delete our grain variable from the AudioProcessor instead declaring an array called grainStack. Note that this is an Array class defined in the JUCE framework that is different from the std::array or the C-style Array classes. Most notably it allows adding and removing of elements much like the vector class but also sorting of the array and other useful features.

The type of object that we want the array to hold is supplied in the declaration:

Array<Grain> grainStack;

For now we will be only adding one grain to array in the constructor of our AudioProcessor class. Later we will write a scheduling loop that will add new grains to the array. Here's our updated constructor:

time = 0;
grainStack.add( Grain(88200, 44100, 0) );

Here we create a new Grain object with the values for onset and length of 88200 and 44100. We plan to test with these values if the grain should be playing or not. startPosition will remain 0 for now.

Also we have to do some changes to the processBlock function:

  // [...]
  const Array<Grain> localStack = grainStack; // [1]

  // [2]
  for (int i = 0; i < numSamplesInBlock; ++i) {
      if (localStack[0].onset < time) {
          if (time < (localStack[0].onset + localStack[0].length)) {
              localStack[0].process(buffer, *currentBuffer, buffer.getNumChannels(), numSamplesInBlock, numSamplesInFile, time);
          }
      }
      ++time; // increment time
  }
  // [...]
  1. We make a copy of the grainStack so when we add and delete grains we don't cause undefined behavior. This array will be declared const because we don't want it to change. If it is changed anyway the compiler will throw an error. This is preferred because it will generate a readable error message instead of causing undefined behavior which might lead to a crash without that is much harder to debug.
  2. Here we first check if the time is greater then the onset (start time) of the grain, and afterwards if the time is not greater than the time the grain starts added to its length. If thats both the case we process the grain in localStack.

In the next chapter we will implement a scheduling loop that will trigger grains with a random value for startPosition.

<<< last Chapter next Chapter >>>

Clone this wiki locally