Files
NeuralSynthEd/DrawWavesAndRevisedAudio/PluginEditor.cpp

696 lines
29 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <JuceHeader.h>
#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<float>::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<float>::pi * 1.25f;
const float end = start + juce::MathConstants<float>::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<int> WavetableSynthAudioProcessorEditor::getTopPanelBounds() const
{
return { 0, 0, getWidth(), 80 };
}
// ↓↓↓ reduced bottom panel height so black area is taller
juce::Rectangle<int> 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<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "MORPH", morphSlider);
addAndMakeVisible (morphSlider);
addAndMakeVisible (morphLoopToggle);
morphLoopToggleAttach = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment> (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<juce::AudioProcessorValueTreeState::ComboBoxAttachment> (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<juce::AudioProcessorValueTreeState::SliderAttachment> (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<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "CUTOFF", cutoffSlider);
attAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "ATTACK", attack);
decAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "DECAY", decay);
susAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "SUSTAIN", sustain);
relAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "RELEASE", release);
lfoRateAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "LFO_RATE", lfoRate);
lfoDepthAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "LFO_DEPTH", lfoDepth);
fenvAAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "FENV_A", fenvA);
fenvDAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "FENV_D", fenvD);
fenvSAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "FENV_S", fenvS);
fenvRAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "FENV_R", fenvR);
fenvAmtAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "FENV_AMT", fenvAmt);
// FX toggles + params
addAndMakeVisible (chorusOn);
addAndMakeVisible (reverbOn);
chOnAttach = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment> (p.apvts, "CHORUS_ON", chorusOn);
rvOnAttach = std::make_unique<juce::AudioProcessorValueTreeState::ButtonAttachment> (p.apvts, "REVERB_ON", reverbOn);
chRateAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "CH_RATE", chRate);
chDepthAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "CH_DEPTH", chDepth);
chDelayAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "CH_DELAY", chDelay);
chFbAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "CH_FB", chFb);
chMixAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "CH_MIX", chMix);
rvRoomAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "RV_ROOM", rvRoom);
rvDampAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "RV_DAMP", rvDamp);
rvWidthAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "RV_WIDTH", rvWidth);
rvWetAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (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<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "SLOT_A", slotAParam);
slotBAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (p.apvts, "SLOT_B", slotBParam);
slotCAttach = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment> (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<juce::AudioProcessorValueTreeState::ButtonAttachment> (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<int> (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<int> (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<int> (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<int> 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();
}