-
Notifications
You must be signed in to change notification settings - Fork 12
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.
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':
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.
We will be implementing a cubic interpolation algorithm from the SuperCollider source.
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;
}
}
-
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. Thestd::ceil
function rounds its argument up to the nearest integer.std::ceil
does still return a value of typefloat
. To use it later we also cast it asint
. -
We also declare a float
fracPos
which is defined as the difference betweenposition
andiPosition
. 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. -
Here we index into the last 4 samples to get all the values for the interpolation function.
-
Then we call the interpolation function and update the value for
currentSample
.
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:
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:
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:
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:
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.