#include "SynthVoice.h" #include #include "SynthVoice/ADSR.h" #include "SynthVoice/Chorus.h" #include "SynthVoice/Distortion.h" #include "SynthVoice/EQ.h" #include "SynthVoice/Flanger.h" #include "SynthVoice/Reverb.h" #include "SynthVoice/SimpleDelay.h" //============================================================================== NeuralSynthVoice::NeuralSynthVoice (NeuralSharedParams& sp) : shared (sp) {} //============================================================================== void NeuralSynthVoice::prepare (const juce::dsp::ProcessSpec& newSpec) { spec = newSpec; // --- Oscillator osc.prepare (spec.sampleRate); osc.setWave (BlepWave::Sine); // --- Wavetable oscillator factory banks --- wtOsc.prepare (spec.sampleRate); morphLfo.prepare (spec.sampleRate); currentWtBankIndex = -1; wtOsc2.prepare (spec.sampleRate); morphLfo2.prepare (spec.sampleRate); currentWtBankIndex2 = -1; const auto& library = WT::FactoryLibrary::get(); if (! library.empty()) { wtOsc.setBank (library.front().bank); currentWtBankIndex = 0; wtOsc2.setBank (library.front().bank); currentWtBankIndex2 = 0; } // --- Scratch buffer (IMPORTANT: allocate real memory) tempBuffer.setSize ((int) spec.numChannels, (int) spec.maximumBlockSize, false, false, true); tempBlock = juce::dsp::AudioBlock (tempBuffer); // --- Prepare chain elements chain.prepare (spec); chain.get().setRampDurationSeconds (0.02f); chain.get().setThreshold (-1.0f); chain.get().setRelease (0.05f); chain.get().reset(); // Set maximum delay sizes BEFORE runtime changes { // Flanger: up to 20 ms auto& flanger = chain.get(); const size_t maxFlangerDelay = (size_t) juce::jmax( 1, (size_t) std::ceil (0.020 * spec.sampleRate)); flanger.setMaximumDelayInSamples (maxFlangerDelay); flanger.reset(); } { // Simple delay: up to 2 s auto& delay = chain.get(); const size_t maxDelay = (size_t) juce::jmax( 1, (size_t) std::ceil (2.0 * spec.sampleRate)); delay.setMaximumDelayInSamples (maxDelay); delay.reset(); } // Envelopes adsr.setSampleRate (spec.sampleRate); filterAdsr.setSampleRate (spec.sampleRate); // Filter svf.reset(); svf.prepare (spec); // Initial filter type const int type = (int) std::lround (juce::jlimit (0.0f, 2.0f, shared.filterType ? shared.filterType->load() : 0.0f)); switch (type) { case 0: svf.setType (juce::dsp::StateVariableTPTFilterType::lowpass); break; case 1: svf.setType (juce::dsp::StateVariableTPTFilterType::highpass); break; case 2: svf.setType (juce::dsp::StateVariableTPTFilterType::bandpass); break; default: break; } } //============================================================================== void NeuralSynthVoice::renderNextBlock (juce::AudioBuffer& outputBuffer, int startSample, int numSamples) { if (numSamples <= 0) return; if (! adsr.isActive()) clearCurrentNote(); // --- Generate oscillator into temp buffer (BLEP or Wavetable) tempBuffer.clear(); const int numCh = juce::jmin ((int) spec.numChannels, tempBuffer.getNumChannels()); const auto& library = WT::FactoryLibrary::get(); const int librarySize = (int) library.size(); if (librarySize > 0 && shared.wtBank) { const int targetBank = juce::jlimit (0, librarySize - 1, (int) std::lround (shared.wtBank->load())); if (targetBank != currentWtBankIndex) { wtOsc.setBank (library[(size_t) targetBank].bank); currentWtBankIndex = targetBank; } } if (librarySize > 0 && shared.wt2Bank) { const int targetBank2 = juce::jlimit (0, librarySize - 1, (int) std::lround (shared.wt2Bank->load())); if (targetBank2 != currentWtBankIndex2) { wtOsc2.setBank (library[(size_t) targetBank2].bank); currentWtBankIndex2 = targetBank2; } } const bool useWTLayerA = (shared.wtOn && shared.wtOn->load() > 0.5f) && wtOsc.getFrameCount() > 0; const bool useWTLayerB = (shared.wt2On && shared.wt2On->load() > 0.5f) && wtOsc2.getFrameCount() > 0; const float morphMaxA = wtOsc.getMaxMorph(); const float morphBaseA = shared.wtMorph ? juce::jlimit (0.0f, morphMaxA, shared.wtMorph->load()) : 0.0f; const float lfoDepthA = shared.wtLfoDepth ? shared.wtLfoDepth->load() : 0.0f; const float lfoRateA = shared.wtLfoRate ? shared.wtLfoRate->load() : 1.0f; const int lfoShapeA = shared.wtLfoShape ? (int) std::lround (shared.wtLfoShape->load()) : 0; morphLfo.setRate (lfoRateA); morphLfo.setShape (lfoShapeA); const float depthFramesA = juce::jlimit (0.0f, morphMaxA, lfoDepthA); const float morphMaxB = wtOsc2.getMaxMorph(); const float morphBaseB = shared.wt2Morph ? juce::jlimit (0.0f, morphMaxB, shared.wt2Morph->load()) : 0.0f; const float lfoDepthB = shared.wt2LfoDepth ? shared.wt2LfoDepth->load() : 0.0f; const float lfoRateB = shared.wt2LfoRate ? shared.wt2LfoRate->load() : 0.3f; const int lfoShapeB = shared.wt2LfoShape ? (int) std::lround (shared.wt2LfoShape->load()) : 0; morphLfo2.setRate (lfoRateB); morphLfo2.setShape (lfoShapeB); const float depthFramesB = juce::jlimit (0.0f, morphMaxB, lfoDepthB); const float levelA = shared.wtLevel ? juce::jlimit (0.0f, 1.0f, shared.wtLevel->load()) : 0.0f; const float levelB = shared.wt2Level ? juce::jlimit (0.0f, 1.0f, shared.wt2Level->load()) : 0.0f; const float safeLevelSum = juce::jlimit (0.5f, 2.0f, levelA + levelB + 0.0001f); const float mixGain = 0.45f / safeLevelSum; for (int i = 0; i < numSamples; ++i) { float sampleA = useWTLayerA ? 0.0f : osc.process(); if (useWTLayerA) { const float lfoValueA = morphLfo.process(); const float headroomNegA = juce::jmin (depthFramesA, morphBaseA); const float headroomPosA = juce::jmin (depthFramesA, morphMaxA - morphBaseA); const float offsetA = (lfoValueA >= 0.0f ? lfoValueA * headroomPosA : lfoValueA * headroomNegA); const float morphValueA = juce::jlimit (0.0f, morphMaxA, morphBaseA + offsetA); sampleA = wtOsc.process (morphValueA); } else { morphLfo.process(); // advance for consistency } float sampleB = 0.0f; if (useWTLayerB) { const float lfoValueB = morphLfo2.process(); const float headroomNegB = juce::jmin (depthFramesB, morphBaseB); const float headroomPosB = juce::jmin (depthFramesB, morphMaxB - morphBaseB); const float offsetB = (lfoValueB >= 0.0f ? lfoValueB * headroomPosB : lfoValueB * headroomNegB); const float morphValueB = juce::jlimit (0.0f, morphMaxB, morphBaseB + offsetB); sampleB = wtOsc2.process (morphValueB); } else { morphLfo2.process(); } const float combined = mixGain * ((sampleA * levelA) + (sampleB * levelB)); for (int ch = 0; ch < numCh; ++ch) tempBuffer.getWritePointer (ch)[i] = combined; } auto block = tempBlock.getSubBlock (0, (size_t) numSamples); renderFlanger(numSamples, numCh); renderADSR(numSamples, numCh); renderChorus(block); renderSimpleDelay(block); renderDistortion(numSamples, numCh, block); renderEQ(block); // ================================================================ // Apply AMP ADSR envelope // ================================================================ { juce::AudioBuffer buf (tempBuffer.getArrayOfWritePointers(), numCh, numSamples); adsr.applyEnvelopeToBuffer (buf, 0, numSamples); } // Mix into output juce::dsp::AudioBlock (outputBuffer) .getSubBlock ((size_t) startSample, (size_t) numSamples) .add (block); } //============================================================================== void NeuralSynthVoice::noteStarted() { const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); const float initPhase = shared.wtPhase ? juce::jlimit (0.0f, 1.0f, shared.wtPhase->load()) : 0.0f; // Oscillator frequency and phase retrigger (BLEP + WT) osc.setFrequency (freqHz); osc.resetPhase (initPhase); wtOsc.setFrequency (freqHz); wtOsc.resetPhase (initPhase); morphLfo.reset(); const float initPhaseB = shared.wt2Phase ? juce::jlimit (0.0f, 1.0f, shared.wt2Phase->load()) : initPhase; wtOsc2.setFrequency (freqHz); wtOsc2.resetPhase (initPhaseB); morphLfo2.reset(); // Chorus snapshot if (shared.chorusCentre) chain.get().setCentreDelay (shared.chorusCentre->load()); if (shared.chorusDepth) chain.get().setDepth (shared.chorusDepth->load()); if (shared.chorusFeedback) chain.get().setFeedback (shared.chorusFeedback->load()); if (shared.chorusMix) chain.get().setMix (shared.chorusMix->load()); if (shared.chorusRate) chain.get().setRate (shared.chorusRate->load()); // Delay time (in samples) if (shared.delayTime) chain.get().setDelay (juce::jmax (0.0f, shared.delayTime->load() * (float) spec.sampleRate)); // Reverb snapshot juce::Reverb::Parameters rp; rp.damping = shared.reverbDamping ? shared.reverbDamping->load() : 0.0f; rp.dryLevel = shared.reverbDryLevel ? shared.reverbDryLevel->load() : 0.0f; rp.freezeMode = shared.reverbFreezeMode ? shared.reverbFreezeMode->load() : 0.0f; rp.roomSize = shared.reverbRoomSize ? shared.reverbRoomSize->load() : 0.0f; rp.wetLevel = shared.reverbWetLevel ? shared.reverbWetLevel->load() : 0.0f; rp.width = shared.reverbWidth ? shared.reverbWidth->load() : 0.0f; chain.get().setParameters (rp); // Amp ADSR juce::ADSR::Parameters ap; ap.attack = shared.adsrAttack ? shared.adsrAttack->load() : 0.01f; ap.decay = shared.adsrDecay ? shared.adsrDecay->load() : 0.10f; ap.sustain = shared.adsrSustain ? shared.adsrSustain->load() : 0.80f; ap.release = shared.adsrRelease ? shared.adsrRelease->load() : 0.40f; adsr.setParameters (ap); adsr.noteOn(); // Filter ADSR juce::ADSR::Parameters fp; fp.attack = shared.fenvAttack ? shared.fenvAttack->load() : 0.01f; fp.decay = shared.fenvDecay ? shared.fenvDecay->load() : 0.10f; fp.sustain = shared.fenvSustain ? shared.fenvSustain->load() : 0.80f; fp.release = shared.fenvRelease ? shared.fenvRelease->load() : 0.40f; filterAdsr.setParameters (fp); filterAdsr.noteOn(); } //============================================================================== void NeuralSynthVoice::notePitchbendChanged() { const float freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz(); osc.setFrequency (freqHz); wtOsc.setFrequency (freqHz); } //============================================================================== void NeuralSynthVoice::noteStopped (bool allowTailOff) { juce::ignoreUnused (allowTailOff); adsr.noteOff(); filterAdsr.noteOff(); } //==============================================================================