From 044e3b7a52394dcdea9d24d407579fe1a302d17c Mon Sep 17 00:00:00 2001 From: Armin Date: Sun, 19 Jul 2026 18:53:17 +0200 Subject: [PATCH] recent --- .gitignore | 1 - CMakeLists.txt | 3 +- Makefile | 5 +- Source/PluginEditor.cpp | 345 ++++++++++++++++++++++++++++++++++--- Source/PluginEditor.h | 29 +++- Source/PluginProcessor.cpp | 267 +++++++++++++++++++++++++++- Source/PluginProcessor.h | 11 ++ 7 files changed, 616 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 888366b..ee21616 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,3 @@ Builds/ *.xcuserdata DerivedData/ .DS_Store -JUCE diff --git a/CMakeLists.txt b/CMakeLists.txt index c738a49..ede812b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(Trommelkiste VERSION 1.0.0 LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -add_subdirectory(JUCE) +add_subdirectory(build/JUCE) juce_add_plugin(Trommelkiste PRODUCT_NAME "Trommelkiste" @@ -64,6 +64,7 @@ target_link_libraries(Trommelkiste juce::juce_audio_utils juce::juce_core juce::juce_data_structures + juce::juce_dsp juce::juce_events juce::juce_graphics juce::juce_gui_basics diff --git a/Makefile b/Makefile index f8a8f93..1e97acb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -JUCE := JUCE +JUCE := build/JUCE NPROC := $(shell sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4) UNAME_S := $(shell uname -s) @@ -52,7 +52,8 @@ all: build/Makefile @find $(BUILD_DIR) -type d \( -name "*.vst3" -o -name "*.component" -o -name "*.app" \) 2>/dev/null | while read f; do echo " $$f"; done build/Makefile: CMakeLists.txt - @if [ ! -d "$(JUCE)" ]; then \ + @if [ ! -f "$(JUCE)/CMakeLists.txt" ]; then \ + rm -rf "$(JUCE)"; \ echo "==> Cloning JUCE..."; \ git clone --depth 1 https://github.com/juce-framework/JUCE.git $(JUCE); \ fi diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 4990c43..d62c57f 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -9,13 +9,34 @@ static const char* trackNames[] = {"BD", "RS", "SN", "CL", "CH", "PH", "OH", "R static const char* noteNames[] = {"C4","C#4","D4","D#4","E4","F4","F#4","G4","G#4","A4"}; static constexpr int ROW_H = 52; -static constexpr int TOP = 44; +static constexpr int TOP = 80; static constexpr int COL_LBL = 8; static constexpr int COL_KNOB = 44; static constexpr int KNOB_W = 44; static constexpr int KNOB_GAP = 4; static constexpr int COL_DECAY = COL_KNOB + 6 * (KNOB_W + KNOB_GAP) + 8; static constexpr int COL_FILE = COL_DECAY + 78; +static constexpr int SAMPLE_W = 110; +static constexpr int COL_DELAY = COL_FILE + SAMPLE_W + 8; +static constexpr int COL_REVERB = COL_DELAY + KNOB_W + KNOB_GAP; +static constexpr int COL_RINGMOD = COL_REVERB + KNOB_W + KNOB_GAP; +static constexpr int COL_DESTR = COL_RINGMOD + KNOB_W + KNOB_GAP; +static constexpr int COL_SHUFFLE = COL_DESTR + KNOB_W + KNOB_GAP; +static constexpr int TOTAL_W = COL_SHUFFLE + KNOB_W + 8; + +// Header knob x positions (compact layout) +static constexpr int HDR_KNOB = 36; +static constexpr int HDR_GAP = 4; +static constexpr int HDR_VOL = 356; +static constexpr int HDR_PAN = HDR_VOL + HDR_KNOB + HDR_GAP; +static constexpr int HDR_RING = HDR_PAN + HDR_KNOB + HDR_GAP; +static constexpr int HDR_DEST = HDR_RING + HDR_KNOB + HDR_GAP; +static constexpr int HDR_DELAY = HDR_DEST + HDR_KNOB + HDR_GAP; +static constexpr int HDR_REVERB= HDR_DELAY + HDR_KNOB + HDR_GAP; +static constexpr int HDR_DLYSYNC= HDR_REVERB+ HDR_KNOB + HDR_GAP; +static constexpr int HDR_DIV = HDR_DLYSYNC+ HDR_KNOB + HDR_GAP + 12; +static constexpr int HDR_SHFL = HDR_DIV + HDR_KNOB + HDR_GAP; +static constexpr int HDR_GRV = HDR_SHFL + HDR_KNOB + HDR_GAP; // ── LookAndFeel ────────────────────────────────────────────────── @@ -29,16 +50,58 @@ void TrommelkisteLookAndFeel::drawRotarySlider(Graphics& g, int x, int y, int w, auto cy = bounds.getCentreY(); auto angle = startAngle + sliderPos * (endAngle - startAngle); - g.setColour(Colour(0xFF333333)); - g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f); + // Outer shadow + { + ColourGradient grad(Colour(0x40000000), cx, cy + radius * 0.15f, + Colour(0x00000000), cx, cy + radius * 0.6f, true); + g.setGradientFill(grad); + g.fillEllipse(cx - radius - 1.0f, cy - radius + 2.0f, (radius + 1.0f) * 2.0f, (radius + 1.0f) * 2.0f); + } + // Main body — vertical gradient (light top, dark bottom) for 3D cylinder feel + { + ColourGradient grad(Colour(0xFF444444), cx, cy - radius, + Colour(0xFF2C2C2C), cx, cy + radius, true); + grad.addColour(0.35, Colour(0xFF3D3D3D)); + g.setGradientFill(grad); + g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f); + } + + // Top highlight ring (specular edge) + { + Path ring; + ring.addCentredArc(cx, cy, radius - 0.5f, radius - 0.5f, 0.0f, 0.0f, MathConstants::pi, true); + g.setColour(Colour(0x1EFFFFFF)); + g.strokePath(ring, PathStrokeType(1.5f, PathStrokeType::curved, PathStrokeType::rounded)); + } + + // Bottom dark ring + { + Path ring; + ring.addCentredArc(cx, cy, radius - 0.5f, radius - 0.5f, 0.0f, + MathConstants::pi, MathConstants::pi * 2.0f, true); + g.setColour(Colour(0x1E000000)); + g.strokePath(ring, PathStrokeType(1.5f, PathStrokeType::curved, PathStrokeType::rounded)); + } + + // Subtle specular highlight blob (top-left) + { + ColourGradient spec(Colour(0x18FFFFFF), cx - radius * 0.35f, cy - radius * 0.5f, + Colour(0x00FFFFFF), cx - radius * 0.1f, cy - radius * 0.1f, true); + spec.isRadial = true; + g.setGradientFill(spec); + g.fillEllipse(cx - radius * 0.7f, cy - radius * 0.85f, radius * 0.9f, radius * 0.7f); + } + + // Indicator arc { Path arc; - arc.addCentredArc(cx, cy, radius - 1.5f, radius - 1.5f, 0.0f, startAngle, angle, true); + arc.addCentredArc(cx, cy, radius - 3.0f, radius - 3.0f, 0.0f, startAngle, angle, true); g.setColour(accent); g.strokePath(arc, PathStrokeType(2.5f, PathStrokeType::curved, PathStrokeType::rounded)); } + // Pointer / indicator line { Path ptr; ptr.addRectangle(-1.25f, -radius + 2.0f, 2.5f, radius * 0.5f); @@ -46,6 +109,14 @@ void TrommelkisteLookAndFeel::drawRotarySlider(Graphics& g, int x, int y, int w, g.setColour(Colours::white); g.fillPath(ptr); } + + // Center cap — small raised circle + { + ColourGradient cap(Colour(0xFF414141), cx, cy - radius * 0.22f, + Colour(0xFF313131), cx, cy + radius * 0.22f, true); + g.setGradientFill(cap); + g.fillEllipse(cx - radius * 0.22f, cy - radius * 0.22f, radius * 0.44f, radius * 0.44f); + } } void TrommelkisteLookAndFeel::drawButtonBackground(Graphics& g, Button& btn, const Colour&, @@ -85,6 +156,49 @@ void TrommelkisteLookAndFeel::positionComboBoxText(ComboBox& box, Label& label) label.setFont(font(11.0f, fontScale)); } +void TrommelkisteLookAndFeel::drawToggleButton(Graphics& g, ToggleButton& btn, + bool /*isMouseOver*/, bool /*isButtonDown*/) +{ + auto bounds = btn.getLocalBounds().toFloat(); + const bool on = btn.getToggleState(); + const float corner = 4.0f; + + // Track background + g.setColour(Colour(0xFF2A2A2A)); + g.fillRoundedRectangle(bounds, corner); + + // Active half highlight + auto half = bounds; + half.setWidth(half.getWidth() * 0.5f); + if (on) + half.setX(half.getX() + half.getWidth()); + + g.setColour(Colour(0xFF444444)); + g.fillRoundedRectangle(half.expanded(-1.0f, -1.0f), corner - 1.0f); + + // Thumb / knob + auto thumb = bounds; + thumb.setWidth(thumb.getWidth() * 0.5f); + if (on) + thumb.setX(thumb.getX() + thumb.getWidth()); + thumb = thumb.reduced(2.0f); + + ColourGradient thumbGrad(Colour(0xFF555555), thumb.getCentreX(), thumb.getY(), + Colour(0xFF333333), thumb.getCentreX(), thumb.getBottom(), true); + g.setGradientFill(thumbGrad); + g.fillRoundedRectangle(thumb, 3.0f); + + // Labels + g.setColour(on ? Colours::white.withAlpha(0.4f) : Colours::white.withAlpha(0.9f)); + g.setFont(fontBold(8.0f, fontScale)); + g.drawText("SAW", bounds.getX(), bounds.getY(), + bounds.getWidth() * 0.5f, bounds.getHeight(), Justification::centred); + + g.setColour(on ? Colours::white.withAlpha(0.9f) : Colours::white.withAlpha(0.4f)); + g.drawText("GATE", bounds.getX() + bounds.getWidth() * 0.5f, bounds.getY(), + bounds.getWidth() * 0.5f, bounds.getHeight(), Justification::centred); +} + // ── Editor ─────────────────────────────────────────────────────── TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) @@ -93,7 +207,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) setLookAndFeel(&lnf); lnf.fontScale = 1.5f; uiScale = 1.5f; - setSize(scaled(660), scaled(TOP + NUM_TRACKS * ROW_H + 12)); + setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H + 12)); setResizable(false, false); setWantsKeyboardFocus(false); @@ -118,6 +232,11 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) setupKnob(t.pitch, "pitch"); setupKnob(t.tone, "tone"); setupKnob(t.pan, "pan"); + setupKnob(t.delaySend, "delaySend"); + setupKnob(t.reverbSend, "reverbSend"); + setupKnob(t.ringMod, "ringMod"); + setupKnob(t.destruction,"destruction"); + setupKnob(t.shuffle, "shuffle"); t.level.formatFn = [](double v) { return String(v, 1); }; t.length.formatFn = [](double v) { return String(v, 1) + "s"; }; @@ -131,6 +250,11 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) int pct = (int) std::round(std::abs(v) * 100.0); return (v < 0 ? "L" : "R") + String(pct); }; + t.delaySend.formatFn = [](double v) { return String(v, 1); }; + t.reverbSend.formatFn = [](double v) { return String(v, 1); }; + t.ringMod.formatFn = [](double v) { return String(v, 1); }; + t.destruction.formatFn = [](double v) { return String(v, 1); }; + t.shuffle.formatFn = [](double v) { return String(v, 1); }; t.levelAtt = std::make_unique(proc.apvts, pfx + "level", t.level); t.lengthAtt = std::make_unique(proc.apvts, pfx + "length", t.length); @@ -138,14 +262,18 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) t.pitchAtt = std::make_unique(proc.apvts, pfx + "pitch", t.pitch); t.toneAtt = std::make_unique(proc.apvts, pfx + "tone", t.tone); t.panAtt = std::make_unique(proc.apvts, pfx + "pan", t.pan); - t.panAtt = std::make_unique(proc.apvts, pfx + "pan", t.pan); + t.delaySendAtt = std::make_unique(proc.apvts, pfx + "delaySend", t.delaySend); + t.reverbSendAtt = std::make_unique(proc.apvts, pfx + "reverbSend", t.reverbSend); + t.ringModAtt = std::make_unique(proc.apvts, pfx + "ringMod", t.ringMod); + t.destructionAtt = std::make_unique(proc.apvts, pfx + "destruction", t.destruction); + t.shuffleAtt = std::make_unique(proc.apvts, pfx + "shuffle", t.shuffle); - t.decay.addItemList({"Saw", "Gate"}, 1); - t.decay.setColour(ComboBox::backgroundColourId, Colour(0xFF333333)); - t.decay.setColour(ComboBox::textColourId, Colours::white); - t.decay.setColour(ComboBox::outlineColourId, Colour(0xFF555555)); + t.decay.setClickingTogglesState(true); + t.decay.setButtonText(""); + t.decay.setColour(ToggleButton::textColourId, Colours::white); + t.decay.setColour(ToggleButton::tickColourId, lnf.accent); addAndMakeVisible(t.decay); - t.decayAtt = std::make_unique(proc.apvts, pfx + "decay", t.decay); + t.decayAtt = std::make_unique(proc.apvts, pfx + "decay", t.decay); t.prevBtn.onClick = [this, i] { proc.prevSample(i); @@ -209,6 +337,88 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) scaleCombo.onChange = [this] { scaleChanged(); }; addAndMakeVisible(scaleCombo); + // Global effect knobs in header + auto setupHeaderKnob = [&](KnobSlider& s, const String& id, const String& paramId) + { + s.setSliderStyle(Slider::RotaryHorizontalVerticalDrag); + s.setTextBoxStyle(Slider::TextBoxBelow, false, 40, 14); + s.setColour(Slider::textBoxTextColourId, Colours::white); + s.setColour(Slider::textBoxOutlineColourId, Colours::transparentBlack); + s.setColour(Slider::textBoxBackgroundColourId, Colours::transparentBlack); + addAndMakeVisible(s); + }; + + setupHeaderKnob(globalRingFreq, "RF", "global_ringFreq"); + setupHeaderKnob(globalDestruction, "DEST", "global_destruction"); + globalRingFreq.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + globalDestruction.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + globalRingFreq.formatFn = [](double v) { return String((int)v) + "Hz"; }; + globalDestruction.formatFn = [](double v) { return String(v, 1); }; + globalRingFreqAtt = std::make_unique(proc.apvts, "global_ringFreq", globalRingFreq); + globalDestructionAtt = std::make_unique(proc.apvts, "global_destruction", globalDestruction); + + setupHeaderKnob(masterVolume, "VOL", "master_volume"); + setupHeaderKnob(masterPan, "PAN", "master_pan"); + masterVolume.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + masterPan.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + masterVolume.formatFn = [](double v) { return String(v, 1); }; + masterPan.formatFn = [](double v) -> String { + if (std::abs(v) < 0.05) return "C"; + int pct = (int) std::round(std::abs(v) * 100.0); + return (v < 0 ? "L" : "R") + String(pct); + }; + masterVolumeAtt = std::make_unique(proc.apvts, "master_volume", masterVolume); + masterPanAtt = std::make_unique(proc.apvts, "master_pan", masterPan); + + setupHeaderKnob(masterDelay, "DELAY", "master_delay"); + setupHeaderKnob(masterReverb, "REVERB", "master_reverb"); + masterDelay.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + masterReverb.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + masterDelay.formatFn = [](double v) { return String(v, 1); }; + masterReverb.formatFn = [](double v) { return String(v, 1); }; + masterDelayAtt = std::make_unique(proc.apvts, "master_delay", masterDelay); + masterReverbAtt = std::make_unique(proc.apvts, "master_reverb", masterReverb); + + // Shuffle section knobs + globalShuffleDiv.setSliderStyle(Slider::RotaryHorizontalVerticalDrag); + globalShuffleDiv.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + globalShuffleDiv.setColour(Slider::textBoxTextColourId, Colours::white); + globalShuffleDiv.setColour(Slider::textBoxOutlineColourId, Colours::transparentBlack); + globalShuffleDiv.setColour(Slider::textBoxBackgroundColourId, Colours::transparentBlack); + globalShuffleDiv.setRange(0, 4, 1); + globalShuffleDiv.setScrollWheelEnabled(false); + globalShuffleDiv.formatFn = [](double v) { + static const char* labels[] = { "OFF", "4", "8", "16", "32" }; + return labels[juce::jlimit(0, 4, (int)v)]; + }; + addAndMakeVisible(globalShuffleDiv); + + setupHeaderKnob(globalShuffleAmt, "SHFL", "global_shuffleAmount"); + setupHeaderKnob(globalGroove, "GRV", "global_groove"); + globalShuffleAmt.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + globalGroove.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + globalShuffleAmt.formatFn = [](double v) { return String(v, 1); }; + globalGroove.formatFn = [](double v) { return String(v, 1); }; + + globalShuffleDivAtt = std::make_unique(proc.apvts, "global_shuffleDiv", globalShuffleDiv); + globalShuffleAmtAtt = std::make_unique(proc.apvts, "global_shuffleAmount", globalShuffleAmt); + globalGrooveAtt = std::make_unique(proc.apvts, "global_groove", globalGroove); + + // Delay sync knob + delaySyncDiv.setSliderStyle(Slider::RotaryHorizontalVerticalDrag); + delaySyncDiv.setTextBoxStyle(Slider::NoTextBox, false, 0, 0); + delaySyncDiv.setColour(Slider::textBoxTextColourId, Colours::white); + delaySyncDiv.setColour(Slider::textBoxOutlineColourId, Colours::transparentBlack); + delaySyncDiv.setColour(Slider::textBoxBackgroundColourId, Colours::transparentBlack); + delaySyncDiv.setRange(0, 8, 1); + delaySyncDiv.setScrollWheelEnabled(false); + delaySyncDiv.formatFn = [](double v) { + static const char* labels[] = { "OFF", "1/4", "1/2", "3/4", "1/1", "5/4", "6/4", "7/4", "2/1" }; + return labels[juce::jlimit(0, 8, (int)v)]; + }; + addAndMakeVisible(delaySyncDiv); + delaySyncDivAtt = std::make_unique(proc.apvts, "delay_syncDiv", delaySyncDiv); + startTimerHz(30); resized(); } @@ -218,6 +428,17 @@ TrommelkisteEditor::~TrommelkisteEditor() setLookAndFeel(nullptr); } +void TrommelkisteEditor::timerCallback() +{ + const double now = juce::Time::getMillisecondCounterHiRes() / 1000.0; + for (int i = 0; i < NUM_TRACKS; ++i) + { + if (proc.trackActive[i]) + trackActivatedTime[i] = now; + } + repaint(); +} + void TrommelkisteEditor::scaleChanged() { static const float scales[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f }; @@ -226,7 +447,7 @@ void TrommelkisteEditor::scaleChanged() { uiScale = scales[selId - 1]; lnf.fontScale = uiScale; - setSize(scaled(660), scaled(TOP + NUM_TRACKS * ROW_H + 12)); + setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H + 12)); } } @@ -259,19 +480,45 @@ void TrommelkisteEditor::paint(Graphics& g) g.drawText("TROMMELKISTE", scaled(10), scaled(4), scaled(400), scaled(20), Justification::centredLeft); g.setColour(lnf.accent.withAlpha(0.3f)); - g.fillRect(scaled(10), scaled(26), scaled(getWidth() - 20), scaled(1)); + g.fillRect(scaled(10), scaled(56), scaled(getWidth() - 20), scaled(1)); + + g.setColour(Colours::white.withAlpha(0.9f)); + g.setFont(font(8.0f, uiScale)); + g.drawText("VOL", scaled(HDR_VOL), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("PAN", scaled(HDR_PAN), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("RING", scaled(HDR_RING), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("DEST", scaled(HDR_DEST), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("DLY", scaled(HDR_DELAY), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("REV", scaled(HDR_REVERB), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("SYNC", scaled(HDR_DLYSYNC), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + + // Shuffle section separator + g.setColour(lnf.accent.withAlpha(0.5f)); + g.fillRect(scaled(HDR_DIV - 8), scaled(6), scaled(1), scaled(46)); + + g.setColour(Colours::white.withAlpha(0.9f)); + g.setFont(font(8.0f, uiScale)); + g.drawText("DIV", scaled(HDR_DIV), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("SHFL", scaled(HDR_SHFL), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("GRV", scaled(HDR_GRV), scaled(46), scaled(HDR_KNOB), scaled(10), Justification::centred); g.setColour(Colours::white.withAlpha(0.9f)); g.setFont(font(9.0f, uiScale)); - g.drawText("LVL", scaled(COL_KNOB), scaled(30), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("LEN", scaled(COL_KNOB + 1*(KNOB_W + KNOB_GAP)), scaled(30), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("VEL", scaled(COL_KNOB + 2*(KNOB_W + KNOB_GAP)), scaled(30), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("PIT", scaled(COL_KNOB + 3*(KNOB_W + KNOB_GAP)), scaled(30), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("TONE", scaled(COL_KNOB + 4*(KNOB_W + KNOB_GAP)), scaled(30), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("PAN", scaled(COL_KNOB + 5*(KNOB_W + KNOB_GAP)), scaled(30), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("DECAY", scaled(COL_DECAY), scaled(30), scaled(68), scaled(12), Justification::centred); - g.drawText("SAMPLE", scaled(COL_FILE), scaled(30), scaled(100), scaled(12), Justification::centredLeft); + g.drawText("LVL", scaled(COL_KNOB), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("LEN", scaled(COL_KNOB + 1*(KNOB_W + KNOB_GAP)), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("VEL", scaled(COL_KNOB + 2*(KNOB_W + KNOB_GAP)), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("PIT", scaled(COL_KNOB + 3*(KNOB_W + KNOB_GAP)), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("TONE", scaled(COL_KNOB + 4*(KNOB_W + KNOB_GAP)), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("PAN", scaled(COL_KNOB + 5*(KNOB_W + KNOB_GAP)), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("DECAY", scaled(COL_DECAY), scaled(60), scaled(68), scaled(12), Justification::centred); + g.drawText("SAMPLE", scaled(COL_FILE), scaled(60), scaled(SAMPLE_W), scaled(12), Justification::centred); + g.drawText("DELAY", scaled(COL_DELAY), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("REVERB", scaled(COL_REVERB), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("RING", scaled(COL_RINGMOD), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("DESTR", scaled(COL_DESTR), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("SHFL", scaled(COL_SHUFFLE), scaled(60), scaled(KNOB_W), scaled(12), Justification::centred); + const double now = juce::Time::getMillisecondCounterHiRes() / 1000.0; for (int i = 0; i < NUM_TRACKS; ++i) { const int ry = TOP + i * ROW_H; @@ -281,6 +528,27 @@ void TrommelkisteEditor::paint(Graphics& g) g.fillRect(0, scaled(ry), getWidth(), scaled(ROW_H)); } + g.setColour(Colour(0xFF3A3A2E).withAlpha(0.4f)); + g.fillRect(scaled(COL_KNOB + 3 * (KNOB_W + KNOB_GAP)), scaled(ry), scaled(KNOB_W), scaled(ROW_H)); + + g.setColour(Colour(0xFF2E3A3A).withAlpha(0.4f)); + g.fillRect(scaled(COL_DELAY), scaled(ry), scaled(KNOB_W), scaled(ROW_H)); + + g.setColour(Colour(0xFF3A2E3A).withAlpha(0.4f)); + g.fillRect(scaled(COL_REVERB), scaled(ry), scaled(KNOB_W), scaled(ROW_H)); + + g.setColour(Colour(0xFF3A3A1E).withAlpha(0.4f)); + g.fillRect(scaled(COL_RINGMOD), scaled(ry), scaled(KNOB_W), scaled(ROW_H)); + + g.setColour(Colour(0xFF3A1E1E).withAlpha(0.4f)); + g.fillRect(scaled(COL_DESTR), scaled(ry), scaled(KNOB_W), scaled(ROW_H)); + + g.setColour(Colour(0xFF1E3A3A).withAlpha(0.4f)); + g.fillRect(scaled(COL_SHUFFLE), scaled(ry), scaled(KNOB_W), scaled(ROW_H)); + + g.setColour(Colour(0xFF2E3A2E).withAlpha(0.4f)); + g.fillRect(scaled(COL_KNOB), scaled(ry), scaled(KNOB_W), scaled(ROW_H)); + g.setColour(Colours::white); g.setFont(fontBold(12.0f, uiScale)); g.drawText(trackNames[i], scaled(COL_LBL), scaled(ry + 6), scaled(32), scaled(16), Justification::centredLeft); @@ -289,9 +557,11 @@ void TrommelkisteEditor::paint(Graphics& g) g.setFont(font(9.0f, uiScale)); g.drawText(noteNames[i], scaled(COL_LBL), scaled(ry + 24), scaled(32), scaled(14), Justification::centredLeft); - if (proc.trackActive[i]) + const double elapsed = now - trackActivatedTime[i]; + if (elapsed < 0.4) { - g.setColour(lnf.accent); + float alpha = elapsed < 0.2 ? 1.0f : (float) (1.0 - (elapsed - 0.2) / 0.2); + g.setColour(lnf.accent.withAlpha(alpha)); g.fillRect(scaled(0), scaled(ry + 4), scaled(3), scaled(ROW_H - 8)); } } @@ -300,10 +570,23 @@ void TrommelkisteEditor::paint(Graphics& g) void TrommelkisteEditor::resized() { // Header: title is painted; position controls in top row - presetPrevBtn.setBounds(scaled(440), scaled(4), scaled(20), scaled(20)); - presetCombo.setBounds(scaled(462), scaled(4), scaled(110), scaled(20)); - presetNextBtn.setBounds(scaled(574), scaled(4), scaled(20), scaled(20)); - scaleCombo.setBounds(scaled(598), scaled(4), scaled(56), scaled(20)); + presetPrevBtn.setBounds(scaled(150), scaled(14), scaled(20), scaled(20)); + presetCombo.setBounds(scaled(172), scaled(14), scaled(100), scaled(20)); + presetNextBtn.setBounds(scaled(274), scaled(14), scaled(20), scaled(20)); + scaleCombo.setBounds(scaled(298), scaled(14), scaled(52), scaled(20)); + + // Header knobs + const int hk = HDR_KNOB; + masterVolume.setBounds(scaled(HDR_VOL), scaled(8), scaled(hk), scaled(hk)); + masterPan.setBounds(scaled(HDR_PAN), scaled(8), scaled(hk), scaled(hk)); + globalRingFreq.setBounds(scaled(HDR_RING), scaled(8), scaled(hk), scaled(hk)); + globalDestruction.setBounds(scaled(HDR_DEST), scaled(8), scaled(hk), scaled(hk)); + masterDelay.setBounds(scaled(HDR_DELAY), scaled(8), scaled(hk), scaled(hk)); + masterReverb.setBounds(scaled(HDR_REVERB), scaled(8), scaled(hk), scaled(hk)); + delaySyncDiv.setBounds(scaled(HDR_DLYSYNC), scaled(8), scaled(hk), scaled(hk)); + globalShuffleDiv.setBounds(scaled(HDR_DIV), scaled(8), scaled(hk), scaled(hk)); + globalShuffleAmt.setBounds(scaled(HDR_SHFL), scaled(8), scaled(hk), scaled(hk)); + globalGroove.setBounds(scaled(HDR_GRV), scaled(8), scaled(hk), scaled(hk)); for (int i = 0; i < NUM_TRACKS; ++i) { @@ -317,10 +600,16 @@ void TrommelkisteEditor::resized() knobs[k]->setBounds(scaled(kx), scaled(ry + 3), scaled(KNOB_W), scaled(ROW_H - 4)); } + t.delaySend.setBounds(scaled(COL_DELAY), scaled(ry + 3), scaled(KNOB_W), scaled(ROW_H - 4)); + t.reverbSend.setBounds(scaled(COL_REVERB), scaled(ry + 3), scaled(KNOB_W), scaled(ROW_H - 4)); + t.ringMod.setBounds(scaled(COL_RINGMOD), scaled(ry + 3), scaled(KNOB_W), scaled(ROW_H - 4)); + t.destruction.setBounds(scaled(COL_DESTR), scaled(ry + 3), scaled(KNOB_W), scaled(ROW_H - 4)); + t.shuffle.setBounds(scaled(COL_SHUFFLE), scaled(ry + 3), scaled(KNOB_W), scaled(ROW_H - 4)); + t.decay.setBounds(scaled(COL_DECAY), scaled(ry + 16), scaled(68), scaled(22)); const int fileX = COL_FILE; - const int fileEnd = 660 - 8; + const int fileEnd = COL_DELAY - 8; t.prevBtn.setBounds(scaled(fileX), scaled(ry + 16), scaled(18), scaled(22)); t.nextBtn.setBounds(scaled(fileEnd - 18), scaled(ry + 16), scaled(18), scaled(22)); t.fileLabel.setBounds(scaled(fileX + 20), scaled(ry + 16), diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 8841350..33f8b07 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -12,6 +12,7 @@ public: void drawRotarySlider(juce::Graphics&, int x, int y, int w, int h, float pos, float start, float end, juce::Slider&) override; + void drawToggleButton(juce::Graphics&, juce::ToggleButton&, bool, bool) override; void drawButtonBackground(juce::Graphics&, juce::Button&, const juce::Colour&, bool, bool) override; void drawComboBox(juce::Graphics&, int w, int h, bool down, @@ -38,7 +39,7 @@ public: void paint(juce::Graphics&) override; void resized() override; void mouseDown(const juce::MouseEvent&) override; - void timerCallback() override { repaint(); } + void timerCallback() override; private: TrommelkisteProcessor& proc; @@ -53,26 +54,46 @@ private: struct TrackUI { using SliderAtt = juce::AudioProcessorValueTreeState::SliderAttachment; - using ComboAtt = juce::AudioProcessorValueTreeState::ComboBoxAttachment; + using ButtonAtt = juce::AudioProcessorValueTreeState::ButtonAttachment; juce::TextButton prevBtn{"<"}; juce::TextButton nextBtn{">"}; KnobSlider level, length, velocity, pitch, tone, pan; - juce::ComboBox decay; + KnobSlider delaySend, reverbSend, ringMod, destruction, shuffle; + juce::ToggleButton decay; juce::Label fileLabel; std::unique_ptr levelAtt, lengthAtt, velAtt, pitchAtt, toneAtt, panAtt; - std::unique_ptr decayAtt; + std::unique_ptr delaySendAtt, reverbSendAtt; + std::unique_ptr ringModAtt, destructionAtt, shuffleAtt; + std::unique_ptr decayAtt; }; TrackUI ui[NUM_TRACKS]; juce::FileChooser chooser{"Load Sample", juce::File{}, "*.wav;*.aif;*.aiff;*.flac"}; + double trackActivatedTime[NUM_TRACKS] = {}; // Preset selector juce::ComboBox presetCombo; juce::TextButton presetPrevBtn{"<"}; juce::TextButton presetNextBtn{">"}; + // Global effect controls in header + KnobSlider globalRingFreq, globalDestruction; + std::unique_ptr globalRingFreqAtt, globalDestructionAtt; + + // Master controls in header + KnobSlider masterVolume, masterPan, masterDelay, masterReverb; + std::unique_ptr masterVolumeAtt, masterPanAtt, masterDelayAtt, masterReverbAtt; + + // Global shuffle controls in header + KnobSlider globalShuffleDiv, globalShuffleAmt, globalGroove; + std::unique_ptr globalShuffleDivAtt, globalShuffleAmtAtt, globalGrooveAtt; + + // Delay sync control in header + KnobSlider delaySyncDiv; + std::unique_ptr delaySyncDivAtt; + void loadForTrack(int idx); void updateFileLabel(int idx); void refreshPresetCombo(); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 41ad0ea..e681902 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -29,6 +29,24 @@ std::optional TrommelkisteProcessor::getNameForMidiNoteNumber(int void TrommelkisteProcessor::prepareToPlay(double sr, int) { currentSampleRate = sr; + + juce::dsp::ProcessSpec spec; + spec.sampleRate = sr; + spec.maximumBlockSize = 512; + spec.numChannels = 2; + reverb.prepare(spec); + reverbParams.roomSize = 0.5f; + reverbParams.damping = 0.5f; + reverbParams.wetLevel = 0.33f; + reverbParams.dryLevel = 0.8f; + reverbParams.width = 1.0f; + reverbParams.freezeMode = 0.0f; + reverb.setParameters(reverbParams); + + const int delaySamples = DELAY_BUFFER_SECONDS * (int)sr; + delayBuffer.setSize(2, delaySamples); + delayBuffer.clear(); + delayWritePos = 0; } void TrommelkisteProcessor::releaseResources() {} @@ -42,6 +60,34 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: for (int i = 0; i < NUM_TRACKS; ++i) trackActive[i] = false; + // Shuffle: add ghost notes (bar-quantized) + const float globalShuffleAmt = apvts.getRawParameterValue("global_shuffleAmount")->load(); + const float globalGroove = apvts.getRawParameterValue("global_groove")->load(); + const int shuffleDivIdx = (int)apvts.getRawParameterValue("global_shuffleDiv")->load(); + // 0=OFF, 1=every 4 bars, 2=every 8, 3=every 16, 4=every 32 + static constexpr int shuffleDivBars[] = { 0, 4, 8, 16, 32 }; + const int divBars = shuffleDivBars[juce::jlimit(0, 4, shuffleDivIdx)]; + + // Try to get bar position from play head + bool hasTransport = false; + int64_t currentBar = 0; + if (auto* ph = getPlayHead()) + { + auto info = ph->getPosition(); + if (info.hasValue() && info->getPpqPosition().hasValue()) + { + hasTransport = true; + double ppq = *info->getPpqPosition(); + double beatsPerBar = 4.0; + if (info->getTimeSignature().hasValue()) + beatsPerBar = (double)(*info->getTimeSignature()).numerator; + currentBar = (int64_t)(ppq / beatsPerBar); + } + } + + const bool barAllowed = (divBars == 0) || !hasTransport || (currentBar % divBars == 0); + + std::uniform_real_distribution dist(0.0f, 1.0f); for (const auto metadata : midi) { auto msg = metadata.getMessage(); @@ -49,7 +95,32 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: { int t = msg.getNoteNumber() - BASE_NOTE; if (t >= 0 && t < NUM_TRACKS) + { handleNoteOn(t, msg.getFloatVelocity()); + + const float shuffleAmt = apvts.getRawParameterValue( + "t" + juce::String(t) + "_shuffle")->load(); + const float effShuffle = shuffleAmt * globalShuffleAmt; + + if (effShuffle > 0.0f && barAllowed) + { + // Random note repeat on same drum + if (dist(rng) < effShuffle * 0.5f) + handleNoteOn(t, msg.getFloatVelocity() * effShuffle * 0.7f); + + // Ghost note on random other drum + if (dist(rng) < effShuffle * 0.9f) + { + int ghostTrack = t; + while (ghostTrack == t) + ghostTrack = std::uniform_int_distribution(0, NUM_TRACKS - 1)(rng); + handleNoteOn(ghostTrack, msg.getFloatVelocity() * effShuffle); + } + } + + if (shuffleAmt > 0.0f && globalGroove > 0.0f) + handleNoteOn(t, msg.getFloatVelocity() * (1.0f + globalGroove * 0.3f * shuffleAmt)); + } } else if (msg.isNoteOff()) { @@ -62,6 +133,12 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: auto* outL = buffer.getWritePointer(0); auto* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : nullptr; + // Temporary send buffers + juce::AudioBuffer delaySend(2, numSamples); + juce::AudioBuffer reverbSend(2, numSamples); + delaySend.clear(); + reverbSend.clear(); + for (auto& v : voices) { if (!v.active) @@ -86,7 +163,11 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: const float pitchSemitones = apvts.getRawParameterValue(p + "pitch")->load(); const float toneHz = apvts.getRawParameterValue(p + "tone")->load(); const float pan = apvts.getRawParameterValue(p + "pan")->load(); - const int decayType = (int)apvts.getRawParameterValue(p + "decay")->load(); + const bool decayGate = apvts.getRawParameterValue(p + "decay")->load() > 0.5f; + const float delaySendAmt = apvts.getRawParameterValue(p + "delaySend")->load(); + const float reverbSendAmt = apvts.getRawParameterValue(p + "reverbSend")->load(); + const float ringModAmt = apvts.getRawParameterValue(p + "ringMod")->load(); + const float destructionAmt = apvts.getRawParameterValue(p + "destruction")->load(); float pitchRatio = std::pow(2.0f, pitchSemitones / 12.0f) * (float)track.fileSampleRate / (float)currentSampleRate; @@ -107,13 +188,17 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: const bool hasR = track.buffer.getNumChannels() > 1; const float* sDataR = hasR ? track.buffer.getReadPointer(1) : nullptr; + const float globalRingFreq = apvts.getRawParameterValue("global_ringFreq")->load(); + const float globalDestr = apvts.getRawParameterValue("global_destruction")->load(); + const float ringModInc = 2.0f * juce::MathConstants::pi * globalRingFreq / (float)currentSampleRate; + for (int s = 0; s < numSamples; ++s) { if (!v.active) break; float env = 1.0f; - if (decayType == 0) + if (!decayGate) { env = 1.0f - (float)v.samplesPlayed / maxSamples; if (env <= 0.0f) { v.active = false; break; } @@ -140,6 +225,30 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: smplR = v.filterStateR; } + // Ring modulation + if (ringModAmt > 0.0f) + { + float mod = std::sin(v.ringModPhase); + smplL = smplL * (1.0f - ringModAmt + ringModAmt * mod); + smplR = smplR * (1.0f - ringModAmt + ringModAmt * mod); + v.ringModPhase += ringModInc; + if (v.ringModPhase >= 2.0f * juce::MathConstants::pi) + v.ringModPhase -= 2.0f * juce::MathConstants::pi; + } + + // Destruction (soft clip + bitcrush) + const float effDestr = juce::jmin(1.0f, destructionAmt + globalDestr); + if (effDestr > 0.0f) + { + float drive = 1.0f + effDestr * 19.0f; + smplL = std::tanh(smplL * drive); + smplR = std::tanh(smplR * drive); + float bits = juce::jmax(2.0f, 16.0f - effDestr * 14.0f); + float levels = std::pow(2.0f, bits); + smplL = std::round(smplL * levels) / levels; + smplR = std::round(smplR * levels) / levels; + } + smplL *= env * gain; smplR *= env * gain; @@ -147,6 +256,12 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: if (outR) outR[s] += smplR * panR; + // Send to effects + delaySend.addSample(0, s, smplL * delaySendAmt); + delaySend.addSample(1, s, smplR * delaySendAmt); + reverbSend.addSample(0, s, smplL * reverbSendAmt); + reverbSend.addSample(1, s, smplR * reverbSendAmt); + v.position += pitchRatio; ++v.samplesPlayed; @@ -154,6 +269,81 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: v.active = false; } } + + // Read master effect levels + const float masterDelayMix = apvts.getRawParameterValue("master_delay")->load(); + const float masterReverbMix = apvts.getRawParameterValue("master_reverb")->load(); + + // Delay sync + const int delaySyncDiv = (int)apvts.getRawParameterValue("delay_syncDiv")->load(); + float delayTimeSec = 0.45f; // default free-running delay time + if (delaySyncDiv >= 1 && delaySyncDiv <= 8) + { + double bpm = 120.0; + if (auto* ph = getPlayHead()) + { + auto info = ph->getPosition(); + if (info.hasValue() && info->getBpm().hasValue()) + bpm = *info->getBpm(); + } + double beats = delaySyncDiv * 0.25; + delayTimeSec = (float)(beats * 60.0 / bpm); + } + + // Process delay + const int delaySamples = delayBuffer.getNumSamples(); + auto* delL = delayBuffer.getWritePointer(0); + auto* delR = delayBuffer.getNumChannels() > 1 + ? delayBuffer.getWritePointer(1) : nullptr; + const float feedback = 0.4f; + const float delayMix = 0.3f; + const int delayTimeSamples = (int)(delayTimeSec * currentSampleRate); + + for (int s = 0; s < numSamples; ++s) + { + const int readPos = (delayWritePos - delayTimeSamples + delaySamples) + % delaySamples; + const float delOutL = delL[readPos]; + const float delOutR = delR ? delR[readPos] : delOutL; + + delL[delayWritePos] = delaySend.getSample(0, s) + delOutL * feedback; + if (delR) + delR[delayWritePos] = delaySend.getSample(1, s) + delOutR * feedback; + + outL[s] += delOutL * delayMix * masterDelayMix; + if (outR) + outR[s] += delOutR * delayMix * masterDelayMix; + + delayWritePos = (delayWritePos + 1) % delaySamples; + } + + // Process reverb + juce::dsp::AudioBlock reverbBlock(reverbSend); + juce::dsp::ProcessContextReplacing reverbCtx(reverbBlock); + reverb.process(reverbCtx); + + for (int s = 0; s < numSamples; ++s) + { + outL[s] += reverbSend.getSample(0, s) * masterReverbMix; + if (outR) + outR[s] += reverbSend.getSample(1, s) * masterReverbMix; + } + + // Master volume & pan + const float masterVol = apvts.getRawParameterValue("master_volume")->load(); + const float masterPan = apvts.getRawParameterValue("master_pan")->load(); + const float mPanAngle = (masterPan + 1.0f) * 0.25f * MathConstants::pi; + const float mPanL = std::cos(mPanAngle) * Decibels::decibelsToGain(-3.0f); + const float mPanR = std::sin(mPanAngle) * Decibels::decibelsToGain(-3.0f); + + for (int s = 0; s < numSamples; ++s) + { + float tmpL = outL[s] * mPanL * masterVol; + float tmpR = outR ? outR[s] * mPanR * masterVol : tmpL; + outL[s] = tmpL; + if (outR) + outR[s] = tmpR; + } } int TrommelkisteProcessor::findFreeVoice(int trackIndex) @@ -206,9 +396,9 @@ void TrommelkisteProcessor::handleNoteOff(int trackIndex) { if (v.active && v.trackIndex == trackIndex) { - const int decayType = (int)apvts.getRawParameterValue( - "t" + juce::String(trackIndex) + "_decay")->load(); - if (decayType == 1) + const bool decayGate = apvts.getRawParameterValue( + "t" + juce::String(trackIndex) + "_decay")->load() > 0.5f; + if (decayGate) v.active = false; } } @@ -515,7 +705,6 @@ void TrommelkisteProcessor::setStateInformation(const void* data, int sizeInByte juce::AudioProcessorValueTreeState::ParameterLayout TrommelkisteProcessor::createParameterLayout() { AudioProcessorValueTreeState::ParameterLayout layout; - const StringArray decayTypes = {"Saw", "Gate"}; for (int i = 0; i < NUM_TRACKS; ++i) { @@ -546,11 +735,71 @@ juce::AudioProcessorValueTreeState::ParameterLayout TrommelkisteProcessor::creat p + "pan", tn + "Pan", NormalisableRange(-1.0f, 1.0f), 0.0f)); - layout.add(std::make_unique( - p + "decay", tn + "Decay", - decayTypes, 0)); + layout.add(std::make_unique( + p + "decay", tn + "Decay", false)); + + layout.add(std::make_unique( + p + "delaySend", tn + "Delay", + NormalisableRange(0.0f, 1.0f), 0.0f)); + + layout.add(std::make_unique( + p + "reverbSend", tn + "Reverb", + NormalisableRange(0.0f, 1.0f), 0.0f)); + + layout.add(std::make_unique( + p + "ringMod", tn + "RingMod", + NormalisableRange(0.0f, 1.0f), 0.0f)); + + layout.add(std::make_unique( + p + "destruction", tn + "Destr", + NormalisableRange(0.0f, 1.0f), 0.0f)); + + layout.add(std::make_unique( + p + "shuffle", tn + "Shuffle", + NormalisableRange(0.0f, 1.0f), 0.0f)); } + // Global controls + layout.add(std::make_unique( + "master_volume", "Master Volume", + NormalisableRange(0.0f, 1.2f, 0.01f), 1.0f)); + + layout.add(std::make_unique( + "master_pan", "Master Pan", + NormalisableRange(-1.0f, 1.0f, 0.01f), 0.0f)); + + layout.add(std::make_unique( + "master_delay", "Master Delay", + NormalisableRange(0.0f, 1.0f, 0.01f), 1.0f)); + + layout.add(std::make_unique( + "master_reverb", "Master Reverb", + NormalisableRange(0.0f, 1.0f, 0.01f), 1.0f)); + + layout.add(std::make_unique( + "delay_syncDiv", "Delay Sync", + NormalisableRange(0.0f, 8.0f, 1.0f), 0.0f)); + + layout.add(std::make_unique( + "global_shuffleAmount", "Shuffle Amt", + NormalisableRange(0.0f, 1.0f, 0.01f), 1.0f)); + + layout.add(std::make_unique( + "global_groove", "Groove", + NormalisableRange(0.0f, 1.0f, 0.01f), 0.0f)); + + layout.add(std::make_unique( + "global_shuffleDiv", "Shuffle Div", + NormalisableRange(0.0f, 4.0f, 1.0f), 0.0f)); + + layout.add(std::make_unique( + "global_ringFreq", "Ring Freq", + NormalisableRange(20.0f, 2000.0f, 1.0f, 0.3f), 80.0f)); + + layout.add(std::make_unique( + "global_destruction", "Destruction", + NormalisableRange(0.0f, 1.0f), 0.0f)); + return layout; } diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index 1eb0c67..1cfaba5 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include static constexpr int NUM_TRACKS = 10; static constexpr int MAX_VOICES = 32; @@ -92,11 +94,20 @@ private: float filterStateL = 0.0f; float filterStateR = 0.0f; float noteVelocity = 1.0f; + float ringModPhase = 0.0f; }; Voice voices[MAX_VOICES]; juce::AudioFormatManager formatManager; double currentSampleRate = 44100.0; + std::mt19937 rng{std::random_device{}()}; + + // Effects + static constexpr int DELAY_BUFFER_SECONDS = 4; + juce::AudioBuffer delayBuffer; + int delayWritePos = 0; + juce::dsp::Reverb reverb; + juce::dsp::Reverb::Parameters reverbParams; int findFreeVoice(int trackIndex); void handleNoteOn(int trackIndex, float velocity);