#include "CustomPresetWindow.h" #include #include #include #include #include namespace { constexpr int kBrowserColumns = 4; constexpr int kBrowserRows = 10; class WaveThumbnail : public juce::Component { public: WaveThumbnail(const std::vector* tbl = nullptr) : table(tbl) {} void setTable(const std::vector* tbl) { table = tbl; repaint(); } void setHighlight(bool shouldHighlight) { if (highlight == shouldHighlight) return; highlight = shouldHighlight; repaint(); } void setOnClick(std::function handler) { onClick = std::move(handler); } void paint(juce::Graphics& g) override { auto bounds = getLocalBounds(); if (highlight) { g.setColour(juce::Colours::darkorange.withAlpha(0.35f)); g.fillRect(bounds); } g.setColour(juce::Colours::darkgrey); g.drawRect(bounds); if (table == nullptr || table->empty()) { if (getName().isNotEmpty()) { g.setColour(juce::Colours::grey); g.setFont(12.0f); g.drawText(getName(), bounds.reduced(2), juce::Justification::centred); } return; } g.setColour(juce::Colours::lime); juce::Path path; auto r = bounds; path.startNewSubPath((float) r.getX(), (float) r.getCentreY()); const int n = (int) table->size(); for (int i = 0; i < n; i += 32) { const float x = juce::jmap((float) i, 0.0f, (float) n, (float) r.getX(), (float) r.getRight()); const float y = juce::jmap((*table)[(size_t) i], -1.0f, 1.0f, (float) r.getBottom(), (float) r.getY()); path.lineTo(x, y); } g.strokePath(path, juce::PathStrokeType(1.2f)); if (getName().isNotEmpty()) { g.setColour(juce::Colours::white.withAlpha(0.9f)); g.setFont(12.0f); g.drawText(getName(), bounds.reduced(2), juce::Justification::topRight); } } void mouseDown(const juce::MouseEvent& e) override { juce::ignoreUnused(e); if (onClick != nullptr) onClick(); } private: const std::vector* table { nullptr }; bool highlight { false }; std::function onClick; }; class DrawWaveComponent : public juce::Component { public: DrawWaveComponent() { samples.assign(kTableSize, 0.0f); } void clear() { std::fill(samples.begin(), samples.end(), 0.0f); repaint(); } void paint(juce::Graphics& g) override { auto r = getLocalBounds(); g.fillAll(juce::Colours::black.withAlpha(0.35f)); g.setColour(juce::Colours::darkgrey); g.drawRect(r); g.setColour(juce::Colours::white.withAlpha(0.08f)); const int hLines = 8, vLines = 16; for (int i = 1; i < hLines; ++i) { const float y = juce::jmap((float) i, 0.0f, (float) hLines, (float) r.getY(), (float) r.getBottom()); g.drawLine((float) r.getX(), y, (float) r.getRight(), y); } for (int i = 1; i < vLines; ++i) { const float x = juce::jmap((float) i, 0.0f, (float) vLines, (float) r.getX(), (float) r.getRight()); g.drawLine(x, (float) r.getY(), x, (float) r.getBottom()); } g.setColour(juce::Colours::white.withAlpha(0.15f)); g.drawLine((float) r.getX(), (float) r.getCentreY(), (float) r.getRight(), (float) r.getCentreY(), 1.2f); if (samples.empty()) return; g.setColour(juce::Colours::deepskyblue); juce::Path p; const int n = (int) samples.size(); p.startNewSubPath((float) r.getX(), juce::jmap(samples[0], -1.0f, 1.0f, (float) r.getBottom(), (float) r.getY())); const int step = 4; for (int i = step; i < n; i += step) { const float x = juce::jmap((float) i, 0.0f, (float) n - 1, (float) r.getX(), (float) r.getRight()); const float y = juce::jmap(samples[(size_t) i], -1.0f, 1.0f, (float) r.getBottom(), (float) r.getY()); p.lineTo(x, y); } g.strokePath(p, juce::PathStrokeType(2.0f)); } void mouseDown(const juce::MouseEvent& e) override { lastDrawIndex = -1; drawAt(e); } void mouseDrag(const juce::MouseEvent& e) override { drawAt(e); } const std::vector& getTable() const { return samples; } private: static constexpr int kTableSize = 2048; void drawAt(const juce::MouseEvent& e) { auto r = getLocalBounds(); const float xNorm = juce::jlimit(0.0f, 1.0f, (e.position.x - (float) r.getX()) / (float) r.getWidth()); const float yNorm = juce::jlimit(-1.0f, 1.0f, juce::jmap(e.position.y, (float) r.getBottom(), (float) r.getY(), -1.0f, 1.0f)); const int n = (int) samples.size(); const int idx = (int) juce::jlimit(0.0f, (float) (n - 1), xNorm * (float) n); if (lastDrawIndex >= 0 && lastDrawIndex != idx) { const int a = juce::jmin(lastDrawIndex, idx); const int b = juce::jmax(lastDrawIndex, idx); const float v0 = samples[(size_t) lastDrawIndex]; const float v1 = yNorm; for (int i = a; i <= b; ++i) { const float t = (b == a ? 1.0f : (float) (i - a) / (float) (b - a)); setSampleAt(i, juce::jmap(t, v0, v1)); } } else { setSampleAt(idx, yNorm); } lastDrawIndex = idx; repaint(); } void setSampleAt(int idx, float value) { if ((size_t) idx < samples.size()) samples[(size_t) idx] = juce::jlimit(-1.0f, 1.0f, value); } std::vector samples; int lastDrawIndex { -1 }; }; class MetalKnobLookAndFeel : public juce::LookAndFeel_V4 { public: void drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, float sliderPosProportional, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) override { juce::ignoreUnused(slider); const float radius = juce::jmin(width, height) * 0.45f; const float cx = (float) x + (float) width * 0.5f; const float cy = (float) y + (float) height * 0.5f; g.setColour(juce::Colour(0xff303030)); g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f); juce::ColourGradient grad(juce::Colours::darkgrey, cx, cy - radius, juce::Colours::black, cx, cy + radius, true); g.setGradientFill(grad); g.fillEllipse(cx - radius * 0.95f, cy - radius * 0.95f, radius * 1.9f, radius * 1.9f); g.setColour(juce::Colours::lightgrey.withAlpha(0.8f)); const int tickCount = 8; for (int i = 0; i < tickCount; ++i) { const float a = juce::MathConstants::twoPi * (float) i / (float) tickCount; const float r1 = radius * 1.08f; const float r2 = radius * 1.25f; g.drawLine(cx + r1 * std::cos(a), cy + r1 * std::sin(a), cx + r2 * std::cos(a), cy + r2 * std::sin(a), 1.6f); } const float angle = rotaryStartAngle + sliderPosProportional * (rotaryEndAngle - rotaryStartAngle); juce::Path needle; const float rInner = radius * 0.18f; const float rOuter = radius * 0.80f; needle.addRoundedRectangle(-2.4f, -rOuter, 4.8f, rOuter - rInner, 1.8f); auto transform = juce::AffineTransform::rotation(angle).translated(cx, cy); g.setColour(juce::Colours::whitesmoke); g.fillPath(needle, transform); } }; class FlatButtonLookAndFeel : public juce::LookAndFeel_V4 { public: void drawButtonBackground(juce::Graphics& g, juce::Button& b, const juce::Colour& bg, bool isHighlighted, bool isDown) override { auto r = b.getLocalBounds().toFloat(); auto base = bg; if (isDown) base = base.darker(0.15f); else if (isHighlighted) base = base.brighter(0.08f); g.setColour(base); g.fillRoundedRectangle(r, 4.0f); g.setColour(juce::Colours::black.withAlpha(0.5f)); g.drawRoundedRectangle(r.reduced(0.5f), 4.0f, 1.0f); } }; struct DummyPreset { juce::String name; }; struct DummyPresetCategory { juce::String name; std::vector presets; }; const std::vector& getDemoPresetCategories() { static const std::vector categories { { "Pads", { { "Nebula Drift" }, { "Glass Sky" } } }, { "Bass", { { "Driver" }, { "Submerge" } } }, { "Leads", { { "Starlit" }, { "Binary Pulse" } } } }; return categories; } class DummyWavetableSynthAudioProcessor { public: static constexpr int kTableSize = 2048; DummyWavetableSynthAudioProcessor() { addWave("Sine", [](float phase) { return std::sin(phase); }); addWave("Square", [](float phase) { return std::signbit(std::sin(phase)) ? -1.0f : 1.0f; }); addWave("Saw", [](float phase) { return 2.0f * (phase / juce::MathConstants::twoPi) - 1.0f; }); addWave("Triangle", [](float phase) { const float v = 2.0f * std::fabs(2.0f * (phase / juce::MathConstants::twoPi) - 1.0f) - 1.0f; return juce::jlimit(-1.0f, 1.0f, -v); }); } int getWaveTableCount() const { return (int) tables.size(); } const std::vector* getPreviewTablePtr(int index) const { if (juce::isPositiveAndBelow(index, (int) tables.size())) return &tables[(size_t) index].samples; return nullptr; } int addOrReplaceUserWavetable(const std::vector& newSamples) { if (newSamples.empty()) return -1; Table table; table.name = "User " + juce::String(tables.size() + 1); table.samples = newSamples; table.samples.resize(kTableSize, 0.0f); tables.push_back(std::move(table)); return (int) tables.size() - 1; } bool isMorphLoopActive() const { return morphLoopActive; } void setMorphLoopActive(bool active) { morphLoopActive = active; } float getMorphDisplayValue() const { return morphDisplayValue; } void setMorphDisplayValue(float value) { morphDisplayValue = value; } void notifyPresetLoaded() {} private: struct Table { juce::String name; std::vector samples; }; void addWave(const juce::String& name, const std::function& generator) { Table table; table.name = name; table.samples.resize(kTableSize); for (int i = 0; i < kTableSize; ++i) { const float phase = juce::MathConstants::twoPi * (float) i / (float) kTableSize; table.samples[(size_t) i] = juce::jlimit(-1.0f, 1.0f, generator(phase)); } tables.push_back(std::move(table)); } std::vector tables; bool morphLoopActive { false }; float morphDisplayValue { 0.0f }; }; class WaveBrowserComponent : public juce::Component { public: explicit WaveBrowserComponent(DummyWavetableSynthAudioProcessor& processorRef) : audioProcessor(processorRef) { } void setOnWaveSelected(std::function handler) { onWaveSelected = std::move(handler); } void paint(juce::Graphics& g) override { g.fillAll(juce::Colours::black.withAlpha(0.35f)); g.setColour(juce::Colours::grey); g.drawRect(getLocalBounds()); auto grid = getLocalBounds().reduced(8); const int cellW = juce::jmax(1, grid.getWidth() / kBrowserColumns); const int cellH = juce::jmax(1, grid.getHeight() / kBrowserRows); browserCells.clear(); browserCells.reserve(kBrowserColumns * kBrowserRows); const int waveCount = audioProcessor.getWaveTableCount(); for (int r = 0; r < kBrowserRows; ++r) for (int c = 0; c < kBrowserColumns; ++c) { const int idx = r * kBrowserColumns + c; auto cell = juce::Rectangle(grid.getX() + c * cellW, grid.getY() + r * cellH, cellW, cellH); browserCells.push_back(cell); g.setColour(juce::Colours::darkgrey); g.drawRect(cell); if (idx < waveCount) { if (const auto* tbl = audioProcessor.getPreviewTablePtr(idx)) { g.setColour(juce::Colours::lime); juce::Path p; p.startNewSubPath((float) cell.getX(), (float) cell.getCentreY()); const int n = (int) tbl->size(); for (int i = 0; i < n; i += 32) { const float x = juce::jmap((float) i, 0.0f, (float) n, (float) cell.getX(), (float) cell.getRight()); const float y = juce::jmap((*tbl)[(size_t) i], -1.0f, 1.0f, (float) cell.getBottom(), (float) cell.getY()); p.lineTo(x, y); } g.strokePath(p, juce::PathStrokeType(1.0f)); } } else { g.setColour(juce::Colours::darkgrey); g.drawLine((float) cell.getX(), (float) cell.getY(), (float) cell.getRight(), (float) cell.getBottom(), 0.5f); g.drawLine((float) cell.getRight(), (float) cell.getY(), (float) cell.getX(), (float) cell.getBottom(), 0.5f); } } } void mouseDown(const juce::MouseEvent& e) override { const int waveCount = audioProcessor.getWaveTableCount(); for (size_t i = 0; i < browserCells.size(); ++i) { if (browserCells[i].contains(e.getPosition())) { if ((int) i >= waveCount) return; if (onWaveSelected != nullptr) onWaveSelected((int) i); break; } } } void refresh() { repaint(); } private: DummyWavetableSynthAudioProcessor& audioProcessor; std::vector> browserCells; std::function onWaveSelected; }; class EditorTabContent : public juce::Component { public: EditorTabContent(juce::Label& titleLabel, DrawWaveComponent& drawComponent) : label(titleLabel), draw(drawComponent) { addAndMakeVisible(label); addAndMakeVisible(draw); } void resized() override { auto area = getLocalBounds().reduced(12); auto labelArea = area.removeFromTop(20); label.setBounds(labelArea); area.removeFromTop(6); draw.setBounds(area); } private: juce::Label& label; DrawWaveComponent& draw; }; class LibraryTabContent : public juce::Component { public: explicit LibraryTabContent(WaveBrowserComponent& browserComponent) : browser(browserComponent) { addAndMakeVisible(browser); } void resized() override { browser.setBounds(getLocalBounds().reduced(8)); } private: WaveBrowserComponent& browser; }; class ExampleUIPanel : public juce::Component, private juce::Timer { public: ExampleUIPanel() { setSize(1100, 720); morphSlider.setSliderStyle(juce::Slider::LinearHorizontal); morphSlider.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); addAndMakeVisible(morphSlider); morphLoopToggle.setButtonText("Loop Morph"); morphLoopToggle.onClick = [this] { audioProcessor.setMorphLoopActive(morphLoopToggle.getToggleState()); }; addAndMakeVisible(morphLoopToggle); morphLoopMode.addItemList(juce::StringArray { "Forward", "Ping-Pong", "Half Trip" }, 1); morphLoopMode.setJustificationType(juce::Justification::centred); morphLoopMode.setSelectedItemIndex(0); addAndMakeVisible(morphLoopMode); configureKnob(master); master.setLookAndFeel(&metalKnobLNF); addAndMakeVisible(master); addAndMakeVisible(lblMaster); labelAbove(lblMaster, "Master"); auto configureDefaultKnob = [this](juce::Slider& slider) { configureKnob(slider); slider.setLookAndFeel(&metalKnobLNF); addAndMakeVisible(slider); }; for (auto* slider : { &cutoffSlider, &attack, &decay, &sustain, &release, &lfoRate, &lfoDepth, &fenvA, &fenvD, &fenvS, &fenvR, &fenvAmt, &chRate, &chDepth, &chDelay, &chFb, &chMix, &rvRoom, &rvDamp, &rvWidth, &rvWet }) { configureDefaultKnob(*slider); } chorusOn.setButtonText("Chorus"); reverbOn.setButtonText("Reverb"); osc2Mute.setButtonText("Deactivate Osc2"); addAndMakeVisible(chorusOn); addAndMakeVisible(reverbOn); addAndMakeVisible(osc2Mute); addToBrowser.setButtonText("Add to Browser"); clearDraw.setButtonText("Clear"); presetButton.setButtonText(selectedPresetLabel); for (auto* btn : { &addToBrowser, &clearDraw, &presetButton }) { btn->setLookAndFeel(&flatBtnLNF); addAndMakeVisible(*btn); } addToBrowser.onClick = [this] { const int slotIndex = audioProcessor.addOrReplaceUserWavetable(userDraw.getTable()); if (slotIndex >= 0) { slotIndices[(size_t) activeSlot] = slotIndex; updateSlotThumbnails(); waveBrowser.refresh(); repaint(); } }; clearDraw.onClick = [this] { userDraw.clear(); }; presetButton.onClick = [this] { showPresetMenu(); }; lblDrawWave.setText("DRAW WAVE", juce::dontSendNotification); lblDrawWave.setColour(juce::Label::textColourId, juce::Colours::white); lblDrawWave.setJustificationType(juce::Justification::left); addAndMakeVisible(wavetableTabs); wavetableTabs.addTab("Editor", juce::Colours::transparentBlack, &editorTabContent, false); wavetableTabs.addTab("Library", juce::Colours::transparentBlack, &libraryTabContent, false); wavetableTabs.setColour(juce::TabbedComponent::outlineColourId, juce::Colours::darkgrey); waveBrowser.setOnWaveSelected([this](int index) { slotIndices[(size_t) activeSlot] = index; updateSlotThumbnails(); repaint(); }); for (auto* box : { &slotABox, &slotBBox, &slotCBox }) addAndMakeVisible(*box); slotABox.setName("A"); slotBBox.setName("B"); slotCBox.setName("C"); slotABox.setOnClick([this]{ setActiveSlot(0); }); slotBBox.setOnClick([this]{ setActiveSlot(1); }); slotCBox.setOnClick([this]{ setActiveSlot(2); }); setActiveSlot(0); updateSlotThumbnails(); startTimerHz(30); } ~ExampleUIPanel() override { stopTimer(); master.setLookAndFeel(nullptr); for (auto* slider : { &cutoffSlider, &attack, &decay, &sustain, &release, &lfoRate, &lfoDepth, &fenvA, &fenvD, &fenvS, &fenvR, &fenvAmt, &chRate, &chDepth, &chDelay, &chFb, &chMix, &rvRoom, &rvDamp, &rvWidth, &rvWet }) { slider->setLookAndFeel(nullptr); } for (auto* btn : { &addToBrowser, &clearDraw, &presetButton }) btn->setLookAndFeel(nullptr); } void paint(juce::Graphics& g) override { g.fillAll(juce::Colours::black); auto top = getTopPanelBounds(); { juce::Colour c1 = juce::Colour::fromRGB(105,105,110); juce::Colour c2 = juce::Colour::fromRGB(75,75,80); g.setGradientFill(juce::ColourGradient(c1, (float) top.getX(), (float) top.getY(), c2, (float) top.getX(), (float) top.getBottom(), false)); g.fillRect(top); g.setColour(juce::Colours::white.withAlpha(0.05f)); for (int y = top.getY(); y < top.getBottom(); y += 3) g.drawLine((float) top.getX(), (float) y, (float) top.getRight(), (float) y, 1.0f); g.setColour(juce::Colours::black.withAlpha(0.6f)); g.drawRect(top, 2); g.setColour(juce::Colours::white); g.setFont(18.0f); g.drawText("RTWAVE - WAVETABLE SYNTH", 12, top.getY() + 14, 400, 22, juce::Justification::left); } auto bottom = getBottomPanelBounds(); { juce::Colour c1 = juce::Colour::fromRGB(110,110,115); juce::Colour c2 = juce::Colour::fromRGB(70,70,75); g.setGradientFill(juce::ColourGradient(c1, (float) bottom.getX(), (float) bottom.getY(), c2, (float) bottom.getX(), (float) bottom.getBottom(), false)); g.fillRect(bottom); g.setColour(juce::Colours::white.withAlpha(0.05f)); for (int y = bottom.getY(); y < bottom.getBottom(); y += 3) g.drawLine((float) bottom.getX(), (float) y, (float) bottom.getRight(), (float) y, 1.0f); g.setColour(juce::Colours::black.withAlpha(0.6f)); g.drawRect(bottom, 2); } const int blackTop = top.getBottom(); const int blackBottom = bottom.getY(); const int leftWidth = getWidth() / 2 - 20; const int rightX = leftWidth + 30; g.setColour(juce::Colours::darkred); g.fillRect(juce::Rectangle(rightX, blackTop + 6, getWidth() - rightX - 30, 6)); g.setColour(juce::Colours::white); g.setFont(18.0f); g.drawText("MORPH", rightX, blackTop + 14, getWidth() - rightX - 30, 20, juce::Justification::centred); } void resized() override { auto top = getTopPanelBounds(); auto bottom = getBottomPanelBounds(); const int blackTop = top.getBottom(); const int blackBottom = bottom.getY(); const int leftWidth = getWidth() / 2 - 20; const int rightX = leftWidth + 30; const int boxH = 28; slotABox.setBounds(rightX, blackTop + 6, 130, boxH); slotBBox.setBounds(rightX + 140, blackTop + 6, 130, boxH); slotCBox.setBounds(rightX + 280, blackTop + 6, 130, boxH); morphSlider.setBounds(rightX, blackTop + boxH + 10, getWidth() - rightX - 30, 18); morphLoopToggle.setBounds(rightX, morphSlider.getBottom() + 10, 120, 22); morphLoopMode.setBounds(morphLoopToggle.getRight() + 8, morphSlider.getBottom() + 6, 150, 26); presetButton.setBounds(getWidth() - 130 - 10, top.getY() + 24, 130, 28); const int padTop = morphLoopMode.getBottom() + 10; auto tabBounds = juce::Rectangle(rightX, padTop, getWidth() - rightX - 16, blackBottom - padTop - 70); wavetableTabs.setBounds(tabBounds); const int buttonRowY = tabBounds.getBottom() + 8; addToBrowser.setBounds(tabBounds.getX() + 220, buttonRowY, 150, 28); clearDraw.setBounds(addToBrowser.getRight() + 12, buttonRowY, 150, 28); const int togglesY = bottom.getY() - 36; reverbOn.setBounds(getWidth() - 280, togglesY, 120, 24); osc2Mute .setBounds(getWidth() - 140, togglesY, 140, 24); chorusOn.setBounds(tabBounds.getX(), buttonRowY, 90, 20); const int masterW = 82; lblMaster.setBounds(getWidth() - masterW - 150, top.getY() + 4, 80, 16); master .setBounds(getWidth() - masterW - 150, top.getY() + 16, masterW, masterW); const int left = bottom.getX() + 18; const int topY = bottom.getY() + 28; const int stepX = 96; const int stepY = 108; const int knobW = 72, knobH = 72; auto place = [&](int col, int row, juce::Label& label, juce::Slider& slider, const char* text) { const int x = left + col * stepX; const int y = topY + row * stepY; labelAbove(label, text); label.setBounds(x, y - 14, 88, 14); slider.setBounds(x, y, knobW, knobH); }; place (0,0, lblCutoff, cutoffSlider, "Cutoff"); place (1,0, lblAttack, attack, "Attack"); place (2,0, lblDecay, decay, "Decay"); place (3,0, lblSustain, sustain, "Sustain"); place (4,0, lblRelease, release, "Release"); place (5,0, lblLfoRate, lfoRate, "LFO Rate"); place (6,0, lblLfoDepth,lfoDepth, "LFO Depth"); place (7,0, lblFenvA, fenvA, "FEnv A"); place (8,0, lblFenvD, fenvD, "FEnv D"); place (9,0, lblFenvS, fenvS, "FEnv S"); place (0,1, lblFenvR, fenvR, "FEnv R"); place (1,1, lblFenvAmt, fenvAmt, "FEnv Amt"); place (2,1, lblChRate, chRate, "Ch Rate"); place (3,1, lblChDepth, chDepth, "Ch Depth"); place (4,1, lblChDelay, chDelay, "Ch Delay"); place (5,1, lblChFb, chFb, "Ch FB"); place (6,1, lblChMix, chMix, "Ch Mix"); place (7,1, lblRvRoom, rvRoom, "Rev Room"); place (8,1, lblRvDamp, rvDamp, "Rev Damp"); place (9,1, lblRvWidth, rvWidth, "Rev Width"); place (10,1,lblRvWet, rvWet, "Rev Wet"); } private: juce::Rectangle getTopPanelBounds() const { return { 0, 0, getWidth(), 80 }; } juce::Rectangle getBottomPanelBounds() const { const int panelHeight = 220; return { 0, getHeight() - panelHeight, getWidth(), panelHeight }; } void configureKnob(juce::Slider& slider) { slider.setRange(0.0, 1.0, 0.0); slider.setSliderStyle(juce::Slider::Rotary); slider.setTextBoxStyle(juce::Slider::NoTextBox, false, 0, 0); const float start = juce::MathConstants::pi * 1.25f; const float end = start + juce::MathConstants::pi * 1.5f; slider.setRotaryParameters(start, end, true); } static void labelAbove(juce::Label& label, const juce::String& text) { label.setText(text, juce::dontSendNotification); label.setJustificationType(juce::Justification::centred); label.setColour(juce::Label::textColourId, juce::Colours::black); } void setActiveSlot(int slot) { activeSlot = juce::jlimit(0, 2, slot); slotABox.setHighlight(activeSlot == 0); slotBBox.setHighlight(activeSlot == 1); slotCBox.setHighlight(activeSlot == 2); } void updateSlotThumbnails() { const int waveCount = audioProcessor.getWaveTableCount(); if (waveCount <= 0) { slotABox.setTable(nullptr); slotBBox.setTable(nullptr); slotCBox.setTable(nullptr); return; } const int maxIndex = waveCount - 1; slotIndices[0] = juce::jlimit(0, maxIndex, slotIndices[0]); slotIndices[1] = juce::jlimit(0, maxIndex, slotIndices[1]); slotIndices[2] = juce::jlimit(0, maxIndex, slotIndices[2]); slotABox.setTable(audioProcessor.getPreviewTablePtr(slotIndices[0])); slotBBox.setTable(audioProcessor.getPreviewTablePtr(slotIndices[1])); slotCBox.setTable(audioProcessor.getPreviewTablePtr(slotIndices[2])); } void showPresetMenu() { juce::PopupMenu menu; const auto& categories = getDemoPresetCategories(); int idBase = 1000; for (size_t ci = 0; ci < categories.size(); ++ci) { juce::PopupMenu sub; const auto& cat = categories[ci]; for (size_t pi = 0; pi < cat.presets.size(); ++pi) sub.addItem(idBase + (int) (ci * 100 + pi), cat.presets[pi].name); menu.addSubMenu(cat.name, sub); } menu.showMenuAsync(juce::PopupMenu::Options().withTargetComponent(&presetButton), [this, idBase](int result) { if (result <= 0) return; const int ci = (result - idBase) / 100; const int pi = (result - idBase) % 100; const auto& categories = getDemoPresetCategories(); if (juce::isPositiveAndBelow(ci, (int) categories.size())) { const auto& cat = categories[(size_t) ci]; if (juce::isPositiveAndBelow(pi, (int) cat.presets.size())) { selectedPresetLabel = cat.name + " / " + cat.presets[(size_t) pi].name; presetButton.setButtonText(selectedPresetLabel); audioProcessor.notifyPresetLoaded(); setActiveSlot(0); repaint(); } } }); } void timerCallback() override { updateSlotThumbnails(); const bool loopActive = morphLoopToggle.getToggleState() || audioProcessor.isMorphLoopActive(); if (! morphSlider.isMouseButtonDown() && loopActive) { const double now = juce::Time::getMillisecondCounterHiRes(); const double dt = (now - lastTimerMs) * 0.001; lastTimerMs = now; const int mode = morphLoopMode.getSelectedItemIndex(); const float speed = 0.25f; localMorphPhase = std::fmod(localMorphPhase + speed * (float) dt, 1.0f); float value = localMorphPhase; switch (mode) { case 1: value = (localMorphPhase < 0.5f) ? (localMorphPhase * 2.0f) : (1.0f - (localMorphPhase - 0.5f) * 2.0f); break; case 2: value = 0.5f * localMorphPhase; break; default: break; } morphSlider.setValue(value, juce::dontSendNotification); audioProcessor.setMorphDisplayValue(value); } setActiveSlot(activeSlot); repaint(); } DummyWavetableSynthAudioProcessor audioProcessor; MetalKnobLookAndFeel metalKnobLNF; FlatButtonLookAndFeel flatBtnLNF; juce::Slider morphSlider; juce::ToggleButton morphLoopToggle; juce::ComboBox morphLoopMode; juce::Slider master; juce::Label lblMaster; juce::Slider cutoffSlider, attack, decay, sustain, release; juce::Slider lfoRate, lfoDepth, fenvA, fenvD, fenvS, fenvR, fenvAmt; juce::Slider chRate, chDepth, chDelay, chFb, chMix; juce::Slider rvRoom, rvDamp, rvWidth, rvWet; juce::ToggleButton chorusOn, reverbOn, osc2Mute; WaveThumbnail slotABox, slotBBox, slotCBox; DrawWaveComponent userDraw; juce::Label lblDrawWave; WaveBrowserComponent waveBrowser { audioProcessor }; EditorTabContent editorTabContent { lblDrawWave, userDraw }; LibraryTabContent libraryTabContent { waveBrowser }; juce::TabbedComponent wavetableTabs { juce::TabbedButtonBar::TabsAtTop }; juce::TextButton addToBrowser, clearDraw, presetButton; juce::Label lblCutoff, lblAttack, lblDecay, lblSustain, lblRelease; juce::Label lblLfoRate, lblLfoDepth, lblFenvA, lblFenvD, lblFenvS, lblFenvR, lblFenvAmt; juce::Label lblChRate, lblChDepth, lblChDelay, lblChFb, lblChMix; juce::Label lblRvRoom, lblRvDamp, lblRvWidth, lblRvWet; std::array slotIndices { 0, 1, 2 }; int activeSlot { 0 }; juce::String selectedPresetLabel { "Presets" }; double lastTimerMs { juce::Time::getMillisecondCounterHiRes() }; float localMorphPhase { 0.0f }; }; } // namespace CustomPresetWindow::CustomPresetWindow() : juce::DocumentWindow("Custom Preset", juce::Colours::darkgrey, juce::DocumentWindow::closeButton) { setUsingNativeTitleBar(true); setResizable(true, false); setContentOwned(new ExampleUIPanel(), true); setSize(5000, 5000); } void CustomPresetWindow::closeButtonPressed() { setVisible(false); }