Files
Piano/PluginProcessor.h
2026-01-10 10:54:49 +00:00

1459 lines
61 KiB
C++

#pragma once
#include <JuceHeader.h>
#include <array>
#include <vector>
#include <atomic>
//=========================== 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<float,3> detuneCents{ { 0.0f, 1.5f, 0.0f } }; // +/-5
std::array<float,3> gain { { 0.6f, 0.4f, 0.0f } }; // normalized
std::array<float,3> 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<EqBand,5> 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<float,3> 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<float,12> temperamentOffsetsCents { { 0.0f } }; // C..B offsets in cents
bool temperamentUseOffsets { false }; // true if offsets provided in preset
std::array<float,128> 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<BoardMode> 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<float>& 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<float,128>& 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<float> 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<float,128> 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<float,128>& offsets) { noteOffsetsCents = offsets; }
// Render the PM block (scaled by ADSR)
void render (juce::AudioBuffer<float>& 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<double>::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<float> 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<float,128> 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<float,float> 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<float> 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<float,128>& 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<float>& 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<double>::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<float> buf;
int write { 0 };
float feedback { 0.5f };
float gain { 0.1f };
float inputGain { 0.15f };
};
struct StringState
{
std::vector<float> 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<AP1, 4> 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<StringState, 3> 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<float, 3> 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<float,128> 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<float> noteHpf;
float noteHpfCutoff { 30.0f };
int noteHpfNumChannels { 0 };
// Sympathetic/coupling filters
juce::dsp::StateVariableTPTFilter<float> couplingBpL, couplingBpR;
juce::dsp::StateVariableTPTFilter<float> 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<float,128>& offsets) { pm.setNoteOffsets (offsets); }
void setMasterTune (float f) { tuneFactor = f; pm.setMasterTuneFactor (f); }
bool canPlaySound (juce::SynthesiserSound* s) override { return dynamic_cast<PmSound*> (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<float>& 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<float,128>& 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<Pm2Sound*> (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<float>& 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<float>& 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<Pm2Voice*> (getVoice (i)))
{
v->setSharedBuses (coupling, symp);
v->setNoteAgeCounter (&noteCounter);
}
}
void setMinNoteDurationSamples (int samples)
{
for (int i = 0; i < getNumVoices(); ++i)
if (auto* v = dynamic_cast<Pm2Voice*> (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<Pm2Voice*> (getVoice (i)))
v->setMinNoteDurationRanges (lowSamples, midLowSamples, midHighSamples, highSamples,
split1, split2, split3);
}
private:
std::atomic<int> pendingDeclickSamples { 0 };
std::atomic<int> 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<float>&, 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<float>& buffer);
void prepareBrightnessFilters();
void updateBrightnessFilters (float targetDb);
void updatePostFiltersSmoothed();
bool anyVoiceActive() const;
void updateSoundboardConvolution (bool force);
juce::AudioBuffer<float> 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<float> breathBp;
// Two simple formant BPs with makeup gain (shared)
struct BP { juce::dsp::StateVariableTPTFilter<float> 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<PresetModel::BoardMode> 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<float,128> noteOffsetsCents { { 0.0f } };
std::array<float,128> 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<EmbeddedPreset> embeddedPresets;
std::atomic<bool> embeddedPresetLoaded { false };
std::atomic<int> activeEmbeddedPresetIndex { 0 };
std::atomic<int> pendingEmbeddedPresetIndex { -1 };
std::atomic<bool> 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::Filter<float>,
juce::dsp::IIR::Coefficients<float>>;
IirHpf pedalSendHpf;
IirHpf sympSendHpf;
IirHpf soundboardSendHpf;
IirHpf soundboardReturnHpf;
juce::dsp::IIR::Filter<float> modalSendHpf;
float sendHpfCutoff { 85.0f };
int sendHpfNumChannels { 0 };
float soundboardReturnHpfCutoff { 140.0f };
// Per-engine post LPFs (key-tracked) + shared tilt
juce::dsp::StateVariableTPTFilter<float> postVaLp1, postVaLp2;
juce::dsp::StateVariableTPTFilter<float> postPmLp1, postPmLp2;
juce::dsp::StateVariableTPTFilter<float> postPm2Lp1, postPm2Lp2;
juce::dsp::IIR::Filter<float> 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<float> 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<float> 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<float> 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<float> releaseThumpLP;
juce::dsp::StateVariableTPTFilter<float> 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<float> bp[2];
float gainLin { 1.0f };
};
std::vector<ModalMode> modalModes;
int modalChannels { 2 };
bool modalDirty { true };
std::vector<float> predelayBuf;
int predelayWrite { 0 };
int predelaySamples { 0 };
int predelayCapacitySamples { 0 };
juce::dsp::ProcessSpec mainSpec{};
// Scratch buffers to avoid per-block allocations
juce::AudioBuffer<float> hybridVaBuf, hybridPmBuf, hybridPm2Buf;
juce::AudioBuffer<float> micScratch;
juce::AudioBuffer<float> breathScratch;
juce::AudioBuffer<float> formantScratch;
juce::AudioBuffer<float> pedalScratch;
juce::AudioBuffer<float> sympScratch;
juce::AudioBuffer<float> modalScratch;
juce::AudioBuffer<float> soundboardScratch;
juce::AudioBuffer<float> 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<float> outputLpf;
bool outputLpfEnabled { false };
float outputLpfCutoff { 18000.0f };
float outputLpfQ { 0.707f };
bool outputEqEnabled { false };
PresetModel::OutputEq outputEqCfg {};
std::array<juce::dsp::IIR::Filter<float>, 5> outputEqFilters;
int outputEqNumChannels { 0 };
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> pm2GainLinSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> outputGainLinSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> postCutoffHzSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> postQSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> postTiltDbSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> outputLpfCutoffSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> outputLpfQSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> brightnessDbSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> sustainGainLinSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> sustainReleaseScaleSmoothed;
juce::SmoothedValue<float, juce::ValueSmoothingTypes::Linear> sustainValueSmoothed;
bool pendingStateReset { false };
struct OutputDcBlock
{
void reset (double sr)
{
a = (float) (1.0 - 2.0 * juce::MathConstants<double>::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, 2> 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<float> 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<juce::dsp::IIR::Filter<float>> 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<float, juce::dsp::DelayLineInterpolationTypes::Linear> delay[2];
juce::dsp::IIR::Filter<float> lowShelf[2];
juce::dsp::IIR::Filter<float> highShelf[2];
float delaySamples { 0.0f };
float gainLin { 1.0f };
};
PresetModel::Mics micCfg {};
std::array<MicState,3> 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)
};