399 lines
15 KiB
C++
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");
|
|
}
|