Add Wavetable Editor

This commit is contained in:
Tim
2025-11-08 15:18:05 +00:00
parent 43b8670d4e
commit 61bcef19aa
6 changed files with 957 additions and 3 deletions

View File

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