What's wrong with the start/stopNote function of my synth voice?

Hello all,

I’m working on a synth project and cannot wrap my head around why what’s happening is happening, which is…

The start/stopNote functions sort of implode a little (at least in my case) when pressing notes on a MIDI keyboard quickly and the sustain is set to 0.0f, resulting in an indefinitely hanging note. This may sound strange, but I noticed that it works as intended as long as the key is held down as long or longer than the decay time.

When the sustain is at 1.0f, it behaves normally and the release time is factored in - all the good stuff. The closer the sustain get’s to 0.0f, the more the behavior above becomes apparent. For example if the sustain is 0.0f, and the decay is relatively long (let’s say 0.6s) then quickly pressing a key for less than 0.6s will result in the note hanging indefinitely at whatever amplitude it was released at.

I have been trying to write some checks to get around this, but I’m a little stumped on it - I think I may be in the weeds a bit too much. My code is below to get an idea of how it’s all implemented…

EDIT - I should add that I’ve got DBG(...) statements logging info, and one of them is printing whatever value comes out of getCurrentlyPlayingNote() every 10,000 samples. This acts as the amplitude does in that if the key is released before the attack / decay duration has passed (and the sustain is 0.0f), the note number returned is note -1… don’t know what’s going on.

TheVoice.h

class TheVoice: public SynthesiserVoice
{
public:
    TheVoice(...);
    ~TheVoice();

    void pitchWheelMoved(int pitchWheelPos) { }
    void controllerMoved(int controllerNumber, int controllerValue) { }

    bool canPlaySound(SynthesiserSound* sound) override;

    void startNote(int midiNoteNumber, float velocity, SynthesiserSound* sound, int currentPitchWheelPosition = 0) override;
    void stopNote(float velocity, bool allowTailOff) override;

    void clearNote();

    void renderNextBlock(AudioBuffer<float>& buffer, int startSample, int numSamples) override;

private:
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TheVoice)

    bool m_isNoteOn = false;
    bool m_isNoteCleared = true;

    float m_velocity = -1.0f;
    float m_tailOff = 0.0f;
};

TheVoice.cpp

void TheVoice::startNote(int midiNoteNumber, float velocity, SynthesiserSound* sound, int currentPitchWheelPosition)
{
    m_primaryOsc->update(m_midiNoteNumber, (float) getSampleRate());

    m_ampEg->noteOn();

    m_isNoteOn = true;
    m_isNoteCleared = false;
}

void TheVoice::stopNote(float velocity, bool allowTailOff)
{
    if(!allowTailOff)
    {
        clear();
        return;
    }

    m_ampEg->noteOff();

    m_isNoteOn = false;
}

void TheVoice::clear()
{
    clearCurrentNote();

    m_ampEg->reset();
    m_primaryOsc->reset();

    m_isNoteCleared = true;
}

void TheVoice::renderNextBlock(AudioBuffer<float>& buffer, int startSample, int numSamples)
{
    if(numSamples == 0) return;

    // Ensuring that stopNote will be called
    if(m_isNoteOn && !isKeyDown())
        stopNote(0.0f, true);

    const float sampleRate = (float) getSampleRate();

    m_ampEg->update(sampleRate);
    m_primaryOsc->update(m_midiNoteNumber, sampleRate);

    for (int sampleIdx = startSample; sampleIdx < (startSample + numSamples); sampleIdx++)
    {
        float ampEgMod = m_ampEg->evaluate();

        float oscVal = m_primaryOsc->evaluate(...);
        float ampVal = oscVal * ampEgMod;

        // The tail mult should remain 1.0f until the voice is inactive
        if(isVoiceActive()) m_tailOff = 1.0f;
        float finalVal = ampVal * m_tailOff;

        m_tailOff *= 0.99f;
        // If the note hasn't been cleared and the tail mult is near-zero, clear the note
        if(!m_isNoteCleared && m_tailOff < 0.00000001f)
            clear();

        for (int channelIdx = 0; channelIdx < buffer.getNumChannels(); channelIdx++)
            buffer.addSample(channelIdx, sampleIdx, finalVal);
    }
}

One point of question may be the evaluate(..) methods of both the EG and oscillator - these just compute eg->getNextSample() and a osc->readWavetable() respectively.

Thank you for looking at the post!