From a43db681202cabe490ec737613bd7af16636befd Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 25 Oct 2025 17:56:23 +0000 Subject: [PATCH] Upload files to "Source" --- Source/NeuralSharedParams.h | 42 +++++- Source/PluginEditor.cpp | 256 ++++++++++++++++++++++++++++++------ Source/PluginEditor.h | 156 ++++++++++++++++------ 3 files changed, 368 insertions(+), 86 deletions(-) diff --git a/Source/NeuralSharedParams.h b/Source/NeuralSharedParams.h index b6ff9df..40fbf7c 100644 --- a/Source/NeuralSharedParams.h +++ b/Source/NeuralSharedParams.h @@ -3,13 +3,14 @@ #include #include #include +#include struct SliderDetail { std::string label; float min, max, interval, defValue; }; -using ParamMap = std::unordered_map; +using ParamMap = std::vector>; // Each SliderDetail: { label, min, max, step, defaultValue } const std::unordered_map PARAM_SETTINGS = { @@ -67,12 +68,30 @@ const std::unordered_map PARAM_SETTINGS = { { "bias", { "Bias", -1.0f, 1.0f, 0.01f, 0.0f } }, { "tone", { "Tone", 100.0f, 8000.0f, 10.0f, 3000.0f } }, { "shape", { "Shape", 0.0f, 2.0f, 1.0f, 0.0f } } + }}, + { "wt", { + { "morph", { "Morph", 0.0f, 15.0f, 0.001f, 0.0f } }, + { "phase", { "Phase", 0.0f, 1.0f, 0.001f, 0.0f } }, + { "bank", { "Bank", 0.0f,255.0f, 1.0f, 0.0f } }, + { "lfoRate", { "LFO Rate", 0.01f, 10.0f, 0.001f, 1.0f } }, + { "lfoDepth",{ "LFO Depth", 0.0f, 8.0f, 0.001f, 0.0f } }, + { "lfoShape",{ "LFO Shape", 0.0f, 3.0f, 1.0f, 0.0f } }, + { "level", { "Level", 0.0f, 1.0f, 0.001f, 0.8f } } + }}, + { "wt2", { + { "morph", { "Morph", 0.0f, 15.0f, 0.001f, 0.0f } }, + { "phase", { "Phase", 0.0f, 1.0f, 0.001f, 0.0f } }, + { "bank", { "Bank", 0.0f,255.0f, 1.0f, 0.0f } }, + { "lfoRate", { "LFO Rate", 0.01f, 10.0f, 0.001f, 1.0f } }, + { "lfoDepth",{ "LFO Depth", 0.0f, 8.0f, 0.001f, 0.0f } }, + { "lfoShape",{ "LFO Shape", 0.0f, 3.0f, 1.0f, 0.0f } }, + { "level", { "Level", 0.0f, 1.0f, 0.001f, 0.0f } } }} }; struct NeuralSharedParams { - std::atomic waveform{ -1 }; + std::atomic* wtPhase{}; // Amp ADSR std::atomic* adsrAttack{}; @@ -137,9 +156,28 @@ struct NeuralSharedParams std::atomic* filterOn{}; std::atomic* eqOn{}; + // Wavetable + std::atomic* wtOn{}; + std::atomic* wtMorph{}; + std::atomic* wtBank{}; + std::atomic* wtLfoRate{}; + std::atomic* wtLfoDepth{}; + std::atomic* wtLfoShape{}; + std::atomic* wtLevel{}; + // EQ + Master std::atomic* lowGainDbls{}; std::atomic* midGainDbls{}; std::atomic* highGainDbls{}; std::atomic* masterDbls{}; + + // Wavetable Layer B + std::atomic* wt2Phase{}; + std::atomic* wt2On{}; + std::atomic* wt2Morph{}; + std::atomic* wt2Bank{}; + std::atomic* wt2LfoRate{}; + std::atomic* wt2LfoDepth{}; + std::atomic* wt2LfoShape{}; + std::atomic* wt2Level{}; }; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 43ecffb..664eddd 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -1,6 +1,8 @@ #include "PluginProcessor.h" #include "PluginEditor.h" #include "ScopeComponent.h" +#include "WavetableOsc.h" +#include //============================================================================== NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor& p) @@ -12,13 +14,9 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud addAndMakeVisible(mainScopeComponent); - waveformSelector.setModel(&waveformContents); - waveformContents.onSelect = [this](int row) - { - // write to the parameter so voices update safely - audioProcessor.parameters.getParameterAsValue("waveform") = (float)juce::jlimit(0, 3, row); - }; - addAndMakeVisible(waveformSelector); + presetMenuButton.setButtonText("Preset"); + presetMenuButton.onClick = [this] { showPresetMenu(); }; + addAndMakeVisible(presetMenuButton); // --- Panels --- adsrComponent.emplace(tree, "adsr", "Amp Env"); @@ -92,6 +90,76 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud }); 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); @@ -102,18 +170,22 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud masterLevelLabel.setJustificationType(juce::Justification::centred); addAndMakeVisible(masterLevelLabel); - // Blank placeholder - addAndMakeVisible(blankPanel); - // Attach master parameter gainAttachment = std::make_unique( audioProcessor.parameters, "master", masterLevelSlider.slider); + lastPresetIndex = audioProcessor.getCurrentPresetIndex(); + updatePresetButtonLabel(); + startTimerHz(10); + setSize(1400, 720); } //============================================================================== -NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() = default; +NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor() +{ + stopTimer(); +} //============================================================================== void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g) @@ -124,47 +196,147 @@ void NeuralSynthAudioProcessorEditor::paint (juce::Graphics& g) //============================================================================== void NeuralSynthAudioProcessorEditor::resized() { - auto bounds = getLocalBounds().reduced(16); + 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(20)), // scope row - juce::Grid::TrackInfo(juce::Grid::Fr(40)), // row 1 - juce::Grid::TrackInfo(juce::Grid::Fr(40)) // row 2 + 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) }; - // 6 columns: 5 content + 1 sidebar (waveform+master) grid.templateColumns = { - juce::Grid::TrackInfo(juce::Grid::Fr(18)), - juce::Grid::TrackInfo(juce::Grid::Fr(18)), - juce::Grid::TrackInfo(juce::Grid::Fr(18)), - juce::Grid::TrackInfo(juce::Grid::Fr(18)), - juce::Grid::TrackInfo(juce::Grid::Fr(18)), - juce::Grid::TrackInfo(juce::Grid::Fr(10)) + 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)) }; - // Row 0 - grid.items.add(juce::GridItem(mainScopeComponent) - .withArea(juce::GridItem::Span(1), juce::GridItem::Span(5))); - grid.items.add(juce::GridItem(waveformSelector) - .withArea(juce::GridItem::Span(1), juce::GridItem::Span(1))); + grid.rowGap = juce::Grid::Px(0); + grid.columnGap = juce::Grid::Px(0); - // Row 1 - 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)); - grid.items.add(juce::GridItem(masterLevelLabel)); + grid.items.clear(); - // Row 2 - 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(blankPanel)); - grid.items.add(juce::GridItem(masterLevelSlider)); + // 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)); - grid.performLayout(bounds); + // 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"); } diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index ec9e583..8ec6691 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -25,6 +25,8 @@ public: attachments.push_back(std::make_unique( tree, paramGroup + "_" + name, *sliders.back())); + paramNames.push_back(name); + labels.back()->setText(sliderDetail.label, juce::dontSendNotification); sliders.back()->setRange(sliderDetail.min, sliderDetail.max); } @@ -55,12 +57,74 @@ public: addAndMakeVisible(titleLabel); } - // Bypass toggle (per panel), id "_on" - bypassButton.setButtonText("On"); - bypassButton.setClickingTogglesState(true); - addAndMakeVisible(bypassButton); - bypassAttachment = std::make_unique( - treeRef, paramGroupId + "_on", bypassButton); + if (tree.getParameter(paramGroupId + "_on") != nullptr) + { + hasBypass = true; + bypassButton.setButtonText("On"); + bypassButton.setClickingTogglesState(true); + addAndMakeVisible(bypassButton); + bypassAttachment = std::make_unique( + 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( + treeRef, paramId, *sliders[i])); + } + + if (hasBypass) + { + bypassAttachment.reset(); + if (treeRef.getParameter(newGroup + "_on") != nullptr) + { + bypassAttachment = std::make_unique( + treeRef, newGroup + "_on", bypassButton); + bypassButton.setEnabled(true); + } + else + { + bypassButton.setEnabled(false); + } + } + + paramGroupId = newGroup; } void enableSampleScope(AudioBufferQueue& audioBufferQueue) { @@ -76,7 +140,17 @@ public: 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); @@ -89,9 +163,22 @@ private: // --- Top bar (manual) ---------------------------------------------- auto area = getLocalBounds().reduced(10); auto top = area.removeFromTop(22); - auto btnW = 46; - bypassButton.setBounds(top.removeFromRight(btnW).reduced(2, 1)); - titleLabel.setBounds(top); + 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; @@ -146,9 +233,14 @@ private: std::vector> sliders; std::vector> labels; std::vector> attachments; + std::vector paramNames; + + juce::Component* topBarAccessory{ nullptr }; + int accessoryPreferredWidth{ 120 }; juce::ToggleButton bypassButton; std::unique_ptr bypassAttachment; + bool hasBypass{ false }; juce::Label titleLabel; @@ -254,34 +346,6 @@ private: juce::Label titleLabel; }; -//============================== Waveform List Model =========================== -struct WaveformSelectorContents final : public juce::ListBoxModel -{ - int getNumRows() override { return 4; } - - void paintListBoxItem(int rowNumber, juce::Graphics& g, - int width, int height, bool rowIsSelected) override - { - if (rowIsSelected) g.fillAll(juce::Colours::lightblue); - g.setColour(juce::LookAndFeel::getDefaultLookAndFeel() - .findColour(juce::Label::textColourId)); - - juce::Font f; f.setHeight((float)height * 0.7f); - g.setFont(f); - g.drawText(waves[(size_t)rowNumber], 5, 0, width, height, - juce::Justification::centredLeft, true); - } - - void selectedRowsChanged (int lastRowSelected) override - { - if (onSelect) onSelect(lastRowSelected); - } - - std::function onSelect; - - std::vector waves { "Sine", "Saw", "Square", "Triangle" }; -}; - //============================== MasterVolumeComponent ========================= class MasterVolumeComponent : public juce::Component { @@ -290,6 +354,7 @@ public: { slider.setSliderStyle(juce::Slider::LinearBarVertical); slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 20, 20); + slider.setRange(-24.0f, 24.0f, 0.1f); addAndMakeVisible(slider); } @@ -302,7 +367,8 @@ public: }; //============================== Editor ======================================= -class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor +class NeuralSynthAudioProcessorEditor : public juce::AudioProcessorEditor, + private juce::Timer { public: NeuralSynthAudioProcessorEditor (NeuralSynthAudioProcessor&); @@ -310,13 +376,18 @@ public: void paint (juce::Graphics&) override; void resized() override; + void timerCallback() override; private: NeuralSynthAudioProcessor& audioProcessor; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NeuralSynthAudioProcessorEditor) - juce::ListBox waveformSelector; - WaveformSelectorContents waveformContents; + void updatePresetButtonLabel(); + void showPresetMenu(); + void handleLayerSelectionChanged(); + + juce::TextButton presetMenuButton; + int lastPresetIndex { -1 }; std::optional adsrComponent; // Amp Env std::optional chorusComponent; @@ -327,6 +398,7 @@ private: std::optional distortionComponent; std::optional filterComponent; std::optional filterEnvComponent; // Filter Env panel + std::optional wtComponent; // Wavetable panel MasterVolumeComponent masterLevelSlider; juce::Label masterLevelLabel; @@ -336,6 +408,6 @@ private: std::unique_ptr gainAttachment; ScopeComponent mainScopeComponent; - - juce::Component blankPanel; + juce::ComboBox layerSelector; + bool controllingLayerB { false }; };