#pragma once #include #include #include #include //=========================== Parameter IDs (VA path) =========================== namespace ParamIDs { static constexpr const char* oscSine = "oscSine"; static constexpr const char* oscSaw = "oscSaw"; static constexpr const char* oscSquare = "oscSquare"; static constexpr const char* cutoff = "cutoff"; static constexpr const char* resonance = "resonance"; static constexpr const char* attack = "attack"; static constexpr const char* decay = "decay"; static constexpr const char* sustain = "sustain"; static constexpr const char* release = "release"; static constexpr const char* noiseDb = "noiseDb"; // Extended controls for existing DSP blocks static constexpr const char* formant1Enable = "formant1Enable"; static constexpr const char* formant1Freq = "formant1Freq"; static constexpr const char* formant1Q = "formant1Q"; static constexpr const char* formant1GainDb = "formant1GainDb"; static constexpr const char* formant2Enable = "formant2Enable"; static constexpr const char* formant2Freq = "formant2Freq"; static constexpr const char* formant2Q = "formant2Q"; static constexpr const char* formant2GainDb = "formant2GainDb"; static constexpr const char* soundboardEnable = "soundboardEnable"; static constexpr const char* soundboardMix = "soundboardMix"; static constexpr const char* soundboardT60 = "soundboardT60"; static constexpr const char* soundboardDamp = "soundboardDamp"; static constexpr const char* postRoomMix = "postRoomMix"; static constexpr const char* postRoomEnable = "postRoomEnable"; static constexpr const char* feltPreload = "feltPreload"; static constexpr const char* feltStiffness = "feltStiffness"; static constexpr const char* feltHysteresis = "feltHysteresis"; static constexpr const char* feltMax = "feltMax"; static constexpr const char* duplexRatio = "duplexRatio"; static constexpr const char* duplexGainDb = "duplexGainDb"; static constexpr const char* duplexDecayMs = "duplexDecayMs"; static constexpr const char* duplexSympSend = "duplexSympSend"; static constexpr const char* duplexSympMix = "duplexSympMix"; static constexpr const char* pm2GainDb = "pm2GainDb"; // Final output low-pass static constexpr const char* outputLpfEnable = "outputLpfEnable"; static constexpr const char* outputLpfCutoff = "outputLpfCutoff"; static constexpr const char* outputLpfQ = "outputLpfQ"; static constexpr const char* masterVolume = "masterVolume"; static constexpr const char* temperament = "temperament"; static constexpr const char* velocityCurve = "velocityCurve"; } //============================= Engine feature toggles ========================== // Set any of these to false to bypass a specific PM2 or post-processing feature. namespace DebugToggles { inline constexpr bool kEnablePm2Duplex = true; inline constexpr bool kEnablePm2PedalResonance = true; inline constexpr bool kEnablePm2Damper = true; inline constexpr bool kEnablePm2Dispersion = true; // ENABLED: Essential for piano inharmonicity inline constexpr bool kEnablePm2WdfBurst = true; //CAN DEACTIVATE WHICH CAUSES CPU REDUCTION inline constexpr bool kEnablePm2ModalSoundboard= true; //CAN DEACTIVATE WHICH CAUSES CPU REDUCTION inline constexpr bool kEnablePm2StringFilters = true; inline constexpr bool kEnablePm2DcBlock = true; inline constexpr bool kEnablePm2SoftClip = false; inline constexpr bool kEnablePm2HammerExcitation = true; inline constexpr bool kEnablePm2FeltShaping = true; // nonlinear felt curve in burst model inline constexpr bool kEnablePm2SimplifiedBurst= true; // allow simplified burst when preset requests inline constexpr bool kEnablePm2EnergyLimiter = true; // per-string energy limiter inline constexpr bool kEnablePm2AntiSwell = true; // anti-swell envelope for high notes inline constexpr bool kEnablePm2HighNoteLoopDamping = true; // extra loop damping for high notes inline constexpr bool kEnablePm2HighNoteCouplingTilt = false; // reduce coupling/symp for high notes inline constexpr bool kEnablePm2MinDurationC4Split = true; // min-note duration split at C4 inline constexpr bool kEnablePm2TrebleLoopGainBoost = false; // loop-gain boost starting at C4 inline constexpr bool kEnablePm2TrebleT60Boost = false; // extra sustain boost starting at C5 inline constexpr bool kEnablePm2TrebleLoopGainComp = false; // extra loop-gain comp starting at C5 inline constexpr bool kEnablePm2TrebleToneComp = false; // tone shaping starting at C5 inline constexpr bool kEnablePm2WdfTrebleAtten = false; // WDF treble attenuation starting at C5 inline constexpr bool kEnablePm2AntiSwellTreblePivot = true; // anti-swell note scaling starting at C5 inline constexpr bool kEnablePm2NoteHpf = true; // per-note HPF in PM2 inline constexpr bool kEnablePm2FracDelayInterp= true; // ENABLED: Better pitch accuracy with Thiran interpolation inline constexpr bool kEnablePm2FreqDependentLoss = true;// per-note loop gain tilt (no filter in loop) inline constexpr bool kEnablePm2PostLpfEnv = true; // ENABLED: Notes darken over time like real piano inline constexpr bool kEnablePm2ExtraLoopGainSmoothing = true; inline constexpr bool kEnablePm2ExtraDamperSmoothing = true; inline constexpr bool kEnablePm2HammerGainRamp = true; inline constexpr bool kEnableGlobalFilters = true; inline constexpr bool kEnableVaFilter = true; inline constexpr bool kEnablePmDcBlock = true; inline constexpr bool kEnableBreathFilter = true; inline constexpr bool kEnableHammerFilter = true; inline constexpr bool kEnableKeyOffFilter = true; inline constexpr bool kEnablePedalThumpFilter = true; inline constexpr bool kEnableReleaseThumpFilter= true; inline constexpr bool kEnableReleaseThudFilter = true; inline constexpr bool kEnableTilt = true; inline constexpr bool kEnableBrightness = true; inline constexpr bool kEnableFormant = true; inline constexpr bool kEnableMic = false; //CAN DEACTIVATE WHICH CAUSES CPU REDUCTION inline constexpr bool kEnableCoupling = true; // ENABLED: Sympathetic string resonance inline constexpr bool kEnableReverb = true; inline constexpr bool kEnableNoiseDb = true; inline constexpr bool kEnableFelt = true; inline constexpr bool kEnableReleaseExtension = true; inline constexpr bool kEnableEq = true; inline constexpr bool kEnableOutputDcBlock = true; inline constexpr bool kEnableFinalLimiter = true; inline constexpr bool kEnableSoundboardConvolution = true;// use procedural IR instead of JUCE reverb inline constexpr int kSoundboardConvolutionDownsample = 2; // 1=full rate, 2=half-rate convolution (4x causes aliasing) inline constexpr bool kEnablePostRoomReverb = true; // ENABLED: Room ambience for naturalness inline constexpr bool kPostRoomIsHall = false; // false=room, true=hall inline constexpr bool kEnableOutputLpf = true; inline constexpr bool kDisableSustainPedal = false; } //============================= Physics data integration ======================= namespace PhysicsToggles { // When true, embedded presets are ignored and physics-based defaults are used. inline constexpr bool kUsePhysicsDefaults = true; // When true, per-note physics (inharmonicity, hammer params, decay) are used. inline constexpr bool kUsePerNotePhysics = true; } //============================= Preset model =================================== struct Formant { bool enabled { false }; float freq { 1800.0f }; float q { 2.0f }; float gainDb { 0.0f }; }; struct PresetModel { struct Hammer { bool enabled { false }; float level { 0.25f }; // 0..1 float decay_s { 0.015f }; // seconds float noise { 0.40f }; // 0..1 float hp_hz { 1800.0f }; }; struct ActionNoise { bool keyOffEnabled { false }; float keyOffLevel { 0.05f }; // 0..1 float keyOffDecay_s { 0.030f }; bool keyOffVelScale { true }; float keyOffHp_hz { 1200.0f }; bool pedalEnabled { false }; float pedalLevel { 0.08f }; float pedalDecay_s { 0.050f }; float pedalLp_hz { 600.0f }; bool releaseEnabled { false }; float releaseLevel { 0.10f }; float releaseDecay_s { 0.060f }; float releaseLp_hz { 450.0f }; float releaseThudMix { 0.5f }; // 0..1 float releaseThudHp_hz { 70.0f }; }; struct Soundboard { bool enabled { false }; float mix { 0.25f }; // 0..1 (increased from 0.20) float t60_s { 2.0f }; // seconds float damp { 0.35f }; // 0..1 (reduced from 0.40 for longer decay) }; struct PmString { int numStrings { 2 }; // 1..3 std::array detuneCents{ { 0.0f, 1.5f, 0.0f } }; // +/-5 std::array gain { { 0.6f, 0.4f, 0.0f } }; // normalized std::array pan { { -0.25f, 0.0f, 0.25f } }; // -1..1, left..right float stereoWidthLow { 1.0f }; // width factor in bass float stereoWidthHigh { 0.35f }; // width factor in treble float stereoWidthNoteLo { 21.0f }; // MIDI for widthLow (A0) float stereoWidthNoteHi { 96.0f }; // MIDI for widthHigh (C7-ish) float dispersionAmt{ 0.15f }; // 0..1 int apStages { 2 }; // 1..4 float loss { 0.003f }; // 0.0005..0.02 (lowered from 0.006 for longer sustain) float dcBlockHz { 8.0f }; // 3..20 }; struct HammerModel { float force { 0.5f }; // 0..1 float toneHz { 2500.0f }; // 1500..6000 bool softclip { true }; float attackMs { 8.0f }; // 1..20 (increased from 6) float gamma { 1.2f }; // velocity curve exponent (1=linear) // Continuous hammer-string interaction float massKg { 0.028f }; // effective hammer mass float contactStiffness { 2600.0f }; // base felt stiffness float contactExponent { 2.4f }; // felt compression exponent float contactDamping { 6.0f }; // damping term float maxPenetration { 0.010f }; // meters (safety clamp) float attackWindowMs { 15.0f }; // interaction window (increased from 6ms for longer transient) bool simplifiedMode { false }; // fallback to legacy burst // Velocity response float stiffnessVelScale { 0.40f }; // scales stiffness vs velocity float toneVelScale { 0.35f }; // scales tone cutoff vs velocity float preloadVelScale { 0.35f }; // scales felt preload vs velocity float toneMinHz { 1400.0f }; float toneMaxHz { 12000.0f }; }; struct FeltModel { float preload { 0.08f }; // small preload offset before compression float stiffness { 2.4f }; // exponent for compression curve float hysteresis { 0.15f }; // bleed from previous contact to mimic rate/return float maxAmp { 1.4f }; // absolute clamp for safety }; struct BoardMode { float f{110.0f}; float q{1.2f}; float gainDb{-2.0f}; }; struct PmFilter { float cutoff{4500.0f}; float q{0.8f}; float keytrack{0.5f}; }; struct OutputLpf { bool enabled{false}; float cutoff{18000.0f}; float q{0.707f}; }; struct EqBand { float freq{80.0f}; float q{0.7f}; float gainDb{0.0f}; }; struct OutputEq { bool enabled{ false }; std::array bands{ { { 80.0f, 0.7f, 0.0f }, { 250.0f, 0.9f, 0.0f }, { 800.0f, 1.0f, 0.0f }, { 2500.0f, 1.0f, 0.0f }, { 8000.0f, 0.8f, 0.0f } } }; }; struct Pedal { float sustainThresh { 0.65f }; // CC64 >= thresh => latch sustain float halfThresh { 0.35f }; // CC64 >= half => lengthen release float halfReleaseScale { 1.6f }; // release multiplier for half-pedal float sustainReleaseScale { 1.2f }; // release multiplier when sustain is down float repedalMs { 120.0f }; // window to catch repedal (info only) float resonanceSend { 0.25f }; // send into pedal resonance float resonanceMix { 0.25f }; // mix of pedal resonance return float resonanceT60 { 1.6f }; // decay of pedal resonance (seconds) float sustainGainDb { 1.5f }; // post gain boost when sustain is down }; struct Damper { float lossDamped { 0.86f }; // multiply loop gain when damper on float lossHalf { 0.94f }; // multiply loop gain in half-pedal zone float lossOff { 1.00f }; // multiply loop gain when pedal fully lifted float smoothMs { 20.0f }; // smoothing for loopGain transitions float softenMs { 12.0f }; // soften filter duration float softenHz { 1200.0f }; // soften filter cutoff }; struct UnaCorda { float detuneCents { -3.0f }; // applied to pm2 strings when soft pedal down float gainScale { 0.85f }; // scale per-string gains when soft pedal down }; struct Duplex { float ratio { 2.2f }; // duplex partial ratio vs main float gainDb { -12.0f }; // clamp to [-20..-6] dB float decayMs { 180.0f }; // decay time for afterlength (increased from 120 for longer tail) float sympSend { 0.18f }; // sympathetic resonance send (increased from 0.15) float sympMix { 0.22f }; // sympathetic return mix (increased from 0.20) float sympNoPedalScale { 0.25f }; // scale sympathetic return when sustain is not down }; struct DispersionCurve { float highMult { 1.25f }; // scale dispersion for highest notes float pow { 1.1f }; // curve steepness vs note norm }; struct Mic { float gainDb { 0.0f }; float delayMs { 0.0f }; float lowShelfDb { 0.0f }; float highShelfDb { 0.0f }; float shelfFreq { 800.0f }; }; struct Mics { Mic close {}; Mic player { 0.0f, 1.5f, 1.0f, -0.5f, 900.0f }; Mic room { -2.0f, 6.0f, 1.5f, 1.5f, 600.0f }; std::array blend { { 1.0f, 0.0f, 0.0f } }; }; Mics mics; DispersionCurve dispersion; // Loudness trim for pm2 engine (applied after pm2 render to match other engines) // FIXED: Changed from +12dB to 0dB to prevent overwhelming output levels float pm2GainDb { 0.0f }; // unity gain - no boost to avoid overloading float outputGainDb { 0.0f }; // per-preset output trim (dB) int schemaVersion { 4 }; // accept older, but save/apply as v4 juce::String engine { "va" }; // "va", "pm", "pm2" (placeholder), or "hybrid" float masterTuneCents { 0.0f }; // global coarse tune in cents // Temperament / per-note tuning juce::String temperamentName { "12-TET" }; std::array temperamentOffsetsCents { { 0.0f } }; // C..B offsets in cents bool temperamentUseOffsets { false }; // true if offsets provided in preset std::array perNoteOffsetsCents { { 0.0f } }; // per MIDI note offsets bool perNoteOffsetsEnabled { false }; // Optional hybrid mix (top-level) float engineMixVa { 1.0f }; float engineMixPm { 0.0f }; float engineMixPm2 { 0.0f }; // placeholder for future pm2 engine // --- VA core (driven by APVTS) --- float oscSine{0.7f}, oscSaw{0.3f}, oscSquare{0.0f}; float cutoff{1800.0f}, q{0.7f}; float attack{0.010f}, decay{0.75f}, sustain{0.0f}, release{0.45f}; float noiseDb{-48.0f}; float pitchCompOffsetCents{ 0.0f }; // global cents offset float pitchCompSlopeCents{ 0.0f }; // cents per MIDI note vs 60 // FIXED: Changed from +0.04 to -0.12 for realistic piano loudness curve // Negative slope means treble is quieter than bass (like real piano) float pm2LoudnessSlopeDbPerSemi{ -0.03f }; float releaseExtension { 1.5f }; // loop gain scale for release tail (increased from 1.0) // Velocity -> brightness high-shelf control bool brightnessEnabled { false }; float brightnessBaseDb { 0.0f }; float brightnessVelSlopeDb { 4.0f }; // additional dB at vel=1 float brightnessNoteSlopeDb { 0.0f }; // dB spread low->high notes float brightnessMaxDb { 6.0f }; float brightnessCutoffHz { 3200.0f }; float brightnessQ { 0.707f }; // Global velocity curve shaping float velocityGamma { 1.0f }; // pow(vel, gamma) juce::String velocityCurve { "linear" }; // optional name from preset // --- Harmonic shaper (post-VA pre-shared) --- bool shaperEnabled{ true }; float shaperDrive { 0.15f }; // 0..1 // --- Breath noise (post) --- bool breathEnabled{ true }; float breathLevelDb{ -40.0f }; float breathBpFreq { 5500.0f }; float breathBpQ { 1.0f }; // --- Formant peaks (post) --- Formant formants[2]; // --- Piano features --- Hammer hammer; ActionNoise action; Damper damper; Soundboard soundboard; // --- pm2 params --- PmString pmString; HammerModel hammerModel; FeltModel feltModel; struct WdfModel { bool enabled { false }; float blend { 0.0f }; // 0=legacy felt only, 1=WDF only float loss { 0.01f }; float bridgeMass { 1.0f }; float plateStiffness { 1.0f }; } wdf; struct Coupling { float gain { 0.02f }; // cross-string bleed float q { 0.7f }; float sympGain { 0.05f }; // sympathetic send float sympHighDamp { 0.5f }; // attenuate highs in sympathetic path } coupling; juce::Array boardModes; float boardSend { 0.35f }; float boardMix { 0.30f }; PmFilter pmFilter; OutputLpf outputLpf; OutputEq outputEq; float tiltDb { 0.0f }; float predelayMs { 5.0f }; float postRoomMix { 1.0f }; // scale for post room/hall reverb bool postRoomEnabled { true }; Pedal pedal; UnaCorda unaCorda; Duplex duplex; static float clamp (float x, float lo, float hi) { return juce::jlimit (lo, hi, x); } }; //============================= Sound & Voice (VA) ============================== struct SimpleSound : public juce::SynthesiserSound { bool appliesToNote (int) override { return true; } bool appliesToChannel (int) override { return true; } }; class FluteVoice : public juce::SynthesiserVoice { public: explicit FluteVoice (juce::AudioProcessorValueTreeState& state); bool canPlaySound (juce::SynthesiserSound* s) override; void startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) override; void stopNote (float, bool allowTailOff) override; void pitchWheelMoved (int) override {} void controllerMoved (int, int) override {} void renderNextBlock (juce::AudioBuffer& buffer, int startSample, int numSamples) override; void prepare (double sr, int samplesPerBlock, int /*numChannels*/); void setMasterTuneFactor (float f) { masterTuneFactor = f; } void setPitchComp (float offsetCents, float slopeCentsPerNote) { pitchCompOffsetCents = offsetCents; pitchCompSlopeCents = slopeCentsPerNote; } void setNoteOffsets (const std::array& offsets) { noteOffsetsCents = offsets; } private: void setFrequency (double hz); float randomUniform(); void updateParams(); // state juce::AudioProcessorValueTreeState& apvts; double sampleRate { 44100.0 }, currentFrequency { 440.0 }; float phase { 0.0f }, phaseDelta { 0.0f }, phaseOverPi { 0.0f }; juce::ADSR adsr; juce::dsp::StateVariableTPTFilter svf; float wSine { 1.0f }, wSaw { 0.0f }, wSquare { 0.0f }; float preNoiseLin { 0.0f }; uint32_t rng { 0x12345678 }; float masterTuneFactor { 1.0f }; float velocityGain { 1.0f }; float pitchCompOffsetCents { 0.0f }; float pitchCompSlopeCents { 0.0f }; std::array noteOffsetsCents { { 0.0f } }; }; //=========================== Simple PM (waveguide) ============================= // Now includes an amplitude ADSR so env.* affects PM as well. class WaveguideFlute { public: void prepare (double sr, int blockSize, int numCh); void setFrequency (double hz); void noteOn (int midi, float /*vel*/); void noteOff(); // New: set amplitude envelope for PM (called from applyPresetToParameters) void setEnvParams (float attack, float decay, float sustain, float release); void setReleaseScale (float baseRelease, float scale); void setPitchComp (float offsetCents, float slopeCentsPerNote) { pitchCompOffsetCents = offsetCents; pitchCompSlopeCents = slopeCentsPerNote; } void setNoteOffsets (const std::array& offsets) { noteOffsetsCents = offsets; } // Render the PM block (scaled by ADSR) void render (juce::AudioBuffer& buffer, int start, int num); bool isActive() const { return active || adsr.isActive(); } void setMasterTuneFactor (float f) { masterTuneFactor = f; } private: struct DCBlock { void reset (double sr) { a = (float) (1.0 - 2.0 * juce::MathConstants::pi * 5.0 / sr); x1 = y1 = 0.0f; } float process (float x) { float y = x - x1 + a * y1; x1 = x; y1 = y; return y; } float a{0.995f}, x1{0}, y1{0}; }; juce::AudioBuffer delay; int writePos { 0 }; double sampleRate { 44100.0 }, frequency { 440.0 }; bool active { false }; // running state (continues through release) float noiseGain { 0.02f }, jetFeedback { 0.2f }, phase { 0.0f }; DCBlock dc; float velocityGain { 1.0f }; // New: amplitude envelope juce::ADSR adsr{}; juce::ADSR::Parameters envParams{ 0.01f, 0.75f, 0.0f, 0.45f }; float baseRelease { 0.45f }; float randomUniform(); uint32_t rng { 0xCAFEBABE }; float masterTuneFactor { 1.0f }; float pitchCompOffsetCents { 0.0f }; float pitchCompSlopeCents { 0.0f }; std::array noteOffsetsCents { { 0.0f } }; }; //=========================== Shared Bus for coupling/sympathetic ============== // FIX #1: Moved from thread_local to regular shared structure // FIX #4: Added block-relative indexing support struct SharedBus { void begin (int n) { if ((int) left.size() < n) { left.assign ((size_t) n, 0.0f); right.assign ((size_t) n, 0.0f); } else { std::fill (left.begin(), left.begin() + n, 0.0f); std::fill (right.begin(), right.begin() + n, 0.0f); } size = n; } void add (int idx, float l, float r) { if (idx < 0 || idx >= size) return; left[(size_t) idx] += l; right[(size_t) idx] += r; } std::pair read (int idx) const { if (idx < 0 || idx >= size) return { 0.0f, 0.0f }; return { left[(size_t) idx], right[(size_t) idx] }; } int size { 0 }; std::vector left, right; }; //=========================== pm2 stiff-string (multi-string) ================== class Pm2StringBank { public: void prepare (double sr, int blockSize, int numCh); void setMinNoteDurationSamples (int samples) { minNoteDurationSamples = juce::jmax (0, samples); } void setEconomyMode (bool enabled) { economyMode = enabled; } void setPolyphonyScale (float s) { polyphonyScale = juce::jlimit (0.2f, 1.0f, s); } void setParams (const PresetModel::PmString& params); void setHammerParams (const PresetModel::HammerModel& h); void setFeltParams (const PresetModel::FeltModel& f); void setDuplexParams (const PresetModel::Duplex& d); void setWdfParams (const PresetModel::WdfModel& wdf); void setSoftPedal (bool down, const PresetModel::UnaCorda& una); void setSustainPedalDown (bool down); void setDamperParams (const PresetModel::Damper& d); void setDamperLift (float lift); void beginVoiceStealFade (float ms); void setEnvParams (float attack, float decay, float sustain, float release); void setReleaseScale (float baseRelease, float scale); void setReleaseExtension (float ext) { if (DebugToggles::kEnableReleaseExtension) releaseExtension = juce::jlimit (1.0f, 2.5f, ext); else releaseExtension = 1.0f; } void setHighPolyMode (bool enabled) { highPolyMode = enabled; } void setPitchComp (float offsetCents, float slopeCentsPerNote) { pitchCompOffsetCents = offsetCents; pitchCompSlopeCents = slopeCentsPerNote; } void setNoteOffsets (const std::array& offsets) { noteOffsetsCents = offsets; } void setCouplingParams (const PresetModel::Coupling& c); void updateNoteHpf (int midiNoteNumber); void setLoudnessSlope (float dbPerSemi) { loudnessSlopeDbPerSemi = dbPerSemi; } void setMasterTuneFactor (float f) { masterTuneFactor = f; } void setDispersionCurve (const PresetModel::DispersionCurve& d) { dispersionHighMult = d.highMult; dispersionPow = d.pow; } void setLowVelSkip (bool skip) { lowVelSkip = skip; } // FIX #1 & #4: Pass shared buses from processor, use block-relative indexing void setSharedBuses (SharedBus* coupling, SharedBus* symp) { couplingBus = coupling; sympBus = symp; } void noteOn (int midiNoteNumber, float velocity); void hardRetrigger (int midiNoteNumber, float velocity); void noteOff(); // FIX #4: Added startSampleInBlock parameter for block-relative bus indexing void render (juce::AudioBuffer& buffer, int startSample, int numSamples, int startSampleInBlock); // FIX #3: Modified to respect steal fade in progress void forceSilence(); // FIX #3: Mark that we're about to steal this voice void markForSteal() { stealInProgress = true; } bool isActive() const { return active || adsr.isActive(); } float getEnvLevel() const { return lastEnv; } bool isKeyHeld() const { return keyHeld; } bool isSustainPedalDown() const { return sustainPedalDown; } float getLoopEnergy() const { return loopEnergySmoothed; } int getCurrentMidiNote() const { return currentMidiNote; } // Voice stealing: track note age (higher = newer) void setNoteAge (uint64_t age) { noteAge = age; } uint64_t getNoteAge() const { return noteAge; } private: void applyNoteOffInternal(); void resetForHardRetrigger(); struct AP1 { float z1 { 0.0f }, g { 0.0f }; float process (float x) { float y = -g * x + z1; z1 = x + g * y; return y; } }; struct DCBlock { void reset (double sr) { a = (float) (1.0 - 2.0 * juce::MathConstants::pi * 5.0 / sr); x1 = y1 = 0.0f; } float process (float x) { float y = x - x1 + a * y1; x1 = x; y1 = y; return y; } float a{0.995f}, x1{0}, y1{0}; }; struct DuplexState { std::vector buf; int write { 0 }; float feedback { 0.5f }; float gain { 0.1f }; float inputGain { 0.15f }; }; struct StringState { std::vector delay; int writePos { 0 }; double delaySamples { 0.0 }; float loopGain { 0.999f }; float baseGain { 1.0f }; float panGainL { 0.7071f }; float panGainR { 0.7071f }; std::array ap; int apStages { 1 }; DCBlock dc; DuplexState duplex; float loopGainSmoothed { 0.999f }; float damperLossPrev { 1.0f }; float damperLossSmoothed { 1.0f }; int damperSoftenCountdown { 0 }; float damperSoftenState { 0.0f }; float loopGainBase { 0.999f }; float loopGainRelease { 0.999f }; float feltState { 0.0f }; float feltLastOut { 0.0f }; float feltEnvPrev { 0.0f }; float lpState { 0.0f }; float lpCoeff { 0.25f }; // Thiran allpass interpolator state (for fractional delay) float interpAlpha { 0.0f }; // coefficient: (1-d)/(1+d) where d is fractional delay float interpZ1 { 0.0f }; // filter state int toneInjectSamplesLeft { 0 }; float toneInjectPhase { 0.0f }; float toneInjectPhaseDelta { 0.0f }; float toneInjectGain { 0.0f }; // Option 1: Energy limiter state - captures peak after attack, soft-limits to prevent swell float energyPeak { 0.0f }; // Reference peak energy captured during calibration float energySmoothed { 0.0f }; // Smoothed current energy for limiter float energyGainSmoothed { 1.0f }; // Smoothed limiter gain to avoid abrupt steps int energyCalibSamplesLeft { 0 }; // Samples remaining in calibration window bool energyCalibComplete { false }; // True once reference is captured struct HammerState { bool active { false }; int samplesLeft { 0 }; int samplesTotal { 0 }; int samplesElapsed { 0 }; float pos { 0.0f }; float vel { 0.0f }; float pen { 0.0f }; float mass { 0.028f }; float k { 2600.0f }; float exp { 2.4f }; float damping { 6.0f }; float preload { 0.08f }; float maxPen { 0.010f }; float toneAlpha { 0.0f }; float toneState { 0.0f }; float gain { 0.0020f }; float gainSmoothed { 0.0f }; bool simplified { false }; } hammer; // Fundamental resonator state - boosts fundamental frequency content // Compensates for physical modeling's tendency to emphasize upper partials float fundResonatorState1 { 0.0f }; float fundResonatorState2 { 0.0f }; float fundResonatorCoeff { 0.0f }; float fundResonatorGain { 0.0f }; }; // FIX #5: Modified to not reset DC blocker void resizeString (StringState& s, double samples); float randomUniform(); double sampleRate { 44100.0 }; bool active { false }; PresetModel::PmString params; PresetModel::HammerModel hammer; PresetModel::FeltModel felt; PresetModel::Duplex duplex; PresetModel::Damper damper; PresetModel::WdfModel wdf; PresetModel::UnaCorda unaCorda; bool softPedalDown { false }; bool sustainPedalDown { false }; bool lowVelSkip { false }; bool keyHeld { false }; bool useReleaseLoopGain { false }; int releaseDelaySamples { 0 }; int noteLifeSamples { 0 }; int minNoteDurationSamples { 0 }; int minNoteOffRemaining { 0 }; bool pendingNoteOff { false }; int damperDelaySamples { 0 }; int pedalChangeSamplesTotal { 0 }; int pedalChangeSamplesRemaining { 0 }; float pedalChangeFade { 1.0f }; float damperLiftTarget { 1.0f }; float damperLiftSmoothed { 1.0f }; float damperLiftSmoothCoeff { 0.05f }; int stealFadeSamples { 0 }; int stealFadeRemaining { 0 }; // FIX #3: Track if voice steal is in progress bool stealInProgress { false }; int keyReleaseSamplesTotal { 0 }; int keyReleaseSamplesRemaining { 0 }; int keyOffFadeSamplesTotal { 0 }; int keyOffFadeSamplesRemaining { 0 }; float damperSmoothCoeff { 0.05f }; int damperSoftenSamples { 0 }; float damperSoftenA { 0.0f }; float releaseExtension { 1.0f }; bool highPolyMode { false }; std::array strings; juce::ADSR adsr; juce::ADSR::Parameters envParams { 0.001f, 0.75f, 0.0f, 0.45f }; // no enforced 8 ms ramp; follow GUI/env settings float baseRelease { 0.45f }; float decayTimeScale { 1.0f }; // Multiplier for physical T60 based on GUI decay int currentMidiNote { -1 }; bool economyMode { false }; float lastEnv { 0.0f }; uint32_t rng { 0x1234BEEF }; // Pink noise state for stochastic excitation (Voss-McCartney approximation) // Creates more realistic spectral density between harmonics std::array pinkNoiseState { { 0.0f, 0.0f, 0.0f } }; int pinkNoiseCounter { 0 }; // Body resonance noise generator - fills gaps between harmonics // Simulates soundboard and cabinet broadband resonance float bodyNoiseState { 0.0f }; float bodyNoiseLp1 { 0.0f }; float bodyNoiseLp2 { 0.0f }; float bodyNoiseHp { 0.0f }; uint32_t bodyNoiseRng { 0x12345678 }; float masterTuneFactor { 1.0f }; float pitchLoudnessGain { 1.0f }; float pitchCompSamples { 0.0f }; float pitchCompBase { 0.0f }; float pitchCompPerHz { 0.0f }; float pitchCompMax { 0.0f }; float pitchCompOffsetCents { 0.0f }; float pitchCompSlopeCents { 0.0f }; std::array noteOffsetsCents { { 0.0f } }; float velocityGain { 1.0f }; // FIXED: Changed from +0.04 to -0.12 for realistic piano loudness curve float loudnessSlopeDbPerSemi { -0.12f }; int currentNumStrings { 3 }; // CPU OPTIMIZATION: Third string gain scale for gradual bass fade (G2 down to C2) // 1.0 = full third string, 0.0 = muted third string float thirdStringGainScale { 1.0f }; float stereoWidth { 1.0f }; // FIXED: Normalization factor for multi-string summing to prevent level buildup float stringGainNorm { 1.0f }; float loopEnergySmoothed { 0.0f }; float loopEnergySmoothCoeff { 0.0f }; float polyphonyScale { 1.0f }; // Frequency-dependent loop loss (scalar, no filter in loop) float freqLossScalar { 1.0f }; // Post-loop LPF with ADSR (per voice) juce::ADSR postLpfEnv; juce::ADSR::Parameters postLpfEnvParams { 0.002f, 0.18f, 0.21f, 0.12f };//Pm2PostLpfEnv envelope values float postLpfMinHz { 500.0f }; float postLpfMaxHz { 6000.0f }; float postLpfStateL { 0.0f }; float postLpfStateR { 0.0f }; // Note-dependent HPF to keep high notes free of sub-bass juce::dsp::StateVariableTPTFilter noteHpf; float noteHpfCutoff { 30.0f }; int noteHpfNumChannels { 0 }; // Sympathetic/coupling filters juce::dsp::StateVariableTPTFilter couplingBpL, couplingBpR; juce::dsp::StateVariableTPTFilter sympBpL, sympBpR; float couplingGain { 0.02f }; float couplingQ { 0.7f }; float sympGain { 0.05f }; float sympHighDamp { 0.5f }; float dispersionHighMult { 1.0f }; float dispersionPow { 1.0f }; int noteFadeSamplesRemaining { 0 }; int noteFadeSamplesTotal { 0 }; // FIX #1: Pointers to shared buses owned by processor SharedBus* couplingBus { nullptr }; SharedBus* sympBus { nullptr }; // Voice stealing: track note age (higher = newer, 0 = no note) uint64_t noteAge { 0 }; // Option 5: Anti-swell envelope - slow decay for high notes to counter energy accumulation float antiSwellEnv { 1.0f }; // Current envelope value (starts at 1.0, slowly decays) float antiSwellDecayPerSample { 0.0f }; // Decay rate (pitch-dependent) float lastOutL { 0.0f }; float lastOutR { 0.0f }; }; //=========================== PM/PM2 polyphonic voices ======================== struct PmSound : public juce::SynthesiserSound { bool appliesToNote (int) override { return true; } bool appliesToChannel (int) override { return true; } }; class PmVoice : public juce::SynthesiserVoice { public: void prepare (double sr, int blockSize, int numCh) { pm.prepare (sr, blockSize, numCh); } void setEnvParams (float a, float d, float s, float r) { pm.setEnvParams (a, d, s, r); } void setReleaseScale (float baseR, float scale) { baseRelease = baseR; releaseScale = scale; } void setPitchComp (float offsetCents, float slopeCentsPerNote) { pm.setPitchComp (offsetCents, slopeCentsPerNote); } void setNoteOffsets (const std::array& offsets) { pm.setNoteOffsets (offsets); } void setMasterTune (float f) { tuneFactor = f; pm.setMasterTuneFactor (f); } bool canPlaySound (juce::SynthesiserSound* s) override { return dynamic_cast (s) != nullptr; } void startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) override { pm.setReleaseScale (baseRelease, releaseScale); pm.setMasterTuneFactor (tuneFactor); pm.noteOn (midiNoteNumber, velocity); } void stopNote (float, bool allowTailOff) override { pm.setReleaseScale (baseRelease, releaseScale); pm.noteOff(); if (! allowTailOff) clearCurrentNote(); } void pitchWheelMoved (int) override {} void controllerMoved (int, int) override {} void renderNextBlock (juce::AudioBuffer& buffer, int startSample, int numSamples) override { pm.render (buffer, startSample, numSamples); if (! pm.isActive()) clearCurrentNote(); } private: WaveguideFlute pm; float baseRelease { 0.25f }; float releaseScale { 1.0f }; float tuneFactor { 1.0f }; }; struct Pm2Sound : public juce::SynthesiserSound { bool appliesToNote (int) override { return true; } bool appliesToChannel (int) override { return true; } }; class Pm2Voice : public juce::SynthesiserVoice { public: void prepare (double sr, int blockSize, int numCh) { pm2.prepare (sr, blockSize, numCh); } void setMinNoteDurationSamples (int samples) { pm2.setMinNoteDurationSamples (samples); } void setMinNoteDurationRanges (int lowSamples, int midLowSamples, int midHighSamples, int highSamples, int split1, int split2, int split3) { minDurLow = lowSamples; minDurMidLow = midLowSamples; minDurMidHigh = midHighSamples; minDurHigh = highSamples; minSplit1 = split1; minSplit2 = split2; minSplit3 = split3; } void setParams (const PresetModel::PmString& params) { pm2.setParams (params); } void setHammerParams (const PresetModel::HammerModel& h) { pm2.setHammerParams (h); } void setFeltParams (const PresetModel::FeltModel& f) { pm2.setFeltParams (f); } void setDuplexParams (const PresetModel::Duplex& d) { pm2.setDuplexParams (d); } void setWdfParams (const PresetModel::WdfModel& wdf) { pm2.setWdfParams (wdf); } void setDamperParams (const PresetModel::Damper& d) { pm2.setDamperParams (d); } void setDamperLift (float lift) { pm2.setDamperLift (lift); } void setSoftPedal (bool down, const PresetModel::UnaCorda& una) { pm2.setSoftPedal (down, una); } void setEnvParams (float a, float d, float s, float r) { pm2.setEnvParams (a, d, s, r); } void setReleaseScale (float baseR, float scale) { baseRelease = baseR; releaseScale = scale; pm2.setReleaseScale (baseR, scale); } void setReleaseExtension (float ext) { pm2.setReleaseExtension (ext); } void setPitchComp (float offsetCents, float slopeCentsPerNote) { pm2.setPitchComp (offsetCents, slopeCentsPerNote); } void setNoteOffsets (const std::array& offsets) { pm2.setNoteOffsets (offsets); } void setLoudnessSlope (float dbPerSemi) { pm2.setLoudnessSlope (dbPerSemi); } void setMasterTune (float f) { tuneFactor = f; pm2.setMasterTuneFactor (f); } void setSustainPedalDown (bool down) { pm2.setSustainPedalDown (down); } void setEconomyMode (bool enabled) { pm2.setEconomyMode (enabled); } void setPolyphonyScale (float s) { pm2.setPolyphonyScale (s); } void setHighPolyMode (bool enabled) { pm2.setHighPolyMode (enabled); } void setCouplingParams (const PresetModel::Coupling& c) { pm2.setCouplingParams (c); } void setDispersionCurve (const PresetModel::DispersionCurve& d) { pm2.setDispersionCurve (d); } // FIX #1: Pass shared buses to the string bank void setSharedBuses (SharedBus* coupling, SharedBus* symp) { pm2.setSharedBuses (coupling, symp); } // Voice stealing: set pointer to note age counter (owned by Pm2Synth) void setNoteAgeCounter (uint64_t* counter) { noteAgeCounter = counter; } bool canPlaySound (juce::SynthesiserSound* s) override { return dynamic_cast (s) != nullptr; } void startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) override { // Assign note age from counter when voice starts (newest notes have highest age) if (noteAgeCounter != nullptr) pm2.setNoteAge (++(*noteAgeCounter)); const int minSamples = (midiNoteNumber < minSplit1) ? minDurLow : (midiNoteNumber < minSplit2) ? minDurMidLow : (midiNoteNumber < minSplit3) ? minDurMidHigh : minDurHigh; pm2.setMinNoteDurationSamples (minSamples); pm2.setReleaseScale (baseRelease, releaseScale); pm2.setMasterTuneFactor (tuneFactor); pm2.noteOn (midiNoteNumber, velocity); } void hardRetrigger (int midiNoteNumber, float velocity) { if (noteAgeCounter != nullptr) pm2.setNoteAge (++(*noteAgeCounter)); pm2.setReleaseScale (baseRelease, releaseScale); pm2.setMasterTuneFactor (tuneFactor); pm2.hardRetrigger (midiNoteNumber, velocity); } // FIX #3: Mark for steal and start a short fade to avoid hard truncation void prepareForSteal() { pm2.markForSteal(); pm2.beginVoiceStealFade (20.0f); } void stopNote (float, bool allowTailOff) override { pm2.setReleaseScale (baseRelease, releaseScale); pm2.noteOff(); if (! allowTailOff) clearCurrentNote(); } void pitchWheelMoved (int) override {} void controllerMoved (int, int) override {} // FIX #4: Pass block-relative start sample void renderNextBlock (juce::AudioBuffer& buffer, int startSample, int numSamples) override { renderWithBlockOffset (buffer, startSample, numSamples, startSample); } // New method that accepts block offset for proper bus indexing void renderWithBlockOffset (juce::AudioBuffer& buffer, int startSample, int numSamples, int startSampleInBlock) { pm2.render (buffer, startSample, numSamples, startSampleInBlock); if (! pm2.isActive()) clearCurrentNote(); } bool isActive() const { return pm2.isActive(); } float getEnvLevel() const { return pm2.getEnvLevel(); } bool isKeyHeld() const { return pm2.isKeyHeld(); } bool isSustainPedalDown() const { return pm2.isSustainPedalDown(); } float getLoopEnergy() const { return pm2.getLoopEnergy(); } int getCurrentMidiNote() const { return pm2.getCurrentMidiNote(); } // Voice stealing: track note age void setNoteAge (uint64_t age) { pm2.setNoteAge (age); } uint64_t getNoteAge() const { return pm2.getNoteAge(); } private: Pm2StringBank pm2; float baseRelease { 0.25f }; float releaseScale { 1.0f }; float tuneFactor { 1.0f }; uint64_t* noteAgeCounter { nullptr }; // Pointer to shared counter for voice stealing int minDurLow { 0 }; int minDurMidLow { 0 }; int minDurMidHigh { 0 }; int minDurHigh { 0 }; int minSplit1 { 60 }; int minSplit2 { 84 }; int minSplit3 { 96 }; }; // Custom pm2 synthesiser with smarter voice steal class Pm2Synth : public juce::Synthesiser { public: // Manual pre-steal hook called by processor before forwarding note-ons void preallocateVoiceForNote (int midiNoteNumber); // FIX: New method to preallocate multiple voices for chords void preallocateVoicesForChord (int numNotesNeeded); bool hasActiveVoiceForNote (int midiNoteNumber) const; bool hardRetriggerActiveVoice (int midiNoteNumber, float velocity); void requestDeclick (int samples) { pendingDeclickSamples.store (juce::jmax (samples, pendingDeclickSamples.load())); } void requestDeclickOut (int samples) { pendingDeclickOutSamples.store (juce::jmax (samples, pendingDeclickOutSamples.load())); } int consumeDeclickSamples() { return pendingDeclickSamples.exchange (0); } int consumeDeclickOutSamples() { return pendingDeclickOutSamples.exchange (0); } // FIX #1: Set shared buses on all voices // Also sets up note age counter for voice stealing void setSharedBuses (SharedBus* coupling, SharedBus* symp) { for (int i = 0; i < getNumVoices(); ++i) if (auto* v = dynamic_cast (getVoice (i))) { v->setSharedBuses (coupling, symp); v->setNoteAgeCounter (¬eCounter); } } void setMinNoteDurationSamples (int samples) { for (int i = 0; i < getNumVoices(); ++i) if (auto* v = dynamic_cast (getVoice (i))) v->setMinNoteDurationSamples (samples); } void setMinNoteDurationRanges (int lowSamples, int midLowSamples, int midHighSamples, int highSamples, int split1, int split2, int split3) { for (int i = 0; i < getNumVoices(); ++i) if (auto* v = dynamic_cast (getVoice (i))) v->setMinNoteDurationRanges (lowSamples, midLowSamples, midHighSamples, highSamples, split1, split2, split3); } private: std::atomic pendingDeclickSamples { 0 }; std::atomic pendingDeclickOutSamples { 0 }; uint64_t noteCounter { 0 }; // Monotonically increasing counter for note age tracking }; //=========================== Main Processor =================================== class FluteSynthAudioProcessor : public juce::AudioProcessor { public: FluteSynthAudioProcessor(); ~FluteSynthAudioProcessor() override = default; // AudioProcessor void prepareToPlay (double, int) override; void releaseResources() override; #ifndef JucePlugin_PreferredChannelConfigurations bool isBusesLayoutSupported (const BusesLayout& layouts) const override; #endif void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; // UI juce::AudioProcessorEditor* createEditor() override; bool hasEditor() const override { return true; } // Misc const juce::String getName() const override { return "FluteSynth"; } bool acceptsMidi() const override { return true; } bool producesMidi() const override { return false; } // Report a non-zero tail so hosts render releases/resonance instead of truncating early. double getTailLengthSeconds() const override { return 12.0; } 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&) override; void setStateInformation (const void*, int) override; // Parameters using APVTS = juce::AudioProcessorValueTreeState; APVTS& getAPVTS() { return apvts; } static APVTS::ParameterLayout createParameterLayout(); // JSON preset loader for headless/CLI bool loadPresetFromJson (const juce::File& file); // Reset to embedded preset (used by GUI reset button) void resetToEmbeddedPreset(); // Schedule preset apply on the audio thread (avoids GUI/audio races) void requestEmbeddedPresetApply(); void requestEmbeddedPresetApply (int index); juce::StringArray getEmbeddedPresetNames() const; int getActiveEmbeddedPresetIndex() const; void selectEmbeddedPreset (int index); bool consumePendingPresetUiSync(); // Test/debug: override WDF parameters without touching the preset JSON void setWdfForTest (const PresetModel::WdfModel& wdfModel); private: bool loadEmbeddedPreset(); bool loadEmbeddedPresetModel(); // --- Helpers for JSON --- static bool hasProp (const juce::DynamicObject&, const juce::Identifier&); static float getFloatProp (const juce::DynamicObject&, const juce::Identifier&, float def); static bool getBoolProp (const juce::DynamicObject&, const juce::Identifier&, bool def); static juce::String getStringProp (const juce::DynamicObject&, const juce::Identifier&, const juce::String& def); PresetModel parsePresetJson (const juce::var& v); PresetModel buildPhysicsPresetModel() const; void applyPresetToParameters (const PresetModel& p); void updatePostFiltersForNote (int midiNote); void updateOutputLpf(); void updateOutputEq(); void updateDamperCoeffs(); void applyMasterTuneToVoices(); void syncExtendedParamsFromAPVTS(); void updateMicProcessors(); void applyMicMix (juce::AudioBuffer& buffer); void prepareBrightnessFilters(); void updateBrightnessFilters (float targetDb); void updatePostFiltersSmoothed(); bool anyVoiceActive() const; void updateSoundboardConvolution (bool force); juce::AudioBuffer buildSoundboardIr (double sampleRate, float t60Sec, float damp) const; // --- Processor state --- APVTS apvts; juce::Synthesiser synth; // VA voices juce::Synthesiser pmSynth; // PM voices Pm2Synth pm2Synth; // PM2 voices juce::String currentEngine { "va" }; bool pm2EconomyMode { true }; // lighter path to reduce CPU // Hybrid mixing float vaMix { 1.0f }; float pmMix { 0.0f }; // Harmonic shaper (tanh) — applied post (shared) bool shaperEnabled { true }; float shaperDrive { 0.15f }; // Breath noise chain (shared) bool breathEnabled { true }; float breathGainLin { 0.0f }; float breathBpFreqStored { 5500.0f }; float breathBpQStored { 1.0f }; juce::dsp::StateVariableTPTFilter breathBp; // Two simple formant BPs with makeup gain (shared) struct BP { juce::dsp::StateVariableTPTFilter f; float gainLin{1.0f}; bool enabled{false}; }; BP formant[2]; // pm2 scaffolding: parsed state only (DSP to be implemented in Phase 2) PresetModel::PmString pmString {}; PresetModel::HammerModel pmHammer {}; PresetModel::FeltModel pmFelt {}; juce::Array pmBoardModes; float pmBoardSend { 0.35f }; float pmBoardMix { 0.30f }; PresetModel::PmFilter pmToneFilter {}; float pmTiltDb { 0.0f }; float pmPredelayMs { 5.0f }; float pm2Mix { 0.0f }; PresetModel::Pedal pedalCfg {}; PresetModel::Damper damperCfg {}; PresetModel::UnaCorda unaCfg {}; PresetModel::Duplex duplexCfg {}; PresetModel::DispersionCurve dispersionCfg {}; // FIXED: Changed from +12dB to 0dB to prevent overwhelming output levels float pm2GainDb { 0.0f }; float pm2GainLin { juce::Decibels::decibelsToGain (pm2GainDb) }; PresetModel::WdfModel wdfCfg {}; PresetModel::Coupling couplingCfg {}; float releaseExtension { 1.0f }; float pitchCompOffsetCents { 0.0f }; float pitchCompSlopeCents { 0.0f }; std::array noteOffsetsCents { { 0.0f } }; std::array presetNoteOffsetsCents { { 0.0f } }; // FIXED: Changed from +0.04 to -0.12 for realistic piano loudness curve float pm2LoudnessSlopeDbPerSemi { -0.03f }; struct EmbeddedPreset { juce::String name; PresetModel model; }; std::vector embeddedPresets; std::atomic embeddedPresetLoaded { false }; std::atomic activeEmbeddedPresetIndex { 0 }; std::atomic pendingEmbeddedPresetIndex { -1 }; std::atomic presetUiSyncPending { false }; // Pedal state bool sustainPedalDown { false }; float sustainValue { 0.0f }; bool softPedalDown { false }; float baseRelease { 0.25f }; float halfReleaseScale { 1.6f }; float damperLift { 1.0f }; float damperSmoothCoeff { 0.05f }; int damperSoftenSamples { 0 }; float damperSoftenA { 0.0f }; juce::Reverb pedalReverb; juce::Reverb::Parameters pedalReverbParams {}; juce::Reverb::Parameters pedalReverbParamsApplied {}; bool pedalReverbParamsValid { false }; juce::Reverb sympReverb; juce::Reverb::Parameters sympParams {}; juce::Reverb::Parameters sympParamsApplied {}; bool sympParamsValid { false }; // Shared send HPFs to keep sends/returns free of rumble using IirHpf = juce::dsp::ProcessorDuplicator, juce::dsp::IIR::Coefficients>; IirHpf pedalSendHpf; IirHpf sympSendHpf; IirHpf soundboardSendHpf; IirHpf soundboardReturnHpf; juce::dsp::IIR::Filter modalSendHpf; float sendHpfCutoff { 85.0f }; int sendHpfNumChannels { 0 }; float soundboardReturnHpfCutoff { 140.0f }; // Per-engine post LPFs (key-tracked) + shared tilt juce::dsp::StateVariableTPTFilter postVaLp1, postVaLp2; juce::dsp::StateVariableTPTFilter postPmLp1, postPmLp2; juce::dsp::StateVariableTPTFilter postPm2Lp1, postPm2Lp2; juce::dsp::IIR::Filter tiltLow, tiltHigh; float postCutoffHz { 4500.0f }; float postQ { 0.8f }; float postKeytrack { 0.5f }; float postTiltDb { 0.0f }; int lastMidiNote { 60 }; float masterVolumeLin { 1.0f }; float outputGainLin { 1.0f }; // Hammer transient (shared, triggered on note-on) bool hammerEnabled { false }; float hammerLevel { 0.25f }; float hammerNoise { 0.40f }; float hammerEnv { 0.0f }; float hammerDecayCoeff{ 0.999f }; float hammerDecaySec { 0.015f }; // Store decay time for recalculation after prepareToPlay bool hammerActive { false }; juce::dsp::StateVariableTPTFilter hammerHP; uint32_t hammerRng { 0xA5A5F00D }; float hammerHpHz { 1800.0f }; // Action / mechanical noises bool keyOffEnabled { false }; bool keyOffVelScale { true }; float keyOffLevel { 0.05f }; float keyOffEnv { 0.0f }; float keyOffDecayCoeff { 0.999f }; float keyOffDecaySec { 0.03f }; float keyOffHpHz { 1200.0f }; juce::dsp::StateVariableTPTFilter keyOffHP; bool pedalThumpEnabled { false }; float pedalThumpLevel { 0.08f }; float pedalThumpEnv { 0.0f }; float pedalThumpDecayCoeff { 0.999f }; float pedalThumpDecaySec { 0.05f }; float pedalThumpLpHz { 600.0f }; juce::dsp::StateVariableTPTFilter pedalThumpLP; bool releaseThumpEnabled{ false }; float releaseThumpLevel { 0.10f }; float releaseThumpEnv { 0.0f }; float releaseThumpDecayCoeff { 0.999f }; float releaseThumpDecaySec { 0.06f }; float releaseThumpLpHz { 450.0f }; float releaseThudMix { 0.5f }; float releaseThudHpHz { 70.0f }; juce::dsp::StateVariableTPTFilter releaseThumpLP; juce::dsp::StateVariableTPTFilter releaseThudHP; // Soundboard (shared) bool soundboardEnabled { false }; float soundboardMix { 0.20f }; juce::Reverb::Parameters soundboardParams {}; juce::Reverb::Parameters soundboardParamsApplied {}; bool soundboardParamsValid { false }; juce::Reverb soundboardReverb; juce::dsp::Convolution soundboardConvolution; juce::dsp::Convolution soundboardConvolutionDs; bool soundboardIrDirty { true }; float soundboardT60Sec { 2.0f }; float soundboardDampParam { 0.35f }; float soundboardIrLastT60 { 0.0f }; float soundboardIrLastDamp { 0.0f }; struct ModalMode { juce::dsp::IIR::Filter bp[2]; float gainLin { 1.0f }; }; std::vector modalModes; int modalChannels { 2 }; bool modalDirty { true }; std::vector predelayBuf; int predelayWrite { 0 }; int predelaySamples { 0 }; int predelayCapacitySamples { 0 }; juce::dsp::ProcessSpec mainSpec{}; // Scratch buffers to avoid per-block allocations juce::AudioBuffer hybridVaBuf, hybridPmBuf, hybridPm2Buf; juce::AudioBuffer micScratch; juce::AudioBuffer breathScratch; juce::AudioBuffer formantScratch; juce::AudioBuffer pedalScratch; juce::AudioBuffer sympScratch; juce::AudioBuffer modalScratch; juce::AudioBuffer soundboardScratch; juce::AudioBuffer soundboardScratchDs; juce::Reverb postReverb; juce::Reverb::Parameters postReverbParamsApplied {}; bool postReverbParamsValid { false }; float postRoomMix { 1.0f }; bool postRoomEnabled { true }; // Final output LPF (post everything) juce::dsp::StateVariableTPTFilter outputLpf; bool outputLpfEnabled { false }; float outputLpfCutoff { 18000.0f }; float outputLpfQ { 0.707f }; bool outputEqEnabled { false }; PresetModel::OutputEq outputEqCfg {}; std::array, 5> outputEqFilters; int outputEqNumChannels { 0 }; juce::SmoothedValue pm2GainLinSmoothed; juce::SmoothedValue outputGainLinSmoothed; juce::SmoothedValue postCutoffHzSmoothed; juce::SmoothedValue postQSmoothed; juce::SmoothedValue postTiltDbSmoothed; juce::SmoothedValue outputLpfCutoffSmoothed; juce::SmoothedValue outputLpfQSmoothed; juce::SmoothedValue brightnessDbSmoothed; juce::SmoothedValue sustainGainLinSmoothed; juce::SmoothedValue sustainReleaseScaleSmoothed; juce::SmoothedValue sustainValueSmoothed; bool pendingStateReset { false }; struct OutputDcBlock { void reset (double sr) { a = (float) (1.0 - 2.0 * juce::MathConstants::pi * 5.0 / sr); x1 = 0.0f; y1 = 0.0f; } float process (float x) { float y = x - x1 + a * y1; x1 = x; y1 = y; return y; } float a { 0.995f }; float x1 { 0.0f }; float y1 { 0.0f }; }; std::array outputDcBlock; int outputDcNumChannels { 0 }; // Final output HPF (post everything) to remove sub-bass/rumble IirHpf outputHpf; float outputHpfCutoff { 120.0f };//the point below which the global high-pass removes frequencies int outputLpfNumChannels { 0 }; int outputHpfNumChannels { 0 }; // Lookahead final limiter (post chain) juce::AudioBuffer limiterDelayBuffer; int limiterDelayBufferSize { 0 }; int limiterWritePos { 0 }; int limiterLookaheadSamples { 0 }; float limiterGain { 1.0f }; float limiterAttackCoeff { 0.0f }; float limiterReleaseCoeff { 0.0f }; float limiterThreshold { 0.98f }; float limiterLookaheadMs { 2.0f }; float limiterAttackMs { 1.0f }; float limiterReleaseMs { 60.0f }; // Final brightness shelf (velocity-driven) bool brightnessEnabled { false }; float brightnessBaseDb { 0.0f }; float brightnessVelSlopeDb { 4.0f }; float brightnessNoteSlopeDb { 0.0f }; float brightnessMaxDb { 6.0f }; float brightnessCutoffHz { 3200.0f }; float brightnessQ { 0.707f }; float brightnessCurrentDb { 0.0f }; int brightnessNumChannels { 0 }; float lastVelocityNorm { 0.8f }; std::vector> brightnessFilters; // Velocity curve shaping float velocityGammaBase { 1.0f }; float velocityGamma { 1.0f }; juce::String velocityCurveName { "linear" }; bool velocityFixed { false }; // Mic mixer (post chain) struct MicState { juce::dsp::DelayLine delay[2]; juce::dsp::IIR::Filter lowShelf[2]; juce::dsp::IIR::Filter highShelf[2]; float delaySamples { 0.0f }; float gainLin { 1.0f }; }; PresetModel::Mics micCfg {}; std::array micState {}; juce::dsp::ProcessSpec micSpec {}; int micMaxDelaySamples { 0 }; bool micReady { false }; double lastSampleRate { 0.0 }; bool prepared { false }; int tiltNumChannels { 0 }; bool tiltReady { false }; // Master tuning applied internally (no schema change) float masterTuneCents { 0.0f }; float masterTuneFactor { 1.0f }; // FIX #1: Shared buses owned by processor (not thread_local) SharedBus couplingBus; SharedBus sympBus; // FIX #2: Smoothed polyphony compensation to prevent gain jumps float polyCompSmoothed { 1.0f }; float polyCompTarget { 1.0f }; static constexpr float polyCompSmoothCoeff { 0.002f }; // ~5ms smoothing at 44.1kHz JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FluteSynthAudioProcessor) };