Skip to content

Transposition

passivist edited this page Jan 16, 2017 · 3 revisions

Transposition

To transpose the grains we have to change the playback speed. To do that we can multiply the position at which we are reading samples by a factor. A value greater than one results in a faster playback speed, values less than one result in slower playback. The digital nature of the process is a problem here. When we slow down or speed up the playback the read position will fall 'in between' samples. When the playback speed is 1.5 for example we would need to read the 1.5th, 3th, 4.5th sample an so on.

interpolation problem

This leads to some pretty nasty aliasing effects. To avoid this we need to interpolate the sample values. The most straight forward solution here would be to just look for the value on a straight line between the two samples we want to interpolate. This is called 'linear interpolation':

linear interpolation

This is better than no interpolation but it still introduces discontinuities into the signal resulting in unwanted artifacts. A better but also more computationally expensive and tricky to implement solution is cubic interpolation.

cubic interpolation

We will be implementing a cubic interpolation algorithm from the SuperCollider source.

Implementation

We start by adding a const float rate member to our Grain class. Adding it to the constructor initializer list and the call to the constructor in our PluginAudioProcessor class. We then define a member function for the interpolation:

inline float cubicinterp(float x, float y0, float y1, float y2, float y3)
{
    // 4-point, 3rd-order Hermite (x-form)
    float c0 = y1;
    float c1 = 0.5f * (y2 - y0);
    float c2 = y0 - 2.5f * y1 + 2.f * y2 - 0.5f * y3;
    float c3 = 0.5f * (y3 - y0) + 1.5f * (y1 - y2);

    return ((c3 * x + c2) * x + c1) * x + c0;
}

This function looks very complicated and it is. But that's not necessarily important for us. We just need to know how to use it. If you want you can read up on the particular function a bit more here. For us the more important task is to find out what values we have to supply to the function for it to work correctly. The first argument x expects a value between 0 and 1 which is the position of the sample we want to interpolate in relation to the sample we have actually read. y0 - y3 are the values of four subsequent samples read from our buffer y0 being the oldest and y3 the newest. These values are used to calculate the value of our interpolated sample.

We then update the process function of our Grain class:

void process (AudioSampleBuffer& currentBlock, AudioSampleBuffer& fileBuffer, int numChannels, int blockNumSamples, int fileNumSamples, int time)
{
  for(int channel=0; channel<numChannels; ++channel){
    const float gain = envelope(time);

    float* channelData = currentBlock.getWritePointer(channel);
    const float* fileData = fileBuffer.getReadPointer(channel%fileBuffer.getNumChannels());

    // [1]
    const float position = (time - onset) * rate;
    const int iPosition = (int) std::ceil(position);

    // [2]
    const float fracPos = position - iPosition;

    // [3]
    float currentSample = fileData[iPosition + startPosition % fileNumSamples];
    float a = fileData[(iPosition + startPosition) - 3 % fileNumSamples];
    float b = fileData[(iPosition + startPosition) - 2 % fileNumSamples];
    float c = fileData[(iPosition + startPosition) - 1 % fileNumSamples];

    // [4]
    currentSample = cubicinterp(fracPos, a, b, c, currentSample);

    channelData[time % blockNumSamples] += currentSample * gain;  
  }
}
  1. Here we redefine the position as a floating point value. This is the exact playback position that, after changing the playback speed, might fall in between samples. We also define a integer iPosition that is is the next larger integer value of position. The std::ceil function rounds its argument up to the nearest integer. std::ceil does still return a value of type float. To use it later we also cast it as int.

  2. We also declare a float fracPos which is defined as the difference between position and iPosition. This will always return a value between 0 and 1. This value will be used to weigh the influence of the different samples during interpolation.

  3. Here we index into the last 4 samples to get all the values for the interpolation function.

  4. Then we call the interpolation function and update the value for currentSample.

Tests

Similar to the envelope, we need to test the interpolation's functionality thoroughly. To test the interpolation I first created a sound file containing a 1khz sine tone. I then played it back with the plugin using the following parameters:

void GrrnlrrAudioProcessor::run()
{
    while (! threadShouldExit()) {
        int dur = 5000;

        // delete grains
        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); // [4]
            }
        }

        // add grains
        if(fileBuffer != nullptr){
            int numSamples = fileBuffer->getAudioSampleBuffer()->getNumSamples();

            int onset = 1000;
            int length = 44100;
            int startPosition = 44100;

            float rate = 0.01;

            float envMid = 0.5;
            float envSus = 1;
            float envCurve = 0;

            grainStack.add( Grain(time + onset, length, wrap(startPosition, 0, numSamples), envMid, envSus, envCurve, rate) );
        }
        wait(dur);
    }
}

In this way we play back a grain every 5 seconds. The grain is 1 second long and with a rate of 0.1 it should have a frequency of 10hz. Then I recorded the output of the plugin in my DAW and looked at the waveform and the spectrum of the recorded output. I didn't listen directly to the output because errors in the interpolation algorithm can lead to very loud noise! In the best case I should get a very smooth 10hz sine tone without overtones. On the first try the output looked like this:

wave bad sine A

spec bad sine A

That's not a very good result. The waveform looks jagged and the spectrum shows very strong aliasing frequencies. To find out what the problem was I set the length of the grain to a very small value (about 10 samples) and outputted some of the values in the function. What I noticed was that the values for a, b, and c would always return 0. I was not calculating them correctly. With that fixed the waveform looked like this:

wave bad sine B

spec bad sine B

The waveform looks a bit better. One can see that the segments in between the original samples are being interpolated. But they are facing the wrong way. The spectrum also shows many aliasing frequencies albeit the frequencies with much lower amplitude. The problem here was that I was calling the cubicinterp function with the arguments in the wrong order. With that fixed the waveform suddenly looked much worse:

wave bad sine C

Again probing the values with short grains I found out that I used the wrong function to acquire a integer value from position. I was using the a round function resulting in position - iPosition sometimes being negative. With this changed to std::ceil the output looked much better:

wave good sine B

spec good sine B

Here the waveform looks perfect. The spectrum still shows a lot of artifacts but these are in fact barely audible. To get rid of those one could implement a low pass filter after the interpolation but I think that is beyond the scope of this tutorial.

<<< last Chapter next Chapter >>>

Clone this wiki locally