Upload files to "/"

This commit is contained in:
ed
2026-01-10 10:54:35 +00:00
commit fcfe75f59d
3 changed files with 1123 additions and 0 deletions

313
PianoPhysicsData.h Normal file
View File

@@ -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 <cmath>
#include <array>
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<StringData, 7> 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<float>(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<float>(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<float>(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<HammerData, 3> 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<float>(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<float>(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<float>(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<float>(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<float>(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<float>(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<float>(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<float>(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

421
PluginEditor.cpp Normal file
View File

@@ -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<ButtonAttachment> (apvts, ParamIDs::soundboardEnable, soundboardEnable);
postRoomEnableAttachment = std::make_unique<ButtonAttachment> (apvts, ParamIDs::postRoomEnable, postRoomEnable);
soundboardMixAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::soundboardMix, soundboardMix);
soundboardT60Attachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::soundboardT60, soundboardT60);
soundboardDampAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::soundboardDamp, soundboardDamp);
postRoomMixAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::postRoomMix, postRoomMix);
stringSustainAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::decay, stringSustain);
hammerHardnessAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::feltStiffness, hammerHardness);
pm2GainAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::pm2GainDb, pm2GainDb);
outputLpfAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::outputLpfCutoff, outputLpfCutoff);
masterVolumeAttachment = std::make_unique<SliderAttachment> (apvts, ParamIDs::masterVolume, masterVolume);
temperamentAttachment = std::make_unique<ComboBoxAttachment> (apvts, ParamIDs::temperament, temperamentSelector);
velocityCurveAttachment = std::make_unique<ComboBoxAttachment> (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<int> (1, 16, getWidth(), 40),
juce::Justification::centredTop);
// Title main
g.setColour (juce::Colour::fromRGB (230, 225, 210));
g.drawText ("Samedi Dimanche MusPiano",
juce::Rectangle<int> (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);
}

389
PluginEditor.h Normal file
View File

@@ -0,0 +1,389 @@
/*
==============================================================================
This file contains the basic framework code for a JUCE plugin editor.
==============================================================================
*/
#pragma once
#include <JuceHeader.h>
#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<ButtonAttachment> soundboardEnableAttachment;
std::unique_ptr<ButtonAttachment> postRoomEnableAttachment;
std::unique_ptr<SliderAttachment> soundboardMixAttachment;
std::unique_ptr<SliderAttachment> soundboardT60Attachment;
std::unique_ptr<SliderAttachment> soundboardDampAttachment;
std::unique_ptr<SliderAttachment> postRoomMixAttachment;
std::unique_ptr<SliderAttachment> stringSustainAttachment;
std::unique_ptr<SliderAttachment> hammerHardnessAttachment;
std::unique_ptr<SliderAttachment> pm2GainAttachment;
std::unique_ptr<SliderAttachment> outputLpfAttachment;
std::unique_ptr<SliderAttachment> masterVolumeAttachment;
std::unique_ptr<ComboBoxAttachment> temperamentAttachment;
std::unique_ptr<ComboBoxAttachment> 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<float> (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<float> (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<int>& 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> ((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<float> 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)
};