#include "PluginEditor.h" using namespace juce; static Font font(float h, float s = 1.0f) { return Font(FontOptions(h * s)); } static Font fontBold(float h, float s = 1.0f) { return Font(FontOptions(h * s).withStyle("Bold")); } static const char* trackNames[] = {"BD", "RS", "SN", "CL", "CH", "PH", "OH", "RD", "LT", "MT"}; 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 = 100; 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 ────────────────────────────────────────────────── void TrommelkisteLookAndFeel::drawRotarySlider(Graphics& g, int x, int y, int w, int h, float sliderPos, float startAngle, float endAngle, Slider&) { auto bounds = Rectangle(x, y, w, h).toFloat(); auto radius = jmin(bounds.getWidth(), bounds.getHeight()) / 2.0f - 3.0f; auto cx = bounds.getCentreX(); auto cy = bounds.getCentreY(); auto angle = startAngle + sliderPos * (endAngle - startAngle); // 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 - 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); ptr.applyTransform(AffineTransform::rotation(angle).translated(cx, cy)); 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&, bool over, bool down) { auto bounds = btn.getLocalBounds().toFloat(); Colour fill; if (btn.getButtonText() == "X") fill = down ? Colour(0xFFCC3333) : (over ? Colour(0xFFAA3333) : Colour(0xFF663333)); else fill = down ? accent.darker(0.3f) : (over ? accent.brighter(0.2f) : accent); g.setColour(fill); g.fillRoundedRectangle(bounds, 3.0f); } void TrommelkisteLookAndFeel::drawComboBox(Graphics& g, int w, int h, bool, int, int, int, int, ComboBox& box) { auto bounds = Rectangle(0, 0, (float)w, (float)h); g.setColour(Colour(0xFF333333)); g.fillRoundedRectangle(bounds, 3.0f); g.setColour(Colour(0xFF555555)); g.drawRoundedRectangle(bounds, 3.0f, 1.0f); Path arrow; arrow.addTriangle((float)w - 13.0f, (float)h * 0.5f - 2.5f, (float)w - 13.0f, (float)h * 0.5f + 2.5f, (float)w - 7.0f, (float)h * 0.5f); g.setColour(Colours::white); g.fillPath(arrow); } void TrommelkisteLookAndFeel::positionComboBoxText(ComboBox& box, Label& label) { label.setBounds(6, 0, box.getWidth() - 22, box.getHeight()); 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) : AudioProcessorEditor(p), proc(p) { setLookAndFeel(&lnf); lnf.fontScale = 1.5f; uiScale = 1.5f; setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H + 12)); setResizable(false, false); setWantsKeyboardFocus(false); for (int i = 0; i < NUM_TRACKS; ++i) { auto& t = ui[i]; const String pfx = "t" + String(i) + "_"; auto setupKnob = [&](Slider& s, const String& /*id*/) { s.setSliderStyle(Slider::RotaryHorizontalVerticalDrag); s.setTextBoxStyle(Slider::TextBoxBelow, false, 44, 15); s.setColour(Slider::textBoxTextColourId, Colours::white); s.setColour(Slider::textBoxOutlineColourId, Colours::transparentBlack); s.setColour(Slider::textBoxBackgroundColourId, Colours::transparentBlack); addAndMakeVisible(s); }; setupKnob(t.level, "level"); setupKnob(t.length, "length"); setupKnob(t.velocity, "velocity"); 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"; }; t.velocity.formatFn = [](double v) { return String(v, 1); }; t.pitch.formatFn = [](double v) { return String(v, 1); }; t.tone.formatFn = [](double v) -> String { return v >= 1000.0 ? String(v / 1000.0, 1) + "k" : String((int) v); }; t.pan.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); }; 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); t.velAtt = std::make_unique(proc.apvts, pfx + "velocity", t.velocity); 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.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.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.prevBtn.onClick = [this, i] { proc.prevSample(i); updateFileLabel(i); }; addAndMakeVisible(t.prevBtn); t.nextBtn.onClick = [this, i] { proc.nextSample(i); updateFileLabel(i); }; addAndMakeVisible(t.nextBtn); t.fileLabel.setFont(font(10.5f, uiScale)); t.fileLabel.setColour(Label::textColourId, Colour(0xFF888888)); t.fileLabel.setJustificationType(Justification::centredLeft); t.fileLabel.setEditable(false); t.fileLabel.addMouseListener(this, true); addAndMakeVisible(t.fileLabel); updateFileLabel(i); } // Preset selector refreshPresetCombo(); presetCombo.setColour(ComboBox::backgroundColourId, Colour(0xFF333333)); presetCombo.setColour(ComboBox::textColourId, Colours::white); presetCombo.setColour(ComboBox::outlineColourId, Colour(0xFF555555)); presetCombo.onChange = [this] { int id = presetCombo.getSelectedId(); if (id >= 1) { proc.loadKit(id - 1); for (int i = 0; i < NUM_TRACKS; ++i) updateFileLabel(i); } }; addAndMakeVisible(presetCombo); presetPrevBtn.onClick = [this] { proc.prevKit(); refreshPresetCombo(); for (int i = 0; i < NUM_TRACKS; ++i) updateFileLabel(i); }; addAndMakeVisible(presetPrevBtn); presetNextBtn.onClick = [this] { proc.nextKit(); refreshPresetCombo(); for (int i = 0; i < NUM_TRACKS; ++i) updateFileLabel(i); }; addAndMakeVisible(presetNextBtn); scaleCombo.addItemList({"100%", "125%", "150%", "175%", "200%"}, 1); scaleCombo.setSelectedId(3, dontSendNotification); scaleCombo.setColour(ComboBox::backgroundColourId, Colour(0xFF333333)); scaleCombo.setColour(ComboBox::textColourId, Colours::white); scaleCombo.setColour(ComboBox::outlineColourId, Colour(0xFF555555)); 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[] = { "1", "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(); } 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 }; int selId = scaleCombo.getSelectedId(); if (selId >= 1 && selId <= 5) { uiScale = scales[selId - 1]; lnf.fontScale = uiScale; setSize(scaled(TOTAL_W), scaled(TOP + NUM_TRACKS * ROW_H + 12)); } } void TrommelkisteEditor::updateFileLabel(int idx) { if (idx < 0 || idx >= NUM_TRACKS) return; const int ci = proc.currentSampleIndex[idx]; if (ci >= 0 && ci < proc.trackSamples[idx].size()) ui[idx].fileLabel.setText(proc.trackSamples[idx][ci].name, dontSendNotification); else ui[idx].fileLabel.setText("--", dontSendNotification); } void TrommelkisteEditor::refreshPresetCombo() { presetCombo.clear(dontSendNotification); const int n = proc.getNumKits(); for (int i = 0; i < n; ++i) presetCombo.addItem(proc.getKitList()[i].name, i + 1); presetCombo.setSelectedId(proc.currentKitIndex + 1, dontSendNotification); } void TrommelkisteEditor::paint(Graphics& g) { g.fillAll(lnf.bg); g.setColour(lnf.accent); g.setFont(fontBold(16.0f, uiScale)); 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(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(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(65)); g.setColour(Colours::white.withAlpha(0.9f)); g.setFont(font(8.0f, uiScale)); 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(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) { const int ry = TOP + i * ROW_H; if (i % 2 == 1) { g.setColour(lnf.rowAlt); 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); g.setColour(Colour(0xFF666666)); g.setFont(font(9.0f, uiScale)); 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) { 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)); } } } void TrommelkisteEditor::resized() { // Header: title is painted; position controls in top row 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(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) { const int ry = TOP + i * ROW_H; auto& t = ui[i]; Slider* knobs[] = {&t.level, &t.length, &t.velocity, &t.pitch, &t.tone, &t.pan}; for (int k = 0; k < 6; ++k) { int kx = COL_KNOB + k * (KNOB_W + KNOB_GAP); 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 = 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), scaled(fileEnd - fileX - 38), scaled(22)); } } void TrommelkisteEditor::loadForTrack(int idx) { chooser.launchAsync(FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, [this, idx](const FileChooser& fc) { auto file = fc.getResult(); if (file.existsAsFile()) { proc.loadSample(idx, file); proc.currentSampleIndex[idx] = -1; ui[idx].fileLabel.setText(file.getFileNameWithoutExtension(), dontSendNotification); } }); } void TrommelkisteEditor::mouseDown(const MouseEvent& event) { for (int i = 0; i < NUM_TRACKS; ++i) { if (event.eventComponent == &ui[i].fileLabel) { loadForTrack(i); return; } } }