#include "Engine.h" #include "RAVEButton.h" namespace serum { namespace { 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); } } Engine::Engine() { wavetables.prebuild(); audioMatrix.connections.reserve (ModulationMatrix::kMaxConnections); for (auto& assignments : audioMacros.assignments) assignments.reserve (MacroControls::kMaxAssignments); } void Engine::prepare (double sampleRate, int maxBlockSize, juce::AudioProcessorValueTreeState& apvts) { const juce::ScopedLock lock (controlLock); sr = sampleRate; blockSize = juce::jmax (1, maxBlockSize); parameterValues.clear(); for (auto* parameter : apvts.processor.getParameters()) if (auto* ranged = dynamic_cast (parameter)) if (auto* raw = apvts.getRawParameterValue (ranged->paramID)) parameterValues.emplace (std::string_view (ranged->paramID.toRawUTF8()), ParameterValue { raw, raw->load() }); for (auto& voice : voices) voice.prepare (sampleRate, blockSize); for (auto& lfo : lfos) lfo.prepare (sampleRate); fx.prepare (sampleRate, blockSize); mixBuffer.setSize (2, blockSize, false, false, true); reset(); captureControls(); } void Engine::reset() { for (auto& voice : voices) voice.reset(); for (auto& lfo : lfos) lfo.reset(); fx.reset(); pitchBend = 0.0f; modWheel = 0.0f; activeVoiceCount.store (0, std::memory_order_relaxed); mixBuffer.clear(); } void Engine::captureControls() { const juce::ScopedTryLock lock (controlLock); if (! lock.isLocked()) return; audioMatrix.connections.assign (matrix.connections.begin(), matrix.connections.begin() + juce::jmin (matrix.size(), ModulationMatrix::kMaxConnections)); for (int i = 0; i < kNumMacros; ++i) { const auto& source = macros.assignments[(size_t) i]; auto& dest = audioMacros.assignments[(size_t) i]; dest.assign (source.begin(), source.begin() + juce::jmin ((int) source.size(), MacroControls::kMaxAssignments)); } for (int i = 0; i < kNumLfos; ++i) lfos[(size_t) i].setShapeData (controlLfos[(size_t) i].getShapeData(), controlLfos[(size_t) i].getShapeSteps()); for (auto& entry : parameterValues) { const float value = entry.second.source->load (std::memory_order_relaxed); entry.second.value = std::isfinite (value) ? juce::jlimit (0.0f, 1.0f, value) : 0.0f; } } void Engine::setLfoShapeData (int index, const std::vector& data, int steps) { const juce::ScopedLock lock (controlLock); index = juce::jlimit (0, kNumLfos - 1, index); controlLfos[(size_t) index].setShapeData (data, steps); } float Engine::v (const char* id) const { const auto it = parameterValues.find (id); return it != parameterValues.end() ? it->second.value : 0.0f; } int Engine::vic (const char* id, int maxValue) const { return juce::jlimit (0, maxValue, (int) std::llround (v (id) * maxValue)); } SynthVoice* Engine::findFreeVoice() { for (auto& voice : voices) if (! voice.isActive()) return &voice; 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& voice : voices) if (voice.isActive() && voice.isReleased() && voice.getNoteId() < bestId) { best = &voice; bestId = voice.getNoteId(); } if (best != nullptr) return best; for (auto& voice : voices) if (voice.isActive() && voice.getNoteId() < bestId) { best = &voice; bestId = voice.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& voice : voices) if (voice.isActive() && voice.getNote() == noteNumber && ! voice.isReleased()) voice.noteOff(); } void Engine::allNotesOff() { for (auto& voice : voices) if (voice.isActive()) voice.noteOff(); } void Engine::readOscParams (const paramIds::Oscillator& ids, OscParams& o) const { o.enabled = v (ids.on) > 0.5f; o.wave = vic (ids.wave, kNumWavetables - 1); o.wtPos = v (ids.wtPos); o.warp = vic (ids.warp, (int) WarpMode::Count - 1); o.warpAmt = v (ids.warpAmt); o.coarse = vic (ids.coarse, 48) - 24; o.fine = vic (ids.fine, 200) - 100; o.level = v (ids.level); o.pan = v (ids.pan) * 2.0f - 1.0f; o.unison = 1 + vic (ids.unison, kMaxUnison - 1); o.detune = v (ids.detune); o.spread = v (ids.spread); o.phase = v (ids.phase); o.randPhase = v (ids.randPhase); } void Engine::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midi, juce::AudioProcessorValueTreeState&, juce::AudioPlayHead* playhead) { const int n = buffer.getNumSamples(); const int numCh = buffer.getNumChannels(); captureControls(); // Tempo. if (playhead != nullptr) if (auto pos = playhead->getPosition()) if (auto tempo = pos->getBpm()) if (std::isfinite (*tempo) && *tempo > 0.0) bpm = *tempo; // Advance LFOs and capture their values at control-rate render boundaries. for (int i = 0; i < kNumLfos; ++i) { auto& lfo = lfos[(size_t) i]; lfo.setTempo (bpm); lfo.setParams (v (paramIds::lfoRate[i]), v (paramIds::lfoSync[i]) > 0.5f, v (paramIds::lfoBeat[i]), vic (paramIds::lfoShape[i], (int) LfoShape::Count - 1), v (paramIds::lfoPhase[i]), v (paramIds::lfoFade[i]), v (paramIds::lfoDelay[i])); } // Build the render context. RenderContext ctx; ctx.sampleRate = sr; ctx.wavetables = &wavetables; ctx.matrix = &audioMatrix; ctx.macros = &audioMacros; for (int i = 0; i < kNumMacros; ++i) ctx.macroValues[i] = v (paramIds::macros[i]); readOscParams (paramIds::oscillators[0], ctx.oscA); readOscParams (paramIds::oscillators[1], ctx.oscB); ctx.subOn = v (ids::subOn) > 0.5f; ctx.subShape = vic (ids::subShape, (int) SubShape::Count - 1); ctx.subOct = vic (ids::subOct, 2) - 2; ctx.subLevel = v (ids::subLevel); ctx.noiseOn = v (ids::noiseOn) > 0.5f; ctx.noiseType = vic (ids::noiseType, (int) NoiseType::Count - 1); ctx.noiseLevel = v (ids::noiseLevel); ctx.filters.f1On = v (ids::f1On) > 0.5f; ctx.filters.f1Type = vic (ids::f1Type, (int) FilterModel::Count - 1); ctx.filters.f1Cutoff = v (ids::f1Cutoff); ctx.filters.f1Res = v (ids::f1Res); ctx.filters.f1Drive = v (ids::f1Drive); ctx.filters.f1Key = v (ids::f1Key); ctx.filters.f1Slope = vic (ids::f1Slope, 2); ctx.filters.f2On = v (ids::f2On) > 0.5f; ctx.filters.f2Type = vic (ids::f2Type, (int) FilterModel::Count - 1); ctx.filters.f2Cutoff = v (ids::f2Cutoff); ctx.filters.f2Res = v (ids::f2Res); ctx.filters.f2Drive = v (ids::f2Drive); ctx.filters.f2Key = v (ids::f2Key); ctx.filters.f2Slope = vic (ids::f2Slope, 2); ctx.filters.route = vic (ids::fRoute, (int) FilterRoute::Count - 1); ctx.filters.mix = v (ids::fMix); ctx.filters.out = v (ids::fOut) * 1.5f; for (int i = 0; i < kNumEnvelopes; ++i) { ctx.envAttack[i] = v (paramIds::envAttack[i]); ctx.envDecay[i] = v (paramIds::envDecay[i]); ctx.envSustain[i] = v (paramIds::envSustain[i]); ctx.envRelease[i] = v (paramIds::envRelease[i]); ctx.envCurve[i] = v (paramIds::envCurve[i]); } // FX rack. std::array slots; for (int i = 0; i < kNumFxSlots; ++i) { auto& slot = slots[(size_t) i]; slot.type = vic (paramIds::fxType[i], (int) FxType::Count - 1); slot.mix = v (paramIds::fxMix[i]); slot.p[0] = v (paramIds::fxP1[i]); slot.p[1] = v (paramIds::fxP2[i]); slot.p[2] = v (paramIds::fxP3[i]); slot.p[3] = v (paramIds::fxP4[i]); } if (v (ids::rave) > 0.5f) RaveController::apply (ctx, slots.data(), kNumFxSlots); int offset = 0; auto renderUntil = [&] (int end) { while (offset < end) { const int count = juce::jmin (end - offset, juce::jmin (blockSize, 64)); ctx.modWheel = modWheel; ctx.pitchBend = pitchBend; for (int i = 0; i < kNumLfos; ++i) ctx.lfoValues[i] = lfos[(size_t) i].getValue(); const SynthVoice* newest = nullptr; for (const auto& voice : voices) if (voice.isActive() && (newest == nullptr || voice.getNoteId() > newest->getNoteId())) newest = &voice; auto sourceValue = [&] (ModSource source) { const int index = (int) source; if (source >= ModSource::Lfo1 && source <= ModSource::Lfo4) return ctx.lfoValues[index - (int) ModSource::Lfo1]; if (source >= ModSource::Macro1 && source <= ModSource::Macro4) return ctx.macroValues[index - (int) ModSource::Macro1]; if (source == ModSource::ModWheel) return modWheel; if (source == ModSource::PitchBend) return pitchBend; return newest != nullptr ? newest->getModulationValue (source) : 0.0f; }; std::array globalMod {}; for (const auto& connection : audioMatrix.connections) if (! isPerVoiceTarget (connection.target)) { float value = sourceValue (connection.source); if (connection.bipolar && ! isBipolarSource (connection.source)) value = value * 2.0f - 1.0f; globalMod[(size_t) connection.target] += value * connection.depth; } for (int i = 0; i < kNumMacros; ++i) for (const auto& assignment : audioMacros.assignments[(size_t) i]) if (! isPerVoiceTarget (assignment.target)) globalMod[(size_t) assignment.target] += ctx.macroValues[i] * assignment.depth; // Prepare the voice mix buffer. juce::AudioBuffer block (mixBuffer.getArrayOfWritePointers(), 2, count); block.clear(); // Render all active voices into the mix buffer. for (auto& voice : voices) if (voice.isActive()) voice.render (block.getWritePointer (0), block.getWritePointer (1), count, ctx); auto modulatedSlots = slots; for (int i = 0; i < kNumFxSlots; ++i) modulatedSlots[(size_t) i].mix = juce::jlimit (0.0f, 1.0f, slots[(size_t) i].mix + globalMod[(size_t) ModTarget::Fx1Mix + (size_t) i]); fx.process (block, modulatedSlots.data(), kNumFxSlots); // Master + soft limiting. const float master = juce::jlimit (0.0f, 1.0f, v (ids::master) + globalMod[(size_t) ModTarget::Master]); for (int ch = 0; ch < numCh; ++ch) { float* dest = buffer.getWritePointer (ch, offset); const float* src = block.getReadPointer (ch < 2 ? ch : 0); for (int i = 0; i < count; ++i) dest[i] = limit (src[i] * master); } for (auto& lfo : lfos) lfo.advance (count); offset += count; } }; // MIDI. for (const auto meta : midi) { renderUntil (juce::jlimit (offset, n, meta.samplePosition)); const auto message = meta.getMessage(); if (message.isNoteOn()) noteOn (message.getNoteNumber(), message.getFloatVelocity()); else if (message.isNoteOff()) noteOff (message.getNoteNumber()); else if (message.isPitchWheel()) pitchBend = (message.getPitchWheelValue() - 8192) / 8192.0f; else if (message.isAllSoundOff()) { for (auto& voice : voices) voice.reset(); fx.reset(); } else if (message.isAllNotesOff()) allNotesOff(); else if (message.isController() && message.getControllerNumber() == 1) modWheel = message.getControllerValue() / 127.0f; } renderUntil (n); int active = 0; for (const auto& voice : voices) active += voice.isActive() ? 1 : 0; activeVoiceCount.store (active, std::memory_order_relaxed); } } // namespace serum