Pitch shifting using Doppler effect

I’m trying to implement pitch shifting using the Doppler effect.

I followed along with this Max MSP tutorial and have a working version in Max MSP 8 which sound wise I am very happy with. (attached the two files needed to listen in max)Doppler-pitch.zip (3.8 KB)

This is what mine sounds like whilst automating the phaser frequency:example_of_mine.mp3.zip (421.2 KB) Beautiful right? ha.

I’ve been re-creating it using JUCE and have (I think) some success, I can get an almost ring modulated sound but fear maybe that’s some sort of aliasing artifact.

Here’s my process block (apologies for any rookie errors, I’ve tried to keep L/R channels separate to stop clicking):

void FeedPitchAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
    juce::ScopedNoDenormals noDenormals;
    auto totalNumInputChannels  = getTotalNumInputChannels();
    auto totalNumOutputChannels = getTotalNumOutputChannels();
    // if you've got more output channels than input clears extra outputs
    for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
        buffer.clear (i, 0, buffer.getNumSamples());
    // set up some handy variables to keep our code cleaner
    const int bufferLength = buffer.getNumSamples();
    // getRawParameterValue returns a std::atomic<float>* as this is thread safe
    // by using ->load() we get the current state of the variable
    mSliderPhaserFreq = treeState.getRawParameterValue(PHASER_FREQ_ID)->load();
    mSliderPhaserFreq2 = treeState.getRawParameterValue(PHASER2_FREQ_ID)->load();
    if (totalNumOutputChannels == 2)
        auto* channelDataL = buffer.getWritePointer(0);
        auto* channelDataR = buffer.getWritePointer(1);
        float delayDataL;
        float delayDataR;
        int phaserSampleL;
        int phaserSampleR;
        for (int i = 0; i < bufferLength; i++)
            mPhaserSampleL = fmod((mPhaserL.getSample() + 0.0f), 1.0f) * 100.0f;
            mPhaserSampleL = workOutMS(mSampleRate, mPhaserSampleL);
            phaserSampleL = juce::roundToInt(mPhaserSampleL) % bufferLength;
            mPhaserSampleR = fmod((mPhaserR.getSample() + 0.0f), 1.0f) * 100.0f;
            mPhaserSampleR = workOutMS(mSampleRate, mPhaserSampleR);
            phaserSampleR = juce::roundToInt(mPhaserSampleR) % bufferLength;
            mDelayLine.pushSample(0, channelDataL[i]);
            mDelayLine.pushSample(1, channelDataR[i]);
            delayDataL = mDelayLine.popSample(0, phaserSampleL);
            delayDataR = mDelayLine.popSample(1, phaserSampleR);
            mDelayLine.pushSample(0, delayDataL * mSliderFeedbackGain);
            mDelayLine.pushSample(1, delayDataR * mSliderFeedbackGain);
            channelDataL[i] += mSmoothLPL.processSample(0, mDelayLine.popSample(0, phaserSampleL));
            channelDataR[i] += mSmoothLPR.processSample(1, mDelayLine.popSample(1, phaserSampleR));

I’m lost as to how to continue with it, I thought if I could convert the number of samples to ms to match Max it would work, here’s how I’m doing it:

float FeedPitchAudioProcessor::workOutMS(float sampleRate, float ms)
    return((sampleRate / 1000) * ms);

Here’s my phaser class (it currently doesn’t behavior exactly like the max phaser~ due to it handling negative values differently:

#ifndef Phaser_h
#define Phaser_h

#define PI 3.14159265358979311599796346854

class Phasor
    double getSample()
        double ret = phase/PI_z_2;
        phase = fmod(phase+phase_inc, TAU); //increment phase
        return ret;
    void setSampleRate(double v) { sampleRate = v; calculateIncrement(); }
    void setFrequency(double v) { frequency = v; calculateIncrement(); }
    void setPhase(double v) { phase = v; calculateIncrement(); }
    void Reset() { phase = 0.0; }
    void calculateIncrement() { phase_inc = TAU * frequency / sampleRate; }
    double sampleRate = 44100.0;
    double frequency = 1.0;
    double phase = 0.0;
    double phase_inc = 0.0;
    const double TAU = 2*PI;
    const double PI_z_2 = PI/2;

#endif /* Phaser_h */

If you increase the sample rate, does the aliasing lessen? It looks like you’re not doing any interpolation on your delay line in JUCE (phaserSample* is rounded to the nearest int), whereas Max’s tapout~ is interpolated.

1 Like

Yeah does seem to have that effect I think, definitely improves the tone of the ‘pitch shifting’. Right okay yes I’ve been looking into that. Probably need something better than the smoothedValue do you think?

I remember reading on the forum where someone took the int value and then the decimal numbers and did something to work out the remainder.

Any ideas or pointing in the right direction?

smoothedValue is essentially a low-pass filter, which can reduce aliasing (which tends to be more prominent at high frequencies), but will also remove the high frequency content in your audio.

Interpolation means basically estimating the value of an audio signal at fractional times in between samples. The most basic form of interpolation would be a linear interpolation (which I believe is what tapout~ uses). Conceptually, it works by “drawing a line” between the two adjacent samples, and finding the value of that line at a fractional time. In pseudocode, it looks like:
y(t) = y[floor(t)] * (ceil(t)-t) + y[ceil(t)] * (t-floor(t))

For more complex, but higher-fidelity forms of interpolation, take a look at JUCE’s GenericInterpolator classes.

1 Like

Cool that’s really helpful thanks for your answer!

So just to see if I’ve got this.

y(t) = // new output interpolated sample
y[floor(t)] // floor value of the current sample
ceil(t)-t // ceil value of the previous sample?
y[(ceil(t)] // ceil value of the current sample
(t-floor(t) // floor value of the previous sample?

Almost! Remember t is in general some non-integer value.

y(t) = // new output interpolated sample
y[floor(t)] // value of the previous sample
ceil(t)-t // time until the next sample
y[(ceil(t)] // value of the next sample
(t-floor(t) // time since the previous sample
1 Like

Close! Thanks for explaining those my mind gears are turning!

So to work out time until next sample…

1 / sample rate * 1000 = per sample time in ms

t here is the time (in units of sampling periods) at which you wish to get an interpolated value of the (discrete) signal y. So y(2.5) is the interpolated value of the signal y at the midpoint between the 3rd and 4th samples (because t is zero-indexed).

1 Like

hard to follow both of you tbh, even though i already know how linear interpolation works. interesting to see that it can be perceived so differently though. i’d personally describe it like this:
imagine some method gave you a new index of 2.8 or so. it should read from a buffer. the decimal .8 balances them. it takes more from sample 3 than from 2. so in the end it would look like this: data[floor] + fraction * (data[ceil] - data[floor])
make your buffer one sample longer than you need, so you don’t have to care for ceil to go out of bounds.

1 Like

Thanks for both your help. I think I get the interpolating bit now and am happy to implement it. :fireworks: :sparkler: :tada: :confetti_ball:

I’ve interpolated the phaser samples which then modulate the delay lines ‘delay time’.
I’m now getting a more pitch shifty sound out of my plugin. Hooray! Progress.

My final question would be am I interpolating the right thing? Or have I fundamentally misunderstood and it’s the output of the delay line, the actual audio data I should be interpolating?

1 Like

I think so! Just to make sure I understand you correctly, you changed phaserSampleL and phaserSampleR from ints to floats, removed the roundToInt function in their assignments, and changed these lines:

delayDataL = mDelayLine.popSample(0, phaserSampleL);
delayDataR = mDelayLine.popSample(1, phaserSampleR);

to something like this:

delayDataL = mDelayLine.popSample(0, std::floor(phaserSampleL))
           * (std::ceil(phaserSampleL)-phaserSampleL)
           + mDelayLine.popSample(0, std::ceil(phaserSampleL))
           * (phaserSampleL - std::floor(phaserSampleL));
delayDataR = mDelayLine.popSample(1, std::floor(phaserSampleR))
           * (std::ceil(phaserSampleR)-phaserSampleR)
           + mDelayLine.popSample(1, std::ceil(phaserSampleR))
           * (phaserSampleR - std::floor(phaserSampleR));


1 Like

Sweet, I was almost there! I had indeed turned them into floats and removed the roundToInt function.

Where I was going wrong is that I wasn’t interpolating the audio stream, rather I was interpolating the phaser output… OOPS!

Got it all straightened out now and with your example above has totally cleared it up for me.

Biggggg thanks for all your help!