#include "Engine.h" namespace serum { namespace { inline float v (juce::AudioProcessorValueTreeState& apvts, const char* id) { if (auto* p = apvts.getRawParameterValue (id)) return p->load(); return 0.0f; } inline int vic (juce::AudioProcessorValueTreeState& apvts, const char* id, int maxValue) { return juce::jlimit (0, maxValue, (int) std::llround (v (apvts, id) * maxValue)); } inline float limit (float x) noexcept { const float ax = std::fabs (x); if (ax < 0.8f) return x; const float over = ax - 0.8f; const float clipped = 0.8f + std::tanh (over) * 0.2f; return std::copysign (clipped, x); } const char* kEnvAttack[kNumEnvelopes] = { ids::env1A, ids::env2A, ids::env3A, ids::env4A }; const char* kEnvDecay[kNumEnvelopes] = { ids::env1D, ids::env2D, ids::env3D, ids::env4D }; const char* kEnvSustain[kNumEnvelopes]= { ids::env1S, ids::env2S, ids::env3S, ids::env4S }; const char* kEnvRelease[kNumEnvelopes]= { ids::env1R, ids::env2R, ids::env3R, ids::env4R }; const char* kEnvCurve[kNumEnvelopes] = { ids::env1Curve, ids::env2Curve, ids::env3Curve, ids::env4Curve }; const char* kLfoRate[kNumLfos] = { ids::lfo1Rate, ids::lfo2Rate, ids::lfo3Rate, ids::lfo4Rate }; const char* kLfoSync[kNumLfos] = { ids::lfo1Sync, ids::lfo2Sync, ids::lfo3Sync, ids::lfo4Sync }; const char* kLfoBeat[kNumLfos] = { ids::lfo1Beat, ids::lfo2Beat, ids::lfo3Beat, ids::lfo4Beat }; const char* kLfoShape[kNumLfos] = { ids::lfo1Shape, ids::lfo2Shape, ids::lfo3Shape, ids::lfo4Shape }; const char* kLfoPhase[kNumLfos] = { ids::lfo1Phase, ids::lfo2Phase, ids::lfo3Phase, ids::lfo4Phase }; const char* kLfoFade[kNumLfos] = { ids::lfo1Fade, ids::lfo2Fade, ids::lfo3Fade, ids::lfo4Fade }; const char* kLfoDelay[kNumLfos] = { ids::lfo1Delay, ids::lfo2Delay, ids::lfo3Delay, ids::lfo4Delay }; const char* kFxType[kNumFxSlots] = { ids::fx1Type, ids::fx2Type, ids::fx3Type, ids::fx4Type, ids::fx5Type, ids::fx6Type, ids::fx7Type, ids::fx8Type }; const char* kFxMix[kNumFxSlots] = { ids::fx1Mix, ids::fx2Mix, ids::fx3Mix, ids::fx4Mix, ids::fx5Mix, ids::fx6Mix, ids::fx7Mix, ids::fx8Mix }; const char* kFxP1[kNumFxSlots] = { ids::fx1P1, ids::fx2P1, ids::fx3P1, ids::fx4P1, ids::fx5P1, ids::fx6P1, ids::fx7P1, ids::fx8P1 }; const char* kFxP2[kNumFxSlots] = { ids::fx1P2, ids::fx2P2, ids::fx3P2, ids::fx4P2, ids::fx5P2, ids::fx6P2, ids::fx7P2, ids::fx8P2 }; const char* kFxP3[kNumFxSlots] = { ids::fx1P3, ids::fx2P3, ids::fx3P3, ids::fx4P3, ids::fx5P3, ids::fx6P3, ids::fx7P3, ids::fx8P3 }; const char* kFxP4[kNumFxSlots] = { ids::fx1P4, ids::fx2P4, ids::fx3P4, ids::fx4P4, ids::fx5P4, ids::fx6P4, ids::fx7P4, ids::fx8P4 }; } void Engine::prepare (double sampleRate, int maxBlockSize) { sr = sampleRate; blockSize = maxBlockSize; for (auto& voice : voices) voice.prepare (sampleRate, maxBlockSize); for (auto& lfo : lfos) lfo.prepare (sampleRate); fx.prepare (sampleRate, maxBlockSize); mixBuffer.setSize (2, maxBlockSize, false, false, true); reset(); } void Engine::reset() { for (auto& voice : voices) voice.reset(); for (auto& lfo : lfos) lfo.reset(); fx.reset(); pitchBend = 0.0f; modWheel = 0.0f; mixBuffer.clear(); } void Engine::setLfoShapeData (int index, const std::vector& data, int steps) { index = juce::jlimit (0, kNumLfos - 1, index); lfos[(size_t) index].setShapeData (data, steps); } int Engine::getActiveVoiceCount() const { int count = 0; for (const auto& v : voices) if (v.isActive()) ++count; return count; } SynthVoice* Engine::findFreeVoice() { for (auto& v : voices) if (! v.isActive()) return &v; return nullptr; } SynthVoice* Engine::stealVoice() { // Prefer stealing an already-released voice, then the oldest active one. SynthVoice* best = nullptr; juce::uint64 bestId = std::numeric_limits::max(); for (auto& v : voices) if (v.isActive() && v.isReleased() && v.getNoteId() < bestId) { best = &v; bestId = v.getNoteId(); } if (best != nullptr) return best; for (auto& v : voices) if (v.isActive() && v.getNoteId() < bestId) { best = &v; bestId = v.getNoteId(); } return best != nullptr ? best : &voices[0]; } void Engine::noteOn (int noteNumber, float velocity01) { SynthVoice* voice = findFreeVoice(); if (voice == nullptr) voice = stealVoice(); const double freq = juce::MidiMessage::getMidiNoteInHertz (noteNumber); voice->noteOn (noteNumber, juce::jlimit (0.0f, 1.0f, velocity01), freq, (juce::uint32) (++noteCounter)); } void Engine::noteOff (int noteNumber) { for (auto& v : voices) if (v.isActive() && v.getNote() == noteNumber && ! v.isReleased()) v.noteOff(); } void Engine::allNotesOff() { for (auto& v : voices) if (v.isActive()) v.noteOff(); } void Engine::readOscParams (juce::AudioProcessorValueTreeState& apvts, const char* prefix, OscParams& o) { const juce::String p = prefix; auto g = [&] (const char* suffix) { return v (apvts, (p + suffix).toRawUTF8()); }; o.enabled = g ("On") > 0.5f; o.wave = juce::jlimit (0, kNumWavetables - 1, (int) std::llround (g ("Wave") * (kNumWavetables - 1))); o.wtPos = g ("WtPos"); o.warp = (int) std::llround (g ("Warp") * 7.0f); o.warpAmt = g ("WarpAmt"); o.coarse = (int) std::llround (g ("Coarse") * 48.0f) - 24; o.fine = (int) std::llround (g ("Fine") * 200.0f) - 100; o.level = g ("Level"); o.pan = g ("Pan") * 2.0f - 1.0f; o.unison = 1 + (int) std::llround (g ("Unison") * 15.0f); o.detune = g ("Detune"); o.spread = g ("Spread"); o.phase = g ("Phase"); o.randPhase = g ("RandPh"); } void Engine::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midi, juce::AudioProcessorValueTreeState& apvts, juce::AudioPlayHead* playhead) { const int n = buffer.getNumSamples(); const int numCh = buffer.getNumChannels(); // Tempo. if (playhead != nullptr) if (auto pos = playhead->getPosition()) if (auto b = pos->getBpm()) bpm = *b; // MIDI. for (const auto meta : midi) { const auto m = meta.getMessage(); if (m.isNoteOn() && m.getVelocity() > 0) noteOn (m.getNoteNumber(), m.getFloatVelocity()); else if (m.isNoteOff() || (m.isNoteOn() && m.getVelocity() == 0)) noteOff (m.getNoteNumber()); else if (m.isPitchWheel()) pitchBend = (m.getPitchWheelValue() - 8192) / 8192.0f; else if (m.isController()) { if (m.getControllerNumber() == 1) modWheel = m.getControllerValue() / 127.0f; else if (m.getControllerNumber() == 120 || m.getControllerNumber() == 123) allNotesOff(); } else if (m.isAllNotesOff() || m.isAllSoundOff()) allNotesOff(); } // Prepare the voice mix buffer. mixBuffer.setSize (2, n, false, false, true); mixBuffer.clear(); float* mixL = mixBuffer.getWritePointer (0); float* mixR = mixBuffer.getWritePointer (1); // Advance LFOs and capture their values (control rate). float lfoValues[kNumLfos]; for (int i = 0; i < kNumLfos; ++i) { lfos[(size_t) i].setTempo (bpm); lfos[(size_t) i].setParams (v (apvts, kLfoRate[i]), v (apvts, kLfoSync[i]) > 0.5f, v (apvts, kLfoBeat[i]), vic (apvts, kLfoShape[i], 6), v (apvts, kLfoPhase[i]), v (apvts, kLfoFade[i]), v (apvts, kLfoDelay[i])); for (int s = 0; s < n; ++s) lfos[(size_t) i].process(); lfoValues[i] = lfos[(size_t) i].getValue(); } const float macroValues[kNumMacros] = { v (apvts, ids::macro1), v (apvts, ids::macro2), v (apvts, ids::macro3), v (apvts, ids::macro4) }; // Build the render context. RenderContext ctx; ctx.sampleRate = sr; ctx.wavetables = &wavetables; for (int i = 0; i < kNumLfos; ++i) ctx.lfoValues[i] = lfoValues[i]; for (int i = 0; i < kNumMacros; ++i) ctx.macroValues[i] = macroValues[i]; ctx.modWheel = modWheel; ctx.pitchBend = pitchBend; ctx.pitchBendRange = 2.0f; ctx.matrix = &matrix; ctx.macros = ¯os; readOscParams (apvts, "oscA", ctx.oscA); readOscParams (apvts, "oscB", ctx.oscB); ctx.subOn = v (apvts, ids::subOn) > 0.5f; ctx.subShape = vic (apvts, ids::subShape, 1); ctx.subOct = vic (apvts, ids::subOct, 2) - 2; ctx.subLevel = v (apvts, ids::subLevel); ctx.noiseOn = v (apvts, ids::noiseOn) > 0.5f; ctx.noiseType = vic (apvts, ids::noiseType, 1); ctx.noiseLevel = v (apvts, ids::noiseLevel); ctx.filters.f1On = v (apvts, ids::f1On) > 0.5f; ctx.filters.f1Type = vic (apvts, ids::f1Type, 6); ctx.filters.f1Cutoff = v (apvts, ids::f1Cutoff); ctx.filters.f1Res = v (apvts, ids::f1Res); ctx.filters.f1Drive = v (apvts, ids::f1Drive); ctx.filters.f1Key = v (apvts, ids::f1Key); ctx.filters.f1Slope = vic (apvts, ids::f1Slope, 2); ctx.filters.f2On = v (apvts, ids::f2On) > 0.5f; ctx.filters.f2Type = vic (apvts, ids::f2Type, 6); ctx.filters.f2Cutoff = v (apvts, ids::f2Cutoff); ctx.filters.f2Res = v (apvts, ids::f2Res); ctx.filters.f2Drive = v (apvts, ids::f2Drive); ctx.filters.f2Key = v (apvts, ids::f2Key); ctx.filters.f2Slope = vic (apvts, ids::f2Slope, 2); ctx.filters.route = vic (apvts, ids::fRoute, 2); ctx.filters.mix = v (apvts, ids::fMix); ctx.filters.out = v (apvts, ids::fOut) * 1.5f; for (int i = 0; i < kNumEnvelopes; ++i) { ctx.envAttack[i] = v (apvts, kEnvAttack[i]); ctx.envDecay[i] = v (apvts, kEnvDecay[i]); ctx.envSustain[i] = v (apvts, kEnvSustain[i]); ctx.envRelease[i] = v (apvts, kEnvRelease[i]); ctx.envCurve[i] = v (apvts, kEnvCurve[i]); } // Render all active voices into the mix buffer. for (auto& voice : voices) if (voice.isActive()) voice.render (mixL, mixR, n, ctx); // FX rack. FxSlotParams slots[kNumFxSlots]; for (int i = 0; i < kNumFxSlots; ++i) { slots[i].type = juce::jlimit (0, (int) FxType::Count - 1, (int) std::llround (v (apvts, kFxType[i]) * ((int) FxType::Count - 1))); slots[i].mix = v (apvts, kFxMix[i]); slots[i].p[0] = v (apvts, kFxP1[i]); slots[i].p[1] = v (apvts, kFxP2[i]); slots[i].p[2] = v (apvts, kFxP3[i]); slots[i].p[3] = v (apvts, kFxP4[i]); } fx.process (mixBuffer, slots, kNumFxSlots); // Master + soft limiting. const float master = v (apvts, ids::master); for (int ch = 0; ch < numCh; ++ch) { float* dest = buffer.getWritePointer (ch); const float* src = mixBuffer.getReadPointer (ch < 2 ? ch : 0); for (int i = 0; i < n; ++i) dest[i] = limit (src[i] * master); } } } // namespace serum