chromaflock/Source/PluginEditor.h
Armin 45d6082657 Reorder filter dropdown: list LP 48dB below LP 24dB
Keep the parameter choice indices stable (so saved projects, .cfl
files and presets stay correct) and only reorder the displayed menu
via explicit item IDs plus a new IndexedComboBoxAttachment, since the
stock JUCE ComboBoxAttachment hard-maps choice index to menu position.
2026-08-13 14:55:33 +02:00

327 lines
13 KiB
C++

#pragma once
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_gui_basics/juce_gui_basics.h>
#include <juce_dsp/juce_dsp.h>
#include "PluginProcessor.h"
#include <vector>
class KnobLookAndFeel : public juce::LookAndFeel_V4 {
public:
KnobLookAndFeel();
void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height,
float sliderPos, float rotaryStartAngle,
float rotaryEndAngle, juce::Slider& slider) override;
private:
juce::Image knobOverlay;
};
class KnobSlider : public juce::Slider {
public:
using juce::Slider::Slider;
juce::String getTextFromValue(double v) override {
return juce::String::formatted("%.1f", v);
}
};
class ComboBoxLookAndFeel : public juce::LookAndFeel_V4 {
public:
void drawComboBox(juce::Graphics& g, int width, int height,
bool isButtonDown, int buttonX, int buttonY,
int buttonW, int buttonH, juce::ComboBox& box) override;
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;
};
class VuMeter : public juce::Component, public juce::Timer {
public:
explicit VuMeter(ChromaFlockProcessor& p) : processor(p) { startTimerHz(30); }
void paint(juce::Graphics& g) override;
void timerCallback() override { repaint(); }
private:
ChromaFlockProcessor& processor;
};
class WaveformDisplay : public juce::Component, public juce::Timer {
public:
explicit WaveformDisplay(ChromaFlockProcessor& p) : processor(p) {
fft = std::make_unique<juce::dsp::FFT>(ChromaFlockProcessor::fftOrder);
startTimerHz(30);
}
void paint(juce::Graphics& g) override;
void timerCallback() override { repaint(); }
private:
ChromaFlockProcessor& processor;
std::unique_ptr<juce::dsp::FFT> fft;
std::vector<float> specSmooth;
};
class PatchLCD : public juce::Component {
public:
std::function<void()> onClick;
void setText(const juce::String& t) { text = t; repaint(); }
const juce::String& getText() const { return text; }
void paint(juce::Graphics& g) override;
void mouseEnter(const juce::MouseEvent&) override { setMouseCursor(juce::MouseCursor::PointingHandCursor); }
void mouseDown(const juce::MouseEvent&) override { if (onClick) onClick(); }
private:
juce::String text = "INIT";
};
class MidiLed : public juce::Component, public juce::Timer {
public:
explicit MidiLed(ChromaFlockProcessor& p) : processor(p) { startTimerHz(30); }
void paint(juce::Graphics& g) override;
void timerCallback() override { repaint(); }
private:
ChromaFlockProcessor& processor;
};
class LedToggle : public juce::Component, public juce::Timer {
public:
LedToggle(juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId,
const juce::String& onText, const juce::String& offText);
void paint(juce::Graphics& g) override;
void mouseDown(const juce::MouseEvent&) override;
void mouseEnter(const juce::MouseEvent&) override;
void timerCallback() override;
bool isLit() const { return lit; }
std::function<void()> onChange;
private:
juce::AudioProcessorValueTreeState& apvts;
juce::String paramId;
juce::String onText, offText;
bool lit = false;
};
class PianoRollComponent : public juce::Component, public juce::Timer {
public:
explicit PianoRollComponent(ChromaFlockProcessor& p) : processor(p) { startTimerHz(60); }
void paint(juce::Graphics& g) override;
void timerCallback() override;
void mouseDown(const juce::MouseEvent& e) override;
void mouseUp(const juce::MouseEvent& e) override;
void mouseDrag(const juce::MouseEvent& e) override;
private:
ChromaFlockProcessor& processor;
static constexpr int firstMidiNote = 48; // C3
static constexpr int numKeys = 49; // C3..C7
int whiteKeyWidth = 54;
int whiteKeyHeight = 130;
int blackKeyWidth = 35;
int blackKeyHeight = 80;
int pitchBendWidth = 40;
int midiNoteForKey(int index) const;
bool isBlackKey(int midiNote) const;
int keyAtPosition(juce::Point<int> pos) const;
void triggerNote(int note, bool on);
int lastTriggeredNote = -1;
int currentPitchBend = 64; // 0..127, 64 = center
bool pitchBendDragging = false;
int pitchBendStartY = 0;
bool pitchBendGliding = false;
float pitchBendGlideStart = 0.0f;
juce::int64 pitchBendGlideStartMs = 0;
static constexpr int pitchBendGlideMs = 50;
};
class MainContentComponent : public juce::Component {
public:
explicit MainContentComponent(ChromaFlockProcessor& p);
void paint(juce::Graphics& g) override;
void resized() override;
std::function<void(float)> onScaleChanged;
private:
ChromaFlockProcessor& processorRef;
KnobLookAndFeel knobLaf;
ComboBoxLookAndFeel comboLaf;
juce::Image bgImage;
bool bgImageLoaded = false;
juce::Image logoImage;
bool logoLoaded = false;
VuMeter vuMeter;
WaveformDisplay waveformDisplay;
PianoRollComponent pianoRoll;
juce::Label osc1Label, osc2Label, filterLabel, envLabel, fEnvLabel, globalLabel;
juce::Label fxLabel, lfoLabel, fx2Label;
juce::Label scaleLabel;
juce::Label midiLabel;
juce::Label octaveTransposeLabel, semitoneTransposeLabel;
juce::ComboBox octaveTransposeBox, semitoneTransposeBox;
juce::ComboBox osc1WaveBox;
KnobSlider osc1OctKnob, osc1SemiKnob, osc1FineKnob, osc1LevelKnob;
juce::ComboBox osc2WaveBox;
KnobSlider osc2OctKnob, osc2SemiKnob, osc2FineKnob, osc2LevelKnob, phaseOffsetKnob;
juce::ComboBox filterTypeBox;
KnobSlider filterCutoffKnob, filterResKnob, filterEnvAmtKnob, keyTrackKnob;
KnobSlider envAttackKnob, envDecayKnob, envSustainKnob, envReleaseKnob;
KnobSlider fEnvAttackKnob, fEnvDecayKnob, fEnvSustainKnob, fEnvReleaseKnob;
KnobSlider panKnob, driveKnob, masterKnob, pbRangeKnob;
// FX controls
juce::ComboBox distTypeBox;
KnobSlider distAmountKnob, distSymmetryKnob, distToneKnob;
KnobSlider compThresholdKnob, compRatioKnob, compAttackKnob, compReleaseKnob, compMakeupKnob;
KnobSlider autoPanRateKnob, autoPanDepthKnob, autoPanPhaseKnob;
KnobSlider limiterThresholdKnob, limiterCeilingKnob, limiterReleaseKnob;
// LFO controls
KnobSlider lfo1RateKnob, lfo1DepthKnob;
juce::ComboBox lfo1ShapeBox, lfo1DestBox;
KnobSlider lfo2RateKnob, lfo2DepthKnob;
juce::ComboBox lfo2ShapeBox, lfo2DestBox;
LedToggle lfo1SyncLed, lfo2SyncLed, delaySyncLed;
// FX2 controls
KnobSlider delayTimeKnob, delayFbKnob, delayMixKnob;
KnobSlider reverbSizeKnob, reverbDampKnob, reverbMixKnob;
// Arpeggiator controls
juce::Label arpEnabledLabel, arpPatternLabel, arpOctavesLabel, arpDirectionLabel, arpRateLabel;
juce::ComboBox arpPatternBox, arpOctavesBox, arpRateBox;
LedToggle arpEnabledLed, arpDirectionLed, delayPingPongLed, reverbOnLed,
delayInvertLed, delayFlattenLed;
// Preset menu
juce::TextButton presetButton{"PRESET"};
PatchLCD patchLCD;
MidiLed midiLed;
juce::TextButton prevPresetButton{"<"}, nextPresetButton{">"};
juce::TextButton randomizeButton{"RANDOM"};
int currentPresetIndex = 0;
juce::ComboBox uiScaleBox;
juce::Label statusBar;
using SliderAttachment = juce::AudioProcessorValueTreeState::SliderAttachment;
using ComboBoxAttachment = juce::AudioProcessorValueTreeState::ComboBoxAttachment;
// Maps a 0-based parameter choice index to a ComboBox itemId (index + 1)
// so the dropdown can be displayed in an order that differs from the
// parameter's index order (e.g. LP 48dB listed under LP 24dB). This keeps
// stored parameter indices meaningful while only reordering the menu.
class IndexedComboBoxAttachment : public juce::AudioProcessorValueTreeState::Listener,
private juce::ComboBox::Listener {
public:
IndexedComboBoxAttachment (juce::AudioProcessorValueTreeState& stateToUse,
const juce::String& paramID,
juce::ComboBox& comboToUse)
: state (stateToUse), paramId (paramID), combo (comboToUse) {
combo.addListener (this);
state.addParameterListener (paramId, this);
auto* p = state.getParameter (paramId);
combo.setSelectedId (1 + juce::roundToInt (p->getValue() * (combo.getNumItems() - 1)),
juce::dontSendNotification);
}
~IndexedComboBoxAttachment() override {
combo.removeListener (this);
state.removeParameterListener (paramId, this);
}
void parameterChanged (const juce::String& parameterID, float newValue) override {
if (parameterID == paramId)
combo.setSelectedId (1 + juce::roundToInt (newValue * (combo.getNumItems() - 1)),
juce::dontSendNotification);
}
void comboBoxChanged (juce::ComboBox* cb) override {
if (cb != &combo)
return;
auto* p = state.getParameter (paramId);
int index = combo.getSelectedId() - 1;
p->setValueNotifyingHost (static_cast<float> (index) / (combo.getNumItems() - 1));
}
private:
juce::AudioProcessorValueTreeState& state;
juce::String paramId;
juce::ComboBox& combo;
};
std::unique_ptr<SliderAttachment> osc1OctAttach, osc1SemiAttach, osc1FineAttach, osc1LevelAttach;
std::unique_ptr<SliderAttachment> osc2OctAttach, osc2SemiAttach, osc2FineAttach, osc2LevelAttach, phaseOffsetAttach;
std::unique_ptr<ComboBoxAttachment> osc1WaveAttach, osc2WaveAttach;
std::unique_ptr<IndexedComboBoxAttachment> filterTypeAttach;
std::unique_ptr<ComboBoxAttachment> octaveTransposeAttach, semitoneTransposeAttach;
std::unique_ptr<SliderAttachment> filterCutoffAttach, filterResAttach, filterEnvAmtAttach, keyTrackAttach;
std::unique_ptr<SliderAttachment> envAttackAttach, envDecayAttach, envSustainAttach, envReleaseAttach;
std::unique_ptr<SliderAttachment> fEnvAttackAttach, fEnvDecayAttach, fEnvSustainAttach, fEnvReleaseAttach;
std::unique_ptr<SliderAttachment> panAttach, driveAttach, masterAttach, pbRangeAttach;
// FX attachments
std::unique_ptr<ComboBoxAttachment> distTypeAttach;
std::unique_ptr<SliderAttachment> distAmountAttach, distSymmetryAttach, distToneAttach;
std::unique_ptr<SliderAttachment> compThresholdAttach, compRatioAttach, compAttackAttach;
std::unique_ptr<SliderAttachment> compReleaseAttach, compMakeupAttach;
std::unique_ptr<SliderAttachment> autoPanRateAttach, autoPanDepthAttach, autoPanPhaseAttach;
std::unique_ptr<SliderAttachment> limiterThresholdAttach, limiterCeilingAttach, limiterReleaseAttach;
// LFO attachments
std::unique_ptr<SliderAttachment> lfo1RateAttach, lfo1DepthAttach, lfo1BeatAttach;
std::unique_ptr<ComboBoxAttachment> lfo1ShapeAttach, lfo1DestAttach;
std::unique_ptr<SliderAttachment> lfo2RateAttach, lfo2DepthAttach, lfo2BeatAttach;
std::unique_ptr<ComboBoxAttachment> lfo2ShapeAttach, lfo2DestAttach;
// FX2 attachments
std::unique_ptr<SliderAttachment> delayTimeAttach, delayFbAttach, delayMixAttach, delayBeatAttach;
std::unique_ptr<SliderAttachment> reverbSizeAttach, reverbDampAttach, reverbMixAttach;
// Arpeggiator attachments
std::unique_ptr<ComboBoxAttachment> arpPatternAttach, arpOctavesAttach, arpRateAttach;
void setupLabel(juce::Label& label, const juce::String& text);
void setupKnob(juce::Slider& knob, const juce::String& name);
void setupCombo(juce::ComboBox& combo);
void setupSyncKnob(juce::Slider& knob,
std::unique_ptr<SliderAttachment>& rateAttach,
std::unique_ptr<SliderAttachment>& beatAttach,
const juce::String& rateId,
const juce::String& beatId,
bool syncOn);
void showPresetMenu();
void showFileMenu();
void loadPresetFile();
void savePresetFile();
juce::String currentPresetName() const;
void applyCurrentPreset();
void randomizeParameters();
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainContentComponent)
};
class ChromaFlockEditor : public juce::AudioProcessorEditor {
public:
ChromaFlockEditor(ChromaFlockProcessor& p);
~ChromaFlockEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
void setUIScale(float newScale);
private:
MainContentComponent content;
float currentScale = 1.0f;
static constexpr int baseWidth = 1660;
static constexpr int baseHeight = 1058;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChromaFlockEditor)
};