diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 59aee2b..9e81ddd 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -9,7 +9,7 @@ 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 = 82; +static constexpr int TOP = 100; static constexpr int COL_LBL = 8; static constexpr int COL_KNOB = 44; static constexpr int KNOB_W = 44; @@ -22,7 +22,7 @@ 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; +static constexpr int TOTAL_W = COL_SHUFFLE + KNOB_W + 8; // Header knob x positions (compact layout) static constexpr int HDR_KNOB = 36; @@ -60,13 +60,30 @@ void TrommelkisteLookAndFeel::drawRotarySlider(Graphics& g, int x, int y, int w, // Main body — vertical gradient (light top, dark bottom) for 3D cylinder feel { - ColourGradient grad(Colour(0xFF323232), cx, cy - radius, - Colour(0xFF1C1C1C), cx, cy + radius, true); - grad.addColour(0.10, Colour(0x883D3D3D)); + 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, @@ -190,7 +207,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) setLookAndFeel(&lnf); lnf.fontScale = 1.5f; uiScale = 1.5f; - setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H)); + setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H + 12)); setResizable(false, false); setWantsKeyboardFocus(false); @@ -371,7 +388,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p) globalShuffleDiv.setRange(0, 4, 1); globalShuffleDiv.setScrollWheelEnabled(false); globalShuffleDiv.formatFn = [](double v) { - static const char* labels[] = { "OFF", "4", "8", "16", "32" }; + static const char* labels[] = { "1", "4", "8", "16", "32" }; return labels[juce::jlimit(0, 4, (int)v)]; }; addAndMakeVisible(globalShuffleDiv); @@ -416,7 +433,7 @@ void TrommelkisteEditor::timerCallback() const double now = juce::Time::getMillisecondCounterHiRes() / 1000.0; for (int i = 0; i < NUM_TRACKS; ++i) { - if (proc.trackActive[i].exchange(false)) + if (proc.trackActive[i]) trackActivatedTime[i] = now; } repaint(); @@ -430,7 +447,7 @@ void TrommelkisteEditor::scaleChanged() { uiScale = scales[selId - 1]; lnf.fontScale = uiScale; - setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H)); + setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H + 12)); } } @@ -462,63 +479,52 @@ void TrommelkisteEditor::paint(Graphics& g) g.setFont(fontBold(16.0f, uiScale)); g.drawText("TROMMELKISTE", scaled(10), scaled(4), scaled(400), scaled(20), Justification::centredLeft); - // VU meter - { - const float level = proc.outputLevel.load(std::memory_order_relaxed); - const float meterX = 10.0f; - const float meterY = 30.0f; - const float meterW = 134.0f; - const float meterH = 6.0f; - - // Background - g.setColour(Colour(0xFF111111)); - g.fillRect(scaled((int) meterX), scaled((int) meterY), scaled((int) meterW), scaled((int) meterH)); - - // Level bar - float norm = jlimit(0.0f, 1.0f, level * 4.0f); - float barW = meterW * norm; - g.setColour(lnf.accent); - g.fillRect(scaled((int) meterX), scaled((int) meterY), scaled((int) barW), scaled((int) meterH)); - } - g.setColour(lnf.accent.withAlpha(0.3f)); - g.fillRect(scaled(10), scaled(60), getWidth() - scaled(15), scaled(1)); + g.fillRect(scaled(10), scaled(71), scaled(getWidth() - 20), scaled(1)); + + // Section labels + g.setColour(Colours::green.brighter(0.6f)); + g.setFont(font(8.0f, uiScale)); + int masterCenter = (HDR_VOL + HDR_DLYSYNC + HDR_KNOB) / 2; + int shuffleCenter = (HDR_DIV + HDR_GRV + HDR_KNOB) / 2; + g.drawText("MASTER", scaled(masterCenter - 20), scaled(0), scaled(40), scaled(8), Justification::centred); + g.drawText("SHUFFLE", scaled(shuffleCenter - 20), scaled(0), scaled(40), scaled(8), Justification::centred); 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); + g.drawText("VOL", scaled(HDR_VOL), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("PAN", scaled(HDR_PAN), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("RING", scaled(HDR_RING), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("DEST", scaled(HDR_DEST), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("DLY", scaled(HDR_DELAY), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("REV", scaled(HDR_REVERB), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("SYNC", scaled(HDR_DLYSYNC), scaled(51), 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.fillRect(scaled(HDR_DIV - 8), scaled(6), scaled(1), scaled(65)); 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.drawText("EVERY", scaled(HDR_DIV), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("AMT", scaled(HDR_SHFL), scaled(51), scaled(HDR_KNOB), scaled(10), Justification::centred); + g.drawText("GROOVE", scaled(HDR_GRV), scaled(51), 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(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("LEN", scaled(COL_KNOB + 1*(KNOB_W + KNOB_GAP)), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("VEL", scaled(COL_KNOB + 2*(KNOB_W + KNOB_GAP)), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("PIT", scaled(COL_KNOB + 3*(KNOB_W + KNOB_GAP)), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("TONE", scaled(COL_KNOB + 4*(KNOB_W + KNOB_GAP)), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("PAN", scaled(COL_KNOB + 5*(KNOB_W + KNOB_GAP)), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("DECAY", scaled(COL_DECAY), scaled(65), scaled(68), scaled(12), Justification::centred); - g.drawText("SAMPLE", scaled(COL_FILE), scaled(65), scaled(SAMPLE_W), scaled(12), Justification::centred); - g.drawText("DELAY", scaled(COL_DELAY), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("REVERB", scaled(COL_REVERB), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("RING", scaled(COL_RINGMOD), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("DESTR", scaled(COL_DESTR), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); - g.drawText("SHFL", scaled(COL_SHUFFLE), scaled(65), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("LVL", scaled(COL_KNOB), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("LEN", scaled(COL_KNOB + 1*(KNOB_W + KNOB_GAP)), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("VEL", scaled(COL_KNOB + 2*(KNOB_W + KNOB_GAP)), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("PIT", scaled(COL_KNOB + 3*(KNOB_W + KNOB_GAP)), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("TONE", scaled(COL_KNOB + 4*(KNOB_W + KNOB_GAP)), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("PAN", scaled(COL_KNOB + 5*(KNOB_W + KNOB_GAP)), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("DECAY", scaled(COL_DECAY), scaled(78), scaled(68), scaled(12), Justification::centred); + g.drawText("SAMPLE", scaled(COL_FILE), scaled(78), scaled(SAMPLE_W), scaled(12), Justification::centred); + g.drawText("DELAY", scaled(COL_DELAY), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("REVERB", scaled(COL_REVERB), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("RING", scaled(COL_RINGMOD), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("DESTR", scaled(COL_DESTR), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); + g.drawText("SHFL", scaled(COL_SHUFFLE), scaled(78), scaled(KNOB_W), scaled(12), Justification::centred); const double now = juce::Time::getMillisecondCounterHiRes() / 1000.0; for (int i = 0; i < NUM_TRACKS; ++i) @@ -553,11 +559,11 @@ void TrommelkisteEditor::paint(Graphics& g) g.setColour(Colours::white); g.setFont(fontBold(12.0f, uiScale)); - g.drawText(trackNames[i], scaled(COL_LBL), scaled(ry + 14), scaled(32), scaled(16), Justification::centredLeft); + g.drawText(trackNames[i], scaled(COL_LBL), scaled(ry + 6), scaled(32), scaled(16), Justification::centredLeft); g.setColour(Colour(0xFF666666)); g.setFont(font(9.0f, uiScale)); - g.drawText(noteNames[i], scaled(COL_LBL), scaled(ry + 28), scaled(32), scaled(14), Justification::centredLeft); + g.drawText(noteNames[i], scaled(COL_LBL), scaled(ry + 24), scaled(32), scaled(14), Justification::centredLeft); const double elapsed = now - trackActivatedTime[i]; if (elapsed < 0.4) @@ -579,16 +585,16 @@ void TrommelkisteEditor::resized() // 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)); + masterVolume.setBounds(scaled(HDR_VOL), scaled(13), scaled(hk), scaled(hk)); + masterPan.setBounds(scaled(HDR_PAN), scaled(13), scaled(hk), scaled(hk)); + globalRingFreq.setBounds(scaled(HDR_RING), scaled(13), scaled(hk), scaled(hk)); + globalDestruction.setBounds(scaled(HDR_DEST), scaled(13), scaled(hk), scaled(hk)); + masterDelay.setBounds(scaled(HDR_DELAY), scaled(13), scaled(hk), scaled(hk)); + masterReverb.setBounds(scaled(HDR_REVERB), scaled(13), scaled(hk), scaled(hk)); + delaySyncDiv.setBounds(scaled(HDR_DLYSYNC), scaled(13), scaled(hk), scaled(hk)); + globalShuffleDiv.setBounds(scaled(HDR_DIV), scaled(13), scaled(hk), scaled(hk)); + globalShuffleAmt.setBounds(scaled(HDR_SHFL), scaled(13), scaled(hk), scaled(hk)); + globalGroove.setBounds(scaled(HDR_GRV), scaled(13), scaled(hk), scaled(hk)); for (int i = 0; i < NUM_TRACKS; ++i) { diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 4ab5bcc..9f77825 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -84,7 +84,33 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: const bool barAllowed = (divBars == 0) || !hasTransport || (currentBar % divBars == 0); + // Process pending shuffle events + const int baseDelay = (int)(currentSampleRate * 0.05 * globalGroove); // up to 50ms at max groove + for (int i = numPendingShuffle - 1; i >= 0; --i) + { + pendingShuffle[i].delaySamples -= numSamples; + if (pendingShuffle[i].delaySamples <= 0) + { + handleNoteOn(pendingShuffle[i].trackIndex, pendingShuffle[i].velocity); + pendingShuffle[i] = pendingShuffle[--numPendingShuffle]; + } + } + + // Independent shuffle: generate events even without MIDI input std::uniform_real_distribution dist(0.0f, 1.0f); + if (globalShuffleAmt > 0.0f && barAllowed) + { + std::uniform_int_distribution trackDist(0, NUM_TRACKS - 1); + float prob = globalShuffleAmt * 0.15f; // base probability per block + if (dist(rng) < prob && numPendingShuffle < MAX_PENDING_SHUFFLE) + { + int target = trackDist(rng); + float vel = (0.3f + dist(rng) * 0.5f) * globalShuffleAmt; + int delay = (int)(baseDelay * (0.5f + dist(rng) * 0.5f)); + pendingShuffle[numPendingShuffle++] = { target, vel, delay }; + } + } + for (const auto metadata : midi) { auto msg = metadata.getMessage(); @@ -101,22 +127,28 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer& buffer, juce: if (effShuffle > 0.0f && barAllowed) { - // Random note repeat on same drum - if (dist(rng) < effShuffle * 0.5f) - handleNoteOn(t, msg.getFloatVelocity() * effShuffle * 0.7f); + int maxEvents = juce::jmax(1, (int)(effShuffle * 3.0f + 0.5f)); + int numEvents = std::uniform_int_distribution(0, maxEvents)(rng); - // Ghost note on random other drum - if (dist(rng) < effShuffle * 0.9f) + for (int e = 0; e < numEvents; ++e) { - int ghostTrack = t; - while (ghostTrack == t) - ghostTrack = std::uniform_int_distribution(0, NUM_TRACKS - 1)(rng); - handleNoteOn(ghostTrack, msg.getFloatVelocity() * effShuffle); + bool sameTrack = dist(rng) < 0.35f; + int target = t; + if (!sameTrack) + { + target = std::uniform_int_distribution(0, NUM_TRACKS - 1)(rng); + if (target == t) + target = (t + 1 + std::uniform_int_distribution(0, NUM_TRACKS - 2)(rng)) % NUM_TRACKS; + } + + float vel = msg.getFloatVelocity() * (0.3f + dist(rng) * 0.7f) * effShuffle; + if (numPendingShuffle < MAX_PENDING_SHUFFLE) + { + int delay = (int)(baseDelay * (0.5f + dist(rng) * 0.5f)); + pendingShuffle[numPendingShuffle++] = { target, vel, delay }; + } } } - - if (shuffleAmt > 0.0f && globalGroove > 0.0f) - handleNoteOn(t, msg.getFloatVelocity() * (1.0f + globalGroove * 0.3f * shuffleAmt)); } } else if (msg.isNoteOff()) @@ -572,7 +604,7 @@ static const TrommelkisteProcessor::Kit kits[] = { "BT7A0D3", "RIM127", "ST7T7S3", "HANDCLP1", "HHCD2", "CLOP1", "HHOD2", "RIDED2", "LT7D3", "MT7D3" } }, { "Soft", { - "BT0A0D7", "RIM63", "ST0T0S7", "HANDCLP2", "HHCD8", + "BT0A0DA", "RIM63", "ST0T0S7", "HANDCLP2", "HHCD8", "CLOP4", "HHOD8", "RIDED8", "LT0D7", "MT0D7" } }, { "Raw", { "BTAAADA", "RIM127", "STATASA", "HANDCLP1", "HHCDA", diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index f39c845..579d664 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -116,5 +116,16 @@ private: void handleNoteOff(int trackIndex); juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout(); + struct PendingShuffleEvent + { + int trackIndex = -1; + float velocity = 0.0f; + int delaySamples = 0; + }; + + static constexpr int MAX_PENDING_SHUFFLE = 32; + PendingShuffleEvent pendingShuffle[MAX_PENDING_SHUFFLE]; + int numPendingShuffle = 0; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TrommelkisteProcessor) };