trommelkiste/Source/PluginEditor.cpp
Armin 2cded52eef Fix track signal indicator sometimes not showing for short samples
Use a latch pattern for trackActive: the audio thread sets it true
whenever a voice is active, and the UI timer atomically reads and
clears it via exchange(false). This eliminates the race where the
flag was cleared at the start of every processBlock, causing the
30Hz timer to miss brief activations from short samples.
2026-07-20 12:33:36 +02:00

645 lines
29 KiB
C++

#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 = 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 ──────────────────────────────────────────────────
void TrommelkisteLookAndFeel::drawRotarySlider(Graphics& g, int x, int y, int w, int h,
float sliderPos, float startAngle, float endAngle,
Slider&)
{
auto bounds = Rectangle<int>(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<float>::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<float>::pi, MathConstants<float>::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<float>(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<TrackUI::SliderAtt>(proc.apvts, pfx + "level", t.level);
t.lengthAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "length", t.length);
t.velAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "velocity", t.velocity);
t.pitchAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "pitch", t.pitch);
t.toneAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "tone", t.tone);
t.panAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "pan", t.pan);
t.delaySendAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "delaySend", t.delaySend);
t.reverbSendAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "reverbSend", t.reverbSend);
t.ringModAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "ringMod", t.ringMod);
t.destructionAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "destruction", t.destruction);
t.shuffleAtt = std::make_unique<TrackUI::SliderAtt>(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<TrackUI::ButtonAtt>(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<juce::AudioProcessorValueTreeState::SliderAttachment>(proc.apvts, "global_ringFreq", globalRingFreq);
globalDestructionAtt = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(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<juce::AudioProcessorValueTreeState::SliderAttachment>(proc.apvts, "master_volume", masterVolume);
masterPanAtt = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(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<juce::AudioProcessorValueTreeState::SliderAttachment>(proc.apvts, "master_delay", masterDelay);
masterReverbAtt = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(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<juce::AudioProcessorValueTreeState::SliderAttachment>(proc.apvts, "global_shuffleDiv", globalShuffleDiv);
globalShuffleAmtAtt = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(proc.apvts, "global_shuffleAmount", globalShuffleAmt);
globalGrooveAtt = std::make_unique<juce::AudioProcessorValueTreeState::SliderAttachment>(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<juce::AudioProcessorValueTreeState::SliderAttachment>(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].exchange(false))
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(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(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;
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(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)
{
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;
}
}
}