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();
|
||||
}
|
||||
172
DrawWavesAndRevisedAudio/PluginEditor.h
Normal file
172
DrawWavesAndRevisedAudio/PluginEditor.h
Normal file
@@ -0,0 +1,172 @@
|
||||
#pragma once
|
||||
|
||||
#include <JuceHeader.h>
|
||||
#include <functional>
|
||||
#include "PluginProcessor.h"
|
||||
|
||||
// === Small waveform thumbnail =================================================
|
||||
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;
|
||||
|
||||
void mouseDown (const juce::MouseEvent& e) override
|
||||
{
|
||||
juce::ignoreUnused (e);
|
||||
if (onClick) onClick();
|
||||
}
|
||||
|
||||
private:
|
||||
const std::vector<float>* table { nullptr };
|
||||
bool highlight { false };
|
||||
std::function<void()> onClick;
|
||||
};
|
||||
|
||||
// === Draw-your-own wave pad ===================================================
|
||||
// (Grid + thicker antialiased stroke + smooth “ink” interpolation while dragging)
|
||||
class DrawWaveComponent : public juce::Component
|
||||
{
|
||||
public:
|
||||
DrawWaveComponent();
|
||||
|
||||
void clear();
|
||||
void paint (juce::Graphics& g) override;
|
||||
void mouseDown (const juce::MouseEvent& e) override;
|
||||
void mouseDrag (const juce::MouseEvent& e) override;
|
||||
|
||||
const std::vector<float>& getTable() const { return samples; }
|
||||
|
||||
private:
|
||||
void drawAt (const juce::MouseEvent& e);
|
||||
void setSampleAt (int idx, float value);
|
||||
|
||||
std::vector<float> samples;
|
||||
int lastDrawIndex { -1 };
|
||||
};
|
||||
|
||||
// === Custom looks =============================================================
|
||||
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;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// === Editor ===================================================================
|
||||
class WavetableSynthAudioProcessorEditor : public juce::AudioProcessorEditor,
|
||||
private juce::Timer
|
||||
{
|
||||
public:
|
||||
explicit WavetableSynthAudioProcessorEditor (WavetableSynthAudioProcessor&);
|
||||
~WavetableSynthAudioProcessorEditor() override = default;
|
||||
|
||||
void paint (juce::Graphics&) override;
|
||||
void resized() override;
|
||||
void mouseDown (const juce::MouseEvent& e) override;
|
||||
|
||||
private:
|
||||
WavetableSynthAudioProcessor& audioProcessor;
|
||||
|
||||
// Morph
|
||||
juce::Slider morphSlider;
|
||||
juce::ToggleButton morphLoopToggle { "Loop Morph" };
|
||||
juce::ComboBox morphLoopMode; // Forward / Ping-Pong / Half Trip
|
||||
|
||||
// Master
|
||||
juce::Slider master;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> morphAttach, masterAttach;
|
||||
|
||||
// A/B/C thumbnails
|
||||
WaveThumbnail slotABox, slotBBox, slotCBox;
|
||||
|
||||
// Synth knobs
|
||||
juce::Slider cutoffSlider, attack, decay, sustain, release;
|
||||
juce::Slider lfoRate, lfoDepth, fenvA, fenvD, fenvS, fenvR, fenvAmt;
|
||||
|
||||
// FX knobs
|
||||
juce::Slider chRate, chDepth, chDelay, chFb, chMix;
|
||||
juce::Slider rvRoom, rvDamp, rvWidth, rvWet;
|
||||
|
||||
// Attachments
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>
|
||||
cutoffAttach, attAttach, decAttach, susAttach, relAttach,
|
||||
lfoRateAttach, lfoDepthAttach, fenvAAttach, fenvDAttach, fenvSAttach, fenvRAttach, fenvAmtAttach,
|
||||
chRateAttach, chDepthAttach, chDelayAttach, chFbAttach, chMixAttach,
|
||||
rvRoomAttach, rvDampAttach, rvWidthAttach, rvWetAttach;
|
||||
|
||||
// Hidden slot params
|
||||
juce::Slider slotAParam, slotBParam, slotCParam;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment>
|
||||
slotAAttach, slotBAttach, slotCAttach;
|
||||
|
||||
// Toggles
|
||||
juce::ToggleButton chorusOn { "Chorus" }, reverbOn { "Reverb" }, osc2Mute { "Deactivate Osc2" };
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> chOnAttach, rvOnAttach, osc2MuteAttach;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ButtonAttachment> morphLoopToggleAttach;
|
||||
std::unique_ptr<juce::AudioProcessorValueTreeState::ComboBoxAttachment> morphLoopModeAttach;
|
||||
|
||||
// Browser grid cells
|
||||
std::vector<juce::Rectangle<int>> browserCells; // 4 x 10 = 40
|
||||
|
||||
// Draw pad + buttons
|
||||
DrawWaveComponent userDraw;
|
||||
juce::TextButton addToBrowser { "Add to Browser" }, clearDraw { "Clear" }, presetButton { "Presets" };
|
||||
|
||||
// Labels
|
||||
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;
|
||||
juce::Label lblMaster, lblDrawWave;
|
||||
|
||||
// L&F
|
||||
MetalKnobLookAndFeel metalKnobLNF;
|
||||
FlatButtonLookAndFeel flatBtnLNF;
|
||||
|
||||
// Layout helpers
|
||||
juce::Rectangle<int> getTopPanelBounds() const;
|
||||
juce::Rectangle<int> getBottomPanelBounds() const;
|
||||
|
||||
// Logic
|
||||
void timerCallback() override;
|
||||
void handleBrowserClick (juce::Point<int> pos);
|
||||
void showPresetMenu();
|
||||
void setActiveSlot (int slotIndex);
|
||||
|
||||
// Helpers
|
||||
static void labelAbove (juce::Label& L, const juce::String& text);
|
||||
int activeSlot { 0 };
|
||||
|
||||
// Local morph animation (fallback)
|
||||
double lastTimerMs { juce::Time::getMillisecondCounterHiRes() };
|
||||
float localMorphPhase { 0.0f }; // 0..1
|
||||
};
|
||||
914
DrawWavesAndRevisedAudio/PluginProcessor.cpp
Normal file
914
DrawWavesAndRevisedAudio/PluginProcessor.cpp
Normal file
@@ -0,0 +1,914 @@
|
||||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
|
||||
// ============================================================
|
||||
// Voice infrastructure
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr float kMorphMin = 0.02f;
|
||||
constexpr float kMorphMax = 0.98f;
|
||||
constexpr float kMorphSmoothCoeff = 0.18f;
|
||||
}
|
||||
|
||||
struct VoiceParams
|
||||
{
|
||||
juce::ADSR::Parameters ampParams;
|
||||
juce::ADSR::Parameters filterParams;
|
||||
float cutoffBase { 8000.0f };
|
||||
float filterEnvAmount { 0.0f };
|
||||
std::array<int, 3> slotIndices { { 0, 1, 2 } };
|
||||
float staticMorph { 0.0f };
|
||||
float perVoiceGain { 0.5f };
|
||||
bool osc2Active { true };
|
||||
float osc2Detune { 1.003f };
|
||||
};
|
||||
|
||||
class WavetableSound : public juce::SynthesiserSound
|
||||
{
|
||||
public:
|
||||
bool appliesToNote (int) override { return true; }
|
||||
bool appliesToChannel (int) override { return true; }
|
||||
};
|
||||
|
||||
class WavetableVoice : public juce::SynthesiserVoice
|
||||
{
|
||||
public:
|
||||
explicit WavetableVoice (WavetableSynthAudioProcessor& proc) : processor (proc)
|
||||
{
|
||||
voiceFilter.setType (juce::dsp::StateVariableTPTFilterType::lowpass);
|
||||
}
|
||||
|
||||
bool canPlaySound (juce::SynthesiserSound* s) override
|
||||
{
|
||||
return dynamic_cast<WavetableSound*> (s) != nullptr;
|
||||
}
|
||||
|
||||
void setParams (const VoiceParams& vp)
|
||||
{
|
||||
params = vp;
|
||||
ampEnv.setParameters (params.ampParams);
|
||||
filterEnv.setParameters (params.filterParams);
|
||||
pendingSlotUpdate = true;
|
||||
secondaryFrequency = currentFrequency * params.osc2Detune;
|
||||
updatePhaseIncrement();
|
||||
updateMipLevel();
|
||||
}
|
||||
|
||||
void setMorphBuffer (const float* ptr) { morphBuffer = ptr; }
|
||||
|
||||
void startNote (int midiNoteNumber, float velocity, juce::SynthesiserSound*, int) override
|
||||
{
|
||||
juce::ignoreUnused (velocity);
|
||||
const float freq = (float) juce::MidiMessage::getMidiNoteInHertz (midiNoteNumber);
|
||||
currentFrequency = freq;
|
||||
secondaryFrequency = freq * params.osc2Detune;
|
||||
updatePhaseIncrement();
|
||||
updateMipLevel();
|
||||
updateSlotWaves();
|
||||
|
||||
primaryPhase = 0.0f;
|
||||
secondaryPhase = 0.0f;
|
||||
ampEnv.noteOn();
|
||||
filterEnv.noteOn();
|
||||
active = true;
|
||||
voiceFilter.reset();
|
||||
}
|
||||
|
||||
void stopNote (float velocity, bool allowTailOff) override
|
||||
{
|
||||
juce::ignoreUnused (velocity);
|
||||
|
||||
if (allowTailOff)
|
||||
{
|
||||
ampEnv.noteOff();
|
||||
filterEnv.noteOff();
|
||||
}
|
||||
else
|
||||
{
|
||||
ampEnv.reset();
|
||||
filterEnv.reset();
|
||||
clearCurrentNote();
|
||||
active = false;
|
||||
currentFrequency = 0.0f;
|
||||
secondaryFrequency = 0.0f;
|
||||
primaryIncrement = 0.0f;
|
||||
secondaryIncrement = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void pitchWheelMoved (int) override {}
|
||||
void controllerMoved (int, int) override {}
|
||||
|
||||
void renderNextBlock (juce::AudioBuffer<float>& buffer, int startSample, int numSamples) override
|
||||
{
|
||||
if (!active || !isVoiceActive()) return;
|
||||
if (pendingSlotUpdate) updateSlotWaves();
|
||||
|
||||
const int channels = buffer.getNumChannels();
|
||||
float* left = buffer.getWritePointer (0, startSample);
|
||||
float* right = channels > 1 ? buffer.getWritePointer (1, startSample) : nullptr;
|
||||
const float* morph = morphBuffer != nullptr ? morphBuffer + startSample : nullptr;
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
const float morphValue = juce::jlimit (kMorphMin, kMorphMax,
|
||||
morph != nullptr ? morph[i] : params.staticMorph);
|
||||
const float framePos = morphValue * (float) (WavetableSynthAudioProcessor::kMorphFrames - 1);
|
||||
const int segment = morphValue < 0.5f ? 0 : 1;
|
||||
const float segAlpha = segment == 0
|
||||
? juce::jlimit (0.0f, 1.0f, morphValue * 2.0f)
|
||||
: juce::jlimit (0.0f, 1.0f, (morphValue - 0.5f) * 2.0f);
|
||||
|
||||
const float primaryMain = sampleWave (slotWaves[segment], framePos, primaryPhase);
|
||||
const float primaryNext = sampleWave (slotWaves[segment + 1], framePos, primaryPhase);
|
||||
float waveSample = primaryMain + segAlpha * (primaryNext - primaryMain);
|
||||
|
||||
if (params.osc2Active)
|
||||
{
|
||||
const float secondaryMain = sampleWave (slotWaves[segment], framePos, secondaryPhase);
|
||||
const float secondaryNext = sampleWave (slotWaves[segment + 1], framePos, secondaryPhase);
|
||||
const float osc2Sample = secondaryMain + segAlpha * (secondaryNext - secondaryMain);
|
||||
waveSample = 0.5f * (waveSample + osc2Sample);
|
||||
}
|
||||
|
||||
const float envValue = ampEnv.getNextSample();
|
||||
const float modValue = filterEnv.getNextSample();
|
||||
const float cutoff = juce::jlimit (20.0f, 20000.0f,
|
||||
params.cutoffBase + params.filterEnvAmount
|
||||
* modValue * (20000.0f - params.cutoffBase));
|
||||
voiceFilter.setCutoffFrequency (cutoff);
|
||||
|
||||
const float filtered = voiceFilter.processSample (0, waveSample);
|
||||
const float output = params.perVoiceGain * envValue * filtered;
|
||||
|
||||
left[i] += output;
|
||||
if (right != nullptr) right[i] += output;
|
||||
|
||||
advancePhase (primaryPhase, primaryIncrement);
|
||||
if (params.osc2Active)
|
||||
advancePhase (secondaryPhase, secondaryIncrement);
|
||||
}
|
||||
|
||||
if (! ampEnv.isActive())
|
||||
{
|
||||
clearCurrentNote();
|
||||
active = false;
|
||||
currentFrequency = 0.0f;
|
||||
secondaryFrequency = 0.0f;
|
||||
primaryIncrement = 0.0f;
|
||||
secondaryIncrement = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
void setCurrentPlaybackSampleRate (double newRate) override
|
||||
{
|
||||
juce::SynthesiserVoice::setCurrentPlaybackSampleRate (newRate);
|
||||
const juce::dsp::ProcessSpec spec { newRate, 32u, 1u };
|
||||
voiceFilter.reset();
|
||||
voiceFilter.prepare (spec);
|
||||
voiceFilter.setType (juce::dsp::StateVariableTPTFilterType::lowpass);
|
||||
ampEnv.setSampleRate (newRate);
|
||||
filterEnv.setSampleRate (newRate);
|
||||
updatePhaseIncrement();
|
||||
}
|
||||
|
||||
private:
|
||||
float sampleWave (const WaveMorph* set, float framePos, float phaseValue) const
|
||||
{
|
||||
if (set == nullptr) return 0.0f;
|
||||
|
||||
const float clamped = juce::jlimit (0.0f,
|
||||
(float) (WavetableSynthAudioProcessor::kMorphFrames - 1),
|
||||
framePos);
|
||||
const int frameIdx0 = (int) clamped;
|
||||
const int frameIdx1 = juce::jmin (frameIdx0 + 1, WavetableSynthAudioProcessor::kMorphFrames - 1);
|
||||
const float frameFrac = clamped - (float) frameIdx0;
|
||||
|
||||
const auto& table0 = set->frames[(size_t) frameIdx0].mip[(size_t) currentMip];
|
||||
const auto& table1 = set->frames[(size_t) frameIdx1].mip[(size_t) currentMip];
|
||||
|
||||
// Interpolate adjacent frames so morph sweeps remain continuous.
|
||||
const float s0 = sampleTable (table0, phaseValue);
|
||||
const float s1 = sampleTable (table1, phaseValue);
|
||||
return s0 + frameFrac * (s1 - s0);
|
||||
}
|
||||
|
||||
float sampleTable (const std::vector<float>& table, float phaseValue) const
|
||||
{
|
||||
if (table.empty()) return 0.0f;
|
||||
const float idx = phaseValue;
|
||||
const int i0 = (int) idx & (WavetableSynthAudioProcessor::kTableSize - 1);
|
||||
const int i1 = (i0 + 1) & (WavetableSynthAudioProcessor::kTableSize - 1);
|
||||
const float frac = idx - (float) i0;
|
||||
const float s0 = table[(size_t) i0];
|
||||
const float s1 = table[(size_t) i1];
|
||||
return s0 + frac * (s1 - s0);
|
||||
}
|
||||
|
||||
void advancePhase (float& phaseValue, float increment)
|
||||
{
|
||||
phaseValue += increment;
|
||||
if (phaseValue >= (float) WavetableSynthAudioProcessor::kTableSize)
|
||||
phaseValue -= (float) WavetableSynthAudioProcessor::kTableSize;
|
||||
}
|
||||
|
||||
void updatePhaseIncrement()
|
||||
{
|
||||
const double sr = getSampleRate();
|
||||
if (sr <= 0.0) return;
|
||||
primaryIncrement = (float) ((double) WavetableSynthAudioProcessor::kTableSize * (double) currentFrequency / sr);
|
||||
if (params.osc2Active)
|
||||
secondaryIncrement = (float) ((double) WavetableSynthAudioProcessor::kTableSize * (double) secondaryFrequency / sr);
|
||||
else
|
||||
secondaryIncrement = 0.0f;
|
||||
}
|
||||
|
||||
void updateMipLevel()
|
||||
{
|
||||
const float freqForMip = params.osc2Active
|
||||
? juce::jmax (currentFrequency, secondaryFrequency)
|
||||
: currentFrequency;
|
||||
currentMip = juce::jlimit (0, WavetableSynthAudioProcessor::kMipLevels - 1,
|
||||
processor.chooseMipLevel (freqForMip));
|
||||
}
|
||||
|
||||
void updateSlotWaves()
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
slotWaves[i] = processor.getWavePtr (params.slotIndices[(size_t) i]);
|
||||
|
||||
pendingSlotUpdate = false;
|
||||
}
|
||||
|
||||
WavetableSynthAudioProcessor& processor;
|
||||
VoiceParams params;
|
||||
const WaveMorph* slotWaves[3] { nullptr, nullptr, nullptr };
|
||||
|
||||
juce::ADSR ampEnv;
|
||||
juce::ADSR filterEnv;
|
||||
juce::dsp::StateVariableTPTFilter<float> voiceFilter;
|
||||
|
||||
const float* morphBuffer { nullptr };
|
||||
|
||||
float primaryPhase { 0.0f };
|
||||
float secondaryPhase { 0.0f };
|
||||
float primaryIncrement { 0.0f };
|
||||
float secondaryIncrement { 0.0f };
|
||||
float currentFrequency { 0.0f };
|
||||
float secondaryFrequency { 0.0f };
|
||||
int currentMip { 0 };
|
||||
bool active { false };
|
||||
bool pendingSlotUpdate { false };
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Utilities
|
||||
|
||||
void WavetableSynthAudioProcessor::normalize (std::vector<float>& t)
|
||||
{
|
||||
float mx = 0.0f;
|
||||
for (auto v : t) mx = juce::jmax (mx, std::abs (v));
|
||||
if (mx > 0.0f)
|
||||
for (auto& v : t)
|
||||
v /= mx;
|
||||
}
|
||||
|
||||
void WavetableSynthAudioProcessor::addSine (std::vector<float>& t, int harmonic, float amp)
|
||||
{
|
||||
const float k = (float) harmonic;
|
||||
const int N = (int) t.size();
|
||||
for (int n = 0; n < N; ++n)
|
||||
t[(size_t) n] += amp * std::sin (juce::MathConstants<float>::twoPi * k * (float) n / (float) N);
|
||||
}
|
||||
|
||||
void WavetableSynthAudioProcessor::removeDC (std::vector<float>& t)
|
||||
{
|
||||
if (t.empty()) return;
|
||||
double sum = 0.0;
|
||||
for (auto v : t) sum += (double) v;
|
||||
const float mean = (float) (sum / (double) t.size());
|
||||
for (auto& v : t) v -= mean;
|
||||
}
|
||||
|
||||
void WavetableSynthAudioProcessor::enforceZeroStart (std::vector<float>& t)
|
||||
{
|
||||
if (t.empty()) return;
|
||||
|
||||
// find first zero crossing; if none, fall back to minimum magnitude point
|
||||
int zeroIndex = 0;
|
||||
for (int i = 1; i < (int) t.size(); ++i)
|
||||
{
|
||||
const float a = t[(size_t) (i - 1)];
|
||||
const float b = t[(size_t) i];
|
||||
if ((a <= 0.0f && b >= 0.0f) || (a >= 0.0f && b <= 0.0f))
|
||||
{
|
||||
zeroIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (zeroIndex > 0 && zeroIndex < (int) t.size())
|
||||
std::rotate (t.begin(), t.begin() + zeroIndex, t.end());
|
||||
|
||||
t[0] = 0.0f;
|
||||
}
|
||||
|
||||
WaveMorph WavetableSynthAudioProcessor::buildAdditiveMorph (std::function<float(int)> ampFn,
|
||||
bool oddOnly, float altPhase)
|
||||
{
|
||||
WaveMorph morph {};
|
||||
const int N = kTableSize;
|
||||
const int NyquistHarmonic = N / 2;
|
||||
|
||||
for (int frame = 0; frame < kMorphFrames; ++frame)
|
||||
{
|
||||
const float frameAlpha = (float) frame / (float) juce::jmax (1, kMorphFrames - 1);
|
||||
|
||||
for (int level = 0; level < kMipLevels; ++level)
|
||||
{
|
||||
auto& table = morph.frames[(size_t) frame].mip[(size_t) level];
|
||||
table.assign ((size_t) N, 0.0f);
|
||||
|
||||
const float levelAttenuation = std::pow (0.5f, (float) level);
|
||||
const int harmonicLimit = juce::jmax (1, (int) std::floor ((float) NyquistHarmonic * levelAttenuation * juce::jlimit (0.1f, 1.0f, frameAlpha + 0.05f)));
|
||||
|
||||
for (int h = 1; h <= harmonicLimit; ++h)
|
||||
{
|
||||
if (oddOnly && (h % 2 == 0)) continue;
|
||||
float a = ampFn (h);
|
||||
if (a == 0.0f) continue;
|
||||
a = (altPhase > 0.0f ? a : ((h % 2) ? a : -a));
|
||||
addSine (table, h, a);
|
||||
}
|
||||
|
||||
removeDC (table);
|
||||
enforceZeroStart (table);
|
||||
normalize (table);
|
||||
}
|
||||
}
|
||||
|
||||
return morph;
|
||||
}
|
||||
|
||||
// ---- preset wave builders ----
|
||||
WaveMorph WavetableSynthAudioProcessor::makeSine()
|
||||
{
|
||||
return buildAdditiveMorph ([](int h) { return (h == 1) ? 1.0f : 0.0f; });
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeSaw()
|
||||
{
|
||||
// Thin the highest frame slightly to keep corrected ramp usable
|
||||
return buildAdditiveMorph ([](int h) { return 1.0f / (float) h; }, false, +1.0f);
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeSquare()
|
||||
{
|
||||
// odd harmonics 1/h
|
||||
return buildAdditiveMorph ([](int h) { return 1.0f / (float) h; }, true, +1.0f);
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeTriangle()
|
||||
{
|
||||
// odd harmonics 1/h^2 with alternating signs
|
||||
return buildAdditiveMorph ([](int h) { return 1.0f / ((float) h * (float) h); }, true, -1.0f);
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makePulse (float duty)
|
||||
{
|
||||
duty = juce::jlimit (0.01f, 0.99f, duty);
|
||||
// Fourier for pulse: amp_k = (2/(k*pi)) * sin(k*pi*duty)
|
||||
return buildAdditiveMorph ([=](int k)
|
||||
{
|
||||
return (2.0f / (juce::MathConstants<float>::pi * (float) k))
|
||||
* std::sin (juce::MathConstants<float>::pi * (float) k * duty);
|
||||
}, false, +1.0f);
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeEven()
|
||||
{
|
||||
// even-only 1/h
|
||||
return buildAdditiveMorph ([](int h) { return (h % 2 == 0) ? 1.0f / (float) h : 0.0f; }, false, +1.0f);
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeOdd()
|
||||
{
|
||||
return makeSquare();
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeHalfSineRect()
|
||||
{
|
||||
// half-rectified sine (rich, smooth)
|
||||
return buildAdditiveMorph ([](int h)
|
||||
{
|
||||
// analytic series for rectified sine → only even harmonics
|
||||
if (h % 2 == 1) return 0.0f;
|
||||
const float k = (float) h;
|
||||
// ~1/k^2 rolloff
|
||||
return 1.0f / (k * k * 0.25f);
|
||||
}, false, +1.0f);
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeBell()
|
||||
{
|
||||
// exponential decay across harmonics
|
||||
return buildAdditiveMorph ([](int h) { return std::exp (-0.25f * (float) (h - 1)); }, false, +1.0f);
|
||||
}
|
||||
WaveMorph WavetableSynthAudioProcessor::makeOrgan()
|
||||
{
|
||||
// 8', 4', 2 2/3', 2' drawbars-ish
|
||||
return buildAdditiveMorph ([](int h)
|
||||
{
|
||||
switch (h)
|
||||
{
|
||||
case 1: return 1.0f;
|
||||
case 2: return 0.5f;
|
||||
case 3: return 0.35f;
|
||||
case 4: return 0.28f;
|
||||
case 5: return 0.22f;
|
||||
default: return 0.0f;
|
||||
}
|
||||
}, false, +1.0f);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Construction
|
||||
|
||||
WavetableSynthAudioProcessor::WavetableSynthAudioProcessor()
|
||||
: apvts (*this, nullptr, "PARAMS", createParameterLayout())
|
||||
{
|
||||
buildFactoryWaves();
|
||||
|
||||
synth.clearVoices();
|
||||
for (int i = 0; i < 16; ++i)
|
||||
synth.addVoice (new WavetableVoice (*this));
|
||||
|
||||
synth.clearSounds();
|
||||
synth.addSound (new WavetableSound());
|
||||
synth.setNoteStealingEnabled (true);
|
||||
|
||||
presetFade.setCurrentAndTargetValue (1.0f);
|
||||
}
|
||||
|
||||
void WavetableSynthAudioProcessor::buildFactoryWaves()
|
||||
{
|
||||
waves.clear();
|
||||
waves.reserve (kBrowserCapacity);
|
||||
|
||||
// 20 factory slots
|
||||
waves.push_back (makeSine()); // 0
|
||||
waves.push_back (makeSaw()); // 1
|
||||
waves.push_back (makeSquare()); // 2
|
||||
waves.push_back (makeTriangle()); // 3
|
||||
waves.push_back (makePulse (0.25f));// 4
|
||||
waves.push_back (makePulse (0.10f));// 5
|
||||
waves.push_back (makePulse (0.60f));// 6
|
||||
waves.push_back (makeEven()); // 7
|
||||
waves.push_back (makeOdd()); // 8
|
||||
waves.push_back (makeHalfSineRect());// 9
|
||||
waves.push_back (makeOrgan()); // 10
|
||||
waves.push_back (makeBell()); // 11
|
||||
// fill to 20 with variations
|
||||
waves.push_back (makePulse (0.33f));// 12
|
||||
waves.push_back (makePulse (0.75f));// 13
|
||||
waves.push_back (makePulse (0.90f));// 14
|
||||
waves.push_back (makeSaw()); // 15
|
||||
waves.push_back (makeSquare()); // 16
|
||||
waves.push_back (makeTriangle()); // 17
|
||||
waves.push_back (makeEven()); // 18
|
||||
waves.push_back (makeBell()); // 19
|
||||
defaultTableCount = kFactorySlots;
|
||||
nextUserInsert = 0;
|
||||
}
|
||||
|
||||
const std::vector<float>* WavetableSynthAudioProcessor::getPreviewTablePtr (int index) const
|
||||
{
|
||||
if (index < 0 || index >= (int) waves.size()) return nullptr;
|
||||
return &waves[(size_t) index].frames[0].mip[0]; // widest-band level for thumbnail
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// APVTS layout
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout
|
||||
WavetableSynthAudioProcessor::createParameterLayout()
|
||||
{
|
||||
using AP = juce::AudioProcessorValueTreeState;
|
||||
std::vector<std::unique_ptr<juce::RangedAudioParameter>> p;
|
||||
|
||||
// Master first so editor can attach even if others change
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>(
|
||||
"MASTER", "Master", juce::NormalisableRange<float> (0.0f, 1.5f, 0.0f, 0.5f), 0.75f));
|
||||
|
||||
// Morph + LFO
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("MORPH", "Morph",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterBool>("MORPH_LOOP_ON", "Morph Loop", false));
|
||||
p.push_back (std::make_unique<juce::AudioParameterChoice>("MORPH_LOOP_MODE", "Morph Loop Mode",
|
||||
juce::StringArray { "Forward", "Ping-Pong", "Half Trip" }, 0));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("LFO_RATE", "LFO Rate",
|
||||
juce::NormalisableRange<float> (0.01f, 10.0f, 0.0f, 0.4f), 0.2f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("LFO_DEPTH", "LFO Depth",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
|
||||
|
||||
// ADSR
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("ATTACK", "Attack",
|
||||
juce::NormalisableRange<float> (0.001f, 5.0f, 0.0f, 0.5f), 0.01f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("DECAY", "Decay",
|
||||
juce::NormalisableRange<float> (0.001f, 5.0f, 0.0f, 0.5f), 0.2f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("SUSTAIN", "Sustain",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.8f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("RELEASE", "Release",
|
||||
juce::NormalisableRange<float> (0.001f, 5.0f, 0.0f, 0.5f), 0.3f));
|
||||
|
||||
// Filter + filter env
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("CUTOFF", "Cutoff",
|
||||
juce::NormalisableRange<float> (20.0f, 20000.0f, 0.0f, 0.5f), 8000.0f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("FENV_A", "FEnv A",
|
||||
juce::NormalisableRange<float> (0.001f, 5.0f, 0.0f, 0.5f), 0.01f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("FENV_D", "FEnv D",
|
||||
juce::NormalisableRange<float> (0.001f, 5.0f, 0.0f, 0.5f), 0.2f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("FENV_S", "FEnv S",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.0f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("FENV_R", "FEnv R",
|
||||
juce::NormalisableRange<float> (0.001f, 5.0f, 0.0f, 0.5f), 0.3f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("FENV_AMT", "FEnv Amt",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.5f));
|
||||
|
||||
// Browser slot indices
|
||||
p.push_back (std::make_unique<juce::AudioParameterInt>("SLOT_A", "Slot A", 0, kBrowserCapacity - 1, 0));
|
||||
p.push_back (std::make_unique<juce::AudioParameterInt>("SLOT_B", "Slot B", 0, kBrowserCapacity - 1, 1));
|
||||
p.push_back (std::make_unique<juce::AudioParameterInt>("SLOT_C", "Slot C", 0, kBrowserCapacity - 1, 2));
|
||||
|
||||
// Osc2 mute toggle
|
||||
p.push_back (std::make_unique<juce::AudioParameterBool>("OSC2_MUTE", "Deactivate Osc2", false));
|
||||
|
||||
// Chorus / Reverb (keep for GUI; safe defaults)
|
||||
p.push_back (std::make_unique<juce::AudioParameterBool>("CHORUS_ON", "Chorus On", false));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("CH_RATE", "Ch Rate",
|
||||
juce::NormalisableRange<float> (0.05f, 5.0f, 0.0f, 0.5f), 1.2f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("CH_DEPTH","Ch Depth",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.3f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("CH_DELAY","Ch Delay",
|
||||
juce::NormalisableRange<float> (1.0f, 30.0f), 8.0f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("CH_FB", "Ch Fb",
|
||||
juce::NormalisableRange<float> (-0.95f, 0.95f), 0.0f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("CH_MIX", "Ch Mix",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.25f));
|
||||
|
||||
p.push_back (std::make_unique<juce::AudioParameterBool>("REVERB_ON", "Reverb On", true));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("RV_ROOM", "Rv Room",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.4f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("RV_DAMP","Rv Damp",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.3f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("RV_WIDTH","Rv Width",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 1.0f));
|
||||
p.push_back (std::make_unique<juce::AudioParameterFloat>("RV_WET", "Rv Wet",
|
||||
juce::NormalisableRange<float> (0.0f, 1.0f), 0.12f));
|
||||
|
||||
return { p.begin(), p.end() };
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Prepare / process
|
||||
|
||||
void WavetableSynthAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
|
||||
{
|
||||
synth.setCurrentPlaybackSampleRate (sampleRate);
|
||||
|
||||
juce::dsp::ProcessSpec spec;
|
||||
spec.sampleRate = sampleRate;
|
||||
spec.maximumBlockSize = (juce::uint32) samplesPerBlock;
|
||||
spec.numChannels = (juce::uint32) getTotalNumOutputChannels();
|
||||
|
||||
chorus.reset();
|
||||
chorus.prepare (spec);
|
||||
|
||||
reverbParams = {};
|
||||
reverb.setParameters (reverbParams);
|
||||
reverb.reset();
|
||||
|
||||
morphBuffer.clear();
|
||||
morphBuffer.resize ((size_t) juce::jmax (1, samplesPerBlock));
|
||||
|
||||
morphState = juce::jlimit (kMorphMin, kMorphMax,
|
||||
apvts.getRawParameterValue ("MORPH")->load());
|
||||
morphLoopPhase = 0.0f;
|
||||
morphLoopDirection = 1;
|
||||
morphLoopStage = 0;
|
||||
morphLoopStagePhase = 0.0f;
|
||||
morphDisplay.store (morphState, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
int WavetableSynthAudioProcessor::chooseMipLevel (float fundamentalHz) const
|
||||
{
|
||||
// Rough mapping: level increases as note goes higher
|
||||
// Level 0 for lowest notes, up to kMipLevels-1 for highest.
|
||||
const float ref = 55.0f; // A1
|
||||
const float ratio = fundamentalHz / ref;
|
||||
int L = (int) std::floor (std::log2 (juce::jmax (1.0f, ratio)));
|
||||
return juce::jlimit (0, kMipLevels - 1, L);
|
||||
}
|
||||
|
||||
const WaveMorph* WavetableSynthAudioProcessor::getWavePtr (int index) const
|
||||
{
|
||||
if (waves.empty()) return nullptr;
|
||||
const int idx = juce::jlimit (0, (int) waves.size() - 1, index);
|
||||
return &waves[(size_t) idx];
|
||||
}
|
||||
|
||||
void WavetableSynthAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
|
||||
juce::MidiBuffer& midi)
|
||||
{
|
||||
juce::ScopedNoDenormals nd;
|
||||
buffer.clear();
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
const double sr = getSampleRate() > 0.0 ? getSampleRate() : 44100.0;
|
||||
|
||||
if ((int) morphBuffer.size() < numSamples)
|
||||
morphBuffer.resize ((size_t) numSamples);
|
||||
|
||||
const float baseMorph = apvts.getRawParameterValue ("MORPH")->load();
|
||||
const float lfoRate = apvts.getRawParameterValue ("LFO_RATE")->load();
|
||||
const float lfoDepth = apvts.getRawParameterValue ("LFO_DEPTH")->load();
|
||||
const float cutoffBase = apvts.getRawParameterValue ("CUTOFF")->load();
|
||||
const float filterAmt = apvts.getRawParameterValue ("FENV_AMT")->load();
|
||||
const bool chorusOn = apvts.getRawParameterValue ("CHORUS_ON")->load() > 0.5f;
|
||||
const bool reverbOn = apvts.getRawParameterValue ("REVERB_ON")->load() > 0.5f;
|
||||
|
||||
auto clampSlot = [this](int idx)
|
||||
{
|
||||
return juce::jlimit (0, juce::jmax (0, (int) waves.size() - 1), idx);
|
||||
};
|
||||
|
||||
VoiceParams params;
|
||||
params.ampParams.attack = apvts.getRawParameterValue ("ATTACK")->load();
|
||||
params.ampParams.decay = apvts.getRawParameterValue ("DECAY")->load();
|
||||
params.ampParams.sustain = apvts.getRawParameterValue ("SUSTAIN")->load();
|
||||
params.ampParams.release = apvts.getRawParameterValue ("RELEASE")->load();
|
||||
|
||||
params.filterParams.attack = apvts.getRawParameterValue ("FENV_A")->load();
|
||||
params.filterParams.decay = apvts.getRawParameterValue ("FENV_D")->load();
|
||||
params.filterParams.sustain = apvts.getRawParameterValue ("FENV_S")->load();
|
||||
params.filterParams.release = apvts.getRawParameterValue ("FENV_R")->load();
|
||||
|
||||
params.cutoffBase = cutoffBase;
|
||||
params.filterEnvAmount = filterAmt;
|
||||
params.slotIndices = { clampSlot ((int) apvts.getRawParameterValue ("SLOT_A")->load()),
|
||||
clampSlot ((int) apvts.getRawParameterValue ("SLOT_B")->load()),
|
||||
clampSlot ((int) apvts.getRawParameterValue ("SLOT_C")->load()) };
|
||||
params.staticMorph = juce::jlimit (kMorphMin, kMorphMax, baseMorph);
|
||||
params.perVoiceGain = 0.5f;
|
||||
params.osc2Active = apvts.getRawParameterValue ("OSC2_MUTE")->load() < 0.5f;
|
||||
params.osc2Detune = 1.003f;
|
||||
|
||||
for (int i = 0; i < synth.getNumVoices(); ++i)
|
||||
if (auto* v = dynamic_cast<WavetableVoice*> (synth.getVoice (i)))
|
||||
{
|
||||
v->setParams (params);
|
||||
v->setMorphBuffer (morphBuffer.data());
|
||||
}
|
||||
|
||||
chorus.setRate (apvts.getRawParameterValue ("CH_RATE")->load());
|
||||
chorus.setDepth (apvts.getRawParameterValue ("CH_DEPTH")->load());
|
||||
chorus.setCentreDelay (apvts.getRawParameterValue ("CH_DELAY")->load());
|
||||
chorus.setFeedback (apvts.getRawParameterValue ("CH_FB")->load());
|
||||
chorus.setMix (apvts.getRawParameterValue ("CH_MIX")->load());
|
||||
|
||||
reverbParams.roomSize = apvts.getRawParameterValue ("RV_ROOM")->load();
|
||||
reverbParams.damping = apvts.getRawParameterValue ("RV_DAMP")->load();
|
||||
reverbParams.width = apvts.getRawParameterValue ("RV_WIDTH")->load();
|
||||
reverbParams.wetLevel = apvts.getRawParameterValue ("RV_WET")->load();
|
||||
reverbParams.dryLevel = 1.0f - reverbParams.wetLevel;
|
||||
reverb.setParameters (reverbParams);
|
||||
|
||||
const bool loopEnabled = apvts.getRawParameterValue ("MORPH_LOOP_ON")->load() > 0.5f;
|
||||
const int loopMode = juce::jlimit (0, 2, (int) apvts.getRawParameterValue ("MORPH_LOOP_MODE")->load());
|
||||
const float depth = juce::jlimit (0.0f, 1.0f, lfoDepth);
|
||||
const float phaseIncrement = juce::jlimit (0.0001f, 20.0f, lfoRate) / (float) sr;
|
||||
|
||||
float loopPhase = morphLoopPhase;
|
||||
int loopDirection = morphLoopDirection;
|
||||
int loopStage = morphLoopStage % 4;
|
||||
float loopStagePhase = morphLoopStagePhase;
|
||||
float smoothed = morphState;
|
||||
|
||||
static constexpr std::array<float, 4> stageStart { 0.0f, 0.5f, 0.0f, 1.0f };
|
||||
static constexpr std::array<float, 4> stageEnd { 0.5f, 0.0f, 1.0f, 0.0f };
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
float modValue = baseMorph;
|
||||
|
||||
if (loopEnabled && depth > 0.0f)
|
||||
{
|
||||
switch (loopMode)
|
||||
{
|
||||
case 0: // forward
|
||||
{
|
||||
loopPhase += phaseIncrement;
|
||||
if (loopPhase >= 1.0f)
|
||||
loopPhase -= std::floor (loopPhase);
|
||||
modValue = loopPhase;
|
||||
break;
|
||||
}
|
||||
case 1: // ping pong
|
||||
{
|
||||
loopPhase += phaseIncrement * (float) loopDirection;
|
||||
if (loopPhase >= 1.0f)
|
||||
{
|
||||
loopPhase = 1.0f;
|
||||
loopDirection = -1;
|
||||
}
|
||||
else if (loopPhase <= 0.0f)
|
||||
{
|
||||
loopPhase = 0.0f;
|
||||
loopDirection = 1;
|
||||
}
|
||||
modValue = loopPhase;
|
||||
break;
|
||||
}
|
||||
case 2: // half trip
|
||||
default:
|
||||
{
|
||||
loopStagePhase += phaseIncrement;
|
||||
if (loopStagePhase >= 1.0f)
|
||||
{
|
||||
loopStagePhase -= 1.0f;
|
||||
loopStage = (loopStage + 1) % 4;
|
||||
}
|
||||
const float start = stageStart[(size_t) loopStage];
|
||||
const float end = stageEnd[(size_t) loopStage];
|
||||
modValue = start + loopStagePhase * (end - start);
|
||||
break;
|
||||
}
|
||||
}
|
||||
modValue = juce::jlimit (kMorphMin, kMorphMax, modValue);
|
||||
}
|
||||
|
||||
const float target = (loopEnabled && depth > 0.0f)
|
||||
? juce::jlimit (kMorphMin, kMorphMax,
|
||||
(1.0f - depth) * baseMorph + depth * modValue)
|
||||
: juce::jlimit (kMorphMin, kMorphMax, baseMorph);
|
||||
|
||||
smoothed += kMorphSmoothCoeff * (target - smoothed);
|
||||
morphBuffer[(size_t) i] = smoothed;
|
||||
}
|
||||
|
||||
morphState = smoothed;
|
||||
morphLoopPhase = loopPhase;
|
||||
morphLoopDirection = loopDirection;
|
||||
morphLoopStage = loopStage;
|
||||
morphLoopStagePhase = loopStagePhase;
|
||||
morphDisplay.store (smoothed, std::memory_order_relaxed);
|
||||
|
||||
synth.renderNextBlock (buffer, midi, 0, numSamples);
|
||||
midi.clear();
|
||||
|
||||
const int channels = buffer.getNumChannels();
|
||||
if (presetFade.isSmoothing() || presetFade.getCurrentValue() < 0.999f)
|
||||
{
|
||||
auto* channelData = buffer.getArrayOfWritePointers();
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
const float g = presetFade.getNextValue();
|
||||
for (int ch = 0; ch < channels; ++ch)
|
||||
channelData[ch][i] *= g;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr float mixHeadroom = 0.75f;
|
||||
buffer.applyGain (mixHeadroom);
|
||||
|
||||
juce::dsp::AudioBlock<float> blk (buffer);
|
||||
if (chorusOn) chorus.process (juce::dsp::ProcessContextReplacing<float> (blk));
|
||||
if (reverbOn) reverb.process (juce::dsp::ProcessContextReplacing<float> (blk));
|
||||
|
||||
const float master = apvts.getRawParameterValue ("MASTER")->load();
|
||||
buffer.applyGain (master);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// State
|
||||
|
||||
void WavetableSynthAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
|
||||
{
|
||||
auto state = apvts.copyState();
|
||||
if (auto xml = state.createXml()) copyXmlToBinary (*xml, destData);
|
||||
}
|
||||
|
||||
void WavetableSynthAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
|
||||
{
|
||||
if (auto xml = getXmlFromBinary (data, sizeInBytes))
|
||||
if (xml->hasTagName (apvts.state.getType()))
|
||||
{
|
||||
apvts.replaceState (juce::ValueTree::fromXml (*xml));
|
||||
notifyPresetLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// User waves
|
||||
|
||||
int WavetableSynthAudioProcessor::addOrReplaceUserWavetable (const std::vector<float>& singleCycle)
|
||||
{
|
||||
const int N = kTableSize;
|
||||
std::vector<float> resampled (N);
|
||||
|
||||
// resample incoming single cycle to our table size
|
||||
for (int i = 0; i < N; ++i)
|
||||
{
|
||||
const float p = (float) i / (float) N;
|
||||
const float idx = p * (float) singleCycle.size();
|
||||
const int i0 = (int) idx;
|
||||
const int i1 = juce::jmin ((int) singleCycle.size() - 1, i0 + 1);
|
||||
const float frac = idx - (float) i0;
|
||||
resampled[(size_t) i] = singleCycle[(size_t) i0]
|
||||
+ frac * (singleCycle[(size_t) i1] - singleCycle[(size_t) i0]);
|
||||
}
|
||||
removeDC (resampled);
|
||||
enforceZeroStart (resampled);
|
||||
normalize (resampled);
|
||||
|
||||
// estimate sine-series amplitudes for harmonics
|
||||
const int Hmax = N / 2;
|
||||
std::vector<float> amps ((size_t) Hmax + 1, 0.0f);
|
||||
for (int h = 1; h <= Hmax; ++h)
|
||||
{
|
||||
double acc = 0.0;
|
||||
for (int n = 0; n < N; ++n)
|
||||
acc += (double) resampled[(size_t) n]
|
||||
* std::sin (juce::MathConstants<double>::twoPi * (double) h * (double) n / (double) N);
|
||||
amps[(size_t) h] = (float) (2.0 * acc / (double) N);
|
||||
}
|
||||
|
||||
WaveMorph morph {};
|
||||
for (int frame = 0; frame < kMorphFrames; ++frame)
|
||||
{
|
||||
const float frameAlpha = (float) frame / (float) juce::jmax (1, kMorphFrames - 1);
|
||||
|
||||
for (int level = 0; level < kMipLevels; ++level)
|
||||
{
|
||||
auto& table = morph.frames[(size_t) frame].mip[(size_t) level];
|
||||
table.assign ((size_t) N, 0.0f);
|
||||
|
||||
const float levelAttenuation = std::pow (0.5f, (float) level);
|
||||
const float limitF = (float) Hmax * levelAttenuation * juce::jlimit (0.1f, 1.0f, frameAlpha + 0.05f);
|
||||
const int harmonicLimit = juce::jlimit (1, Hmax, (int) std::floor (limitF));
|
||||
|
||||
for (int h = 1; h <= harmonicLimit; ++h)
|
||||
addSine (table, h, amps[(size_t) h]);
|
||||
|
||||
removeDC (table);
|
||||
enforceZeroStart (table);
|
||||
normalize (table);
|
||||
}
|
||||
}
|
||||
|
||||
// store into browser grid (append or replace round-robin in user region)
|
||||
if ((int) waves.size() < kBrowserCapacity)
|
||||
{
|
||||
waves.push_back (std::move (morph));
|
||||
return (int) waves.size() - 1;
|
||||
}
|
||||
|
||||
const int userCap = kBrowserCapacity - defaultTableCount;
|
||||
if (userCap <= 0) return -1;
|
||||
const int slot = defaultTableCount + (nextUserInsert % userCap);
|
||||
nextUserInsert++;
|
||||
waves[(size_t) slot] = std::move (morph);
|
||||
return slot;
|
||||
}
|
||||
|
||||
void WavetableSynthAudioProcessor::notifyPresetLoaded()
|
||||
{
|
||||
constexpr float safeMaster = 0.85f;
|
||||
if (auto* masterParam = apvts.getParameter ("MASTER"))
|
||||
{
|
||||
const float current = masterParam->convertFrom0to1 (masterParam->getValue());
|
||||
if (current > safeMaster)
|
||||
masterParam->setValueNotifyingHost (masterParam->convertTo0to1 (safeMaster));
|
||||
}
|
||||
|
||||
double sr = getSampleRate();
|
||||
if (sr <= 0.0)
|
||||
sr = 44100.0;
|
||||
|
||||
// Trigger a short fade so freshly-loaded presets come in under control.
|
||||
presetFade.reset (sr, 0.02); // gentle 20ms fade
|
||||
presetFade.setCurrentAndTargetValue (0.0f);
|
||||
presetFade.setTargetValue (1.0f);
|
||||
}
|
||||
|
||||
bool WavetableSynthAudioProcessor::isMorphLoopActive() const noexcept
|
||||
{
|
||||
const bool enabled = apvts.getRawParameterValue ("MORPH_LOOP_ON")->load() > 0.5f;
|
||||
if (! enabled)
|
||||
return false;
|
||||
return apvts.getRawParameterValue ("LFO_DEPTH")->load() > 0.0f;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
juce::AudioProcessorEditor* WavetableSynthAudioProcessor::createEditor()
|
||||
{
|
||||
return new WavetableSynthAudioProcessorEditor (*this);
|
||||
}
|
||||
|
||||
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new WavetableSynthAudioProcessor();
|
||||
}
|
||||
Reference in New Issue
Block a user