#include "PluginProcessor.h" #include "PluginEditor.h" #include "ScopeComponent.h" #include "WavetableOsc.h" #include //============================================================================== NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p) : AudioProcessorEditor (&p), audioProcessor (p), mainScopeComponent(audioProcessor.getAudioBufferQueue()) { auto& tree = audioProcessor.parameters; addAndMakeVisible(mainScopeComponent); 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 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 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( audioProcessor.parameters, "master", masterLevelSlider.slider); lastPresetIndex = audioProcessor.getCurrentPresetIndex(); updatePresetButtonLabel(); startTimerHz(10); setSize(1400, 720); } //============================================================================== NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() { stopTimer(); } //============================================================================== 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 = 160; // 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(22)), // scope band juce::Grid::TrackInfo(juce::Grid::Fr(39)), // row 1 juce::Grid::TrackInfo(juce::Grid::Fr(78)) // row 2 (wider to absorb layer panel) }; grid.templateColumns = { juce::Grid::TrackInfo(juce::Grid::Fr(1)), juce::Grid::TrackInfo(juce::Grid::Fr(1)), juce::Grid::TrackInfo(juce::Grid::Fr(1)), juce::Grid::TrackInfo(juce::Grid::Fr(1)), juce::Grid::TrackInfo(juce::Grid::Fr(1)) }; 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(1, 1, 1, 5)); // 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 ).withArea(2, 1)); grid.items.add(juce::GridItem(*chorusComponent ).withArea(2, 2)); grid.items.add(juce::GridItem(*delayComponent ).withArea(2, 3)); grid.items.add(juce::GridItem(*reverbComponent ).withArea(2, 4)); grid.items.add(juce::GridItem(*eqComponent ).withArea(2, 5)); // Row 3 (bottom row of panels): Flanger, Distortion, Filter, Filter Env, Wavetable grid.items.add(juce::GridItem(*flangerComponent ).withArea(3, 1)); grid.items.add(juce::GridItem(*distortionComponent).withArea(3, 2)); grid.items.add(juce::GridItem(*filterComponent ).withArea(3, 3)); grid.items.add(juce::GridItem(*filterEnvComponent ).withArea(3, 4)); grid.items.add(juce::GridItem(*wtComponent ).withArea(3, 5)); grid.performLayout(gridArea); } 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; 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.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this), [this, baseId](int result) { if (result >= baseId) { const int index = result - baseId; audioProcessor.applyPreset(index); lastPresetIndex = index; updatePresetButtonLabel(); } }); } 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"); }