Upload files to "DrawWavesAndRevisedAudio"

This commit is contained in:
ed
2025-10-26 10:35:08 +00:00
parent b2b4633005
commit 17259cc569
3 changed files with 1781 additions and 0 deletions

View File

@@ -0,0 +1,695 @@
#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();
}