#pragma once #include #include #include #include "PluginProcessor.h" #include 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& 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(ChromaFlockProcessor::fftOrder); startTimerHz(30); } void paint(juce::Graphics& g) override; void timerCallback() override { repaint(); } private: ChromaFlockProcessor& processor; std::unique_ptr fft; std::vector specSmooth; }; class PatchLCD : public juce::Component { public: std::function 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 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 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 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 (index) / (combo.getNumItems() - 1)); } private: juce::AudioProcessorValueTreeState& state; juce::String paramId; juce::ComboBox& combo; }; std::unique_ptr osc1OctAttach, osc1SemiAttach, osc1FineAttach, osc1LevelAttach; std::unique_ptr osc2OctAttach, osc2SemiAttach, osc2FineAttach, osc2LevelAttach, phaseOffsetAttach; std::unique_ptr osc1WaveAttach, osc2WaveAttach; std::unique_ptr filterTypeAttach; std::unique_ptr octaveTransposeAttach, semitoneTransposeAttach; std::unique_ptr filterCutoffAttach, filterResAttach, filterEnvAmtAttach, keyTrackAttach; std::unique_ptr envAttackAttach, envDecayAttach, envSustainAttach, envReleaseAttach; std::unique_ptr fEnvAttackAttach, fEnvDecayAttach, fEnvSustainAttach, fEnvReleaseAttach; std::unique_ptr panAttach, driveAttach, masterAttach, pbRangeAttach; // FX attachments std::unique_ptr distTypeAttach; std::unique_ptr distAmountAttach, distSymmetryAttach, distToneAttach; std::unique_ptr compThresholdAttach, compRatioAttach, compAttackAttach; std::unique_ptr compReleaseAttach, compMakeupAttach; std::unique_ptr autoPanRateAttach, autoPanDepthAttach, autoPanPhaseAttach; std::unique_ptr limiterThresholdAttach, limiterCeilingAttach, limiterReleaseAttach; // LFO attachments std::unique_ptr lfo1RateAttach, lfo1DepthAttach, lfo1BeatAttach; std::unique_ptr lfo1ShapeAttach, lfo1DestAttach; std::unique_ptr lfo2RateAttach, lfo2DepthAttach, lfo2BeatAttach; std::unique_ptr lfo2ShapeAttach, lfo2DestAttach; // FX2 attachments std::unique_ptr delayTimeAttach, delayFbAttach, delayMixAttach, delayBeatAttach; std::unique_ptr reverbSizeAttach, reverbDampAttach, reverbMixAttach; // Arpeggiator attachments std::unique_ptr 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& rateAttach, std::unique_ptr& 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) };