Add Wavetable Editor
This commit is contained in:
@@ -155,7 +155,7 @@
|
|||||||
#define JucePlugin_ARAFactoryID "com.SamediDimanche.NeuralSynth.factory"
|
#define JucePlugin_ARAFactoryID "com.SamediDimanche.NeuralSynth.factory"
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_ARADocumentArchiveID
|
#ifndef JucePlugin_ARADocumentArchiveID
|
||||||
#define JucePlugin_ARADocumentArchiveID "com.SamediDimanche.NeuralSynth.aradocumentarchive.0.0.1"
|
#define JucePlugin_ARADocumentArchiveID "com.SamediDimanche.NeuralSynth.aradocumentarchive.1.0.0"
|
||||||
#endif
|
#endif
|
||||||
#ifndef JucePlugin_ARACompatibleArchiveIDs
|
#ifndef JucePlugin_ARACompatibleArchiveIDs
|
||||||
#define JucePlugin_ARACompatibleArchiveIDs ""
|
#define JucePlugin_ARACompatibleArchiveIDs ""
|
||||||
|
|||||||
@@ -8,6 +8,21 @@
|
|||||||
pluginFormats="buildVST3">
|
pluginFormats="buildVST3">
|
||||||
<MAINGROUP id="UQstsW" name="NeuralSynth">
|
<MAINGROUP id="UQstsW" name="NeuralSynth">
|
||||||
<GROUP id="{D5B48DA9-9A47-914A-8C72-EE5E8DD868A3}" name="Source">
|
<GROUP id="{D5B48DA9-9A47-914A-8C72-EE5E8DD868A3}" name="Source">
|
||||||
|
<GROUP id="{7B17D83D-70A9-0C31-D663-B953A624AE3F}" name="UI">
|
||||||
|
<FILE id="ZL6eFk" name="CustomPresetWindow.cpp" compile="1" resource="0"
|
||||||
|
file="Source/UI/CustomPresetWindow.cpp"/>
|
||||||
|
<FILE id="Zb36sR" name="CustomPresetWindow.h" compile="0" resource="0"
|
||||||
|
file="Source/UI/CustomPresetWindow.h"/>
|
||||||
|
</GROUP>
|
||||||
|
<GROUP id="{B2D8F867-A0C5-54CA-75AD-EFA0141DDFE9}" name="SynthVoice">
|
||||||
|
<FILE id="rbLVkZ" name="ADSR.h" compile="0" resource="0" file="Source/SynthVoice/ADSR.h"/>
|
||||||
|
<FILE id="lYeoyk" name="Chorus.h" compile="0" resource="0" file="Source/SynthVoice/Chorus.h"/>
|
||||||
|
<FILE id="vBX0Mt" name="Distortion.h" compile="0" resource="0" file="Source/SynthVoice/Distortion.h"/>
|
||||||
|
<FILE id="jAtEqL" name="EQ.h" compile="0" resource="0" file="Source/SynthVoice/EQ.h"/>
|
||||||
|
<FILE id="Zeb5Xf" name="Flanger.h" compile="0" resource="0" file="Source/SynthVoice/Flanger.h"/>
|
||||||
|
<FILE id="UnqRtH" name="Reverb.h" compile="0" resource="0" file="Source/SynthVoice/Reverb.h"/>
|
||||||
|
<FILE id="ChzbrW" name="SimpleDelay.h" compile="0" resource="0" file="Source/SynthVoice/SimpleDelay.h"/>
|
||||||
|
</GROUP>
|
||||||
<FILE id="Mkx0uo" name="BlepOsc.h" compile="0" resource="0" file="Source/BlepOsc.h"/>
|
<FILE id="Mkx0uo" name="BlepOsc.h" compile="0" resource="0" file="Source/BlepOsc.h"/>
|
||||||
<FILE id="axDpEq" name="WavetableOsc.h" compile="0" resource="0" file="Source/WavetableOsc.h"/>
|
<FILE id="axDpEq" name="WavetableOsc.h" compile="0" resource="0" file="Source/WavetableOsc.h"/>
|
||||||
<FILE id="nmKMnf" name="GraphComponent.h" compile="0" resource="0"
|
<FILE id="nmKMnf" name="GraphComponent.h" compile="0" resource="0"
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ NeuralSynthAudioProcessorEditor::NeuralSynthAudioProcessorEditor (NeuralSynthAud
|
|||||||
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor()
|
NeuralSynthAudioProcessorEditor::~NeuralSynthAudioProcessorEditor()
|
||||||
{
|
{
|
||||||
stopTimer();
|
stopTimer();
|
||||||
|
if (customPresetWindow != nullptr)
|
||||||
|
customPresetWindow.reset();
|
||||||
keyboardState.removeListener(this);
|
keyboardState.removeListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,6 +313,7 @@ void NeuralSynthAudioProcessorEditor::showPresetMenu()
|
|||||||
categories.add(preset.category);
|
categories.add(preset.category);
|
||||||
|
|
||||||
const int baseId = 1000;
|
const int baseId = 1000;
|
||||||
|
const int customPresetMenuId = baseId - 1;
|
||||||
for (const auto& category : categories)
|
for (const auto& category : categories)
|
||||||
{
|
{
|
||||||
juce::PopupMenu sub;
|
juce::PopupMenu sub;
|
||||||
@@ -325,11 +328,18 @@ void NeuralSynthAudioProcessorEditor::showPresetMenu()
|
|||||||
menu.addSubMenu(category, sub);
|
menu.addSubMenu(category, sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.addItem(categories.size() + 1, "Custom ...", true, false);
|
menu.addSeparator();
|
||||||
|
menu.addItem(customPresetMenuId, "Custom ...", true, false);
|
||||||
|
|
||||||
menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this),
|
menu.showMenuAsync(juce::PopupMenu::Options().withParentComponent(this),
|
||||||
[this, baseId](int result)
|
[this, baseId, customPresetMenuId](int result)
|
||||||
{
|
{
|
||||||
|
if (result == customPresetMenuId)
|
||||||
|
{
|
||||||
|
showCustomPresetWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (result >= baseId)
|
if (result >= baseId)
|
||||||
{
|
{
|
||||||
const int index = result - baseId;
|
const int index = result - baseId;
|
||||||
@@ -340,6 +350,22 @@ void NeuralSynthAudioProcessorEditor::showPresetMenu()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NeuralSynthAudioProcessorEditor::showCustomPresetWindow()
|
||||||
|
{
|
||||||
|
constexpr int windowWidth = 420;
|
||||||
|
constexpr int windowHeight = 320;
|
||||||
|
|
||||||
|
if (customPresetWindow == nullptr)
|
||||||
|
{
|
||||||
|
customPresetWindow = std::make_unique<CustomPresetWindow>();
|
||||||
|
customPresetWindow->setSize(windowWidth, windowHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
customPresetWindow->centreAroundComponent(this, windowWidth, windowHeight);
|
||||||
|
customPresetWindow->setVisible(true);
|
||||||
|
customPresetWindow->toFront(true);
|
||||||
|
}
|
||||||
|
|
||||||
void NeuralSynthAudioProcessorEditor::updatePresetButtonLabel()
|
void NeuralSynthAudioProcessorEditor::updatePresetButtonLabel()
|
||||||
{
|
{
|
||||||
const auto& presets = audioProcessor.getFactoryPresets();
|
const auto& presets = audioProcessor.getFactoryPresets();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "PluginProcessor.h"
|
#include "PluginProcessor.h"
|
||||||
#include "GraphComponent.h"
|
#include "GraphComponent.h"
|
||||||
#include "ScopeComponent.h"
|
#include "ScopeComponent.h"
|
||||||
|
#include "UI/CustomPresetWindow.h"
|
||||||
|
|
||||||
//============================== ScopeSliderComponent ==========================
|
//============================== ScopeSliderComponent ==========================
|
||||||
// A generic panel: optional scope/graph + rotary sliders + labels.
|
// A generic panel: optional scope/graph + rotary sliders + labels.
|
||||||
@@ -387,10 +388,12 @@ private:
|
|||||||
|
|
||||||
void updatePresetButtonLabel();
|
void updatePresetButtonLabel();
|
||||||
void showPresetMenu();
|
void showPresetMenu();
|
||||||
|
void showCustomPresetWindow();
|
||||||
void handleLayerSelectionChanged();
|
void handleLayerSelectionChanged();
|
||||||
|
|
||||||
juce::TextButton presetMenuButton;
|
juce::TextButton presetMenuButton;
|
||||||
int lastPresetIndex { -1 };
|
int lastPresetIndex { -1 };
|
||||||
|
std::unique_ptr<CustomPresetWindow> customPresetWindow;
|
||||||
|
|
||||||
std::optional<ScopeSliderComponent> adsrComponent; // Amp Env
|
std::optional<ScopeSliderComponent> adsrComponent; // Amp Env
|
||||||
std::optional<ScopeSliderComponent> chorusComponent;
|
std::optional<ScopeSliderComponent> chorusComponent;
|
||||||
|
|||||||
900
Source/UI/CustomPresetWindow.cpp
Normal file
900
Source/UI/CustomPresetWindow.cpp
Normal 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);
|
||||||
|
}
|
||||||
10
Source/UI/CustomPresetWindow.h
Normal file
10
Source/UI/CustomPresetWindow.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
class CustomPresetWindow : public juce::DocumentWindow
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CustomPresetWindow();
|
||||||
|
void closeButtonPressed() override;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user