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;
}
}
}
