This commit is contained in:
Armin 2026-07-19 18:53:17 +02:00
commit 044e3b7a52
7 changed files with 618 additions and 47 deletions

View file

@ -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<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 - 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<TrackUI::SliderAtt>(proc.apvts, pfx + "level", t.level);
t.lengthAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "length", t.length);
@ -138,14 +262,18 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
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.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.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<TrackUI::ComboAtt>(proc.apvts, pfx + "decay", t.decay);
t.decayAtt = std::make_unique<TrackUI::ButtonAtt>(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<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();
}
@ -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),