LagrangeInterpolator sounds weird when I try to implement it in SamplerVoice

Hi, I tried to implement Lagrange interpolation in a SamplerVoice inherited class replacing this:

     while (--numSamples >= 0)
    {
        auto pos = (int) sourceSamplePosition;
        auto alpha = (float) (sourceSamplePosition - pos);
        auto invAlpha = 1.0f - alpha;

        // just using a very simple linear interpolation here..
        float l = (inL[pos] * invAlpha + inL[pos + 1] * alpha);

       
        sourceSamplePosition += pitchRatio;
    }

By somethine like this:

        leftInterpolator.processAdding(pitchRatio, inL + (int)(sourceSamplePosition), outL, numSamples, 1.0f);
       sourceSamplePosition += numSamples * pitchRatio;

But it produces strange artifacts when playing. Iā€™m sure Iā€™m doing something wrong, but I ignore the reason.

Iā€™m playing an upgraded to 96Khz sine wave sample (using the same Lagrange class, which works fine here), and then resampling it to 44.1Khz in the code used before.

Please, could someone point me in the right direction?

You need to render the amount of samples into the resampler based on the resampling factor, then produce the true numSamplesNeeded.

So if you wanted to go from 88.2k to 44.1k

for (int sample = 0; sample < numSample * 2; sample++) {
  in[sample] = sampleAt88.2k[readhead]
  readhead += 1 // or your "pitchRatio"
}

interpolator.process(2, in, out, numSamples)

Apologies if iā€™m mistaken reading your source code and this is the approach, but it looks like pitchRatio is somehow being used as a factor in resampling. you should iterate through your source by 1 and resample at the ratio (sourceRate / sampleRate) ā€“ to render it at the same pitch. In any case your desired pitch change / speed you go through the source buffer, doesnā€™t have much to do with you resampling factor

1 Like

Thank you for the reply @Jake_Penn :slight_smile:

Well, the solution wasā€¦ embarrassingā€¦ the weird things I was listening was the host clippingā€¦ The piece of code I placed originally seems work fine.

Juan could you provide the full code?
I am also hearing artifacts. I believe because of the casting of the sourceSamplePosition from float to int. I think the ā€œquantisingā€ introduces little clicks each render of a blockā€¦

Hi @dataexcessaudio, the juce interpolator store these remaining decimals inside a variable to use it in the next call. I had a lot of problems with the interpolators (non-related to juce) and I ended creating one myself, based on hermite. But the main point here is it works in the same way. This how I do the call, and as you see, Iā€™m casting a double variable (inPos) to int too:

           const int inPos32 = static_cast<int>(inPos);
           for (int c=0; c!=numOutChannels; ++c)
           {
              SampleType* outBuffer = block.getChannelPointer(c);
              const SampleType* inBuffer = m_playedSample.m_sampleData[c];
              m_interpolators[c].process(m_inputAdvance, inBuffer + inPos32, outBuffer + s, n);
           }

Nice.

I actually already fixed it. The problem I was having was the fractional offset caused by the fractional ratio. I had to recalculate the input buffer to account for the fractional part. Shifting all the values so to speak. Iā€™ve put my answer in this thread if youā€™re interested:

Hey Juan!

I am trying to implement LagrangeInterpolation into my sampler voice, and iā€™m having issues, iā€™m getting a click every buffer length (2 samples of silence).

This is my current code - wonder if you or anyone else can see what Iā€™m doing wrong?
I feel like it may have to do with your inPos32 or s variables. Thanks

    auto& data = *playingSound->data;
    const float* const inL = data.getReadPointer (0);
    const float* const inR = data.getNumChannels() > 1 ? data.getReadPointer (1) : nullptr;

    float* outL = outputBuffer.getWritePointer (0, startSample);
    float* outR = outputBuffer.getNumChannels() > 1 ? outputBuffer.getWritePointer (1, startSample) : nullptr;
    
    LagrangeInterpolator resampler;
    
    resampler.process(pitchRatio, inL + (int) sourceSamplePosition, outL, numSamples);
    resampler.reset();
    resampler.process(pitchRatio, inR + (int) sourceSamplePosition, outR, numSamples);
    resampler.reset();
    
    sourceSamplePosition += (numSamples * pitchRatio);

Try these things:

  1. You are passing to your interpolator a nullptr when the signal is mono! (that will produce an error) Simply create an ā€œifā€ instead that ternary operator thing.

  2. If the code you are showing belong to any kind of processBlock() call (and I really suspect it due to the last line where you advance the position), keep in mind that resamplers are stateful, this mean you need to create an object per audio channel and donā€™t destroy it. Just create them as member variable. Then only use reset when something really drastic happens to the sample, ie, you load a new one.

Thank You Juan!!

The sample dropouts are not as bad as they were!
But still getting clicks when I play pitched samples, any thoughts?? You have no idea how much I appreciate it!

    auto& data = *playingSound->data;
    const float* const inL = data.getReadPointer (0);
    const float* const inR = data.getNumChannels() > 1 ? data.getReadPointer (1) : nullptr;

    float* outL = outputBuffer.getWritePointer (0, startSample);
    float* outR = outputBuffer.getNumChannels() > 1 ? outputBuffer.getWritePointer (1, startSample) : nullptr;
    
    if (outL != nullptr)  {   lagrangeResamplerL.process(pitchRatio, inL + (int) sourceSamplePosition, outL, numSamples);  }
    if (outR != nullptr)  {   lagrangeResamplerR.process(pitchRatio, inR + (int) sourceSamplePosition, outR, numSamples);    }
    
    sourceSamplePosition += (numSamples * pitchRatio);

Assuming pitchRatio is right, I donā€™t see nothing wrong with your source. However, when I posted this I was having similar issues. I found another interpolation algo which I adapted and it worked for me. Unfortunately I donā€™t remember the link, but I could share it with you (itā€™s an hermite one, however).

You also may try with another juce interpolator to check if there are any problem with it

I would really really really appreciate you sharing yours with me if you donā€™t mind!

Same problems even with Linear Interpretation :frowning:

Hi, just copy and paste this interpolator. It works in the same way native Juce interpolators do. Please, let me know if this work for you.

(pasted in the next message)

#pragma once

#include "../JuceLibraryCode/JuceHeader.h"

// Interpoladores
template<typename SampleType>
class JHermiteInterpolator
{
public:
   JHermiteInterpolator() {reset();}

   inline void JUCE_VECTOR_CALLTYPE process(const double speedRatio, const SampleType* inputSamples, SampleType& outputSamples)
   {
      // Cada outsample es speedRation samples de input
      int inputPos = 0;

      m_currentPos += speedRatio;
      while (m_currentPos >= 1.0)
      {
         advanceSample(inputSamples[inputPos++]);
         m_currentPos -= 1.0;
      }

      const SampleType x = static_cast<SampleType>(m_currentPos);

#if JUCE_DEBUG
      const SampleType y0 = m_buffer[0];
      const SampleType y1 = m_buffer[1];
      const SampleType y2 = m_buffer[2];
      const SampleType y3 = m_buffer[3];

      const SampleType c0 = y1;
      const SampleType c1 = y2 - u13*y0 - u12*y1 - u16*y3;
      const SampleType c2 = u12 * (y0+y2) - y1;
      const SampleType c3 = u16 * (y3-y0) + u12 * (y1-y2);

      outputSamples = ((c3*x+c2) * x + c1) * x + c0;
#else
      // Es simplemente la formula sustituida, para no crear tanto sampletype, que parece que no, pero cuesta
      outputSamples = (((u16 * (m_buffer[2]-m_buffer[0]) + u12 * (m_buffer[1]-m_buffer[2]))*x+(u12 * (m_buffer[0]+m_buffer[2]) - m_buffer[1])) * x + (m_buffer[2] - u13*m_buffer[0] - u12*m_buffer[1] - u16*m_buffer[2])) * x + m_buffer[1];
#endif

   }
   inline void process(const double speedRatio, const SampleType* inputSamples, SampleType* outputSamples, int numOutputSamplesToProduce)
   {
      // Cada outsample es speedRation samples de input
      int inputPos = 0;

      while (--numOutputSamplesToProduce >= 0)
      {
         m_currentPos += speedRatio;
         while (m_currentPos >= 1.0)
         {
            advanceSample(inputSamples[inputPos++]);
            m_currentPos -= 1.0;
         }

         const SampleType y0 = m_buffer[0];
         const SampleType y1 = m_buffer[1];
         const SampleType y2 = m_buffer[2];
         const SampleType y3 = m_buffer[3];

         const SampleType c0 = y1;
         const SampleType c1 = y2 - u13*y0 - u12*y1 - u16*y3;
         const SampleType c2 = u12 * (y0+y2) - y1;
         const SampleType c3 = u16 * (y3-y0) + u12 * (y1-y2);

         const SampleType x = static_cast<SampleType>(m_currentPos);
         const SampleType l = ((c3*x+c2) * x + c1) * x + c0;

         *outputSamples++ = l;
      }
   }

   void reset(const SampleType value = SampleType(0)) {m_currentPos = 0.0; m_buffer.fill(SampleType(value));}
   void resetPos() {m_currentPos = 0.0;}
private:
   double m_currentPos = 0;
   std::array<SampleType, 4> m_buffer;
   inline void JUCE_VECTOR_CALLTYPE advanceSample(const SampleType newSample)
   {
      if (abs(newSample - m_buffer[3]) > 0.4f)
      {
         m_buffer[0] = newSample;
         m_buffer[1] = newSample;
         m_buffer[2] = newSample;
         m_buffer[3] = newSample;
      }
      else
      {
         m_buffer[0] = m_buffer[1];
         m_buffer[1] = m_buffer[2];
         m_buffer[2] = m_buffer[3];
         m_buffer[3] = newSample;
      }
   }
   inline void JUCE_VECTOR_CALLTYPE processCurrentPos(SampleType& outputSamples) const
   {
      const SampleType x = static_cast<SampleType>(m_currentPos);

#if JUCE_DEBUG
      const SampleType y0 = m_buffer[0];
      const SampleType y1 = m_buffer[1];
      const SampleType y2 = m_buffer[2];
      const SampleType y3 = m_buffer[3];

      const SampleType c0 = y1;
      const SampleType c1 = y2 - u13*y0 - u12*y1 - u16*y3;
      const SampleType c2 = u12 * (y0+y2) - y1;
      const SampleType c3 = u16 * (y3-y0) + u12 * (y1-y2);

      outputSamples = ((c3*x+c2) * x + c1) * x + c0;
#else
      // Es simplemente la formula sustituida, para no crear tanto sampletype, que parece que no, pero cuesta
      outputSamples = (((u16 * (m_buffer[2]-m_buffer[0]) + u12 * (m_buffer[1]-m_buffer[2]))*x+(u12 * (m_buffer[0]+m_buffer[2]) - m_buffer[1])) * x + (m_buffer[2] - u13*m_buffer[0] - u12*m_buffer[1] - u16*m_buffer[2])) * x + m_buffer[1];
#endif
   }

   constexpr inline static SampleType u12 = (SampleType(1) / SampleType(2));
   constexpr inline static SampleType u13 = (SampleType(1) / SampleType(3));
   constexpr inline static SampleType u16 = (SampleType(1) / SampleType(6));

   JUCE_LEAK_DETECTOR(JHermiteInterpolator);
};

Thanks!! Got it working but iā€™m still having some light clicking noises happening that I cant figure out the source of :frowning: They seem to be happening at the start of each buffer. I believe that its my implementation thatā€™s wrong, not the interpolators.

Would love to know if you can see anything wrong with what iā€™m doing ?? even has the clicks when I use a linear interpreter. Thank you !!

    auto& data = *playingSound->data;
    const float* const inL = data.getReadPointer (0);
    const float* const inR = data.getNumChannels() > 1 ? data.getReadPointer (1) : nullptr;
    
    AudioBuffer<float> voiceBuffer = AudioBuffer<float>(2, numSamples);
    float* voiceWriteL = voiceBuffer.getWritePointer (0, 0);
    float* voiceWriteR = voiceBuffer.getWritePointer (1, 0);

    float* outL = outputBuffer.getWritePointer (0, startSample);
    float* outR = outputBuffer.getNumChannels() > 1 ? outputBuffer.getWritePointer (1, startSample) : nullptr;
    
    if (outL != nullptr)  {   hermiteResamplerL1.process(pitchRatio, inL + (int) sourceSamplePosition, voiceWriteL, numSamples);  }
    if (outR != nullptr)  {   hermiteResamplerR1.process(pitchRatio, inR + (int) sourceSamplePosition, voiceWriteR, numSamples);  }
    
    for (int i = 0; i < numSamples; ++i)
    {
        
        float l = voiceBuffer.getSample(0, i);
        float r = voiceBuffer.getSample(1, i);

        auto envelopeValue = adsr.getNextSample();

        l *= lgain * envelopeValue;
        r *= rgain * envelopeValue;

        if (outR != nullptr)
        {
            *outL++ += l;
            *outR++ += r;
        }
        else
        {
            *outL++ += (l + r) * 0.5f;
        }
        
        sourceSamplePosition += pitchRatio;

        if (sourceSamplePosition > playingSound->length)
        {
            stopNote (0.0f, false);
            break;
        }
    }

The first thing I noticed is you are allocating a new buffer in this line

AudioBuffer<float> voiceBuffer = AudioBuffer<float>(2, numSamples);

Instead, each voice should have its own buffers created and resized as a member variable. Use the AudioProcessor::prepareToPlay() for that. And instead using that buffer, use an dsp::AudioBlock which point to it. The reason is that AudioBlock is lighter (since it hasnā€™t that atomic flag). Usually I keep both of them as member variables in each voice, then I only use the AudioBlock.

Then, if you suspect the host may send you bigger buffers sizes, you can do something like this at the beggining of your processBlock:

   const int numSamples = audioBuffer.getNumSamples();


   for (int i=0; i!=numSamples; )
   {
      const int max = jmin((numSamples - i), m_maxBufferSize);
      dsp::AudioBlock<float> outBlock = dsp::AudioBlock<float>(writeData, 2, i, max);

      // Then you process the blocks here

      i += max;
   }

Maybe this may fix the noises!

How are you creating a dsp::AudioBlock inside each voice? Are you modifying the modules or creating an inherited class?

Iā€™ve been modifying the juce modules (which I know your advised not to do but I cant see how to do it another way) but I canā€™t get the dsp files to be included and im getting the error ā€œUse of undeclared identifier ā€˜dspā€™ā€.

Thanks again for the help it means so much !!!

For me, each voice is an object. I write that object in this way:

class MyVoice
{
public:
	void prepare(const int numSamples, const int numChannels)
	{
		m_workBuffer.setSize(numChannels, numSamples);
		m_workBuffer.clear();
		m_workBlock = dsp::AudioBlock<float>(m_workBuffer);
	}

	void process()
	{
		// I use here m_workBlock
		float* channelL = m_workBlock.getChannelPointer(0);
		float* channelR = m_workBlock.getChannelPointer(1);

	}

private:
	AudioSampleBuffer m_workBuffer;
	dsp::AudioBlock<float> m_workBlock;
};

Ah, and you need to include ā€˜dspā€™ using Projucer:

In my case itā€™s grey, because I have it already added. You may use a simple float** pointer instead the AudioBlock, but itā€™s less safe

Ohh so you are not using a juce::SamplerVoice? you are making your own class?

I donā€™t, but you could inherit from it if you want to use it!