#include "SynthEngine.h" #include namespace sm26 { SynthEngine::SynthEngine() { auto def = defaultParams(); for (int i = 0; i < kNumParams; ++i) params[i] = def[i]; knobParams[0] = PCUTOFF; knobParams[1] = PRES; knobParams[2] = PFENV; knobParams[3] = PDMIX; knobNames[0] = "CUTOFF"; knobNames[1] = "RES"; knobNames[2] = "F-ENV"; knobNames[3] = "DELAY"; for (int i = 0; i < kNumKnobs; ++i) { knobLows[i] = 0.0f; knobHighs[i] = 1.0f; } std::srand (12345); } void SynthEngine::prepare (double sr, int mb) { sampleRate = sr; maxBlock = mb; delay.prepare (sr); allNotesOff(); reset(); } void SynthEngine::reset() { allNotesOff(); delay.clear(); } void SynthEngine::loadPreset (const Preset& p) { juce::SpinLock::ScopedLockType lock (paramLock); for (int i = 0; i < kNumParams; ++i) params[i] = p.base[i]; for (int i = 0; i < kNumKnobs; ++i) { knobParams[i] = p.ctl[i].param; knobNames[i] = p.ctl[i].name; knobLows[i] = p.ctl[i].lo; knobHighs[i] = p.ctl[i].hi; } allNotesOff(); delay.clear(); } void SynthEngine::setPresetIndex (int i) { currentPresetIndex = i; loadPreset (*PresetStore::findByIndex (i)); } void SynthEngine::setParam (int id, float n01) { if (id < 0 || id >= kNumParams) return; juce::SpinLock::ScopedLockType lock (paramLock); params[id] = juce::jlimit (0.0f, 1.0f, n01); } float SynthEngine::getParam (int id) const { if (id < 0 || id >= kNumParams) return 0.0f; return params[id]; } SynthEngine::KnobView SynthEngine::knobView (int knob) const { if (knob < 0 || knob >= kNumKnobs) return {}; KnobView k; k.name = knobNames[knob]; k.param = knobParams[knob]; k.value01 = getParam (k.param); k.lo = knobLows[knob]; k.hi = knobHighs[knob]; return k; } float SynthEngine::knobValue (int knob) const { if (knob < 0 || knob >= kNumKnobs) return 0.0f; return getParam (knobParams[knob]); } const char* SynthEngine::waveNameFor (int index) const { return sm26::waveName (juce::jlimit (0, 4, index)); } void SynthEngine::noteOn (int note, float vel) { Voice* v = findFreeOrSteal(); resetVoice (*v, note, vel); } void SynthEngine::noteOff (int note) { for (auto& v : voices) if (v.active && v.note == note) v.envState = 3; } void SynthEngine::allNotesOff() { for (auto& v : voices) { v.active = false; v.env = 0.0; v.envState = 0; } activeNotes = 0; } SynthEngine::Voice* SynthEngine::findFreeOrSteal() { int freeIdx = -1; int oldest = -1; int oldestAge = -1; for (int i = 0; i < kNumVoices; ++i) { if (! voices[i].active) { freeIdx = i; break; } if (voices[i].age > oldestAge) { oldest = i; oldestAge = voices[i].age; } } if (freeIdx >= 0) { activeNotes++; return &voices[freeIdx]; } voices[oldest].envState = 3; voices[oldest].env = 0.0f; return &voices[oldest]; } void SynthEngine::resetVoice (Voice& v, int note, float vel) { v.active = true; v.note = note; v.velocity = vel; v.envState = 0; v.env = 0.0f; v.age = 0; v.ic1eq = 0.0; v.ic2eq = 0.0; double freq = 440.0 * std::pow (2.0, (note - 69) / 12.0); double inc = freq / sampleRate; double oct1 = paramReal (PO1OCT, params[PO1OCT]); double oct2 = paramReal (PO2OCT, params[PO2OCT]); double dt1 = paramReal (PO1CT, params[PO1CT]) / 1200.0; double dt2 = paramReal (PO2CT, params[PO2CT]) / 1200.0; double f1 = freq * std::pow (2.0, oct1 + dt1); double f2 = freq * std::pow (2.0, oct2 + dt2); v.inc1 = f1 / sampleRate; v.inc2 = f2 / sampleRate; v.glidedInc1 = v.inc1; v.ph1 = 0.0; v.ph2 = 0.0; } float SynthEngine::oscWave (int wave, double phase) { double ph = phase - std::floor(phase); switch (wave) { case 0: return (float) std::sin (ph * 6.283185307179586); case 1: return (float) (1.0 - 4.0 * std::fabs (ph - 0.5)); case 2: return (float) (2.0 * ph - 1.0); case 3: return (float) (ph < 0.5 ? 1.0 : -1.0); case 4: return (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0); default: return 0.0f; } } static float softClip (float x) { return std::tanh (x * 1.5f) / std::tanh(1.5f); } double SynthEngine::cutoffFor (const Voice& v, double baseCutoff, double envAmt, double envLevel) { double baseHz = paramReal (PCUTOFF, (float) baseCutoff); baseHz = std::clamp (baseHz, 20.0, 20000.0); double mod = envAmt * envLevel * 5.0; double hz = baseHz * std::pow (2.0, mod); hz = std::clamp (hz, 20.0, std::min (sampleRate * 0.45, 20000.0)); return (float) hz; } void SynthEngine::process (AudioBuffer& buffer, MidiBuffer& midi, int n, double bpm, bool transportPlaying) { wasPlaying = transportPlaying; lastBpmValue = (bpm > 10.0 && bpm < 300.0) ? bpm : 120.0; std::array snap; { juce::SpinLock::ScopedLockType lock (paramLock); for (int i = 0; i < kNumParams; ++i) snap[(size_t) i] = params[i]; } const float* const params = snap.data(); const float cutN01 = params[PCUTOFF]; const float resN01 = params[PRES]; const float fenvN01 = params[PFENV]; const float ftypeN01= params[PFTYPE]; const float atkN01 = params[PATK]; const float decN01 = params[PDEC]; const float susN01 = params[PSUS]; const float relN01 = params[PREL]; const float driveN01= params[PDRIVE]; const float glideN01= params[PGLIDE]; const float masterN01= params[PMASTER]; const float mixN01 = params[PMIX]; const float noiseN01= params[PNOISE]; const double atkSec = paramReal (PATK, atkN01); const double decSec = paramReal (PDEC, decN01); const double susLevel = susN01; const double relSec = paramReal (PREL, relN01); const double atkCoef = 1.0 - std::exp (-1.0 / (atkSec * sampleRate + 1.0)); const double decCoef = 1.0 - std::exp (-1.0 / (decSec * sampleRate + 1.0)); const double relCoef = 1.0 - std::exp (-1.0 / (relSec * sampleRate + 1.0)); const double drive = 0.1 + driveN01 * 2.4; const double glideCoef = glideN01 < 0.001 ? 1.0 : 1.0 - std::exp (-1.0 / (glideN01 * sampleRate + 1.0)); const double master = masterN01; const double fenvScale = fenvN01 * 5.0; (void) fenvScale; const float gMix = mixN01; const float g1 = 1.0f - gMix; const float g2 = gMix; const float g1p = (float) std::sqrt (g1); const float g2p = (float) std::sqrt (g2); const int filterType = paramFilterType (ftypeN01); bool delaySync = params[PDSYNC] > 0.5f; int stepIdx = juce::jlimit (0, 15, (int) juce::roundToInt (params[PDSTEPS])); float delayMsParam = paramReal (PDTIME, params[PDTIME]); float delaySec; if (delaySync) delaySec = (float) (stepsBeats()[stepIdx] * 60.0 / lastBpmValue); else delaySec = delayMsParam * 0.001f; delaySec = std::clamp (delaySec, 0.001f, 4.0f); lastDelaySamples = delaySec * sampleRate; const float delayFB = params[PDFB]; const float delayMix= params[PDMIX]; const float dryMix = 1.0f - delayMix; const float wetMix = delayMix; for (const auto& metadata : midi) { MidiMessage msg = metadata.getMessage(); int sm = juce::jlimit (0, n - 1, metadata.samplePosition); if (msg.isNoteOn()) noteOn (msg.getNoteNumber(), msg.getFloatVelocity()); else if (msg.isNoteOff()) noteOff (msg.getNoteNumber()); else if (msg.isAllNotesOff() || msg.isAllSoundOff()) allNotesOff(); } float* outL = buffer.getWritePointer (0); float* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer (1) : nullptr; for (int s = 0; s < n; ++s) { float sumL = 0.0f, sumR = 0.0f; for (auto& v : voices) { if (! v.active) continue; v.age++; v.ph1 += v.inc1; v.ph2 += v.inc2; if (v.ph1 > 1.0) v.ph1 -= 1.0; if (v.ph2 > 1.0) v.ph2 -= 1.0; v.glidedInc1 = (float) ((1.0 - glideCoef) * v.glidedInc1 + glideCoef * v.inc1); switch (v.envState) { case 0: v.env += (1.0 - v.env) * atkCoef; if (v.env > 0.999) v.envState = 1; break; case 1: v.env += (susLevel - v.env) * decCoef; break; case 2: break; case 3: v.env += (0.0 - v.env) * relCoef; if (v.env < 0.001) { v.active = false; v.env = 0.0; activeNotes--; } break; } float o1 = oscWave (paramWaveIndex (params[PO1WAVE]), v.ph1); float o2 = oscWave (paramWaveIndex (params[PO2WAVE]), v.ph2); float raw = g1p * o1 + g2p * o2 + noiseN01 * (float) (((double) std::rand() / (double) RAND_MAX) * 2.0 - 1.0) * 0.5f; raw *= 0.7f; float cutoffHz = cutoffFor (v, cutN01, fenvN01, v.env); float Q = 0.5f + resN01 * 14.0f; float k = 1.0f / Q; double f = cutoffHz / (float) sampleRate; if (f > 0.49) f = 0.49; double g = std::tan (juce::MathConstants::pi * f); double a1 = 1.0 / (1.0 + g * (g + k)); double a2 = g * a1; double a3 = g * a2; double vin = raw; double v3 = vin - v.ic2eq; double v1 = a1 * v.ic1eq + a2 * v3; double v2 = v.ic2eq + a2 * v.ic1eq + a3 * v3; v.ic1eq = 2.0 * v1 - v.ic1eq; v.ic2eq = 2.0 * v2 - v.ic2eq; float filtered; switch (filterType) { case 0: filtered = (float) v2; break; case 1: filtered = (float) v1; break; case 2: filtered = (float) (vin - k * v1 - v2); break; default: filtered = (float) v2; break; } float sig = softClip (filtered * (float) drive); sig *= (float) (v.env * v.velocity * master); sumL += sig; sumR += sig; } float dry = sumL; int w = delay.writePos[0]; int r = (w - (int) lastDelaySamples + delay.maxLen * 2) % delay.maxLen; float delayed = delay.buf[0][r]; float fb = dry + delayed * (float) delayFB; fb = softClip (fb * 0.7f); delay.buf[0][w] = fb; delay.buf[1][w] = fb; delay.writePos[0] = (w + 1) % delay.maxLen; delay.writePos[1] = (w + 1) % delay.maxLen; outL[s] = dryMix * dry + wetMix * delayed; if (outR) outR[s] = outL[s]; } } } // namespace sm26