diff --git a/DrawWavesAndRevisedAudio/PluginEditor.cpp b/DrawWavesAndRevisedAudio/PluginEditor.cpp new file mode 100644 index 0000000..c1eca5d --- /dev/null +++ b/DrawWavesAndRevisedAudio/PluginEditor.cpp @@ -0,0 +1,695 @@ +#include +#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::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::pi * 1.25f; + const float end = start + juce::MathConstants::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 WavetableSynthAudioProcessorEditor::getTopPanelBounds() const +{ + return { 0, 0, getWidth(), 80 }; +} + +// ↓↓↓ reduced bottom panel height so black area is taller +juce::Rectangle 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 (p.apvts, "MORPH", morphSlider); + addAndMakeVisible (morphSlider); + + addAndMakeVisible (morphLoopToggle); + morphLoopToggleAttach = std::make_unique (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 (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 (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 (p.apvts, "CUTOFF", cutoffSlider); + attAttach = std::make_unique (p.apvts, "ATTACK", attack); + decAttach = std::make_unique (p.apvts, "DECAY", decay); + susAttach = std::make_unique (p.apvts, "SUSTAIN", sustain); + relAttach = std::make_unique (p.apvts, "RELEASE", release); + + lfoRateAttach = std::make_unique (p.apvts, "LFO_RATE", lfoRate); + lfoDepthAttach = std::make_unique (p.apvts, "LFO_DEPTH", lfoDepth); + + fenvAAttach = std::make_unique (p.apvts, "FENV_A", fenvA); + fenvDAttach = std::make_unique (p.apvts, "FENV_D", fenvD); + fenvSAttach = std::make_unique (p.apvts, "FENV_S", fenvS); + fenvRAttach = std::make_unique (p.apvts, "FENV_R", fenvR); + fenvAmtAttach = std::make_unique (p.apvts, "FENV_AMT", fenvAmt); + + // FX toggles + params + addAndMakeVisible (chorusOn); + addAndMakeVisible (reverbOn); + chOnAttach = std::make_unique (p.apvts, "CHORUS_ON", chorusOn); + rvOnAttach = std::make_unique (p.apvts, "REVERB_ON", reverbOn); + + chRateAttach = std::make_unique (p.apvts, "CH_RATE", chRate); + chDepthAttach = std::make_unique (p.apvts, "CH_DEPTH", chDepth); + chDelayAttach = std::make_unique (p.apvts, "CH_DELAY", chDelay); + chFbAttach = std::make_unique (p.apvts, "CH_FB", chFb); + chMixAttach = std::make_unique (p.apvts, "CH_MIX", chMix); + + rvRoomAttach = std::make_unique (p.apvts, "RV_ROOM", rvRoom); + rvDampAttach = std::make_unique (p.apvts, "RV_DAMP", rvDamp); + rvWidthAttach = std::make_unique (p.apvts, "RV_WIDTH", rvWidth); + rvWetAttach = std::make_unique (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 (p.apvts, "SLOT_A", slotAParam); + slotBAttach = std::make_unique (p.apvts, "SLOT_B", slotBParam); + slotCAttach = std::make_unique (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 (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 (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 (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 (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 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(); +} diff --git a/DrawWavesAndRevisedAudio/PluginEditor.h b/DrawWavesAndRevisedAudio/PluginEditor.h new file mode 100644 index 0000000..1b910ed --- /dev/null +++ b/DrawWavesAndRevisedAudio/PluginEditor.h @@ -0,0 +1,172 @@ +#pragma once + +#include +#include +#include "PluginProcessor.h" + +// === Small waveform thumbnail ================================================= +class WaveThumbnail : public juce::Component +{ +public: + WaveThumbnail (const std::vector* tbl = nullptr) : table (tbl) {} + + void setTable (const std::vector* tbl) { table = tbl; repaint(); } + + void setHighlight (bool shouldHighlight) + { + if (highlight == shouldHighlight) return; + highlight = shouldHighlight; + repaint(); + } + + void setOnClick (std::function 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* table { nullptr }; + bool highlight { false }; + std::function 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& getTable() const { return samples; } + +private: + void drawAt (const juce::MouseEvent& e); + void setSampleAt (int idx, float value); + + std::vector 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 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 + 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 + slotAAttach, slotBAttach, slotCAttach; + + // Toggles + juce::ToggleButton chorusOn { "Chorus" }, reverbOn { "Reverb" }, osc2Mute { "Deactivate Osc2" }; + std::unique_ptr chOnAttach, rvOnAttach, osc2MuteAttach; + std::unique_ptr morphLoopToggleAttach; + std::unique_ptr morphLoopModeAttach; + + // Browser grid cells + std::vector> 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 getTopPanelBounds() const; + juce::Rectangle getBottomPanelBounds() const; + + // Logic + void timerCallback() override; + void handleBrowserClick (juce::Point 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 +}; diff --git a/DrawWavesAndRevisedAudio/PluginProcessor.cpp b/DrawWavesAndRevisedAudio/PluginProcessor.cpp new file mode 100644 index 0000000..ae154f3 --- /dev/null +++ b/DrawWavesAndRevisedAudio/PluginProcessor.cpp @@ -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 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 (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& 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& 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 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& 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& 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::twoPi * k * (float) n / (float) N); +} + +void WavetableSynthAudioProcessor::removeDC (std::vector& 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& 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 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::pi * (float) k)) + * std::sin (juce::MathConstants::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* 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> p; + + // Master first so editor can attach even if others change + p.push_back (std::make_unique( + "MASTER", "Master", juce::NormalisableRange (0.0f, 1.5f, 0.0f, 0.5f), 0.75f)); + + // Morph + LFO + p.push_back (std::make_unique("MORPH", "Morph", + juce::NormalisableRange (0.0f, 1.0f), 0.0f)); + p.push_back (std::make_unique("MORPH_LOOP_ON", "Morph Loop", false)); + p.push_back (std::make_unique("MORPH_LOOP_MODE", "Morph Loop Mode", + juce::StringArray { "Forward", "Ping-Pong", "Half Trip" }, 0)); + p.push_back (std::make_unique("LFO_RATE", "LFO Rate", + juce::NormalisableRange (0.01f, 10.0f, 0.0f, 0.4f), 0.2f)); + p.push_back (std::make_unique("LFO_DEPTH", "LFO Depth", + juce::NormalisableRange (0.0f, 1.0f), 0.0f)); + + // ADSR + p.push_back (std::make_unique("ATTACK", "Attack", + juce::NormalisableRange (0.001f, 5.0f, 0.0f, 0.5f), 0.01f)); + p.push_back (std::make_unique("DECAY", "Decay", + juce::NormalisableRange (0.001f, 5.0f, 0.0f, 0.5f), 0.2f)); + p.push_back (std::make_unique("SUSTAIN", "Sustain", + juce::NormalisableRange (0.0f, 1.0f), 0.8f)); + p.push_back (std::make_unique("RELEASE", "Release", + juce::NormalisableRange (0.001f, 5.0f, 0.0f, 0.5f), 0.3f)); + + // Filter + filter env + p.push_back (std::make_unique("CUTOFF", "Cutoff", + juce::NormalisableRange (20.0f, 20000.0f, 0.0f, 0.5f), 8000.0f)); + p.push_back (std::make_unique("FENV_A", "FEnv A", + juce::NormalisableRange (0.001f, 5.0f, 0.0f, 0.5f), 0.01f)); + p.push_back (std::make_unique("FENV_D", "FEnv D", + juce::NormalisableRange (0.001f, 5.0f, 0.0f, 0.5f), 0.2f)); + p.push_back (std::make_unique("FENV_S", "FEnv S", + juce::NormalisableRange (0.0f, 1.0f), 0.0f)); + p.push_back (std::make_unique("FENV_R", "FEnv R", + juce::NormalisableRange (0.001f, 5.0f, 0.0f, 0.5f), 0.3f)); + p.push_back (std::make_unique("FENV_AMT", "FEnv Amt", + juce::NormalisableRange (0.0f, 1.0f), 0.5f)); + + // Browser slot indices + p.push_back (std::make_unique("SLOT_A", "Slot A", 0, kBrowserCapacity - 1, 0)); + p.push_back (std::make_unique("SLOT_B", "Slot B", 0, kBrowserCapacity - 1, 1)); + p.push_back (std::make_unique("SLOT_C", "Slot C", 0, kBrowserCapacity - 1, 2)); + + // Osc2 mute toggle + p.push_back (std::make_unique("OSC2_MUTE", "Deactivate Osc2", false)); + + // Chorus / Reverb (keep for GUI; safe defaults) + p.push_back (std::make_unique("CHORUS_ON", "Chorus On", false)); + p.push_back (std::make_unique("CH_RATE", "Ch Rate", + juce::NormalisableRange (0.05f, 5.0f, 0.0f, 0.5f), 1.2f)); + p.push_back (std::make_unique("CH_DEPTH","Ch Depth", + juce::NormalisableRange (0.0f, 1.0f), 0.3f)); + p.push_back (std::make_unique("CH_DELAY","Ch Delay", + juce::NormalisableRange (1.0f, 30.0f), 8.0f)); + p.push_back (std::make_unique("CH_FB", "Ch Fb", + juce::NormalisableRange (-0.95f, 0.95f), 0.0f)); + p.push_back (std::make_unique("CH_MIX", "Ch Mix", + juce::NormalisableRange (0.0f, 1.0f), 0.25f)); + + p.push_back (std::make_unique("REVERB_ON", "Reverb On", true)); + p.push_back (std::make_unique("RV_ROOM", "Rv Room", + juce::NormalisableRange (0.0f, 1.0f), 0.4f)); + p.push_back (std::make_unique("RV_DAMP","Rv Damp", + juce::NormalisableRange (0.0f, 1.0f), 0.3f)); + p.push_back (std::make_unique("RV_WIDTH","Rv Width", + juce::NormalisableRange (0.0f, 1.0f), 1.0f)); + p.push_back (std::make_unique("RV_WET", "Rv Wet", + juce::NormalisableRange (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& 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 (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 stageStart { 0.0f, 0.5f, 0.0f, 1.0f }; + static constexpr std::array 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 blk (buffer); + if (chorusOn) chorus.process (juce::dsp::ProcessContextReplacing (blk)); + if (reverbOn) reverb.process (juce::dsp::ProcessContextReplacing (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& singleCycle) +{ + const int N = kTableSize; + std::vector 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 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::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(); +}