Upload files to "/"
This commit is contained in:
313
PianoPhysicsData.h
Normal file
313
PianoPhysicsData.h
Normal 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
421
PluginEditor.cpp
Normal 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
389
PluginEditor.h
Normal 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)
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user