commit fcfe75f59d3bc99ffe173320042bd0cb76424b15 Author: ed Date: Sat Jan 10 10:54:35 2026 +0000 Upload files to "/" diff --git a/PianoPhysicsData.h b/PianoPhysicsData.h new file mode 100644 index 0000000..2c37749 --- /dev/null +++ b/PianoPhysicsData.h @@ -0,0 +1,313 @@ +/* + * PianoPhysicsData.h + * + * Implementation-ready parameter values for piano physical modeling + * Extracted from peer-reviewed acoustic research papers + * + * Sources: + * - Chaigne et al., JASA 133(4), 2013 - Soundboard dynamics + * - Hall & Askenfelt, JASA 83(4), 1988 - Hammer-string interaction + * - Russell & Rossing, Acta Acustica 84, 1998 - Hammer nonlinearity + * - Giordano, JASA 103(4), 1998 - Bridge impedance + * - Conklin, JASA 99-100, 1996 - Piano design + * - Fletcher, JASA 36(1), 1964 - String inharmonicity + * + * This data is scientific fact and not subject to copyright. + */ + +#pragma once +#include +#include + +namespace PianoPhysics { + +//============================================================================== +// MATERIAL PROPERTIES +//============================================================================== + +// European Spruce (Picea excelsa) - Soundboard +namespace Spruce { + constexpr float density = 440.0f; // kg/m³ + constexpr float E_longitudinal = 15.9e9f; // Pa (along grain) + constexpr float E_radial = 0.69e9f; // Pa + constexpr float E_tangential = 0.39e9f; // Pa + constexpr float G_LR = 0.62e9f; // Pa (shear modulus) + constexpr float G_LT = 0.77e9f; // Pa + constexpr float G_RT = 0.0036e9f; // Pa + constexpr float nu_LR = 0.44f; // Poisson's ratio + constexpr float nu_LT = 0.38f; + constexpr float nu_RT = 0.47f; +} + +// Beech - Bridge material +namespace Beech { + constexpr float density = 674.0f; // kg/m³ + constexpr float E_longitudinal = 14.0e9f; // Pa + constexpr float E_radial = 2.28e9f; // Pa + constexpr float E_tangential = 1.16e9f; // Pa +} + +// Steel - Strings +namespace Steel { + constexpr float density = 7800.0f; // kg/m³ + constexpr float youngsModulus = 210.0e9f; // Pa +} + +// Copper - Bass string wrapping +namespace Copper { + constexpr float density = 8960.0f; // kg/m³ +} + +//============================================================================== +// SOUNDBOARD PARAMETERS +//============================================================================== + +namespace Soundboard { + // Modal damping (loss factor eta) + constexpr float lossFactor_low = 0.007f; // < 300 Hz (0.7%) + constexpr float lossFactor_mid = 0.012f; // 300-1000 Hz (1.2%) + constexpr float lossFactor_high = 0.020f; // 1000-3000 Hz (2.0%) + + // Typical loss factor for good overall match + constexpr float typicalLossFactor = 0.015f; // 1.5% + + // Waveguide transition frequency (ribs become waveguides above this) + constexpr float waveguideTransitionHz = 1100.0f; + + // Mean rib spacing (Pleyel P131 measurements) + constexpr float meanRibSpacing_m = 0.132f; // 13.2 cm + + // T60 values by frequency band (seconds) + inline float getT60(float freqHz) { + if (freqHz < 120.0f) return 1.8f - (freqHz - 60.0f) * 0.012f; + if (freqHz < 500.0f) return 1.1f - (freqHz - 120.0f) * 0.002f; + if (freqHz < 2000.0f) return 0.7f - (freqHz - 500.0f) * 0.0003f; + return 0.25f - (freqHz - 2000.0f) * 0.00003f; + } +} + +//============================================================================== +// STRING PARAMETERS (Broadwood Grand measurements) +//============================================================================== + +struct StringData { + int midiNote; + float frequency; // Hz (equal temperament) + float length; // m + float coreDiameter; // mm + float wrapDiameter; // mm (0 = unwrapped) + float mass; // g (total vibrating mass) + float tension; // N + float strikingRatio; // hammer position / string length +}; + +// Measured string parameters for C notes +constexpr std::array stringDataC = {{ + // MIDI, freq, length, core, wrap, mass, tension, strike + { 24, 32.7f, 1.945f, 1.00f, 4.55f, 142.0f, 680.0f, 0.117f }, // C1 + { 36, 65.4f, 1.425f, 1.00f, 3.60f, 52.0f, 710.0f, 0.102f }, // C2 + { 48, 130.8f, 0.975f, 1.025f,1.75f, 21.0f, 740.0f, 0.109f }, // C3 + { 60, 261.6f, 0.620f, 1.025f,0.0f, 15.0f, 770.0f, 0.125f }, // C4 + { 72, 523.3f, 0.337f, 1.00f, 0.0f, 8.1f, 810.0f, 0.134f }, // C5 + { 84, 1046.5f, 0.172f, 0.925f,0.0f, 3.9f, 680.0f, 0.151f }, // C6 + { 96, 2093.0f, 0.089f, 0.850f,0.0f, 1.7f, 560.0f, 0.180f }, // C7 +}}; + +//============================================================================== +// INHARMONICITY +//============================================================================== + +namespace Inharmonicity { + // Measured B values at specific points + // f_n = n * f1 * sqrt(1 + B * n²) + constexpr float B_A0 = 0.00020f; // MIDI 21 + constexpr float B_C2 = 0.00030f; // MIDI 36 (wound) + constexpr float B_C3 = 0.00070f; // MIDI 48 + constexpr float B_C4 = 0.0015f; // MIDI 60 + constexpr float B_C5 = 0.0050f; // MIDI 72 + constexpr float B_C6 = 0.018f; // MIDI 84 + constexpr float B_C7 = 0.080f; // MIDI 96 + constexpr float B_C8 = 0.40f; // MIDI 108 + + // Interpolation function for any MIDI note + inline float getB(int midiNote) { + // Bass wound strings (MIDI 21-52): slow exponential growth + if (midiNote <= 52) { + float norm = static_cast(midiNote - 21) / 31.0f; + return 0.00018f * std::pow(10.0f, norm * 0.7f); + } + // Plain steel strings (MIDI 53-108): faster growth + float norm = static_cast(midiNote - 53) / 55.0f; + return 0.0008f * std::pow(10.0f, norm * 2.7f); + } + + // Compute partial frequency with inharmonicity + inline float partialFreq(float f1, int n, float B) { + return static_cast(n) * f1 * std::sqrt(1.0f + B * n * n); + } +} + +//============================================================================== +// HAMMER PARAMETERS +//============================================================================== + +namespace Hammer { + // Nonlinear felt model: F = K * C^p + + // Measured values for specific notes (Chaigne & Askenfelt) + struct HammerData { + int midiNote; + float K; // Stiffness (SI units, varies with p) + float p; // Nonlinearity exponent + float mass_kg; // Mass in kg + }; + + constexpr std::array measuredHammers = {{ + { 36, 4.0e8f, 2.3f, 0.0110f }, // C2 + { 60, 4.5e9f, 2.8f, 0.0092f }, // C4 + { 84, 1.0e10f, 3.0f, 0.0052f }, // C6 + }}; + + // Exponent p varies smoothly from bass to treble + // Source: Russell & Rossing (1998) + inline float getExponent(int midiNote) { + // Bass: p ≈ 2.0-2.3 + // Middle: p ≈ 2.6-3.0 + // Treble: p ≈ 3.0-4.0 + float norm = static_cast(midiNote - 21) / 87.0f; + return 2.0f + norm * 2.0f; // 2.0 to 4.0 + } + + // Stiffness coefficient K (log-interpolated) + inline float getStiffness(int midiNote) { + float norm = static_cast(midiNote - 21) / 87.0f; + return 4.0e8f * std::pow(10.0f, norm * 1.4f); + } + + // Hammer mass (kg) + inline float getMass(int midiNote) { + float norm = static_cast(midiNote - 21) / 87.0f; + return 0.012f - norm * 0.008f; // 12g to 4g + } + + // Hysteresis (reduces force on return stroke) + // Source: Stulov + inline float getHysteresis(int midiNote) { + float norm = static_cast(midiNote - 21) / 87.0f; + return 0.18f - norm * 0.10f; // 0.18 to 0.08 + } + + // Striking position as fraction of string length + inline float getStrikingRatio(int midiNote) { + // Varies from ~1/8.5 in bass to ~1/5.5 in treble + float norm = static_cast(midiNote - 21) / 87.0f; + return 0.10f + norm * 0.08f; // 0.10 to 0.18 + } + + // Contact duration scaling with velocity + // τ ∝ F_max^((1-p)/(2p)) + // At higher velocity, contact is shorter + inline float getContactDurationScale(float velocity01, float p) { + // velocity01: 0 = pp, 1 = ff + // Returns multiplier relative to pp duration + float Fscale = 0.2f + 0.8f * velocity01; // Force scales with velocity + float exponent = (1.0f - p) / (2.0f * p); + return std::pow(Fscale, exponent); + } +} + +//============================================================================== +// BRIDGE IMPEDANCE +//============================================================================== + +namespace Bridge { + // Mean impedance values (kg/s) + // Source: Wogram, Conklin, Giordano + + constexpr float Z_low = 1000.0f; // < 1 kHz + constexpr float Z_mid = 700.0f; // 1-2 kHz + constexpr float Z_high = 400.0f; // 2-5 kHz + constexpr float Z_veryHigh = 250.0f; // > 5 kHz + + // Typical fluctuation around mean: ±12 dB + + // High-pass cutoff frequencies + constexpr float bassBridgeHP_Hz = 70.0f; + constexpr float trebleBridgeHP_Hz = 140.0f; + + // Bridge/soundboard coupling strength + constexpr float bassCoupling = 0.06f; + constexpr float trebleCoupling = 0.04f; + + // Bass/treble split point + constexpr int splitMidi = 52; // E3 + + inline float getImpedance(float freqHz) { + if (freqHz < 1000.0f) return Z_low; + if (freqHz < 2000.0f) return Z_low - (freqHz - 1000.0f) * 0.0003f; + if (freqHz < 5000.0f) return Z_mid - (freqHz - 2000.0f) * 0.0001f; + return Z_veryHigh; + } +} + +//============================================================================== +// DAMPER PARAMETERS +//============================================================================== + +namespace Damper { + // Damping time constant when damper engages (approximate) + constexpr float engageTime_s = 0.015f; // 15 ms + + // Final damping coefficient (very high = quick stop) + constexpr float maxDamping = 0.90f; + + // Bass strings have more mass, damp slower + inline float getDampingRate(int midiNote) { + float norm = static_cast(midiNote - 21) / 87.0f; + return 0.85f + norm * 0.10f; // 0.85 to 0.95 + } +} + +//============================================================================== +// OVERALL STRING DECAY (T60) +//============================================================================== + +namespace StringDecay { + // Frequency-dependent T60 for string modes + // Higher partials decay faster + + inline float getT60(float freqHz, int midiNote) { + // Base T60 varies with register + float norm = static_cast(midiNote - 21) / 87.0f; + float baseT60 = 20.0f - norm * 16.5f; // 20s bass to 3.5s treble + + // High frequencies decay faster (loss filter effect) + float freqFactor = 1.0f / (1.0f + freqHz / 5000.0f); + + return baseT60 * freqFactor; + } +} + +//============================================================================== +// VELOCITY MAPPING +//============================================================================== + +namespace Velocity { + // MIDI velocity to hammer velocity (m/s) + // Typical range: 0.5 m/s (pp) to 5.5 m/s (fff) + + inline float midiToHammerVelocity(int midiVel) { + float norm = static_cast(midiVel) / 127.0f; + // Logarithmic-ish mapping for musical response + return 0.5f + 5.0f * norm * norm; + } + + // Brightness tilt with velocity + // Louder = brighter (more high frequency content) + inline float getBrightnessTilt(float hammerVel) { + // 0 to 1 range + return std::min(1.0f, hammerVel / 5.0f); + } +} + +} // namespace PianoPhysics diff --git a/PluginEditor.cpp b/PluginEditor.cpp new file mode 100644 index 0000000..9710baa --- /dev/null +++ b/PluginEditor.cpp @@ -0,0 +1,421 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin editor. + + ============================================================================== +*/ + +#include "PluginProcessor.h" +#include "PluginEditor.h" + +//============================================================================== +FluteSynthAudioProcessorEditor::FluteSynthAudioProcessorEditor (FluteSynthAudioProcessor& p) + : AudioProcessorEditor (&p), audioProcessor (p) +{ + auto& apvts = audioProcessor.getAPVTS(); + + // Preset selector setup + auto presetNames = audioProcessor.getEmbeddedPresetNames(); + if (presetNames.isEmpty()) + presetNames.add ("Preset 1"); + presetSelector.addItemList (presetNames, 1); + presetSelector.onChange = [this]() + { + audioProcessor.selectEmbeddedPreset (presetSelector.getSelectedItemIndex()); + }; + presetSelector.setSelectedItemIndex (juce::jlimit (0, presetSelector.getNumItems() - 1, + audioProcessor.getActiveEmbeddedPresetIndex()), + juce::dontSendNotification); + presetSelector.setLookAndFeel (&comboLook); + + // Preset label styling + presetLabel.setJustificationType (juce::Justification::centred); + presetLabel.setColour (juce::Label::textColourId, juce::Colour::fromRGB (160, 155, 145)); + presetLabel.setFont (juce::Font (13.0f, juce::Font::plain)); + + // Temperament selector setup + temperamentSelector.addItemList ( + { "Preset", "12-TET", "Werckmeister", "Kirnberger", "Meantone", "Pythagorean" }, 1); + temperamentSelector.setLookAndFeel (&comboLook); + + // Temperament label styling + temperamentLabel.setJustificationType (juce::Justification::centred); + temperamentLabel.setColour (juce::Label::textColourId, juce::Colour::fromRGB (160, 155, 145)); + temperamentLabel.setFont (juce::Font (13.0f, juce::Font::plain)); + + // Velocity Curve selector setup + velocityCurveSelector.addItemList ( + { "Light", "Normal", "Heavy", "Fixed" }, 1); + velocityCurveSelector.setLookAndFeel (&comboLook); + + // Velocity Curve label styling + velocityCurveLabel.setJustificationType (juce::Justification::centred); + velocityCurveLabel.setColour (juce::Label::textColourId, juce::Colour::fromRGB (160, 155, 145)); + velocityCurveLabel.setFont (juce::Font (13.0f, juce::Font::plain)); + + // Toggle buttons + soundboardEnable.setLookAndFeel (&toggleLook); + postRoomEnable.setLookAndFeel (&toggleLook); + + // Slider setup helper + auto prepSlider = [] (juce::Slider& s, juce::String suffix) + { + s.setSliderStyle (juce::Slider::LinearVertical); + s.setTextBoxStyle (juce::Slider::TextBoxBelow, false, 120, 18); + s.setTextValueSuffix (suffix); + }; + + prepSlider (soundboardMix, ""); + prepSlider (soundboardT60, " s"); + prepSlider (soundboardDamp, ""); + prepSlider (postRoomMix, ""); + prepSlider (stringSustain, " s"); + prepSlider (hammerHardness, ""); + prepSlider (pm2GainDb, " dB"); + prepSlider (outputLpfCutoff, " Hz"); + prepSlider (masterVolume, ""); + + soundboardMix.setLookAndFeel (&sliderLook); + soundboardT60.setLookAndFeel (&sliderLook); + soundboardDamp.setLookAndFeel (&sliderLook); + postRoomMix.setLookAndFeel (&sliderLook); + stringSustain.setLookAndFeel (&sliderLook); + hammerHardness.setLookAndFeel (&sliderLook); + pm2GainDb.setLookAndFeel (&sliderLook); + outputLpfCutoff.setLookAndFeel (&sliderLook); + masterVolume.setLookAndFeel (&sliderLook); + + // Parameter attachments + soundboardEnableAttachment = std::make_unique (apvts, ParamIDs::soundboardEnable, soundboardEnable); + postRoomEnableAttachment = std::make_unique (apvts, ParamIDs::postRoomEnable, postRoomEnable); + soundboardMixAttachment = std::make_unique (apvts, ParamIDs::soundboardMix, soundboardMix); + soundboardT60Attachment = std::make_unique (apvts, ParamIDs::soundboardT60, soundboardT60); + soundboardDampAttachment = std::make_unique (apvts, ParamIDs::soundboardDamp, soundboardDamp); + postRoomMixAttachment = std::make_unique (apvts, ParamIDs::postRoomMix, postRoomMix); + stringSustainAttachment = std::make_unique (apvts, ParamIDs::decay, stringSustain); + hammerHardnessAttachment = std::make_unique (apvts, ParamIDs::feltStiffness, hammerHardness); + pm2GainAttachment = std::make_unique (apvts, ParamIDs::pm2GainDb, pm2GainDb); + outputLpfAttachment = std::make_unique (apvts, ParamIDs::outputLpfCutoff, outputLpfCutoff); + masterVolumeAttachment = std::make_unique (apvts, ParamIDs::masterVolume, masterVolume); + temperamentAttachment = std::make_unique (apvts, ParamIDs::temperament, temperamentSelector); + velocityCurveAttachment = std::make_unique (apvts, ParamIDs::velocityCurve, velocityCurveSelector); + + syncControlsFromParams(); + startTimerHz (30); + + // Add components + addAndMakeVisible (soundboardEnable); + addAndMakeVisible (postRoomEnable); + resetButton.setVisible (false); + addAndMakeVisible (presetSelector); + addAndMakeVisible (presetLabel); + addAndMakeVisible (temperamentSelector); + addAndMakeVisible (temperamentLabel); + addAndMakeVisible (velocityCurveSelector); + addAndMakeVisible (velocityCurveLabel); + addAndMakeVisible (soundboardMix); + addAndMakeVisible (soundboardDamp); + addAndMakeVisible (postRoomMix); + addAndMakeVisible (stringSustain); + addAndMakeVisible (hammerHardness); + addAndMakeVisible (pm2GainDb); + addAndMakeVisible (outputLpfCutoff); + addAndMakeVisible (masterVolume); + + // Slider label styling + auto styleLabel = [] (juce::Label& label) + { + label.setJustificationType (juce::Justification::centred); + label.setColour (juce::Label::textColourId, juce::Colour::fromRGB (160, 155, 145)); + label.setFont (juce::Font (13.0f, juce::Font::plain)); + }; + + styleLabel (soundboardMixLabel); + styleLabel (soundboardDampLabel); + styleLabel (postRoomMixLabel); + styleLabel (stringSustainLabel); + styleLabel (hammerHardnessLabel); + styleLabel (pm2GainLabel); + styleLabel (outputLpfLabel); + styleLabel (masterVolumeLabel); + + addAndMakeVisible (soundboardMixLabel); + addAndMakeVisible (soundboardDampLabel); + addAndMakeVisible (postRoomMixLabel); + addAndMakeVisible (stringSustainLabel); + addAndMakeVisible (hammerHardnessLabel); + addAndMakeVisible (pm2GainLabel); + addAndMakeVisible (outputLpfLabel); + addAndMakeVisible (masterVolumeLabel); + + resetButton.onClick = [this]() + { + audioProcessor.resetToEmbeddedPreset(); + }; + + setSize (1040, 520); +} + +FluteSynthAudioProcessorEditor::~FluteSynthAudioProcessorEditor() +{ + soundboardMix.setLookAndFeel (nullptr); + soundboardT60.setLookAndFeel (nullptr); + soundboardDamp.setLookAndFeel (nullptr); + postRoomMix.setLookAndFeel (nullptr); + stringSustain.setLookAndFeel (nullptr); + hammerHardness.setLookAndFeel (nullptr); + pm2GainDb.setLookAndFeel (nullptr); + outputLpfCutoff.setLookAndFeel (nullptr); + masterVolume.setLookAndFeel (nullptr); + presetSelector.setLookAndFeel (nullptr); + temperamentSelector.setLookAndFeel (nullptr); + soundboardEnable.setLookAndFeel (nullptr); + postRoomEnable.setLookAndFeel (nullptr); + velocityCurveSelector.setLookAndFeel (nullptr); +} + +void FluteSynthAudioProcessorEditor::syncControlsFromParams() +{ + auto& apvts = audioProcessor.getAPVTS(); + if (auto* v = apvts.getRawParameterValue (ParamIDs::soundboardEnable)) + soundboardEnable.setToggleState (v->load() >= 0.5f, juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::postRoomEnable)) + postRoomEnable.setToggleState (v->load() >= 0.5f, juce::dontSendNotification); + + if (auto* v = apvts.getRawParameterValue (ParamIDs::soundboardMix)) + soundboardMix.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::soundboardDamp)) + soundboardDamp.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::postRoomMix)) + postRoomMix.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::decay)) + stringSustain.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::feltStiffness)) + hammerHardness.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::pm2GainDb)) + pm2GainDb.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::outputLpfCutoff)) + outputLpfCutoff.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::masterVolume)) + masterVolume.setValue (v->load(), juce::dontSendNotification); + if (auto* v = apvts.getRawParameterValue (ParamIDs::velocityCurve)) + velocityCurveSelector.setSelectedItemIndex ((int) std::round (v->load()), + juce::dontSendNotification); + if (presetSelector.getNumItems() > 0) + { + const int idx = juce::jlimit (0, presetSelector.getNumItems() - 1, + audioProcessor.getActiveEmbeddedPresetIndex()); + presetSelector.setSelectedItemIndex (idx, juce::dontSendNotification); + } +} + +void FluteSynthAudioProcessorEditor::timerCallback() +{ + if (audioProcessor.consumePendingPresetUiSync()) + syncControlsFromParams(); +} + + +//============================================================================== +void FluteSynthAudioProcessorEditor::paint (juce::Graphics& g) +{ + auto bounds = getLocalBounds().toFloat(); + + // Deep piano-black lacquer background - darker for more contrast + auto deepBlack = juce::Colour::fromRGB (2, 2, 3); + g.fillAll (deepBlack); + + // Outer frame with subtle gradient + auto frameArea = bounds.reduced (4.0f); + juce::ColourGradient frameGradient (juce::Colour::fromRGB (14, 14, 18), + frameArea.getX(), frameArea.getY(), + juce::Colour::fromRGB (4, 4, 6), + frameArea.getRight(), frameArea.getBottom(), + false); + g.setGradientFill (frameGradient); + g.fillRoundedRectangle (frameArea, 12.0f); + + // Inner content area - very dark + auto contentArea = frameArea.reduced (6.0f); + juce::ColourGradient contentGradient (juce::Colour::fromRGB (6, 6, 8), + contentArea.getCentreX(), contentArea.getY(), + juce::Colour::fromRGB (2, 2, 3), + contentArea.getCentreX(), contentArea.getBottom(), + false); + g.setGradientFill (contentGradient); + g.fillRoundedRectangle (contentArea, 8.0f); + + // High-contrast gloss highlight - brighter and more defined + juce::Path glossPath; + glossPath.addRoundedRectangle (contentArea, 8.0f); + + juce::ColourGradient gloss; + auto glossCenterX = contentArea.getX() + contentArea.getWidth() * 0.30f; + gloss.point1 = { glossCenterX - contentArea.getWidth() * 0.22f, contentArea.getY() }; + gloss.point2 = { glossCenterX + contentArea.getWidth() * 0.32f, contentArea.getBottom() }; + gloss.addColour (0.0, juce::Colours::transparentWhite); + gloss.addColour (0.26, juce::Colours::transparentWhite); + gloss.addColour (0.40, juce::Colours::white.withAlpha (0.22f)); // Brighter + gloss.addColour (0.48, juce::Colours::white.withAlpha (0.28f)); // Peak brightness - increased + gloss.addColour (0.56, juce::Colours::white.withAlpha (0.18f)); + gloss.addColour (0.70, juce::Colours::transparentWhite); + gloss.addColour (1.0, juce::Colours::transparentWhite); + g.setGradientFill (gloss); + g.fillPath (glossPath); + + // Additional vertical sheen for lacquer depth - brighter + juce::ColourGradient sheen (juce::Colours::white.withAlpha (0.16f), + contentArea.getCentreX() - contentArea.getWidth() * 0.12f, + contentArea.getY(), + juce::Colours::transparentWhite, + contentArea.getCentreX() - contentArea.getWidth() * 0.12f, + contentArea.getY() + contentArea.getHeight() * 0.45f, + false); + g.setGradientFill (sheen); + g.fillRoundedRectangle (contentArea.withHeight (contentArea.getHeight() * 0.35f), 8.0f); + + // Subtle top edge highlight + g.setColour (juce::Colour::fromRGB (50, 50, 55)); + g.drawHorizontalLine ((int) (contentArea.getY() + 1), contentArea.getX() + 20, contentArea.getRight() - 20); + + // Decorative brass-like accent line below title + auto accentY = 58.0f; + juce::ColourGradient accentGradient (juce::Colours::transparentBlack, + contentArea.getX(), accentY, + juce::Colours::transparentBlack, + contentArea.getRight(), accentY, + false); + accentGradient.addColour (0.15, juce::Colour::fromRGB (80, 72, 55).withAlpha (0.0f)); + accentGradient.addColour (0.35, juce::Colour::fromRGB (120, 108, 80).withAlpha (0.5f)); + accentGradient.addColour (0.5, juce::Colour::fromRGB (140, 128, 95).withAlpha (0.6f)); + accentGradient.addColour (0.65, juce::Colour::fromRGB (120, 108, 80).withAlpha (0.5f)); + accentGradient.addColour (0.85, juce::Colour::fromRGB (80, 72, 55).withAlpha (0.0f)); + g.setGradientFill (accentGradient); + g.fillRect (contentArea.getX(), accentY, contentArea.getWidth(), 1.5f); + + // Section divider line above sliders + auto dividerY = 175.0f; + g.setColour (juce::Colour::fromRGB (30, 30, 35)); + g.drawHorizontalLine ((int) dividerY, contentArea.getX() + 30, contentArea.getRight() - 30); + g.setColour (juce::Colour::fromRGB (12, 12, 15)); + g.drawHorizontalLine ((int) dividerY + 1, contentArea.getX() + 30, contentArea.getRight() - 30); + + // Title with elegant styling + g.setColour (juce::Colour::fromRGB (220, 215, 200)); + juce::Font titleFont (juce::Font::getDefaultSerifFontName(), 30.0f, juce::Font::italic); + g.setFont (titleFont); + + // Title shadow + g.setColour (juce::Colours::black.withAlpha (0.6f)); + g.drawText ("Samedi Dimanche MusPiano", + juce::Rectangle (1, 16, getWidth(), 40), + juce::Justification::centredTop); + + // Title main + g.setColour (juce::Colour::fromRGB (230, 225, 210)); + g.drawText ("Samedi Dimanche MusPiano", + juce::Rectangle (0, 15, getWidth(), 40), + juce::Justification::centredTop); + + // Stronger vignette corners for depth + auto vignetteSize = 180.0f; + juce::ColourGradient vignetteTL (juce::Colours::black.withAlpha (0.4f), + contentArea.getX(), contentArea.getY(), + juce::Colours::transparentBlack, + contentArea.getX() + vignetteSize, contentArea.getY() + vignetteSize, + true); + g.setGradientFill (vignetteTL); + g.fillRect (contentArea.getX(), contentArea.getY(), vignetteSize, vignetteSize); + + juce::ColourGradient vignetteBR (juce::Colours::black.withAlpha (0.45f), + contentArea.getRight(), contentArea.getBottom(), + juce::Colours::transparentBlack, + contentArea.getRight() - vignetteSize, contentArea.getBottom() - vignetteSize, + true); + g.setGradientFill (vignetteBR); + g.fillRect (contentArea.getRight() - vignetteSize, contentArea.getBottom() - vignetteSize, vignetteSize, vignetteSize); + + // Inner frame border + g.setColour (juce::Colour::fromRGB (25, 25, 30)); + g.drawRoundedRectangle (contentArea.reduced (0.5f), 8.0f, 1.0f); + + // Outer frame border with subtle brass tint + g.setColour (juce::Colour::fromRGB (40, 38, 34)); + g.drawRoundedRectangle (frameArea.reduced (0.5f), 12.0f, 1.5f); +} + +void FluteSynthAudioProcessorEditor::resized() +{ + auto area = getLocalBounds().reduced (20); + + // Title area - increased space + area.removeFromTop (55); + + // Control row - dropdowns and toggles (moved up with more spacing) + auto controlRow = area.removeFromTop (90); + controlRow.removeFromTop (8); // Add top padding + + // Preset dropdown + auto presetArea = controlRow.removeFromLeft (200); + presetLabel.setBounds (presetArea.removeFromTop (22)); + presetSelector.setBounds (presetArea.removeFromTop (36).reduced (4, 2)); + + controlRow.removeFromLeft (20); // Spacing between dropdowns + + // Temperament dropdown + auto temperArea = controlRow.removeFromLeft (200); + temperamentLabel.setBounds (temperArea.removeFromTop (22)); + temperamentSelector.setBounds (temperArea.removeFromTop (36).reduced (4, 2)); + + controlRow.removeFromLeft (20); // Spacing between dropdowns + + // Velocity curve dropdown + auto velocityArea = controlRow.removeFromLeft (200); + velocityCurveLabel.setBounds (velocityArea.removeFromTop (22)); + velocityCurveSelector.setBounds (velocityArea.removeFromTop (36).reduced (4, 2)); + + controlRow.removeFromLeft (20); // Spacing before toggles + + // Toggle buttons - vertically stacked for cleaner look + auto toggleArea = controlRow.removeFromLeft (150); + toggleArea.removeFromTop (4); + const int toggleHeight = 24; + const int toggleGap = 4; + soundboardEnable.setBounds (toggleArea.removeFromTop (toggleHeight)); + toggleArea.removeFromTop (toggleGap); + postRoomEnable.setBounds (toggleArea.removeFromTop (toggleHeight)); + + resetButton.setBounds (0, 0, 0, 0); + + // Spacing before sliders + area.removeFromTop (35); + + // Slider section with labels above - smaller sliders with more spacing + auto sliderSection = area.removeFromTop (210); + const int numSliders = 8; + const int sliderWidth = sliderSection.getWidth() / numSliders; + const int labelHeight = 34; + const int sliderPadding = 16; + + auto layoutSlider = [&] (juce::Slider& slider, juce::Label& label, int index) + { + juce::ignoreUnused (index); + auto sliderArea = sliderSection.removeFromLeft (sliderWidth); + sliderArea = sliderArea.reduced (sliderPadding, 0); + + auto labelArea = sliderArea.removeFromTop (labelHeight); + label.setBounds (labelArea); + + slider.setBounds (sliderArea.reduced (10, 8)); // More padding around sliders + }; + + layoutSlider (soundboardMix, soundboardMixLabel, 0); + layoutSlider (soundboardDamp, soundboardDampLabel, 1); + layoutSlider (postRoomMix, postRoomMixLabel, 2); + layoutSlider (stringSustain, stringSustainLabel, 3); + layoutSlider (hammerHardness, hammerHardnessLabel, 4); + layoutSlider (pm2GainDb, pm2GainLabel, 5); + layoutSlider (outputLpfCutoff, outputLpfLabel, 6); + layoutSlider (masterVolume, masterVolumeLabel, 7); +} diff --git a/PluginEditor.h b/PluginEditor.h new file mode 100644 index 0000000..fe90c62 --- /dev/null +++ b/PluginEditor.h @@ -0,0 +1,389 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin editor. + + ============================================================================== +*/ + +#pragma once + +#include +#include "PluginProcessor.h" + +//============================================================================== +/** +*/ +class FluteSynthAudioProcessorEditor : public juce::AudioProcessorEditor, + private juce::Timer +{ +public: + FluteSynthAudioProcessorEditor (FluteSynthAudioProcessor&); + ~FluteSynthAudioProcessorEditor() override; + + //============================================================================== + void paint (juce::Graphics&) override; + void resized() override; + +private: + FluteSynthAudioProcessor& audioProcessor; + + juce::ComboBox presetSelector; + juce::Label presetLabel { {}, "Preset" }; + juce::ComboBox temperamentSelector; + juce::Label temperamentLabel { {}, "Temperament" }; + juce::ComboBox velocityCurveSelector; + juce::Label velocityCurveLabel { {}, "Velocity Curve" }; + juce::ToggleButton soundboardEnable { "Soundboard" }; + juce::ToggleButton postRoomEnable { "Post Reverb" }; + juce::TextButton resetButton { "Reset to preset" }; + juce::Slider soundboardMix; + juce::Slider soundboardT60; + juce::Slider soundboardDamp; + juce::Slider postRoomMix; + juce::Slider stringSustain; + juce::Slider hammerHardness; + juce::Slider pm2GainDb; + juce::Slider outputLpfCutoff; + juce::Slider masterVolume; + juce::Label soundboardMixLabel { {}, "Soundboard\nMix" }; + juce::Label soundboardT60Label { {}, "Reverb Decay" }; + juce::Label soundboardDampLabel{ {}, "Damp" }; + juce::Label postRoomMixLabel { {}, "Post Reverb" }; + juce::Label stringSustainLabel { {}, "String\nSustain" }; + juce::Label hammerHardnessLabel { {}, "Hammer\nHardness" }; + juce::Label pm2GainLabel { {}, "PM2 Gain" }; + juce::Label outputLpfLabel { {}, "Brightness" }; + juce::Label masterVolumeLabel { {}, "Master" }; + + using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment; + using ButtonAttachment = juce::AudioProcessorValueTreeState::ButtonAttachment; + using ComboBoxAttachment = juce::AudioProcessorValueTreeState::ComboBoxAttachment; + + std::unique_ptr soundboardEnableAttachment; + std::unique_ptr postRoomEnableAttachment; + std::unique_ptr soundboardMixAttachment; + std::unique_ptr soundboardT60Attachment; + std::unique_ptr soundboardDampAttachment; + std::unique_ptr postRoomMixAttachment; + std::unique_ptr stringSustainAttachment; + std::unique_ptr hammerHardnessAttachment; + std::unique_ptr pm2GainAttachment; + std::unique_ptr outputLpfAttachment; + std::unique_ptr masterVolumeAttachment; + std::unique_ptr temperamentAttachment; + std::unique_ptr velocityCurveAttachment; + + //============================================================================== + // Custom LookAndFeel for elegant dropdown menus + struct ElegantComboBoxLookAndFeel : juce::LookAndFeel_V4 + { + ElegantComboBoxLookAndFeel() + { + // Set popup menu colors + setColour (juce::PopupMenu::backgroundColourId, juce::Colour::fromRGB (18, 20, 24)); + setColour (juce::PopupMenu::textColourId, juce::Colour::fromRGB (220, 220, 225)); + setColour (juce::PopupMenu::highlightedBackgroundColourId, juce::Colour::fromRGB (60, 70, 85)); + setColour (juce::PopupMenu::highlightedTextColourId, juce::Colours::white); + } + + void drawComboBox (juce::Graphics& g, int width, int height, bool isButtonDown, + int buttonX, int buttonY, int buttonW, int buttonH, + juce::ComboBox& box) override + { + juce::ignoreUnused (buttonX, buttonY, buttonW, buttonH); + + auto bounds = juce::Rectangle (0, 0, (float) width, (float) height); + auto cornerSize = 3.0f; // Reduced from 6.0f + + // Outer glow/shadow effect + g.setColour (juce::Colours::black.withAlpha (0.4f)); + g.fillRoundedRectangle (bounds.translated (0, 2).reduced (1), cornerSize); + + // Main background gradient - dark metallic + juce::ColourGradient bgGradient (juce::Colour::fromRGB (38, 42, 48), + bounds.getCentreX(), bounds.getY(), + juce::Colour::fromRGB (22, 24, 28), + bounds.getCentreX(), bounds.getBottom(), + false); + g.setGradientFill (bgGradient); + g.fillRoundedRectangle (bounds.reduced (1), cornerSize); + + // Subtle inner highlight at top + juce::ColourGradient innerHighlight (juce::Colours::white.withAlpha (0.08f), + bounds.getCentreX(), bounds.getY() + 1, + juce::Colours::transparentWhite, + bounds.getCentreX(), bounds.getY() + height * 0.4f, + false); + g.setGradientFill (innerHighlight); + g.fillRoundedRectangle (bounds.reduced (2), cornerSize - 1); + + // Border - subtle golden/brass tint for piano aesthetic + auto borderColour = isButtonDown ? juce::Colour::fromRGB (140, 130, 100) + : juce::Colour::fromRGB (80, 75, 65); + if (box.hasKeyboardFocus (false)) + borderColour = juce::Colour::fromRGB (160, 150, 120); + + g.setColour (borderColour); + g.drawRoundedRectangle (bounds.reduced (1.5f), cornerSize, 1.0f); + + // Arrow indicator + auto arrowZone = bounds.removeFromRight ((float) height).reduced (8); + juce::Path arrow; + auto arrowH = arrowZone.getHeight() * 0.3f; + auto arrowW = arrowZone.getWidth() * 0.5f; + arrow.addTriangle (arrowZone.getCentreX() - arrowW * 0.5f, arrowZone.getCentreY() - arrowH * 0.3f, + arrowZone.getCentreX() + arrowW * 0.5f, arrowZone.getCentreY() - arrowH * 0.3f, + arrowZone.getCentreX(), arrowZone.getCentreY() + arrowH * 0.5f); + + g.setColour (juce::Colour::fromRGB (180, 175, 165)); + g.fillPath (arrow); + } + + void drawPopupMenuBackground (juce::Graphics& g, int width, int height) override + { + auto bounds = juce::Rectangle (0, 0, (float) width, (float) height); + + // Shadow + g.setColour (juce::Colours::black.withAlpha (0.5f)); + g.fillRoundedRectangle (bounds.translated (2, 3), 3.0f); // Reduced from 6.0f + + // Main background + juce::ColourGradient bgGradient (juce::Colour::fromRGB (28, 32, 38), + bounds.getCentreX(), bounds.getY(), + juce::Colour::fromRGB (18, 20, 24), + bounds.getCentreX(), bounds.getBottom(), + false); + g.setGradientFill (bgGradient); + g.fillRoundedRectangle (bounds, 3.0f); // Reduced from 6.0f + + // Border + g.setColour (juce::Colour::fromRGB (60, 58, 52)); + g.drawRoundedRectangle (bounds.reduced (0.5f), 3.0f, 1.0f); // Reduced from 6.0f + } + + void drawPopupMenuItem (juce::Graphics& g, const juce::Rectangle& area, + bool isSeparator, bool isActive, bool isHighlighted, + bool isTicked, bool hasSubMenu, + const juce::String& text, const juce::String& shortcutKeyText, + const juce::Drawable* icon, const juce::Colour* textColour) override + { + juce::ignoreUnused (shortcutKeyText, icon, textColour, hasSubMenu); + + if (isSeparator) + { + auto r = area.reduced (5, 0).toFloat(); + r.removeFromTop ((float) r.getHeight() * 0.5f - 0.5f); + g.setColour (juce::Colour::fromRGB (50, 52, 58)); + g.fillRect (r.removeFromTop (1.0f)); + return; + } + + auto r = area.reduced (2); + + if (isHighlighted && isActive) + { + // Highlighted background with subtle gradient + juce::ColourGradient hlGradient (juce::Colour::fromRGB (55, 65, 80), + (float) r.getCentreX(), (float) r.getY(), + juce::Colour::fromRGB (45, 52, 65), + (float) r.getCentreX(), (float) r.getBottom(), + false); + g.setGradientFill (hlGradient); + g.fillRoundedRectangle (r.toFloat(), 4.0f); + } + + auto textColourToUse = isHighlighted ? juce::Colours::white + : juce::Colour::fromRGB (200, 200, 205); + if (! isActive) + textColourToUse = textColourToUse.withAlpha (0.4f); + + g.setColour (textColourToUse); + g.setFont (getPopupMenuFont().withHeight (15.0f)); + + auto textArea = r.reduced (12, 0); + + if (isTicked) + { + // Checkmark for selected item + auto tickArea = textArea.removeFromLeft (20); + juce::Path tick; + tick.startNewSubPath ((float) tickArea.getX() + 4, (float) tickArea.getCentreY()); + tick.lineTo ((float) tickArea.getX() + 8, (float) tickArea.getCentreY() + 4); + tick.lineTo ((float) tickArea.getX() + 14, (float) tickArea.getCentreY() - 4); + g.setColour (juce::Colour::fromRGB (180, 170, 140)); + g.strokePath (tick, juce::PathStrokeType (2.0f)); + } + + g.drawFittedText (text, textArea, juce::Justification::centredLeft, 1); + } + + juce::Font getComboBoxFont (juce::ComboBox&) override + { + return juce::Font (15.0f); + } + + void positionComboBoxText (juce::ComboBox& box, juce::Label& label) override + { + label.setBounds (12, 0, box.getWidth() - 30, box.getHeight()); + label.setFont (getComboBoxFont (box)); + label.setColour (juce::Label::textColourId, juce::Colour::fromRGB (220, 218, 212)); + } + }; + + //============================================================================== + // Custom LookAndFeel for toggle buttons + struct ElegantToggleLookAndFeel : juce::LookAndFeel_V4 + { + void drawToggleButton (juce::Graphics& g, juce::ToggleButton& button, + bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override + { + juce::ignoreUnused (shouldDrawButtonAsDown); + + auto bounds = button.getLocalBounds().toFloat(); + auto tickBounds = bounds.removeFromLeft (bounds.getHeight()).reduced (4); + + // Checkbox background + juce::ColourGradient bgGradient (juce::Colour::fromRGB (35, 38, 44), + tickBounds.getCentreX(), tickBounds.getY(), + juce::Colour::fromRGB (20, 22, 26), + tickBounds.getCentreX(), tickBounds.getBottom(), + false); + g.setGradientFill (bgGradient); + g.fillRoundedRectangle (tickBounds, 4.0f); + + // Border + auto borderColour = shouldDrawButtonAsHighlighted ? juce::Colour::fromRGB (100, 95, 80) + : juce::Colour::fromRGB (70, 68, 62); + g.setColour (borderColour); + g.drawRoundedRectangle (tickBounds.reduced (0.5f), 4.0f, 1.0f); + + if (button.getToggleState()) + { + // Filled state with warm glow + auto innerBounds = tickBounds.reduced (3); + juce::ColourGradient fillGradient (juce::Colour::fromRGB (160, 150, 120), + innerBounds.getCentreX(), innerBounds.getY(), + juce::Colour::fromRGB (120, 110, 85), + innerBounds.getCentreX(), innerBounds.getBottom(), + false); + g.setGradientFill (fillGradient); + g.fillRoundedRectangle (innerBounds, 2.0f); + + // Checkmark + juce::Path tick; + auto cx = tickBounds.getCentreX(); + auto cy = tickBounds.getCentreY(); + tick.startNewSubPath (cx - 4, cy); + tick.lineTo (cx - 1, cy + 3); + tick.lineTo (cx + 5, cy - 4); + g.setColour (juce::Colour::fromRGB (30, 28, 24)); + g.strokePath (tick, juce::PathStrokeType (2.2f, juce::PathStrokeType::curved)); + } + + // Label text + g.setColour (juce::Colour::fromRGB (210, 208, 200)); + g.setFont (14.5f); + g.drawText (button.getButtonText(), bounds.reduced (4, 0), + juce::Justification::centredLeft, true); + } + }; + + //============================================================================== + // Custom LookAndFeel for sliders - compact design + struct DarkRectSliderLookAndFeel : juce::LookAndFeel_V4 + { + void drawLinearSlider (juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float min, float max, const juce::Slider::SliderStyle style, + juce::Slider& slider) override + { + juce::ignoreUnused (min, max, style, slider); + + // Track - narrower + auto trackWidth = width * 0.11f; // Further reduced + auto track = juce::Rectangle ((float) x + (width - trackWidth) * 0.5f, + (float) y + 4.0f, + trackWidth, + (float) height - 8.0f); + + // Track shadow + g.setColour (juce::Colours::black.withAlpha (0.3f)); + g.fillRoundedRectangle (track.translated (1, 1), 2.0f); + + // Track gradient + juce::ColourGradient trackGradient (juce::Colour::fromRGB (15, 15, 18), + track.getCentreX(), track.getY(), + juce::Colour::fromRGB (8, 8, 10), + track.getCentreX(), track.getBottom(), + false); + g.setGradientFill (trackGradient); + g.fillRoundedRectangle (track, 2.0f); + + // Track border + g.setColour (juce::Colour::fromRGB (45, 45, 50)); + g.drawRoundedRectangle (track, 2.0f, 1.0f); + + // Fill from bottom to thumb position + auto fillTrack = track; + fillTrack.setTop (sliderPos); + juce::ColourGradient fillGradient (juce::Colour::fromRGB (80, 75, 60).withAlpha (0.4f), + fillTrack.getCentreX(), fillTrack.getY(), + juce::Colour::fromRGB (60, 55, 45).withAlpha (0.2f), + fillTrack.getCentreX(), fillTrack.getBottom(), + false); + g.setGradientFill (fillGradient); + g.fillRoundedRectangle (fillTrack, 2.0f); + + // Thumb - smaller + float thumbW = (float) width * 0.50f; + float thumbH = 9.0f; // Reduced from 11.0f + float thumbX = (float) x + (width - thumbW) * 0.5f; + float thumbY = sliderPos - thumbH * 0.5f; + juce::Rectangle thumb (thumbX, thumbY, thumbW, thumbH); + + // Thumb shadow + g.setColour (juce::Colours::black.withAlpha (0.4f)); + g.fillRoundedRectangle (thumb.translated (0, 1.5f), 2.0f); + + // Thumb gradient + juce::ColourGradient thumbGradient (juce::Colour::fromRGB (130, 125, 115), + thumb.getCentreX(), thumb.getY(), + juce::Colour::fromRGB (70, 68, 62), + thumb.getCentreX(), thumb.getBottom(), + false); + g.setGradientFill (thumbGradient); + g.fillRoundedRectangle (thumb, 2.0f); + + // Thumb highlight + auto highlightRect = thumb.reduced (1.5f).withHeight (thumb.getHeight() * 0.35f); + g.setColour (juce::Colours::white.withAlpha (0.12f)); + g.fillRoundedRectangle (highlightRect, 1.0f); + + // Thumb border + g.setColour (juce::Colour::fromRGB (25, 25, 28)); + g.drawRoundedRectangle (thumb, 2.0f, 1.0f); + + // Center notch on thumb + g.setColour (juce::Colour::fromRGB (40, 40, 44)); + g.fillRect (thumb.getCentreX() - 5.0f, thumb.getCentreY() - 0.5f, 10.0f, 1.0f); + } + + juce::Label* createSliderTextBox (juce::Slider& slider) override + { + auto* label = LookAndFeel_V4::createSliderTextBox (slider); + label->setColour (juce::Label::textColourId, juce::Colour::fromRGB (180, 178, 170)); + label->setColour (juce::Label::backgroundColourId, juce::Colour::fromRGB (20, 22, 26)); + label->setColour (juce::Label::outlineColourId, juce::Colour::fromRGB (50, 50, 55)); + return label; + } + }; + + ElegantComboBoxLookAndFeel comboLook; + ElegantToggleLookAndFeel toggleLook; + DarkRectSliderLookAndFeel sliderLook; + + void syncControlsFromParams(); + void timerCallback() override; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FluteSynthAudioProcessorEditor) +};