Upload files to "DrawWavesAndRevisedAudio"
This commit is contained in:
695
DrawWavesAndRevisedAudio/PluginEditor.cpp
Normal file
695
DrawWavesAndRevisedAudio/PluginEditor.cpp
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user