Adding glide/portamento to synth

I am working on a JUCE 8 audio plugin (my first audio plugin tbh), and while I managed to get most of the features I wanted (with some outside help), there is still one feature I can’t get to work. I have a switch to go from polyphonic mode to monophonic, and I’d like to have a glide/portamento slider to go with the mono mode (because what’s the point of a mono mode without a glide).
I did try to ask AIs, but aside from tanking performance, making mono mode silent or just not working, it wasn’t too helpful.
Below is the code for the SynthVoice class, hoping it helps.

// SynthVoice : voices management
bool SynthVoice::canPlaySound (juce::SynthesiserSound* sound) {
    if (!isEnabled) return false;
    return dynamic_cast<SynthSound*> (sound) != nullptr;
}

void SynthVoice::startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) {
    if (isMonoMode) {
        if (heldNotes.empty()){
            level = velocity * 0.15f;
            adsr.noteOn();
        }
        // Update legato stack
        heldNotes.erase(std::remove(heldNotes.begin(), heldNotes.end(), midiNoteNumber), heldNotes.end());
        heldNotes.push_back(midiNoteNumber);
    }
    else {
        // Poly: retrigger note normally
        level = velocity * 0.15f;
        adsr.noteOn();
        heldNotes.push_back(midiNoteNumber);
    }
}

void SynthVoice::stopNote(float velocity, bool allowTailOff) {
    if (isMonoMode) {
        // Remove current note from legato stack
        heldNotes.erase(std::remove(heldNotes.begin(), heldNotes.end(), getCurrentlyPlayingNote()), heldNotes.end());
    }
    // Poly or last mono note: release normally
    heldNotes.clear();
    if (allowTailOff)
        adsr.noteOff();
    else {
        adsr.reset();
        clearCurrentNote();
    }
}

void SynthVoice::pitchWheelMoved (int) {}
void SynthVoice::controllerMoved (int, int) {}

void SynthVoice::setADSRParameters (const juce::ADSR::Parameters& params) {
    adsr.setParameters (params);
}

void SynthVoice::setOscParams (int oscIndex, const OscParams& params) {
    oscStates[oscIndex].params = params;
}

void SynthVoice::setLFOModulation (float ampMod, float pitchMult) {
    lfoAmpMod    = ampMod;
    lfoPitchMult = pitchMult;
}

float SynthVoice::generateSample (int waveType, double angle, juce::Random& rng) {
    switch (waveType) {
        case 0: return (float) std::sin (angle);
        case 1: {
            double phase = angle / juce::MathConstants<double>::twoPi;
            return (float) (2.0 * (phase - std::floor (phase + 0.5)));
        }
        case 2: return (float) (2.0 / juce::MathConstants<double>::pi * std::asin (std::sin (angle)));
        case 3: return std::sin (angle) >= 0.0 ? 1.0f : -1.0f;
        case 4: return rng.nextFloat() * 2.0f - 1.0f;
        default: return (float) std::sin (angle);
    }
}

void SynthVoice::renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) {
    if (!adsr.isActive()) {
        clearCurrentNote();
        return;
    }
    const double sr = getSampleRate();
    const bool isStereo = outputBuffer.getNumChannels() >= 2;
    const float currentPitch = (float)getCurrentlyPlayingNote();
    // Pre-loop: compute pan and initial angleDeltas (no glide, pitch is constant per block)
    for (int o = 0; o < numOscillators; ++o) {
        auto& state = oscStates[o];
        if (!state.params.enabled) continue;
        int n = juce::jlimit(1, maxUnisonVoices, state.params.numUnisonVoices);
        for (int v = 0; v < n; ++v) {
            double spacing = (n > 1) ? (double(v) / (n - 1) * 2.0 - 1.0) : 0.0;
            double detune  = std::pow(2.0, (spacing * state.params.detuneCents) / 1200.0);
            double freq = juce::MidiMessage::getMidiNoteInHertz(
                currentPitch + state.params.pitchSemitones) * detune;
            state.angleDeltas[v] = (freq / sr)
                                 * juce::MathConstants<double>::twoPi
                                 * lfoPitchMult;
            float panPos   = juce::jlimit(-1.0f, 1.0f, (float)spacing * state.params.spread);
            float panAngle = (panPos + 1.0f) * juce::MathConstants<float>::pi * 0.25f;
            state.panL[v]  = std::cos(panAngle);
            state.panR[v]  = std::sin(panAngle);
        }
    }
    for (int s = 0; s < numSamples; ++s) {
        float adsrGain   = adsr.getNextSample();
        float leftSample = 0.0f, rightSample = 0.0f;
        for (int o = 0; o < numOscillators; ++o) {
            auto& state = oscStates[o];
            if (!state.params.enabled) continue;
            int n = juce::jlimit(1, maxUnisonVoices, state.params.numUnisonVoices);
            float oscL = 0.0f, oscR = 0.0f;
            for (int v = 0; v < n; ++v) {
                float sample = generateSample(state.params.waveType, state.angles[v], noiseRandom);
                oscL += sample * state.panL[v];
                oscR += sample * state.panR[v];
                state.angles[v] += state.angleDeltas[v];
                if (state.angles[v] >= juce::MathConstants<double>::twoPi)
                    state.angles[v] -= juce::MathConstants<double>::twoPi;
            }
            float unisonGain = 1.0f / std::sqrt((float)n);
            leftSample  += oscL * unisonGain * state.params.volume;
            rightSample += oscR * unisonGain * state.params.volume;
        }
        float finalL = leftSample  * level * adsrGain * lfoAmpMod;
        float finalR = rightSample * level * adsrGain * lfoAmpMod;
        if (isStereo) {
            outputBuffer.addSample(0, startSample, finalL);
            outputBuffer.addSample(1, startSample, finalR);
        }
        else {
            outputBuffer.addSample(0, startSample, (finalL + finalR) * 0.5f);
        }
        ++startSample;
        if (!adsr.isActive()) {
            clearCurrentNote();
            break;
        }
    }
}

What is the actual problem you’re running into?

I admit my post is not clear, my bad

I am looking for guidance on how to implement glide in this current architecture, while keeping the CPU performance

Glide requires that the pitch of the oscillator can change over time.

In a simple synth you may set the pitch of the oscillator on Note On and then just let the oscillator run at this same pitch until Note Off. With legato-style playing, if the voice is active and there is a new Note On, you tell the voice to use the pitch of the new note (and you ignore the Note Off of the old note when that comes in).

With glide, the voice does not immediately switch to the new pitch but ramps up or down to it over a certain amount of time. So on every sample, you will need to update the pitch of the oscillator so that it becomes a little bit closer to the target pitch value. An easy way to do this is to use a one-pole filter.

1 Like

I see, my issue is that I kept the Note Off of the old note, while also trying to interpolate pitch per block. no wonder it didn’t work

Thanks for the answer !