#pragma once #include #include //============================================================================== // Forward declaration class TwoOscAudioProcessor; //============================================================================== // SimpleSound: a basic sound object (always valid for poly synth) class SimpleSound : public juce::SynthesiserSound { public: bool appliesToNote (int) override; bool appliesToChannel (int) override; }; //============================================================================== // SimpleVoice: single voice instance for the TwoOsc synth //============================================================================== class SimpleVoice : public juce::SynthesiserVoice { public: SimpleVoice() = default; bool canPlaySound (juce::SynthesiserSound*) override; void startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) override; void stopNote (float velocity, bool allowTailOff) override; void pitchWheelMoved (int) override {} void controllerMoved (int, int) override {} void renderNextBlock (juce::AudioBuffer&, int startSample, int numSamples) override; void reset(); // Pull the latest APVTS values into this voice void setParameters (juce::AudioProcessorValueTreeState& apvts); private: // ===== Helpers ===== static inline float wrap01 (float x) { return x - std::floor (x); } // PolyBLEP step correction static inline float polyBLEP (float t, float dt) { if (t < dt) { float x = t / dt; return x + x - x * x - 1.0f; // 2x - x^2 - 1 } else if (t > 1.0f - dt) { float x = (t - 1.0f) / dt; return x * x + 2.0f * x + 1.0f; // x^2 + 2x + 1 } return 0.0f; } // Anti-aliased oscillators using PolyBLEP + DPW tri inline float oscSaw (float t, float dt) const { float y = 2.0f * t - 1.0f; y -= polyBLEP (t, dt); return y; } inline float oscSquare (float t, float dt) const { float y = (t < 0.5f ? 1.0f : -1.0f); y += polyBLEP (t, dt); y -= polyBLEP (wrap01 (t + 0.5f), dt); return y; } inline float oscTriangle (float t, float dt) { // DPW via BL square -> leaky integrator (SR-aware leak computed in startNote) float sq = (t < 0.5f ? 1.0f : -1.0f); sq += polyBLEP (t, dt); sq -= polyBLEP (wrap01 (t + 0.5f), dt); // One-pole leaky integrator (avoid denormals) const float k = 2.0f * dt; // integration step triState += k * sq - triLeakCoeff * triState; if (! std::isfinite (triState)) triState = 0.0f; return juce::jlimit (-1.0f, 1.0f, triState); } inline float oscSine (float t) const { return std::sin (juce::MathConstants::twoPi * t); } inline float oscBLEP (int mode, float t, float dt) { switch (mode) { case 1: return oscSaw (t, dt); case 2: return oscSquare (t, dt); case 3: return oscTriangle(t, dt); default: return oscSine (t); } } // ===== parameters pushed from APVTS ===== int oscAChoice = 1, oscBChoice = 2; float mix = 0.35f; float detune = -5.0f; // cents float cutoff = 180.0f; // Hz float reso = 0.18f; float envAmt = 0.25f; bool filterBypass = false; // --- LFO (per-voice, sine) --- float lfoRateHz = 0.0f; // Hz float lfoToCutOct = 0.0f; // octaves float lfoToPitchCt = 0.0f; // cents float lfoPhase = 0.0f; // 0..1 // ADSR juce::ADSR adsrAmp, adsrFilter; juce::ADSR::Parameters ampParams, filterParams; // State-variable filter (per-voice) – set in startNote() juce::dsp::StateVariableTPTFilter lpFilter; // Free-running phases (0..1) float phaseA = 0.0f, phaseB = 0.0f; // Voice state float currentNoteHz = 440.0f; float velocityGain = 1.0f; // Triangle integrator state & leak coefficient float triState = 0.0f; float triLeakCoeff = 0.0f; // Start/stop ramps int rampSamples = 0; int rampCounter = 0; bool forcedOffActive = false; int forcedOffSamples = 0; int forcedOffCounter = 0; // Smoothed params juce::SmoothedValue detuneSmoothed { 0.0f }; juce::SmoothedValue cutoffSmoothed { 1200.0f }; juce::SmoothedValue resoSmoothed { 0.3f }; juce::SmoothedValue envAmtSmoothed { 0.2f }; juce::SmoothedValue cutoffModSmooth{ 1000.0f }; // LFO smoothed depths juce::SmoothedValue lfoCutSmoothed { 0.0f }; juce::SmoothedValue lfoPitchSmoothed { 0.0f }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleVoice) }; //============================================================================== // Main processor //============================================================================== class TwoOscAudioProcessor : public juce::AudioProcessor, public juce::AudioProcessorValueTreeState::Listener { public: TwoOscAudioProcessor(); ~TwoOscAudioProcessor() override; //========================================================================== void prepareToPlay (double sampleRate, int samplesPerBlock) override; void releaseResources() override {} bool isBusesLayoutSupported (const BusesLayout& layouts) const override; void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; //========================================================================== juce::AudioProcessorEditor* createEditor() override; bool hasEditor() const override; //========================================================================== const juce::String getName() const override; bool acceptsMidi() const override; bool producesMidi() const override; bool isMidiEffect() const override; double getTailLengthSeconds() const override; //========================================================================== int getNumPrograms() override { return 1; } int getCurrentProgram() override { return 0; } void setCurrentProgram (int) override {} const juce::String getProgramName (int) override { return {}; } void changeProgramName (int, const juce::String&) override {} //========================================================================== void getStateInformation (juce::MemoryBlock& destData) override; void setStateInformation (const void* data, int sizeInBytes) override; //========================================================================== void parameterChanged (const juce::String& paramID, float newValue) override; // Preset helpers juce::StringArray getPresetCategories() const; juce::Array getPresetIndicesForCategory (const juce::String& category) const; juce::String getPresetLabel (int index) const; juce::String getCurrentPresetLabel() const; void applyPresetByIndex (int index, bool setParamToo); // APVTS juce::AudioProcessorValueTreeState apvts; static juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); private: //========================================================================== void updateFilter(); juce::Synthesiser synth; double lastSampleRate = 44100.0; // Hidden HPF juce::dsp::ProcessorDuplicator< juce::dsp::IIR::Filter, juce::dsp::IIR::Coefficients> hpFilter; // Global smoothers juce::SmoothedValue smoothedCutoff; juce::SmoothedValue smoothedReso; juce::SmoothedValue smoothedEnvAmt; std::atomic isApplyingPreset { false }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TwoOscAudioProcessor) }; //============================================================================== // Factory function //============================================================================== juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter();