Can you help me to understand "Interpolation" on reading samples?

DSP, Plugin and Host development discussion.
RELATED
PRODUCTS

Post

Here's the code I have from a Sampler plugin (it works correctly) that read progressively (at samplerate) the samples from an array (which represent the list of all samples of the loaded wave file) and output (always at samplerate) the corresponding values of them:

Code: Select all

streamin L1;
streamin R1;
streamin L2;
streamin R2;
streamin frac;
streamout left;
streamout right;
float frac2;

frac2 = 1-frac;
left = (L1*frac2)+(L2*frac);
right = (R1*frac2)+(R2*frac);
- L1 is the value of the Left sample at index p
- R1 is the value of the Right sample at index p
- L2 is the value of the Left sample at index p+1
- R2 is the value of the Right sample at index p+1
- frac is the fractional part of the step. If I play at original pitch (base C4), frac=1; if I change the base of pitch to C5 (pitching down), frac=0.5. And so on.
- left is the final Left value at index p
- right is the final Right value at index p

p goes between 0 to wavefile size-1 (i.e. first sample value is at index 0).

My first question is: if I'm on original pitch (and so frac2=1-1=0, frac1=1 and p=0):

left=L2
right=R2
why it outputs the values of the "next" sample (p+1)? In this way it will ignore the "first" sample and the last one (because will be null).

What's the reason of this? I've analyzed many Sampler plugins, and the Interpolation method is often this.

Post

How should one interpret the your source code? Is it a mockup/pseudo of a processing function, or is it looped? Because:
frac is the fractional part of the step. If I play at original pitch (base C4), frac=1; if I change the base of pitch to C5 (pitching down), frac=0.5. And so on.
Normally you have a real sample index pointer that gets accumulated by the playback speed. So if at T = 0, index pointer is 0. If playback speed is 1, then

Code: Select all

foreach sample:
	base = floor(index_pointer)
	frac = index_pointer - base
	out = in[base] * (1 - frac) + in[base + 1] * frac

	index_pointer += speed
As we see, frac will be zero for the first sample, so at T = 0, out[0] = in[0] (thus, no delay).

e: I think you're misinterpreting 'frac' in your case. It has no obvious relation to the playback speed, it is the fractionate offset of p (the same as my index_pointer), and thus it changes constantly. You're thinking of the 'speed' variable, which is not included in your code.

Post

Mayae wrote:How should one interpret the your source code? Is it a mockup/pseudo of a processing function, or is it looped?
As I said, it runs at samplerate. So at every sample, it process that code (inputs are the ones I've described above).
Mayae wrote:Normally you have a real sample index pointer that gets accumulated by the playback speed. So if at T = 0, index pointer is 0.
Yes, it is what I have. Playback speed (as I said, even if not directly) is 1.
At T=0, I have:

speed=1
frac=1
p=1

and I read Array[p-1] (that gives to me L1 and R1, first sample) and Array[p] (that gives to me L2 and R2, second sample).

At T=1, I have:

speed=1
frac=1
p=2

and I read Array[p-1] (that gives to me L1 and R1, second sample) and Array[speed] (that gives to me L2 and R2, third sample).

Thus, for my code:

Code: Select all

frac2 = 1-frac;
left = (L1*frac2)+(L2*frac);
right = (R1*frac2)+(R2*frac);
at T0, frac2 is always 0, and I still output only L2 and R2 :idea:
The same at T1. That's what I don't get...

Post

frac is, per definition, p - floor(p). So you can't have p = 1 => frac = 1. That doesn't add up. frac's interval is [0, 1[ so it can never be one. Also, why isn't p starting at zero?

e: for reference, it should look like this:

For speed = 1:

Code: Select all

T0:
p = 0
frac = 0
T1:
p = 1
frac = 0
T2:
p = 2
frac = 0
For speed = 0.5:

Code: Select all

T0:
p = 0
frac = 0
T1:
p = 0.5
frac = 0.5
T2:
p = 1
frac = 0
For speed = 1.75:

Code: Select all

T0:
p = 0
frac = 0
T1:
p = 1.75
frac = 0.75
T2:
p = 3.5
frac = 0.5
T3:
p = 5.25
frac = 0.25

Post

Mayae wrote:frac is, per definition, p - floor(p). So you can't have p = 1 => frac = 1. That doesn't add up. frac's interval is [0, 1[ so it can never be one. Also, why isn't p starting at zero?
In the plugin I'm working on, there is "round to even" approx, so frac could be 1, because odd values are not considered.

If p is 2, CountInt = round(2 - 0.5) = 2 (CountFrac is 0, p-CountInt).
If p is 3, CountInt = round(3 - 0.5) = 2 (again; but CountFrac now is 1).

Not sure why it subtract 0.5 honestly...

Post

Nowhk wrote:
Mayae wrote:frac is, per definition, p - floor(p). So you can't have p = 1 => frac = 1. That doesn't add up. frac's interval is [0, 1[ so it can never be one. Also, why isn't p starting at zero?
In the plugin I'm working on, there is "round to even" approx, so frac could be 1, because odd values are not considered.

If p is 2, CountInt = round(2 - 0.5) = 2 (CountFrac is 0, p-CountInt).
If p is 3, CountInt = round(3 - 0.5) = 2 (again; but CountFrac now is 1).

Not sure why it subtract 0.5 honestly...
Okay, but you're still not supposed to round it!? It needs to be truncation (floor towards minus infinity). frac is the fractionate part of p, if it's anything else than that (which I severely doubt), the audio will be distorted.

They subtract 0.5 to emulate a truncation function, however if it's round to even it won't work.

Post

Mayae wrote:They subtract 0.5 to emulate a truncation function, however if it's round to even it won't work.
Uhm no, in fact it works correct. Now I see...

Round primitive in that environment "round to even" when there is "tie-breaking". Else, round to nearest int.
So if I have (for example) index of 6.75 (and I don't sub 0.5), I'll have 1.25 * sample[7] + (-0.25) * sample[8] (which is wrong). Instead, sub 0.5, I got the correct 0.25 * sample[6] + 0.75 * sample[7].

The trick of that plugin is that (for int value), if I have index 3 (which become 2.5 for the reason above), it approx to 2 instead of 1 (round to even), and frac become 1 to compensate it (that's why frac can be 1 there):

CountInt = round(3 - 0.5) = 2
CountFrac = 3 - 2 = 1
(1.0 - 1.0) * sample[2] + 1.0 * sample[3] = sample[3]

and its correct...

Post

Does your language have some kind of truncate operator? That seems what you need (or what I've always used in these cases anyway). Sometimes called TRUNC(). Sometimes called FLOOR().
Last edited by JCJR on Thu Feb 11, 2016 5:19 pm, edited 2 times in total.

Post

Nowhk wrote:
Mayae wrote:They subtract 0.5 to emulate a truncation function, however if it's round to even it won't work.
Uhm no, in fact it works correct. Now I see...

Round primitive in that environment "round to even" when there is "tie-breaking". Else, round to nearest int.
So if I have (for example) index of 6.75 (and I don't sub 0.5), I'll have 1.25 * sample[7] + (-0.25) * sample[8] (which is wrong). Instead, sub 0.5, I got the correct 0.25 * sample[6] + 0.75 * sample[7].

The trick of that plugin is that (for int value), if I have index 3 (which become 2.5 for the reason above), it approx to 2 instead of 1 (round to even), and frac become 1 to compensate it (that's why frac can be 1 there):

CountInt = round(3 - 0.5) = 2
CountFrac = 3 - 2 = 1
(1.0 - 1.0) * sample[2] + 1.0 * sample[3] = sample[3]

and its correct...

Perhaps, it's late. Not sure why one would bother not going with the correct definition... My original comment was that a fractional part of 1 doesn't make sense mathematically, and for indexing purposes I would never go with your method (as you're theoretically never considering x+1 sample on every other tie, which you algorithmically should).

Anyway, back to your original question: You claim that frac is 1 at p/T = 0. This is not correct. This is all you have to realize.

Code: Select all

p = 0
rp = roundToEven(p - 0.5) => 0
frac = p - rp => 0
frac2 = 1 - frac => 1

y = x1 * frac2 + x2 * frac
y => x1 * frac2 + 0
y => x1 * 1
y => x1
q.e.d.

Post

Mayae wrote:Perhaps, it's late. Not sure why one would bother not going with the correct definition... My original comment was that a fractional part of 1 doesn't make sense mathematically, and for indexing purposes I would never go with your method (as you're theoretically never considering x+1 sample on every other tie, which you algorithmically should).
My code and your is actually almost the same:

Code: Select all

// your code
foreach sample:
   base = floor(index_pointer)
   frac = index_pointer - base
   out = in[base] * (1 - frac) + in[base + 1] * frac
   index_pointer += speed

// my code
foreach sample:   
   index_pointer += speed
   base = round(index_pointer - 0.5)
   frac = index_pointer - base 
   out = in[base-1] * (1 - frac) + in[base] * frac
the differences is that my "round" can't do the same of your "floor", due to how the environment (FlowStone) round.

For example, you will have (I suppose floor is round to down/below):

floor(0.5) => 0
floor(1.5) => 1
floor(1.51) => 1
floor(2.5) => 2
floor(2.51) => 2
floor(3.49) => 3
floor(3.5) => 3
floor(3.51) => 3

in FlowStone I'll have:

round(0.5) => 0
round(1.5) => 2
round(1.51) => 2
round(2.5) => 2
round(2.51) => 3
floor(3.49) => 3
floor(3.5) => 4
floor(3.51) => 4

this of course will mess with index_pointer and frac, and the next task (interpolation) will got weird results.
That's why it needs to subtract 0.5 (force round to down). But this means that I'll miss some values; so, it compensates having base 1-based and frac that can take 1.

For what I see, both produce exactly the same output values. Can you prove the opposite?

Post

Nowhk wrote:

Code: Select all

frac2 = 1-frac;
left = (L1*frac2)+(L2*frac);
right = (R1*frac2)+(R2*frac);
I think you may need to swap the weight multipliers here. If frac is the relative weight of earliest sample and frac2 is the relative weight of the later sample (the sample following the earliest sample), then the earlier samples (L1,R1) are being multiplied by the weight (frac2) intended for the later samples so the interpolation is weighting opposite of what you intended. See if this works:

Code: Select all

frac2 = 1-frac;
left = (L1*frac)+(L2*frac2);
right = (R1*frac)+(R2*frac2);

Post

But it works. The fact is that I want simple code as the Mayae one. I need to loop, settings start and end point, and to be able to pitch it up and down during loop. With the code I have Im not able to calculate the algorithm...

Post

Nowhk wrote:But it works. The fact is that I want simple code as the Mayae one. I need to loop, settings start and end point, and to be able to pitch it up and down during loop. With the code I have Im not able to calculate the algorithm...
Nowhk wrote:But it works. The fact is that I want simple code as the Mayae one. I need to loop, settings start and end point, and to be able to pitch it up and down during loop. With the code I have Im not able to calculate the algorithm...
Okay, I see .. I was responding to this:
Nowhk wrote: left=L2
right=R2
why it outputs the values of the "next" sample (p+1)? In this way it will ignore the "first" sample and the last one (because will be null).
Mayaes algorithm seems correct to me. Your code fragment (flowstone, right?) is doing the interpolation step, given the sample values and the fractional weighting. I am barely familiar with flowstone but it seems to me that some other part of the dataflow needs to compute the index position 'p' based on the playback rate, extract the fractional part of 'p' and assign that to the 'frac' input of your code fragment, and assign the L/R 1 and 2 inputs from Array[floor(p)] and Array[floor(p) + 1] (accounting for wrapping around at the ends) so that your code fragment has the right samples and fractional amount to apply when it runs.

Post

Ok. So, going in order. Considering the code of Mayae (and adding my "hand-made" floor function):

Code: Select all

foreach sample:
   base = index_pointer - index_pointer % 1 // floor emulation
   frac = index_pointer - base
   out = in[base] * (1 - frac) + in[base + 1] * frac
   index_pointer += speed
it should works nice if I pitch up/down. i.e. if I halve or double the speed (speed=0.5 or speed=2). Right?

Let say that now I want to loop the wavesample when it ends (it has got sample_length=9975). Is this enough?

Code: Select all

foreach sample:
   base = index_pointer - index_pointer % 1 // floor emulation
   frac = index_pointer - base
   out = in[base] * (1 - frac) + in[base + 1] * frac
   index_pointer += speed
   if(index_pointer>=sample_length) 
   {
      index_pointer = 0
   }
Or will it miss some steps/slope interpolation?

Post

Nowhk wrote:Ok. So, going in order. Considering the code of Mayae:

Code: Select all

foreach sample:
   base = floor(index_pointer)
   frac = index_pointer - base
   out = in[base] * (1 - frac) + in[base + 1] * frac
   index_pointer += speed
it should works nice if I pitch up/down. i.e. if I halve or double the speed (speed=0.5 or speed=2). Right?

Let say that now I want to loop the wavesample when it ends (it has got sample_length=9975). Is this enough?

Code: Select all

foreach sample:
   base = floor(index_pointer)
   frac = index_pointer - base
   out = in[base] * (1 - frac) + in[base + 1] * frac
   index_pointer += speed
   if(index_pointer>=sample_length) 
   {
      index_pointer = 0
   }
Or will it miss some steps/slope interpolation?
That is the right general approach but you have to be careful about a couple of things. The 'base + 1' could exceed the end of the array when base is at the end of the array. Setting the index_pointer to 0 would discard any fractional part that has accumulated when wrapping to the front of the buffer (essentially resetting the fraction to 0 when it should not be).

Post Reply

Return to “DSP and Plugin Development”