414 lines
15 KiB
C++
414 lines
15 KiB
C++
#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 };
|
||
};
|