#include "SynthEngine.h" SynthEngine::SynthEngine() { rng.setSeedRandomly(); } SynthEngine::~SynthEngine() {} void SynthEngine::setSampleRate (double s) { sr = s; } float SynthEngine::osc (int wave, double ph) { switch (wave) { case 0: return (float) std::sin (ph * 2.0 * juce::MathConstants::pi); case 1: return (float) (ph < 0.5 ? 4.0 * ph - 1.0 : 3.0 - 4.0 * ph); case 2: return (float) (1.0 - 2.0 * ph); case 3: return (float) (ph < 0.5 ? -1.0 : 1.0); case 4: return rng.nextFloat() * 2.0f - 1.0f; default: return 0.0f; } } void SynthEngine::allNotesOff (bool immediate) { for (auto& v : voices) { if (v.active) { if (immediate) v.active = false; else v.amp.noteOff(); } } sustainedNotes.clear(); if (immediate) active = 0; } void SynthEngine::sustainPedal (bool down) { pedal = down; if (! down) { for (auto n : sustainedNotes) for (auto& v : voices) if (v.active && v.note == n) v.amp.noteOff(); sustainedNotes.clear(); } } void SynthEngine::pitchBend (float cents) { bendCents = cents; } void SynthEngine::modWheel (float v) { mod = juce::jlimit (0.0f, 1.0f, v); } SynthEngine::Voice* SynthEngine::acquireVoice() { const int lim = juce::jlimit (1, maxVoice, polyLimit); for (int i = 0; i < lim; ++i) if (! voices[i].active) return &voices[i]; stealIdx = (stealIdx + 1) % lim; return &voices[stealIdx]; } void SynthEngine::glideParams (VParams& sp, const VParams& tgt, float k) { auto glide = [k] (float& cur, float tgtv) { cur += (tgtv - cur) * k; }; glide (sp.detune, tgt.detune); glide (sp.oscMix, tgt.oscMix); glide (sp.fltCut, tgt.fltCut); glide (sp.fltRes, tgt.fltRes); glide (sp.fltEnv, tgt.fltEnv); glide (sp.fltKey, tgt.fltKey); glide (sp.ampA, tgt.ampA); glide (sp.ampD, tgt.ampD); glide (sp.ampS, tgt.ampS); glide (sp.ampR, tgt.ampR); glide (sp.fA, tgt.fA); glide (sp.fD, tgt.fD); glide (sp.fS, tgt.fS); glide (sp.fR, tgt.fR); glide (sp.lfoRate, tgt.lfoRate); glide (sp.lfoDepth, tgt.lfoDepth); glide (sp.fxAmt, tgt.fxAmt); glide (sp.vol, tgt.vol); glide (sp.pan, tgt.pan); glide (sp.glide, tgt.glide); } void SynthEngine::noteOn (int midiNote, float velocity) { const float noteFreq = (float) (440.0 * std::exp2 ((midiNote - 69.0) / 12.0)); Voice* v = acquireVoice(); const bool fresh = ! v->active; v->active = true; v->note = midiNote; v->vel = 1.0f - std::pow (1.0f - velocity, 1.35f); v->curFreq = (lastGlide > 0.001f && lastNoteFreq > 0.0f) ? lastNoteFreq : noteFreq; v->targetFreq = noteFreq; v->ph1 = rng.nextDouble() * 0.5; v->ph2 = rng.nextDouble() * 0.5; v->lfoPh = rng.nextDouble(); v->lfoIdx = 0; v->lfoSH = rng.nextFloat() * 2.0f - 1.0f; v->lp = v->bp = 0.0f; v->fcSm = 0.0f; v->outSmooth = 0.0f; const float p = juce::jlimit (-1.0f, 1.0f, lastPan + (rng.nextFloat() - 0.5f) * 0.1f); const float ang = (p + 1.0f) * juce::MathConstants::pi * 0.25f; const float mag = 0.7071f + 0.25f * rng.nextFloat(); v->panL = std::cos (ang) * mag; v->panR = std::sin (ang) * mag; v->amp.reset(); v->flt.reset(); v->amp.noteOn(); v->flt.noteOn(); lastNoteFreq = noteFreq; if (fresh) ++active; } void SynthEngine::noteOff (int midiNote) { if (pedal) { if (! sustainedNotes.contains (midiNote)) sustainedNotes.add (midiNote); return; } for (auto& v : voices) if (v.active && v.note == midiNote) v.amp.noteOff(); } void SynthEngine::render (const VParams& p, float* left, float* right, int num) { // Parameter glide: smooth CC/automation changes that arrive between blocks. // Without this, several MIDI CC values that fall inside one audio block are // collapsed into a single block step, so fast knob rotation appears to skip // intermediate values ("zipper"/stepped sound). We ramp the continuous // parameters per-sample toward their targets instead of snapping. if (! svValid) { sv = p; svValid = true; } VParams sp = sv; const float k = 0.02f; for (auto& v : voices) { v.amp.setRates (sp.ampA, sp.ampD, sp.ampS, sp.ampR, sr); v.flt.setRates (sp.fA, sp.fD, sp.fS, sp.fR, sr); } lastGlide = p.glide; lastPan = sp.pan; polyLimit = juce::jlimit (1, maxVoice, (int) p.poly); const float glideRate = sp.glide <= 0.001f ? 1.0f : 1.0f - std::exp (-1.0f / (sp.glide * (float) sr)); const float detuneRatio = std::exp2f (sp.detune / 1200.0f); const float tuneRatio = std::exp2f (p.tune / 12.0f); const float maxFc = 18000.0f, minFc = 30.0f; for (int s = 0; s < num; ++s) { float mixL = 0.0f, mixR = 0.0f; glideParams (sp, p, k); for (auto& v : voices) { if (! v.active) continue; if (sp.glide > 0.001f) v.curFreq += (v.targetFreq - v.curFreq) * glideRate; else v.curFreq = v.targetFreq; // LFO v.lfoPh += (double) sp.lfoRate / sr; while (v.lfoPh >= 1.0) { v.lfoPh -= 1.0; v.lfoIdx++; v.lfoSH = rng.nextFloat() * 2.0f - 1.0f; } float lfo = 0.0f; switch ((int) p.lfoShape) { case 0: lfo = (float) std::sin (v.lfoPh * 2.0 * juce::MathConstants::pi); break; case 1: lfo = (float) (v.lfoPh < 0.5 ? 4.0 * v.lfoPh - 1.0 : 3.0 - 4.0 * v.lfoPh); break; case 2: lfo = v.lfoPh < 0.5 ? -1.0f : 1.0f; break; case 3: lfo = v.lfoSH; break; default: lfo = 0.0f; break; } // frequency, vibrato, bend, tune float freq = v.curFreq * tuneRatio; if ((int) p.lfoTarget == 1) freq *= std::exp2f (lfo * sp.lfoDepth * 1.6f / 12.0f); if (bendSmooth != bendCents) bendSmooth += (bendCents - bendSmooth) * 0.08f; freq *= std::exp2f (bendSmooth / 1200.0f); const float d1 = (float) (freq / sr); const float d2 = d1 * detuneRatio; v.ph1 += d1; if (v.ph1 >= 1.0) v.ph1 -= 1.0; v.ph2 += d2; if (v.ph2 >= 1.0) v.ph2 -= 1.0; const float o1 = osc ((int) p.osc1w, v.ph1); const float o2 = osc ((int) p.osc2w, v.ph2); const float mixb = sp.oscMix * 0.01f; float sig = o1 * (1.0f - mixb) + o2 * mixb; const float fe = v.flt.next(); // cutoff with keytrack + filter envelope + LFO + mod wheel float fc = sp.fltCut; fc *= std::exp2f (sp.fltKey * 0.01f * std::log2f (freq / 261.6256f)); fc *= std::exp2f (sp.fltEnv * 0.01f * fe * std::log2f (maxFc / fc)); if ((int) p.lfoTarget == 0) fc *= std::exp2f (lfo * sp.lfoDepth * 10.0f / 12.0f); fc *= std::exp2f (mod); fc = std::min (maxFc, std::max (minFc, fc)); v.fcSm += (fc - v.fcSm) * 0.25f; fc = v.fcSm; // SVF (cap f1 to keep the Chamberlin structure stable; past ~1.5 the // resonance feedback can self-oscillate and sputter at maxed cutoff+res) const float f1 = std::min (1.50f, 2.0f * std::sin (juce::MathConstants::pi * fc / (float) sr)); const float q1 = 1.0f / (1.0f + sp.fltRes * 3.0f); v.f1c = f1; const float hp = sig - v.lp - q1 * v.bp; v.bp += f1 * hp; v.lp += f1 * v.bp; float filtered = sig; switch ((int) p.fltType) { case 1: filtered = v.lp; break; case 2: filtered = v.bp; break; case 3: filtered = hp; break; default: break; } // amplitude envelope (+ tremolo if LFO targets amp) const float ae = v.amp.next(); float ampMod = 1.0f; if ((int) p.lfoTarget == 2) ampMod = 1.0f - sp.lfoDepth * 0.5f * (1.0f + lfo); v.outSmooth += (filtered - v.outSmooth) * 0.12f; float o = v.outSmooth * (ae * ampMod) * v.vel; if (! std::isfinite (o)) o = 0.0f; mixL += o * v.panL; mixR += o * v.panR; if (v.amp.getState() == Env::Off) { v.active = false; if (active > 0) --active; } } if (! std::isfinite (mixL)) mixL = 0.0f; if (! std::isfinite (mixR)) mixR = 0.0f; mixL = std::tanh (mixL); mixR = std::tanh (mixR); left[s] += mixL; right[s] += mixR; sv = sp; } }