Popping sounds in SynthesiserVoice implementation?

I was having this issue a bit ago, so I followed @martinrobinson-2 advice in this thread and it seemed to fix the issue. Then, I was testing the same project today and noticed the very occasional click/pop, but not consistently at the beginning/end of midi notes like before. I changed the renderNextBlock() function to keep track of when the output samples were making large jumps:

void FmVoice::renderNextBlock(juce::AudioBuffer<float> &outputBuffer, int startSample, int numSamples)
{
    for(int i = 0; i < numSamples; ++i)
    {
        //handle FM routing
        applyModulations();
        //calculate LFO effects
        for(int lfo = 0; lfo < 4; ++ lfo)
        {
            applyLfo(lfo);
        }
        auto sum = 0.0f;
        //add up the samples from all the operators set to 'audible'
        for(int o = 0; o < operatorCount; ++o)
        {
            auto newSample = operators[o]->sample(fundamental);
            if(operators[o]->isAudible)
                sum += newSample;
        }
        //add that sum to the output buffer
        for(int channel = 0; channel < outputBuffer.getNumChannels(); ++channel)
        {
            outputBuffer.addSample(channel, i + startSample, sum);
        }
        auto difference = fabs(sum - lastSample);
        if(difference > 0.2)
        {
            printf("Output jumped: %f at sample %d buffer %d\n", difference, i, numBuffers);
        }
        lastSample = sum;
    }
 printf("Buffer %d completed\n", numBuffers);
            numBuffers++;
}

The results of those last two printf statements are strange, it seems like almost every buffer finishes without any big jump, but occasionally a jump will occur, always on sample 0. Those jumps then seem to come in groups, each one occurring maybe 3-4 buffers after the last. It looks like this:

buffer 9659 completed
buffer 9660 completed
Output jumped: 0.531951 at sample 0 buffer 9661
buffer 9661 completed
buffer 9662 completed
buffer 9663 completed
buffer 9664 completed
buffer 9665 completed
Output jumped: 0.555626 at sample 0 buffer 9666
buffer 9666 completed
Output jumped: 0.488747 at sample 0 buffer 9667

With 512 samples per buffer and a sample rate of 44.1kHz, it seems like those jumps are coming far too close together to be caused by a single midi on/off message, but I can’t think what might be causing the first sample of a buffer to change dramatically in this sort of pattern. Any ideas as to what might be causing this are greatly appreciated.

Are you re-triggering the LFO by any chance? That would explain why you always get 3 or 4 jumps, too.

1 Like

As in restarting the LFO wave with the midi noteOn? No, but in the audio processor’s processBlock() function I do update the parameters of the LFOs from an AudioProcessorValueTreeState.

Other things I’d check:

  • Is “startSample” always zero ? Your main for loop assumes it is, but when adding the sum to the output, you use the variable “startSample”, not just zero.

  • Make sure you are clearing the output buffer before processing each block.

  • Can disable the LFO just to make sure that’s not your culprit.

Otherwise, might be something in applyModulations();

1 Like

Thanks, I’m clearing the buffer and not running the LFO. I have it set up to use six synth voices, so I kept track of the large jumps per each voice and recorded them on the console and I get this:
0 total jumps
0 total jumps
0 total jumps
284 total jumps
809 total jumps
604 total jumps
The first three always have 0 jumps, could there be some reason that the last three don’t do the same thing?

Yes, there is definitely a reason. Just need to find out what that reason is.

My guess is it’s something wrong inside applyModulations(), or maybe in the Operators class.

1 Like

Ok so I’ve been working on this and even though my processBlock() function just has the line: synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
I was curious so I added a line to show in the console any time the voice’s renderNextBlock() function starts with a non-zero value for startSample,and it turns out that it is happening. Usually, I’ll see several buffers in a row which each start with the same non-zero sample index, like this:
startSample is non-zero: (62) in buffer 3764
buffer 3764 completed
startSample is non-zero: (62) in buffer 3765
buffer 3765 completed
startSample is non-zero: (62) in buffer 3766
buffer 3766 completed
startSample is non-zero: (62) in buffer 3767
buffer 3767 completed
Output jumped: 0.454227 at sample 0 buffer 3768
buffer 3768 completed
It seems like this must be down the way in which the Synthesiser object renders each voice. Has anyone seen this before?

I wonder if we need to see a bit more of your code? Having had the pain on this in the past it’s one of my “charity” donations to the community to help out when they crop up! :innocent::relaxed:

2 Likes

Thanks! I’ve figured out that it doesn’t jump if I just open the plugin and don’t give it any midi notes, so I think the issue must be either to do with how it’s handling MIDI events or the actual synthesis code. The processor’s processBlock function just looks like this:

void HexFmAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    for(int voice = 0; voice < synth.getNumVoices(); ++ voice)
    {
        if((thisVoice =  dynamic_cast<FmVoice*>(synth.getVoice(voice))))
        {
             //set parameters for each of the four LFOs
            thisVoice->setRoutingFromGrid(&tree);
            for(int n = 0; n < 4; ++n)
            {
                auto nStr = juce::String(n);
                auto rateParam = "lfoRateParam" + nStr;
                auto levelParam = "lfoLevelParam" + nStr;
                auto waveParam = "lfoWaveParam" + nStr;
                auto targetParam = "lfoTargetParam" + nStr;
                
                thisVoice->updateLfoRate(tree.getRawParameterValue(rateParam), n);
                thisVoice->updateLfoLevel(tree.getRawParameterValue(levelParam), n);
                thisVoice->updateLfoWave(tree.getRawParameterValue(waveParam), n);
                thisVoice->updateLfoTarget(tree.getRawParameterValue(targetParam), n);
            }
            //updating each of the 6 oscillators
            for(int i = 0; i < numOperators; ++i)
            {
                auto iStr = juce::String(i);
                auto ratioId = "ratioParam" + iStr;
                auto levelId = "levelParam" + iStr;
                auto modIndexId = "indexParam" + iStr;
                auto audibleId = "audibleParam" + iStr;
                
                auto delayId = "delayParam" + iStr;
                auto attackId = "attackParam" + iStr;
                auto holdId = "holdParam" + iStr;
                auto decayId = "decayParam" + iStr;
                auto sustainId = "sustainParam" + iStr;
                auto releaseId = "releaseParam" + iStr;
                
                thisVoice->setParameters(i, tree.getRawParameterValue(ratioId),
                                         tree.getRawParameterValue(levelId),
                                         tree.getRawParameterValue(modIndexId),
                                         tree.getRawParameterValue(audibleId),
                                         tree.getRawParameterValue(delayId),
                                         tree.getRawParameterValue(attackId),
                                         tree.getRawParameterValue(holdId),
                                         tree.getRawParameterValue(decayId),
                                         tree.getRawParameterValue(sustainId),
                                         tree.getRawParameterValue(releaseId)
                                        );
            }
        }
    }
    buffer.clear();
    synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
}

My first instinct is to try just commenting out each of the parameter updating/ modulation calculating functions that run on every buffer to see if I can’t narrow down the issue

As several previous replies suggest, we’re not sure what applyModulations() does so it could be something in there?

Another issue I notice, not sure if it’s relevant, is doing that stuff with Strings on the audio thread. You are almost certainly doing memory allocation on the audio thread there. Which must be avoided.

2 Likes

Here’s a list of a few knowns from the code we can see:

  • Constructing and concatenating a juce::String on the audio thread allocates memory and must be avoided on the audio thread (as mentioned above)
  • Ironically, the debugging printf() in FmVoice::renderNextBlock() might be a problem since that also makes systems calls, probably allocates memory and so on

Here’s a list of a few unknowns from the code we can see:

  • What applyModulations() does (as we have mentioned above)
  • What applyLfo() does
  • What auto newSample = operators[o]->sample(fundamental); actually does — e.g., what type is operators (and therefore what the operaror[](int) call does); presumably newSample returns a float but is that call very expensive, even if only sometimes?
  • What FmVoice::setRoutingFromGrid() does
  • What the various FmVoice::updateLfoXXX() functions do — e.g., FmVoice::updateLfoRate()

From my experience, the issue is going to be annoyingly simple! But possibly very hard to find!

1 Like

Ahh, good to know, I’ll have to make sure I’m not allocating memory anywhere. I tried running it with applyModulations() and applyLfo() and setRoutingFromGrid() commented out to no avail. Right now, I’ve decided to rewrite pretty much all of the audio thread code just to see how far down the issue starts.

Ok, so I redid the PluginProcessor processBlock() and the synth voice renderNextBlock() functions, took out all the modulations such that I’m basically just finding a sine wave value, multiplying it by an envelope, and putting that into the buffers, yet I still get a few hundred jumps for the first several voices.
The processBlock():

void HexFmAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    for(int i = 0; i < numVoices; ++i)
    {
        thisVoice = dynamic_cast<FmVoice*>(synth.getVoice(i));
        for(int op = 0; op < numOperators; ++op)
        {
            thisVoice->setParameters(op, tree.getRawParameterValue(ratioIds[op]),
                             tree.getRawParameterValue(levelIds[op]),
                             tree.getRawParameterValue(modIndexIds[op]),
                             tree.getRawParameterValue(audibleIds[op]),
                             tree.getRawParameterValue(delayIds[op]),
                             tree.getRawParameterValue(attackIds[op]),
                             tree.getRawParameterValue(holdIds[op]),
                             tree.getRawParameterValue(decayIds[op]),
                             tree.getRawParameterValue(sustainIds[op]),
                             tree.getRawParameterValue(releaseIds[op])
                            );
            
        }
        thisVoice->setRoutingFromGrid(&tree, routingIds);
    }
    buffer.clear();
    synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
}

And the renderNextBlock():

void FmVoice::renderNextBlock(juce::AudioBuffer<float> &outputBuffer, int startSample, int numSamples)
{
    for(int sample = startSample; sample < (startSample + numSamples); ++sample)
    {
        voiceSample = 0.0f;
        for(int op = 0; op < operatorCount; ++op)
        {
            lastOpSample = operators[op]->sample(fundamental);
            if(operators[op]->isAudible)
                voiceSample += lastOpSample;
        }
        for(int channel = 0; channel < outputBuffer.getNumChannels(); ++channel)
        {
            outputBuffer.addSample(channel, sample, voiceSample);
        }
        if(fabs(lastSample - voiceSample >= 0.2f))
            ++numJumps;
        lastSample = voiceSample;
    }
}

The operator’s sample() function is based on the sine wave buffer oscillator in the Maximilian library, the samples are generated with this:

float Operator::sample(float fundamental) 
{
    lastOutputSample = envelope.process(osc.sinebuf((double)(fundamental * ratio) + (modOffset * modIndex)) * level * amplitudeMod);
    return lastOutputSample;
}

Where osc is just a maxiOsc object. Is it possible that there’s something wrong with the way the samples are generated or is the issue more likely to do with the handling of buffers/midi events?

If you’re multiplying a sine wave by an envelope, and you’re pretty sure the sine wave is correct, then that leaves the envelope as a likely culprit.

I’m guessing the envelope is re-triggering to zero every time a note-on is received.

What if you you (temporarily) remove the envelope processing and just process the sine without enveloping? Does that stop the clicks?

1 Like

I had come to the same conclusion, I did some reading and changed the envelope class to be logarithmic rather than linear :expressionless: That plus switching to a few range based for loops seemed to solve the issue. @martinrobinson-2 was right about annoyingly simple!

1 Like