mirror of
https://codeberg.org/armin/trommelkiste.git
synced 2026-09-01 04:10:46 +02:00
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.
645 lines
29 KiB
C++
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;
|
|
}
|
|
}
|
|
}
|