#include #include "PluginEditor.h" #include "PluginProcessor.h" #include "PresetsCode.cpp" // embedded preset data // ============================================================================ // WaveThumbnail void WaveThumbnail::paint (juce::Graphics& g) { 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 p; auto r = bounds; p.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()); p.lineTo (x, y); } g.strokePath (p, 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); } } // ============================================================================ // Draw pad (grid + antialiased thick stroke + “ink” interpolation) DrawWaveComponent::DrawWaveComponent() { samples.assign (WavetableSynthAudioProcessor::kTableSize, 0.0f); } void DrawWaveComponent::clear() { std::fill (samples.begin(), samples.end(), 0.0f); repaint(); } void DrawWaveComponent::paint (juce::Graphics& g) { auto r = getLocalBounds(); // background g.fillAll (juce::Colours::black.withAlpha (0.35f)); g.setColour (juce::Colours::darkgrey); g.drawRect (r); // grid { 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)); // zero line g.drawLine ((float) r.getX(), (float) r.getCentreY(), (float) r.getRight(), (float) r.getCentreY(), 1.2f); } if (samples.empty()) return; // path 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 DrawWaveComponent::mouseDown (const juce::MouseEvent& e) { lastDrawIndex = -1; drawAt (e); } void DrawWaveComponent::mouseDrag (const juce::MouseEvent& e) { drawAt (e); } void DrawWaveComponent::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); // ink interpolation 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 DrawWaveComponent::setSampleAt (int idx, float value) { if ((size_t) idx < samples.size()) samples[(size_t) idx] = juce::jlimit (-1.0f, 1.0f, value); } // ============================================================================ // Knob L&F void MetalKnobLookAndFeel::drawRotarySlider (juce::Graphics& g, int x, int y, int w, int h, float pos, float start, float end, juce::Slider& s) { juce::ignoreUnused (s); const float radius = juce::jmin (w, h) * 0.45f; const float cx = (float) x + (float) w * 0.5f; const float cy = (float) y + (float) h * 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, 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 = start + pos * (end - start); 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); juce::AffineTransform t = juce::AffineTransform::rotation (angle).translated (cx, cy); g.setColour (juce::Colours::whitesmoke); g.fillPath (needle, t); } // ============================================================================ // Helpers static void setRotaryRangeBottomLeftToRight (juce::Slider& s) { const float start = juce::MathConstants::pi * 1.25f; const float end = start + juce::MathConstants::pi * 1.5f; s.setRotaryParameters (start, end, true); } void WavetableSynthAudioProcessorEditor::labelAbove (juce::Label& L, const juce::String& text) { L.setText (text, juce::dontSendNotification); L.setJustificationType (juce::Justification::centred); L.setColour (juce::Label::textColourId, juce::Colours::black); } juce::Rectangle WavetableSynthAudioProcessorEditor::getTopPanelBounds() const { return { 0, 0, getWidth(), 80 }; } // ↓↓↓ reduced bottom panel height so black area is taller juce::Rectangle WavetableSynthAudioProcessorEditor::getBottomPanelBounds() const { const int panelHeight = 220; // was 300 return { 0, getHeight() - panelHeight, getWidth(), panelHeight }; } // ============================================================================ // Editor WavetableSynthAudioProcessorEditor::WavetableSynthAudioProcessorEditor (WavetableSynthAudioProcessor& p) : juce::AudioProcessorEditor (&p), audioProcessor (p) { setSize (1150, 740); // MORPH morphSlider.setSliderStyle (juce::Slider::LinearHorizontal); morphSlider.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); morphAttach = std::make_unique (p.apvts, "MORPH", morphSlider); addAndMakeVisible (morphSlider); addAndMakeVisible (morphLoopToggle); morphLoopToggleAttach = std::make_unique (p.apvts, "MORPH_LOOP_ON", morphLoopToggle); morphLoopMode.addItemList (juce::StringArray { "Forward", "Ping-Pong", "Half Trip" }, 1); morphLoopMode.setJustificationType (juce::Justification::centred); addAndMakeVisible (morphLoopMode); morphLoopModeAttach = std::make_unique (p.apvts, "MORPH_LOOP_MODE", morphLoopMode); // MASTER (remove number box below) master.setSliderStyle (juce::Slider::Rotary); master.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); // << no readout master.setLookAndFeel (&metalKnobLNF); setRotaryRangeBottomLeftToRight (master); addAndMakeVisible (master); addAndMakeVisible (lblMaster); labelAbove (lblMaster, "Master"); if (p.apvts.getParameter ("MASTER") != nullptr) masterAttach = std::make_unique (p.apvts, "MASTER", master); // quick knob maker (no number boxes on any knobs) auto makeKnob = [this](juce::Slider& s) { s.setSliderStyle (juce::Slider::Rotary); s.setTextBoxStyle (juce::Slider::NoTextBox, false, 0, 0); s.setLookAndFeel (&metalKnobLNF); setRotaryRangeBottomLeftToRight (s); addAndMakeVisible (s); }; for (auto* s : { &cutoffSlider, &attack, &decay, &sustain, &release, &lfoRate, &lfoDepth, &fenvA, &fenvD, &fenvS, &fenvR, &fenvAmt, &chRate, &chDepth, &chDelay, &chFb, &chMix, &rvRoom, &rvDamp, &rvWidth, &rvWet }) makeKnob (*s); // Attach main parameters cutoffAttach = std::make_unique (p.apvts, "CUTOFF", cutoffSlider); attAttach = std::make_unique (p.apvts, "ATTACK", attack); decAttach = std::make_unique (p.apvts, "DECAY", decay); susAttach = std::make_unique (p.apvts, "SUSTAIN", sustain); relAttach = std::make_unique (p.apvts, "RELEASE", release); lfoRateAttach = std::make_unique (p.apvts, "LFO_RATE", lfoRate); lfoDepthAttach = std::make_unique (p.apvts, "LFO_DEPTH", lfoDepth); fenvAAttach = std::make_unique (p.apvts, "FENV_A", fenvA); fenvDAttach = std::make_unique (p.apvts, "FENV_D", fenvD); fenvSAttach = std::make_unique (p.apvts, "FENV_S", fenvS); fenvRAttach = std::make_unique (p.apvts, "FENV_R", fenvR); fenvAmtAttach = std::make_unique (p.apvts, "FENV_AMT", fenvAmt); // FX toggles + params addAndMakeVisible (chorusOn); addAndMakeVisible (reverbOn); chOnAttach = std::make_unique (p.apvts, "CHORUS_ON", chorusOn); rvOnAttach = std::make_unique (p.apvts, "REVERB_ON", reverbOn); chRateAttach = std::make_unique (p.apvts, "CH_RATE", chRate); chDepthAttach = std::make_unique (p.apvts, "CH_DEPTH", chDepth); chDelayAttach = std::make_unique (p.apvts, "CH_DELAY", chDelay); chFbAttach = std::make_unique (p.apvts, "CH_FB", chFb); chMixAttach = std::make_unique (p.apvts, "CH_MIX", chMix); rvRoomAttach = std::make_unique (p.apvts, "RV_ROOM", rvRoom); rvDampAttach = std::make_unique (p.apvts, "RV_DAMP", rvDamp); rvWidthAttach = std::make_unique (p.apvts, "RV_WIDTH", rvWidth); rvWetAttach = std::make_unique (p.apvts, "RV_WET", rvWet); // Labels for (auto* L : { &lblCutoff,&lblAttack,&lblDecay,&lblSustain,&lblRelease, &lblLfoRate,&lblLfoDepth,&lblFenvA,&lblFenvD,&lblFenvS,&lblFenvR,&lblFenvAmt, &lblChRate,&lblChDepth,&lblChDelay,&lblChFb,&lblChMix, &lblRvRoom,&lblRvDamp,&lblRvWidth,&lblRvWet }) addAndMakeVisible (*L); labelAbove (lblCutoff, "Cutoff"); labelAbove (lblAttack, "Attack"); labelAbove (lblDecay, "Decay"); labelAbove (lblSustain, "Sustain"); labelAbove (lblRelease, "Release"); labelAbove (lblLfoRate, "LFO Rate"); labelAbove (lblLfoDepth, "LFO Depth"); labelAbove (lblFenvA, "FEnv A"); labelAbove (lblFenvD, "FEnv D"); labelAbove (lblFenvS, "FEnv S"); labelAbove (lblFenvR, "FEnv R"); labelAbove (lblFenvAmt, "FEnv Amt"); labelAbove (lblChRate, "Ch Rate"); labelAbove (lblChDepth, "Ch Depth"); labelAbove (lblChDelay,"Ch Delay"); labelAbove (lblChFb, "Ch FB"); labelAbove (lblChMix, "Ch Mix"); labelAbove (lblRvRoom, "Rev Room"); labelAbove (lblRvDamp, "Rev Damp"); labelAbove (lblRvWidth,"Rev Width"); labelAbove (lblRvWet, "Rev Wet"); // Hidden slot params slotAParam.setVisible (false); slotBParam.setVisible (false); slotCParam.setVisible (false); slotAAttach = std::make_unique (p.apvts, "SLOT_A", slotAParam); slotBAttach = std::make_unique (p.apvts, "SLOT_B", slotBParam); slotCAttach = std::make_unique (p.apvts, "SLOT_C", slotCParam); // Draw pad + buttons + label addAndMakeVisible (userDraw); addAndMakeVisible (addToBrowser); addAndMakeVisible (clearDraw); addAndMakeVisible (presetButton); addAndMakeVisible (lblDrawWave); lblDrawWave.setText ("DRAW WAVE", juce::dontSendNotification); lblDrawWave.setColour (juce::Label::textColourId, juce::Colours::white); lblDrawWave.setJustificationType (juce::Justification::left); addToBrowser.setLookAndFeel (&flatBtnLNF); clearDraw .setLookAndFeel (&flatBtnLNF); presetButton.setLookAndFeel (&flatBtnLNF); addToBrowser.onClick = [this] { const int slotIndex = audioProcessor.addOrReplaceUserWavetable (userDraw.getTable()); if (slotIndex >= 0) { const double di = (double) slotIndex; switch (activeSlot) { case 0: slotAParam.setValue (di, juce::sendNotificationAsync); break; case 1: slotBParam.setValue (di, juce::sendNotificationAsync); break; case 2: slotCParam.setValue (di, juce::sendNotificationAsync); break; default: break; } repaint(); } }; clearDraw.onClick = [this]{ userDraw.clear(); }; presetButton.onClick = [this]{ showPresetMenu(); }; // Osc2 mute checkbox addAndMakeVisible (osc2Mute); osc2MuteAttach = std::make_unique (p.apvts, "OSC2_MUTE", osc2Mute); // Thumbnails for A, B, C addAndMakeVisible (slotABox); addAndMakeVisible (slotBBox); addAndMakeVisible (slotCBox); 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); startTimerHz (30); } // ============================================================================ // Painting & layout void WavetableSynthAudioProcessorEditor::paint (juce::Graphics& g) { g.fillAll (juce::Colours::black); // Top brushed header 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); } // Bottom brushed panel (shorter now) 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); } // Working area const int blackTop = top.getBottom(); const int blackBottom = bottom.getY(); // moved lower => taller black area const int leftWidth = getWidth() / 2 - 20; const int rightX = leftWidth + 30; // Wave browser grid (4 x 10) auto browser = juce::Rectangle (10, blackTop + 8, leftWidth - 20, blackBottom - blackTop - 16); g.setColour (juce::Colours::grey); g.drawRect (browser); const int cols = 4, rows = 10; const int cellW = browser.getWidth() / cols; const int cellH = browser.getHeight() / rows; browserCells.clear(); browserCells.reserve (cols * rows); const int waveCount = audioProcessor.getWaveTableCount(); for (int r = 0; r < rows; ++r) for (int c = 0; c < cols; ++c) { const int idx = r * cols + c; auto cell = juce::Rectangle (browser.getX() + c*cellW, browser.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); } } // Morph bar + label 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 WavetableSynthAudioProcessorEditor::resized() { 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; // Morph slider + A/B/C thumbnails (taller boxes) 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); // Presets button (top-right) presetButton.setBounds (getWidth() - 130 - 10, top.getY() + 24, 130, 28); // Draw pad – taller now due to shorter bottom panel const int padTop = morphLoopMode.getBottom() + 10; userDraw.setBounds (rightX, padTop, getWidth() - rightX - 16, blackBottom - padTop - 70); lblDrawWave.setBounds (userDraw.getX(), userDraw.getY() - 18, 120, 16); // Buttons under the pad addToBrowser.setBounds (userDraw.getX() + 220, userDraw.getBottom() + 8, 150, 28); clearDraw .setBounds (addToBrowser.getRight() + 12, userDraw.getBottom() + 8, 150, 28); // Toggles bottom-right (stay clear of osc lights) const int togglesY = bottom.getY() - 36; reverbOn.setBounds (getWidth() - 280, togglesY, 120, 24); osc2Mute .setBounds (getWidth() - 140, togglesY, 140, 24); chorusOn.setBounds (userDraw.getX(), userDraw.getBottom() + 8, 90, 20); // Header right: Master const int masterW = 82; lblMaster.setBounds (getWidth() - masterW - 150, top.getY() + 4, 80, 16); master .setBounds (getWidth() - masterW - 150, top.getY() + 16, masterW, masterW); // Knob grid (fits the 220 px panel) 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& L, juce::Slider& S, const char* txt) { const int x = left + col * stepX; const int y = topY + row * stepY; L.setText (txt, juce::dontSendNotification); L.setJustificationType (juce::Justification::centred); L.setColour (juce::Label::textColourId, juce::Colours::black); L.setBounds (x, y - 14, 88, 14); S.setBounds (x, y, knobW, knobH); }; // Row 0 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"); // Row 1 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"); } // ============================================================================ // Interaction void WavetableSynthAudioProcessorEditor::mouseDown (const juce::MouseEvent& e) { handleBrowserClick (e.getPosition()); } void WavetableSynthAudioProcessorEditor::handleBrowserClick (juce::Point pos) { const int waveCount = audioProcessor.getWaveTableCount(); for (size_t i = 0; i < browserCells.size(); ++i) { if (browserCells[i].contains (pos)) { if ((int) i >= waveCount) return; const double di = (double) i; switch (activeSlot) { case 0: slotAParam.setValue (di, juce::sendNotificationAsync); break; case 1: slotBParam.setValue (di, juce::sendNotificationAsync); break; case 2: slotCParam.setValue (di, juce::sendNotificationAsync); break; default: break; } repaint(); break; } } } void WavetableSynthAudioProcessorEditor::setActiveSlot (int slotIndex) { activeSlot = juce::jlimit (0, 2, slotIndex); slotABox.setHighlight (activeSlot == 0); slotBBox.setHighlight (activeSlot == 1); slotCBox.setHighlight (activeSlot == 2); } void WavetableSynthAudioProcessorEditor::showPresetMenu() { juce::PopupMenu root; const auto cats = PresetsCode::getCategories(); for (size_t ci = 0; ci < cats.size(); ++ci) { juce::PopupMenu sub; const auto& cat = cats[ci]; for (size_t pi = 0; pi < cat.presets.size(); ++pi) sub.addItem ((int) (1000 + ci*100 + pi), cat.presets[pi].name); root.addSubMenu (cat.name, sub); } root.showMenuAsync (juce::PopupMenu::Options().withTargetComponent (presetButton), [this] (int result) { if (result <= 0) return; const int ci = (result - 1000) / 100; const int pi = (result - 1000) % 100; const auto cats = PresetsCode::getCategories(); if (ci >= 0 && ci < (int) cats.size() && pi >= 0 && pi < (int) cats[(size_t) ci].presets.size()) { PresetsCode::loadPreset (audioProcessor.apvts, cats[(size_t) ci].presets[(size_t) pi]); audioProcessor.notifyPresetLoaded(); // if implemented setActiveSlot (0); } }); } void WavetableSynthAudioProcessorEditor::timerCallback() { // keep thumbnails in sync if parameters changed externally slotABox.setTable (audioProcessor.getPreviewTablePtr ((int) slotAParam.getValue())); slotBBox.setTable (audioProcessor.getPreviewTablePtr ((int) slotBParam.getValue())); slotCBox.setTable (audioProcessor.getPreviewTablePtr ((int) slotCParam.getValue())); // Morph slider auto-play (as before) bool guiLoopOn = morphLoopToggle.getToggleState(); bool processorLoopOn = false; float processorValue = 0.0f; try { processorLoopOn = audioProcessor.isMorphLoopActive(); processorValue = audioProcessor.getMorphDisplayValue(); } catch (...) { } const bool loopActive = processorLoopOn || guiLoopOn; if (! morphSlider.isMouseButtonDown() && loopActive) { float value = processorLoopOn ? processorValue : 0.0f; if (! processorLoopOn) { const double now = juce::Time::getMillisecondCounterHiRes(); const double dt = (now - lastTimerMs) * 0.001; // seconds lastTimerMs = now; const int mode = morphLoopMode.getSelectedItemIndex(); // 0 FWD, 1 PINGPONG, 2 HALF const float speed = 0.25f; // cycles/s localMorphPhase = std::fmod (localMorphPhase + speed * (float) dt, 1.0f); 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: value = localMorphPhase; break; } } morphSlider.setValue (value, juce::dontSendNotification); } setActiveSlot (activeSlot); repaint(); }