Files
NeuralSynth/Source/PluginEditor.cpp
2025-11-08 15:18:05 +00:00

399 lines
15 KiB
C++

#include "PluginProcessor.h"
#include "PluginEditor.h"
#include "ScopeComponent.h"
#include "WavetableOsc.h"
#include <array>
//==============================================================================
NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p)
: AudioProcessorEditor (&p),
audioProcessor (p),
mainScopeComponent(audioProcessor.getAudioBufferQueue()),
keyboardComponent(keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard)
{
auto& tree = audioProcessor.parameters;
addAndMakeVisible(mainScopeComponent);
keyboardState.addListener(this);
keyboardComponent.setMidiChannel(1);
keyboardComponent.setScrollButtonsVisible(true);
keyboardComponent.setKeyWidth(36.0f);
keyboardComponent.setAvailableRange(36, 96);
keyboardComponent.setVelocity(1.0f, true);
addAndMakeVisible(keyboardComponent);
presetMenuButton.setButtonText("Preset");
presetMenuButton.onClick = [this] { showPresetMenu(); };
addAndMakeVisible(presetMenuButton);
// --- Panels ---
adsrComponent.emplace(tree, "adsr", "Amp Env");
adsrComponent->enableGraphScope([this](float x) {
auto& tree = this->audioProcessor.parameters;
float A = tree.getParameter("adsr_attack")->getValue();
float D = tree.getParameter("adsr_decay")->getValue();
float S = tree.getParameter("adsr_sustain")->getValue();
float R = tree.getParameter("adsr_release")->getValue();
const float sustainLen = 1.0f;
const float total = A + D + sustainLen + R;
A /= total; D /= total; R /= total;
float m = 0.0f, c = 0.0f;
if (x < A) { m = 1.0f / A; c = 0.0f; }
else if (x < A + D) { m = (S - 1.0f) / D; c = 1.0f - m * A; }
else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; }
else { m = (S / -R); c = -m; }
return m * x + c;
});
addAndMakeVisible(*adsrComponent);
chorusComponent.emplace(tree, "chorus", "Chorus");
chorusComponent->enableSampleScope(audioProcessor.getChorusAudioBufferQueue());
addAndMakeVisible(*chorusComponent);
delayComponent.emplace(tree, "delay", "Delay");
delayComponent->enableSampleScope(audioProcessor.getDelayAudioBufferQueue());
addAndMakeVisible(*delayComponent);
reverbComponent.emplace(tree, "reverb", "Reverb");
reverbComponent->enableSampleScope(audioProcessor.getReverbAudioBufferQueue());
addAndMakeVisible(*reverbComponent);
eqComponent.emplace(tree, "EQ");
addAndMakeVisible(*eqComponent);
flangerComponent.emplace(tree, "flanger", "Flanger");
flangerComponent->enableSampleScope(audioProcessor.getFlangerAudioBufferQueue());
addAndMakeVisible(*flangerComponent);
distortionComponent.emplace(tree, "distortion", "Distortion");
distortionComponent->enableSampleScope(audioProcessor.getDistortionAudioBufferQueue());
addAndMakeVisible(*distortionComponent);
filterComponent.emplace(tree, "filter", "Filter");
filterComponent->enableSampleScope(audioProcessor.getFilterAudioBufferQueue());
addAndMakeVisible(*filterComponent);
filterEnvComponent.emplace(tree, "fenv", "Filter Env");
filterEnvComponent->enableGraphScope([this](float x) {
auto& tree = this->audioProcessor.parameters;
float A = tree.getParameter("fenv_attack")->getValue();
float D = tree.getParameter("fenv_decay")->getValue();
float S = tree.getParameter("fenv_sustain")->getValue();
float R = tree.getParameter("fenv_release")->getValue();
const float sustainLen = 1.0f;
const float total = A + D + sustainLen + R;
A /= total; D /= total; R /= total;
float m = 0.0f, c = 0.0f;
if (x < A) { m = 1.0f / A; c = 0.0f; }
else if (x < A + D) { m = (S - 1.0f) / D; c = 1.0f - m * A; }
else if (x < A + D + (sustainLen / total)) { m = 0.0f; c = S; }
else { m = (S / -R); c = -m; }
return m * x + c;
});
addAndMakeVisible(*filterEnvComponent);
auto configureBankSlider = [](juce::Slider* slider)
{
if (slider == nullptr)
return;
const auto& presets = WT::FactoryLibrary::get();
std::vector<juce::String> names;
names.reserve(presets.size());
for (const auto& preset : presets)
names.push_back(preset.name);
const double maxIndex = names.empty() ? 0.0 : (double) (names.size() - 1);
slider->setRange (0.0, maxIndex, 1.0);
slider->setNumDecimalPlacesToDisplay(0);
slider->textFromValueFunction = [names](double value)
{
if (names.empty())
return juce::String ((int) std::lround (value));
const int idx = juce::jlimit (0, (int) names.size() - 1, (int) std::lround (value));
return names[(size_t) idx];
};
slider->valueFromTextFunction = [names](const juce::String& text)
{
if (! names.empty())
{
for (size_t i = 0; i < names.size(); ++i)
if (text.equalsIgnoreCase (names[i]))
return (double) i;
}
return text.getDoubleValue();
};
};
auto configureShapeSlider = [](juce::Slider* slider)
{
if (slider == nullptr)
return;
static const std::array<juce::String, 4> shapeNames { "Sine", "Triangle", "Ramp Up", "Ramp Down" };
slider->setNumDecimalPlacesToDisplay(0);
slider->textFromValueFunction = [](double value)
{
const int idx = juce::jlimit (0, (int) shapeNames.size() - 1, (int) std::lround (value));
return shapeNames[(size_t) idx];
};
slider->valueFromTextFunction = [](const juce::String& text)
{
for (size_t i = 0; i < shapeNames.size(); ++i)
if (text.equalsIgnoreCase (shapeNames[i]))
return (double) i;
return text.getDoubleValue();
};
};
wtComponent.emplace(tree, "wt", "Layer A");
configureBankSlider (wtComponent->getSlider("bank"));
configureShapeSlider (wtComponent->getSlider("lfoShape"));
addAndMakeVisible(*wtComponent);
wtComponent->setTitleText("Layer A");
layerSelector.addItem("Layer A", 1);
layerSelector.addItem("Layer B", 2);
layerSelector.setSelectedId(1, juce::dontSendNotification);
layerSelector.onChange = [this] { handleLayerSelectionChanged(); };
wtComponent->setTopBarAccessory(&layerSelector, 118);
handleLayerSelectionChanged();
// Master fader + label
addAndMakeVisible(masterLevelSlider);
masterLevelLabel.setText("Master", juce::dontSendNotification);
{
juce::Font f; f.setHeight(12.0f); f.setBold(true);
masterLevelLabel.setFont(f);
}
masterLevelLabel.setJustificationType(juce::Justification::centred);
addAndMakeVisible(masterLevelLabel);
// Attach master parameter
gainAttachment = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(
audioProcessor.parameters, "master", masterLevelSlider.slider);
lastPresetIndex = audioProcessor.getCurrentPresetIndex();
updatePresetButtonLabel();
startTimerHz(10);
setSize(1400, 720);
}
//==============================================================================
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor()
{
stopTimer();
if (customPresetWindow != nullptr)
customPresetWindow.reset();
keyboardState.removeListener(this);
}
//==============================================================================
void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g)
{
g.fillAll(getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
}
//==============================================================================
void NeuralSynthAudioProcessorEditor::resized()
{
auto outer = getLocalBounds().reduced(16);
// --- carve out sidebar for MASTER (right side) --------------------------
const int sidebarWidth = 100; // tweak if you want it wider/narrower
auto gridArea = outer;
auto sidebar = gridArea.removeFromRight(sidebarWidth).reduced(8);
// Master label + fader in the sidebar (stacked)
{
auto top = sidebar.removeFromTop(24);
masterLevelLabel.setBounds(top.withTrimmedLeft(4));
// leave a little top margin before the fader
sidebar.removeFromTop(8);
masterLevelSlider.setBounds(sidebar);
}
// --- Grid: Scope + two rows of five boxes (no gaps) ---------------------
juce::Grid grid;
grid.templateRows = {
juce::Grid::TrackInfo(juce::Grid::Fr(10)), // scope band
juce::Grid::TrackInfo(juce::Grid::Fr(35)), // row 1
juce::Grid::TrackInfo(juce::Grid::Fr(35)), // row 2
juce::Grid::TrackInfo(juce::Grid::Fr(20)) // keyboard
};
grid.templateColumns = {
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20)),
juce::Grid::TrackInfo(juce::Grid::Fr(20))
};
grid.rowGap = juce::Grid::Px(0);
grid.columnGap = juce::Grid::Px(0);
grid.items.clear();
// Row 1 (scope row)
grid.items.add(juce::GridItem(mainScopeComponent).withArea({}, juce::GridItem::Span(4)));
// Put preset button at the top-right cell of the scope row
grid.items.add(juce::GridItem(presetMenuButton)
.withArea(1, 5)
.withJustifySelf(juce::GridItem::JustifySelf::end)
.withAlignSelf(juce::GridItem::AlignSelf::start));
// Row 2 (top row of panels): Amp Env, Chorus, Delay, Reverb, EQ
grid.items.add(juce::GridItem(*adsrComponent ));
grid.items.add(juce::GridItem(*chorusComponent ));
grid.items.add(juce::GridItem(*delayComponent ));
grid.items.add(juce::GridItem(*reverbComponent ));
grid.items.add(juce::GridItem(*eqComponent ));
// Row 3 (bottom row of panels): Flanger, Distortion, Filter, Filter Env, Wavetable
grid.items.add(juce::GridItem(*flangerComponent ));
grid.items.add(juce::GridItem(*distortionComponent));
grid.items.add(juce::GridItem(*filterComponent ));
grid.items.add(juce::GridItem(*filterEnvComponent ));
grid.items.add(juce::GridItem(*wtComponent ));
// Row 4: MIDI keyboard spans entire width
grid.items.add(juce::GridItem(keyboardComponent).withArea({}, juce::GridItem::Span(5)));
grid.performLayout(gridArea);
}
void NeuralSynthAudioProcessorEditor::handleNoteOn(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity)
{
auto message = juce::MidiMessage::noteOn(midiChannel, midiNoteNumber, velocity);
message.setTimeStamp(juce::Time::getMillisecondCounterHiRes() * 0.001);
audioProcessor.midiMessageCollector.addMessageToQueue(message);
}
void NeuralSynthAudioProcessorEditor::handleNoteOff(juce::MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity)
{
auto message = juce::MidiMessage::noteOff(midiChannel, midiNoteNumber, velocity);
message.setTimeStamp(juce::Time::getMillisecondCounterHiRes() * 0.001);
audioProcessor.midiMessageCollector.addMessageToQueue(message);
}
void NeuralSynthAudioProcessorEditor::timerCallback()
{
const int current = audioProcessor.getCurrentPresetIndex();
if (current != lastPresetIndex)
{
lastPresetIndex = current;
updatePresetButtonLabel();
}
}
void NeuralSynthAudioProcessorEditor::showPresetMenu()
{
const auto& presets = audioProcessor.getFactoryPresets();
if (presets.empty())
return;
juce::PopupMenu menu;
juce::StringArray categories;
for (const auto& preset : presets)
if (! categories.contains(preset.category))
categories.add(preset.category);
const int baseId = 1000;
const int customPresetMenuId = baseId - 1;
for (const auto& category : categories)
{
juce::PopupMenu sub;
for (int i = 0; i < (int) presets.size(); ++i)
{
if (presets[(size_t) i].category == category)
{
sub.addItem(baseId + i, presets[(size_t) i].name,
true, audioProcessor.getCurrentPresetIndex() == i);
}
}
menu.addSubMenu(category, sub);
}
menu.addSeparator();
menu.addItem(customPresetMenuId, "Custom ...", true, false);
menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this),
[this, baseId, customPresetMenuId](int result)
{
if (result == customPresetMenuId)
{
showCustomPresetWindow();
return;
}
if (result >= baseId)
{
const int index = result - baseId;
audioProcessor.applyPreset(index);
lastPresetIndex = index;
updatePresetButtonLabel();
}
});
}
void NeuralSynthAudioProcessorEditor::showCustomPresetWindow()
{
constexpr int windowWidth = 420;
constexpr int windowHeight = 320;
if (customPresetWindow == nullptr)
{
customPresetWindow = std::make_unique<CustomPresetWindow>();
customPresetWindow->setSize(windowWidth, windowHeight);
}
customPresetWindow->centreAroundComponent(this, windowWidth, windowHeight);
customPresetWindow->setVisible(true);
customPresetWindow->toFront(true);
}
void NeuralSynthAudioProcessorEditor::updatePresetButtonLabel()
{
const auto& presets = audioProcessor.getFactoryPresets();
const int current = audioProcessor.getCurrentPresetIndex();
juce::String label = "Preset: ";
if (current >= 0 && current < (int) presets.size())
{
const auto& preset = presets[(size_t) current];
label += preset.category + " / " + preset.name;
}
else
{
label += "Custom";
}
presetMenuButton.setButtonText(label);
}
void NeuralSynthAudioProcessorEditor::handleLayerSelectionChanged()
{
const bool useLayerB = (layerSelector.getSelectedId() == 2);
if (! wtComponent || controllingLayerB == useLayerB)
return;
controllingLayerB = useLayerB;
const std::string group = useLayerB ? "wt2" : "wt";
wtComponent->reassignParamGroup(group);
wtComponent->setTitleText(useLayerB ? "Layer B" : "Layer A");
}