I’m trying my hand at writing some delay-based effects for the first time and am trying to create a chorus effect. I’ve been able to figure out how to write the ring buffer for the delay and how to use a sine wave as an LFO to modulate the read position of the ring buffer to create a vibrato. But I’m getting quite a bit of noise from the LFO and can’t quite figure out how to get rid of it.
An example of the noise using the Sine Wave Synth in the AudioPluginHost:
sine_noise.zip (381.0 KB)
I based the sine wave LFO on JUCE’s sine wave synth tutorial:
class SineGenerator
{
public:
SineGenerator() {};
~SineGenerator() {};
void initSineWave(const double sampleRate, const float frequency)
{
currentSampleRate = sampleRate;
targetFrequency = currentFrequency = frequency;
updateAngleDelta();
}
float renderSineWave()
{
float currentSample = 0.f;
if (! juce::approximatelyEqual (targetFrequency, currentFrequency))
{
currentSample = std::sin ((float) currentAngle);
currentFrequency += frequencyIncrement;
updateAngleDelta();
currentAngle += angleDelta;
}
else
{
currentSample = std::sin ((float) currentAngle);
currentAngle += angleDelta;
}
return currentSample;
}
inline void resetCurrentFrequency() { currentFrequency = targetFrequency; }
inline void setTargetFrequency(const double frequency) { targetFrequency = frequency; }
inline void setIncrement(const int numSamples) { frequencyIncrement = (targetFrequency - currentFrequency) / (double) numSamples; }
private:
inline void updateAngleDelta() { angleDelta = (currentFrequency / currentSampleRate) * juce::MathConstants<double>::twoPi; }
double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0, frequencyIncrement = 0.0;
double currentFrequency = 500.0, targetFrequency = 500.0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SineGenerator)
};
And for the PluginProcessor.h:
class ChorusAudioProcessor : public juce::AudioProcessor
{
public:
/* standard PluginProcessor code */
...
private:
SineGenerator lfoSine;
std::unique_ptr<juce::AudioBuffer<float>> delayBuffer;
float *delayInL, *delayInR;
const float *delayOutL, *delayOutR;
int delayWritePos = 0;
float mix = 0.5f, lfoVibe = 1.f, delayL = 0.f, delayR = 0.f;
double devSampleRate = 48000.0;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Juce_delayAudioProcessor)
};
PluginProcessor.cpp initialization…
ChorusAudioProcessor::ChorusAudioProcessor()
...
{
delayBuffer = std::make_unique<juce::AudioBuffer<float>>();
}
...
void ChorusAudioProcessor::prepareToPlay (double sampleRate, int /* samplesPerBlock */)
{
const int delayNumChannels = getTotalNumInputChannels();
devSampleRate = sampleRate;
lfoSine.initSineWave(devSampleRate, 2.0);
delayBuffer->setSize(delayNumChannels, (int) devSampleRate);
delayOutL = delayBuffer->getReadPointer(0);
delayOutR = delayNumChannels > 1 ? delayBuffer->getReadPointer(1) : nullptr;
delayInL = delayBuffer->getWritePointer(0),
delayInR = delayNumChannels > 1 ? delayBuffer->getWritePointer(1) : nullptr;
And process block:
void ChorusAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& /* midiMessages */)
{
const int totalNumInputChannels = getTotalNumInputChannels();
const int totalNumOutputChannels = getTotalNumOutputChannels();
const int delayNumSamples = delayBuffer->getNumSamples();
const float *inL = buffer.getReadPointer(0),
*inR = totalNumInputChannels > 1 ? buffer.getReadPointer(1) : nullptr;
float *outL = buffer.getWritePointer(0),
*outR = totalNumOutputChannels > 1 ? buffer.getWritePointer(1) : nullptr;
int numSamples = buffer.getNumSamples();
for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
buffer.clear (i, 0, buffer.getNumSamples());
if(inL == nullptr || outL == nullptr) return;
if(delayInL == nullptr || delayOutL == nullptr) return;
lfoSine.setIncrement(numSamples);
while(--numSamples >= 0)
{
const int delayTime = (int) std::floor(devSampleRate * (15e-3 * (double) lfoVibe)),
delayReadPos = delayWritePos < delayTime ? delayNumSamples + (delayWritePos - delayTime) : delayWritePos - delayTime;
float l = *inL++, r = inR == nullptr ? l : *inR++, tempL, tempR;
*(delayInL + delayWritePos) = l;
if(delayInR != nullptr) *(delayInR + delayWritePos) = r;
delayL = tempL = *(delayOutL + delayReadPos);
delayR = tempR = delayOutR == nullptr ? delayL : *(delayOutR + delayReadPos);
l = (l * mix) + (delayL * mix);
r = (r * mix) + (delayR * mix);
lfoVibe = 1.f + (0.05f * lfoSine.renderSineWave());
if(outR == nullptr)
{
*outL++ = (l + r) * 0.5f;
}
else
{
*outL++ = l;
*outR++ = r;
}
delayWritePos++;
if(delayNumSamples < delayWritePos) delayWritePos = 0;
}
lfoSine.resetCurrentFrequency();
}
For now, I’ve hard-coded a few values like the delay time (15ms), LFO speed (2hz), and vibrato depth.
The noise seems to be linked with the number of samples in the buffer and difference of the number of samples between each delayReadPos. Normalizing this difference removes the noise but also removes the modulation needed for the vibrato. I’ve tried calculating the delayTime outside of the while loop and incrementing delayReadPos inside the while loop, but the noise remained the same and the modulation worsened with larger buffer sizes.
Adding cubic spline interpolation and IIR bandpass filters to the vibrato signal reduces the noise:
sine_noise_filter.zip (401.2 KB)
But it’s obviously still present and at this point I’m at a loss ![]()

