Files
NeuralSynth/Source/PluginEditor.h
2025-10-25 17:56:23 +00:00

414 lines
15 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
#include <JuceHeader.h>
#include "PluginProcessor.h"
#include "GraphComponent.h"
#include "ScopeComponent.h"
//============================== ScopeSliderComponent ==========================
// A generic panel: optional scope/graph + rotary sliders + labels.
// Adds a per-panel "On" toggle (bound to "<group>_on").
class ScopeSliderComponent : public juce::Component {
static const int fontSize = 11;
public:
ScopeSliderComponent(juce::AudioProcessorValueTreeState& tree,
const std::string paramGroup,
const juce::String& titleText = {})
: paramGroupId(paramGroup), treeRef(tree)
{
const auto& sliderDetails = PARAM_SETTINGS.at(paramGroup);
for (const auto& [name, sliderDetail] : sliderDetails) {
sliders.push_back(std::make_unique<juce::Slider>());
labels.push_back(std::make_unique<juce::Label>());
attachments.push_back(std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
tree, paramGroup + "_" + name, *sliders.back()));
paramNames.push_back(name);
labels.back()->setText(sliderDetail.label, juce::dontSendNotification);
sliders.back()->setRange(sliderDetail.min, sliderDetail.max);
}
for (auto& slider : sliders)
{
slider->setSliderStyle(juce::Slider::Rotary);
slider->setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20);
addAndMakeVisible(*slider);
}
for (auto& label : labels)
{
juce::Font f; f.setHeight((float)fontSize); f.setBold(true);
label->setFont(f);
label->setColour(juce::Label::textColourId, juce::Colours::lightgreen);
label->setJustificationType(juce::Justification::centred);
addAndMakeVisible(*label);
}
if (titleText.isNotEmpty())
{
titleLabel.setText(titleText, juce::dontSendNotification);
juce::Font tf; tf.setHeight(12.0f); tf.setBold(true);
titleLabel.setFont(tf);
titleLabel.setJustificationType(juce::Justification::centredLeft);
titleLabel.setColour(juce::Label::textColourId, juce::Colours::white);
addAndMakeVisible(titleLabel);
}
if (tree.getParameter(paramGroupId + "_on") != nullptr)
{
hasBypass = true;
bypassButton.setButtonText("On");
bypassButton.setClickingTogglesState(true);
addAndMakeVisible(bypassButton);
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
treeRef, paramGroupId + "_on", bypassButton);
}
}
void setTitleText(const juce::String& text)
{
if (titleLabel.isVisible())
titleLabel.setText(text, juce::dontSendNotification);
}
void setTopBarAccessory(juce::Component* component, int preferredWidth = 120)
{
topBarAccessory = component;
accessoryPreferredWidth = preferredWidth;
if (topBarAccessory != nullptr)
addAndMakeVisible(*topBarAccessory);
}
void reassignParamGroup(const std::string& newGroup)
{
if (newGroup == paramGroupId)
return;
const auto& sliderDetails = PARAM_SETTINGS.at(newGroup);
jassert(sliderDetails.size() == sliders.size());
jassert(sliderDetails.size() == labels.size());
jassert(sliderDetails.size() == paramNames.size());
for (size_t i = 0; i < sliderDetails.size(); ++i)
{
const auto& [name, detail] = sliderDetails[i];
paramNames[i] = name;
sliders[i]->setRange(detail.min, detail.max, detail.interval);
labels[i]->setText(detail.label, juce::dontSendNotification);
}
attachments.clear();
attachments.reserve(sliderDetails.size());
for (size_t i = 0; i < sliderDetails.size(); ++i)
{
const auto paramId = newGroup + "_" + paramNames[i];
attachments.push_back(std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
treeRef, paramId, *sliders[i]));
}
if (hasBypass)
{
bypassAttachment.reset();
if (treeRef.getParameter(newGroup + "_on") != nullptr)
{
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(
treeRef, newGroup + "_on", bypassButton);
bypassButton.setEnabled(true);
}
else
{
bypassButton.setEnabled(false);
}
}
paramGroupId = newGroup;
}
void enableSampleScope(AudioBufferQueue<float>& audioBufferQueue) {
scope.emplace(audioBufferQueue);
useGraphScope = false;
addAndMakeVisible(*scope);
}
void enableGraphScope(const std::function<float(float)>& func) {
graphScope.emplace(0.0f, 1.0f, 100);
graphScope->setFunction(func);
useGraphScope = true;
addAndMakeVisible(*graphScope);
}
juce::Slider* getSlider(const std::string& name) { return findSlider(name); }
private:
juce::Slider* findSlider(const std::string& name)
{
for (size_t i = 0; i < paramNames.size(); ++i)
if (paramNames[i] == name)
return sliders[i].get();
return nullptr;
}
void paint(juce::Graphics& g) override
{
g.fillAll(juce::Colours::darkgrey);
g.setColour(juce::Colours::white);
g.drawRect(getLocalBounds());
}
void resized() override
{
// --- Top bar (manual) ----------------------------------------------
auto area = getLocalBounds().reduced(10);
auto top = area.removeFromTop(22);
if (hasBypass)
{
auto btnW = 46;
auto buttonArea = top.removeFromRight(btnW);
bypassButton.setBounds(buttonArea.reduced(2, 1));
}
if (topBarAccessory != nullptr)
{
const int widthLimit = juce::jmax(60, juce::jmin(accessoryPreferredWidth, top.getWidth()));
auto accessoryArea = top.removeFromRight(widthLimit);
topBarAccessory->setBounds(accessoryArea.reduced(2, 1));
}
if (titleLabel.isVisible())
titleLabel.setBounds(top);
// --- Rest (grid) ----------------------------------------------------
juce::Grid grid;
grid.templateRows = {
juce::Grid::TrackInfo(juce::Grid::Fr(55)), // scope/graph
juce::Grid::TrackInfo(juce::Grid::Fr(30)), // sliders
juce::Grid::TrackInfo(juce::Grid::Fr(15)) // labels
};
const int n = (int)sliders.size();
grid.templateColumns.resize(n);
for (int i = 0; i < n; ++i)
grid.templateColumns.getReference(i) = juce::Grid::TrackInfo(juce::Grid::Fr(1));
grid.items.clear();
// Row 1: scope/graph only add if constructed
if (useGraphScope)
{
if (graphScope)
grid.items.add(juce::GridItem(*graphScope)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
else
grid.items.add(juce::GridItem()
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
}
else
{
if (scope)
grid.items.add(juce::GridItem(*scope)
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
else
grid.items.add(juce::GridItem()
.withArea(juce::GridItem::Span(1), juce::GridItem::Span(n)));
}
// Row 2: sliders
for (int i = 0; i < n; ++i)
grid.items.add(juce::GridItem(*sliders[(size_t)i]));
// Row 3: labels
for (int i = 0; i < n; ++i)
grid.items.add(juce::GridItem(*labels[(size_t)i]));
grid.performLayout(area);
}
bool useGraphScope{ false };
std::optional<ScopeComponent<float>> scope;
std::optional<GraphComponent<float>> graphScope;
std::vector<std::unique_ptr<juce::Slider>> sliders;
std::vector<std::unique_ptr<juce::Label>> labels;
std::vector<std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>> attachments;
std::vector<std::string> paramNames;
juce::Component* topBarAccessory{ nullptr };
int accessoryPreferredWidth{ 120 };
juce::ToggleButton bypassButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment;
bool hasBypass{ false };
juce::Label titleLabel;
std::string paramGroupId;
juce::AudioProcessorValueTreeState& treeRef;
};
//============================== EqualizerComponent ============================
// Adds an On/Off toggle bound to "eq_on".
class EqualizerComponent : public juce::Component {
static const int fontSize = 11;
public:
explicit EqualizerComponent(juce::AudioProcessorValueTreeState& tree,
const juce::String& titleText = {})
{
setupSlider(lowGainSlider);
setupSlider(midGainSlider);
setupSlider(highGainSlider);
setupLabel(lowGainLabel, "L");
setupLabel(midGainLabel, "M");
setupLabel(highGainLabel, "H");
if (titleText.isNotEmpty())
{
titleLabel.setText(titleText, juce::dontSendNotification);
juce::Font tf; tf.setHeight(13.0f); tf.setBold(true);
titleLabel.setFont(tf);
titleLabel.setJustificationType(juce::Justification::centredLeft);
titleLabel.setColour(juce::Label::textColourId, juce::Colours::white);
addAndMakeVisible(titleLabel);
}
// Attachments
lowGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "lowEQ", lowGainSlider);
midGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "midEQ", midGainSlider);
highGainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(tree, "highEQ", highGainSlider);
// EQ bypass toggle
bypassButton.setButtonText("On");
bypassButton.setClickingTogglesState(true);
addAndMakeVisible(bypassButton);
bypassAttachment = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment>(tree, "eq_on", bypassButton);
}
private:
void setupSlider(juce::Slider& slider) {
slider.setRange(-24.0f, 24.0f, 0.1f);
slider.setSliderStyle(juce::Slider::LinearBarVertical);
slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 50, 20);
addAndMakeVisible(slider);
}
void setupLabel(juce::Label& lbl, juce::String txt) {
juce::Font f; f.setHeight((float)fontSize); f.setBold(true);
lbl.setFont(f);
lbl.setColour(juce::Label::textColourId, juce::Colours::lightgreen);
lbl.setJustificationType(juce::Justification::centred);
lbl.setText(txt, juce::dontSendNotification);
addAndMakeVisible(lbl);
}
void paint(juce::Graphics& g) override {
g.fillAll(juce::Colours::darkgrey);
g.setColour(juce::Colours::white);
g.drawRect(getLocalBounds());
}
void resized() override {
auto area = getLocalBounds().reduced(10);
auto top = area.removeFromTop(22);
auto btnW = 46;
bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1));
titleLabel.setBounds(top);
juce::Grid grid;
grid.templateRows = {
juce::Grid::TrackInfo(juce::Grid::Fr(1)),
juce::Grid::TrackInfo(juce::Grid::Fr(1))
};
grid.templateColumns = {
juce::Grid::TrackInfo(juce::Grid::Fr(1)),
juce::Grid::TrackInfo(juce::Grid::Fr(1)),
juce::Grid::TrackInfo(juce::Grid::Fr(1))
};
grid.items = {
lowGainSlider, midGainSlider, highGainSlider,
lowGainLabel, midGainLabel, highGainLabel
};
grid.performLayout(area);
}
juce::Slider lowGainSlider, midGainSlider, highGainSlider;
juce::Label lowGainLabel, midGainLabel, highGainLabel;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> lowGainAttachment, midGainAttachment, highGainAttachment;
juce::ToggleButton bypassButton;
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> bypassAttachment;
juce::Label titleLabel;
};
//============================== MasterVolumeComponent =========================
class MasterVolumeComponent : public juce::Component
{
public:
MasterVolumeComponent()
{
slider.setSliderStyle(juce::Slider::LinearBarVertical);
slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 20, 20);
slider.setRange(-24.0f, 24.0f, 0.1f);
addAndMakeVisible(slider);
}
void resized() override
{
slider.setBounds(getLocalBounds().reduced(30));
}
juce::Slider slider;
};
//============================== Editor =======================================
class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor,
private juce::Timer
{
public:
NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&);
~NeuralSynthAudioProcessorEditor() override;
void paint (juce::Graphics&) override;
void resized() override;
void timerCallback() override;
private:
NeuralSynthAudioProcessor& audioProcessor;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessorEditor)
void updatePresetButtonLabel();
void showPresetMenu();
void handleLayerSelectionChanged();
juce::TextButton presetMenuButton;
int lastPresetIndex { -1 };
std::optional<ScopeSliderComponent> adsrComponent; // Amp Env
std::optional<ScopeSliderComponent> chorusComponent;
std::optional<ScopeSliderComponent> delayComponent;
std::optional<ScopeSliderComponent> reverbComponent;
std::optional<ScopeSliderComponent> flangerComponent;
std::optional<ScopeSliderComponent> distortionComponent;
std::optional<ScopeSliderComponent> filterComponent;
std::optional<ScopeSliderComponent> filterEnvComponent; // Filter Env panel
std::optional<ScopeSliderComponent> wtComponent; // Wavetable panel
MasterVolumeComponent masterLevelSlider;
juce::Label masterLevelLabel;
std::optional<EqualizerComponent> eqComponent;
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> gainAttachment;
ScopeComponent<float> mainScopeComponent;
juce::ComboBox layerSelector;
bool controllingLayerB { false };
};