#include "SynthVoice.h" namespace serum { namespace { inline float clampF (float v, float lo, float hi) noexcept { return v < lo ? lo : (v > hi ? hi : v); } } void SynthVoice::prepare (double sampleRate, int maxBlockSize) { oscA.prepare (sampleRate); oscB.prepare (sampleRate); sub.prepare (sampleRate); noise.prepare (sampleRate); filters.prepare (sampleRate, maxBlockSize); for (auto& e : env) e.prepare (sampleRate); scratch.setSize (2, maxBlockSize, false, false, true); reset(); } void SynthVoice::reset() { oscA.reset(); oscB.reset(); sub.reset(); noise.reset(); filters.reset(); for (auto& e : env) e.reset(); note = -1; velocity = 0.0f; baseFreq = 0.0; active = released = false; lastUnisonA = lastUnisonB = 1; noteRandom = 0.5f; scratch.clear(); } void SynthVoice::noteOn (int noteNumber, float velocity01, double freqHz, juce::uint32 noteSeed) { note = noteNumber; velocity = velocity01; baseFreq = freqHz; active = true; released = false; seed = noteSeed; lastUnisonA = lastUnisonB = 0; // force unison reconfigure on first render juce::Random rng (noteSeed); noteRandom = rng.nextFloat(); sub.noteOn (freqHz, -1); noise.noteOn (noteSeed); for (auto& e : env) e.noteOn(); } void SynthVoice::noteOff() { if (! active || released) return; released = true; for (auto& e : env) e.noteOff(); } void SynthVoice::render (float* outL, float* outR, int numSamples, const RenderContext& ctx) noexcept { if (! active || numSamples <= 0) return; // 1. Refresh envelope parameters (cheap; also lets UI edits affect held notes). for (int i = 0; i < kNumEnvelopes; ++i) env[(size_t) i].setParams (maps::toSeconds (ctx.envAttack[i]), maps::toSeconds (ctx.envDecay[i]), ctx.envSustain[i], maps::toSeconds (ctx.envRelease[i]), ctx.envCurve[i]); // 2. Block-start envelope values (control-rate modulation sources). float envStart[kNumEnvelopes]; for (int i = 0; i < kNumEnvelopes; ++i) envStart[i] = env[(size_t) i].getValue(); // 3. Per-voice modulation source values. float src[kNumModSources]; src[(int) ModSource::Lfo1] = ctx.lfoValues[0]; src[(int) ModSource::Lfo2] = ctx.lfoValues[1]; src[(int) ModSource::Lfo3] = ctx.lfoValues[2]; src[(int) ModSource::Lfo4] = ctx.lfoValues[3]; src[(int) ModSource::Env1] = envStart[0]; src[(int) ModSource::Env2] = envStart[1]; src[(int) ModSource::Env3] = envStart[2]; src[(int) ModSource::Env4] = envStart[3]; src[(int) ModSource::Velocity] = velocity; src[(int) ModSource::Note] = clampF ((float) note / 127.0f, 0.0f, 1.0f); src[(int) ModSource::ModWheel] = ctx.modWheel; src[(int) ModSource::PitchBend]= ctx.pitchBend; src[(int) ModSource::Macro1] = ctx.macroValues[0]; src[(int) ModSource::Macro2] = ctx.macroValues[1]; src[(int) ModSource::Macro3] = ctx.macroValues[2]; src[(int) ModSource::Macro4] = ctx.macroValues[3]; src[(int) ModSource::Random] = noteRandom; // 4. Accumulate modulation offsets (block rate). std::array mod { { } }; if (ctx.matrix != nullptr) { for (const auto& c : ctx.matrix->connections) { float v = src[(int) c.source]; if (c.bipolar && ! isBipolarSource (c.source)) v = v * 2.0f - 1.0f; mod[(int) c.target] += v * c.depth; } } if (ctx.macros != nullptr) { for (int m = 0; m < kNumMacros; ++m) for (const auto& a : ctx.macros->assignments[(size_t) m]) mod[(int) a.target] += ctx.macroValues[m] * a.depth; } // 5. Modulated oscillator parameters. OscParams a = ctx.oscA; OscParams b = ctx.oscB; a.level = clampF (a.level + mod[(int) ModTarget::OscALevel], 0.0f, 1.0f); a.pan = clampF (a.pan + mod[(int) ModTarget::OscAPan], -1.0f, 1.0f); a.wtPos = clampF (a.wtPos + mod[(int) ModTarget::OscAWtPos], 0.0f, 1.0f); a.unison = (int) std::llround (clampF ((float) a.unison + mod[(int) ModTarget::OscAUnison] * 16.0f, 1.0f, 16.0f)); a.detune = clampF (a.detune + mod[(int) ModTarget::OscADetune], 0.0f, 1.0f); a.spread = clampF (a.spread + mod[(int) ModTarget::OscASpread], 0.0f, 1.0f); a.warpAmt = clampF (a.warpAmt+ mod[(int) ModTarget::OscAWarpAmt], 0.0f, 1.0f); b.level = clampF (b.level + mod[(int) ModTarget::OscBLevel], 0.0f, 1.0f); b.pan = clampF (b.pan + mod[(int) ModTarget::OscBPan], -1.0f, 1.0f); b.wtPos = clampF (b.wtPos + mod[(int) ModTarget::OscBWtPos], 0.0f, 1.0f); b.unison = (int) std::llround (clampF ((float) b.unison + mod[(int) ModTarget::OscBUnison] * 16.0f, 1.0f, 16.0f)); b.detune = clampF (b.detune + mod[(int) ModTarget::OscBDetune], 0.0f, 1.0f); b.spread = clampF (b.spread + mod[(int) ModTarget::OscBSpread], 0.0f, 1.0f); b.warpAmt = clampF (b.warpAmt+ mod[(int) ModTarget::OscBWarpAmt], 0.0f, 1.0f); // 6. Frequency (pitch bend + mod matrix pitch + per-osc coarse/fine). const double semis = ctx.pitchBend * ctx.pitchBendRange + mod[(int) ModTarget::Pitch] * 24.0; const double bent = baseFreq * std::pow (2.0, semis / 12.0); const double freqA = bent * std::pow (2.0, (double) a.coarse / 12.0 + (double) a.fine / 1200.0); const double freqB = bent * std::pow (2.0, (double) b.coarse / 12.0 + (double) b.fine / 1200.0); // Reconfigure unison only when the integer count changes (avoids phase reset // on every block when unison is LFO-modulated at control rate). if (a.unison != lastUnisonA) { oscA.noteOn (freqA, a, seed); lastUnisonA = a.unison; } if (b.unison != lastUnisonB) { oscB.noteOn (freqB, b, seed + 1); lastUnisonB = b.unison; } // 7. Modulated filter parameters. FilterBankParams fb = ctx.filters; fb.f1Cutoff = clampF (fb.f1Cutoff + mod[(int) ModTarget::Filter1Cutoff], 0.0f, 1.0f); fb.f1Res = clampF (fb.f1Res + mod[(int) ModTarget::Filter1Res], 0.0f, 1.0f); fb.f1Drive = clampF (fb.f1Drive + mod[(int) ModTarget::Filter1Drive], 0.0f, 1.0f); fb.f2Cutoff = clampF (fb.f2Cutoff + mod[(int) ModTarget::Filter2Cutoff], 0.0f, 1.0f); fb.f2Res = clampF (fb.f2Res + mod[(int) ModTarget::Filter2Res], 0.0f, 1.0f); fb.f2Drive = clampF (fb.f2Drive + mod[(int) ModTarget::Filter2Drive], 0.0f, 1.0f); fb.mix = clampF (fb.mix + mod[(int) ModTarget::FilterMix], 0.0f, 1.0f); fb.out = clampF (fb.out + mod[(int) ModTarget::FilterOut], 0.0f, 1.5f); // 8. Amp modulation + velocity. const float ampMod = 1.0f + mod[(int) ModTarget::Amp]; const float velGain = velocity * velocity + 0.001f; // 9. Generate oscillators/sub/noise into scratch. float* sL = scratch.getWritePointer (0); float* sR = scratch.getWritePointer (1); const Wavetable& wtA = ctx.wavetables->getTable (a.wave); const Wavetable& wtB = ctx.wavetables->getTable (b.wave); const double subMult = (ctx.subOct == -2) ? 0.25 : (ctx.subOct == -1) ? 0.5 : 1.0; for (int i = 0; i < numSamples; ++i) { float l = 0.0f, r = 0.0f; oscA.processAdd (wtA, a, freqA, l, r); oscB.processAdd (wtB, b, freqB, l, r); float mono = 0.0f; if (ctx.subOn) sub.processAdd (bent * subMult, ctx.subShape, ctx.subLevel, mono); if (ctx.noiseOn) noise.processAdd (ctx.noiseType, ctx.noiseLevel, mono); l += mono; r += mono; sL[i] = l; sR[i] = r; } // 10. Filter bank (control rate). filters.process (sL, sR, numSamples, fb, (float) baseFreq); // 11. Amp envelope (per sample) and advance the remaining envelopes. for (int i = 0; i < numSamples; ++i) { const float amp = env[0].process(); env[1].process(); env[2].process(); env[3].process(); const float gain = amp * ampMod * velGain; outL[i] += sL[i] * gain; outR[i] += sR[i] * gain; } // 12. Release completes when the amp envelope has fully decayed. if (released && ! env[0].isActive()) active = false; } } // namespace serum