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();
mPhaserL.setFrequency(mSliderPhaserFreq);
mPhaserR.setFrequency(mSliderPhaserFreq2);
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);
mSmoothLPL.snapToZero();
mSmoothLPR.snapToZero();
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
{
public:
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; }
protected:
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 */