1459 lines
61 KiB
C++
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 (¬eCounter);
|
|
}
|
|
}
|
|
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)
|
|
};
|