Clicking sound at beginning and end of MIDI notes using juce_dsp

I’ve declared the synthesis buffer in the class scope, and I’m assuming newBlocksize is meant to be samplesPerBlock. It still has a clicking noise at the end of MIDI notes, but now I’ve noticed that it’s only some notes. For example, a C clicks, while a D# does not. My current code is below.

void prepareToPlay(double sampleRate, int samplesPerBlock, int outputChannels)
{
    synthesisBuffer.setSize(1, samplesPerBlock, true, true, true);

    juce::dsp::ProcessSpec spec;
    spec.maximumBlockSize = samplesPerBlock;
    spec.sampleRate = sampleRate;
    spec.numChannels = outputChannels;

    sinOsc.prepare(spec);
    triangleOsc.prepare(spec);
    sawOsc.prepare(spec);
    squareOsc.prepare(spec);

    gain.prepare(spec);

    adsr.setSampleRate(sampleRate);
}

void renderNextBlock(AudioBuffer<float>& outputBuffer, int startSample, int numSamples)
{
    dsp::AudioBlock<float> audioBlock{ synthesisBuffer };

    if (waveType == sine)
        sinOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
    else if (waveType == triangle)
        triangleOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
    else if (waveType == saw)
        sawOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
    else if (waveType == square)
        squareOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));

    gain.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));

    adsr.applyEnvelopeToBuffer(synthesisBuffer, startSample, numSamples);

    for (int chan = 0; chan < outputBuffer.getNumChannels(); ++chan)
        outputBuffer.addFrom(chan, startSample, synthesisBuffer, 0, startSample, numSamples, 1.0f);
}

OK, so it’s possible that the first line of your renderNextBlock makes an AudioBlock that includes the ENTIRE synthesis buffer, even if there will be extra empty samples at the end (if numSamples is less than the current size of the synthesisBuffer).

You can use AudioBuffer’s special alias constructor to make a proxy buffer that refers to only the segment of synthesisBuffer that you’ll be using:

void renderNextBlock(AudioBuffer<float>& outputBuffer, int startSample, int numSamples)
{
    AudioBlock<float> synthesisBufferProxy (synthesisBuffer.getArrayOfWritePointers(), 1, 0, numSamples);
    dsp::AudioBlock<float> audioBlock{ synthesisBufferProxy };

    if (waveType == sine)
        sinOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
    else if (waveType == triangle)
        triangleOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
    else if (waveType == saw)
        sawOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
    else if (waveType == square)
        squareOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));

    gain.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));

    adsr.applyEnvelopeToBuffer(synthesisBufferProxy);

    for (int chan = 0; chan < outputBuffer.getNumChannels(); ++chan)
        outputBuffer.addFrom(chan, startSample, synthesisBufferProxy, 0, 0, numSamples, 1.0f);
}

I get an error on a couple of lines:

adsr.applyEnvelopeToBuffer(synthesisBufferProxy, startSample, numSamples);

“no instance of function template matches the argument list”

and

outputBuffer.addFrom(chan, startSample, synthesisBufferProxy, 0, 0, numSamples, 1.0f);

“no instance of overloaded function matches the argument list”

Seems to be a problem with the audioBlock being an alias for an AudioBlock instead of an AudioBuffer.

synthesisBufferProxy needs to be an AudioBuffer that is an alias to another audio buffer. So when you create your AudioBlock by referencing synthesisBufferProxy, the AudioBlock should indeed be an alias for an AudioBuffer

you should change your line

adsr.applyEnvelopeToBuffer(synthesisBufferProxy, startSample, numSamples);

to be

adsr.applyEnvelopeToBuffer(synthesisBufferProxy, 0, numSamples);

because the length of synthesisBufferProxy is numSamples.

synthesisBufferProxy is an AudioBuffer than references sample #s 0 to numSamples-1 in the class’s internal synthesisBuffer.

I know that renderNextBlock has a startSample argument – but up until the very end of your rendering process, when you’re actually writing from synthesisBuffer to the output, you should be working with sample #s 0 to numSamples-1 in the synthesisBuffer. To ensure that synthesisBuffer will always have enough samples allocated internally.

Then, when you write to output, what you have is correct:

outputBuffer.addFrom(chan, startSample, synthesisBufferProxy, 0, 0, numSamples, 1.0f);

This line takes sample #s 0 to numSamples-1 of the synthesisBufferProxy (which is really a reference to sample #s 0 to numSamples-1 of the synthesisBuffer, which is where we have rendered our output to), and adds that to sample #s startSample to startSample + numSamples - 1 of the outuptBuffer, which is where the segment needs to go in the Synth parent’s larger output buffer.

This should be correct, based on my current understanding of how things work… ¯_(ツ)_/¯

can you post your entire renderNextBlock() of your SynthVoice class with how it looks now?

OMG I see that it was my error! I did write AudioBlock in my example code, I’m so sorry about that!

it should be

void renderNextBlock(AudioBuffer<float>& outputBuffer, int startSample, int numSamples)
{
    AudioBuffer<float> synthesisBufferProxy (synthesisBuffer.getArrayOfWritePointers(), 1, 0, numSamples);
    dsp::AudioBlock<float> audioBlock{ synthesisBufferProxy };
}

I’m sorry! This should fix it, hopefully!

No worries, thanks for all the help! The clicking sound is completely gone now.

1 Like

Yay!! I’m glad it works now :grinning:

What ended up being the final solution for this?

I’m also struggling with this. I thought I had it working until I noticed I get different behaviour when I trigger a note using Reaper’s virtual MIDI keyboard, than I do when sequencing notes in the piano roll. :man_shrugging: So everything works fine until I sequence a MIDI note during performance. Here’s a short video, apologies for the quality of the sound but there’s no denying those clicks!

working on this now aswell!

Let me know how it goes. The advice dished out by @benvining was spot on in terms of fixing the initial clicks. My synth is a little more complex in that it is part of an AudioProcessorGraph. I need to see if that is the root of the problem.

1 Like

I’m having the same issue even if note on or note off doesn’t directly touch the write pointer in the processBlock

I believe it is due to a misplacement of the thread being interrupted meaning.

If the entire block in the voice class is playing no problem no clicks. However, if you start at sample 314 then our sound is being triggered by the offset built into the Juce voice class. with the start sample.

For me I am passing in the size of the buffer but calculating a repitch with only about half that amount.

I have not fixed it but I’m pretty sure thats the problem. Because my other synth class has no problem.

Thanks for the update. I can’t look over this now as I’m not by my PC. But is this not what @benvining was talking about here, i.e, use 0 instead of start sample?

I’ve also found that noteOFF gets called when you press a key. I have isolated my clicks to only note off and playing multiple notes at different times.

I was able to ensure no clicks on initial press by making sure everything was cleared for new calculations. I also found my beloved envelope class did not work backwards counting from the last sample to 0. whoops!

Ok I got it working.

I juce use the voice class to check if a note is playing.

Then use the child of that voice to generate a sound in a parent buffer no clicks.

It’s because juce subdivides the buffer. I just need the midi split up… nah mean?

Well it works in debug. Nor in release but three days of clicks are enough to make anyone go insane!

I should be able to get it tomorrow!

1 Like

Just one thing to look out for: If you apply your adsr or gainRamp or whatever method to the buffer you got handed in, you are attenuating all previously rendered voices as well.
Your adsr needs to act on a local buffer and afterwards add the generated buffer to the bufer you got handed in.
This was mentioned way above by @alibarker1, but I thought it’s worth mentioning.