mirror of
https://codeberg.org/armin/trommelkiste.git
synced 2026-09-01 04:10:46 +02:00
356 lines
14 KiB
C++
356 lines
14 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 = 44;
|
|
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;
|
|
|
|
// ── 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);
|
|
|
|
g.setColour(Colour(0xFF333333));
|
|
g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
|
|
|
|
{
|
|
Path arc;
|
|
arc.addCentredArc(cx, cy, radius - 1.5f, radius - 1.5f, 0.0f, startAngle, angle, true);
|
|
g.setColour(accent);
|
|
g.strokePath(arc, PathStrokeType(2.5f, PathStrokeType::curved, PathStrokeType::rounded));
|
|
}
|
|
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
// ── Editor ───────────────────────────────────────────────────────
|
|
|
|
TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
|
|
: AudioProcessorEditor(p), proc(p)
|
|
{
|
|
setLookAndFeel(&lnf);
|
|
lnf.fontScale = 1.5f;
|
|
uiScale = 1.5f;
|
|
setSize(scaled(660), 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");
|
|
|
|
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.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.panAtt = std::make_unique<TrackUI::SliderAtt>(proc.apvts, pfx + "pan", t.pan);
|
|
|
|
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));
|
|
addAndMakeVisible(t.decay);
|
|
t.decayAtt = std::make_unique<TrackUI::ComboAtt>(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);
|
|
|
|
startTimerHz(30);
|
|
resized();
|
|
}
|
|
|
|
TrommelkisteEditor::~TrommelkisteEditor()
|
|
{
|
|
setLookAndFeel(nullptr);
|
|
}
|
|
|
|
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(660), 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(26), scaled(getWidth() - 20), scaled(1));
|
|
|
|
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);
|
|
|
|
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(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);
|
|
|
|
if (proc.trackActive[i])
|
|
{
|
|
g.setColour(lnf.accent);
|
|
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(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));
|
|
|
|
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.decay.setBounds(scaled(COL_DECAY), scaled(ry + 16), scaled(68), scaled(22));
|
|
|
|
const int fileX = COL_FILE;
|
|
const int fileEnd = 660 - 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;
|
|
}
|
|
}
|
|
}
|