mirror of
https://codeberg.org/armin/trommelkiste.git
synced 2026-09-01 04:10:46 +02:00
init
This commit is contained in:
commit
8d5ffc6b56
168 changed files with 1436 additions and 0 deletions
356
Source/PluginEditor.cpp
Normal file
356
Source/PluginEditor.cpp
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
Source/PluginEditor.h
Normal file
81
Source/PluginEditor.h
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
#pragma once
|
||||
#include "PluginProcessor.h"
|
||||
#include <juce_gui_basics/juce_gui_basics.h>
|
||||
|
||||
class TrommelkisteLookAndFeel : public juce::LookAndFeel_V4
|
||||
{
|
||||
public:
|
||||
juce::Colour bg{0xFF1A1A1A};
|
||||
juce::Colour rowAlt{0xFF222222};
|
||||
juce::Colour accent{0xFFE8600A};
|
||||
float fontScale = 1.5f;
|
||||
|
||||
void drawRotarySlider(juce::Graphics&, int x, int y, int w, int h,
|
||||
float pos, float start, float end, juce::Slider&) override;
|
||||
void drawButtonBackground(juce::Graphics&, juce::Button&, const juce::Colour&,
|
||||
bool, bool) override;
|
||||
void drawComboBox(juce::Graphics&, int w, int h, bool down,
|
||||
int bx, int by, int bw, int bh, juce::ComboBox&) override;
|
||||
void positionComboBoxText(juce::ComboBox&, juce::Label&) override;
|
||||
};
|
||||
|
||||
struct KnobSlider : juce::Slider
|
||||
{
|
||||
std::function<juce::String(double)> formatFn;
|
||||
juce::String getTextFromValue(double v) override
|
||||
{
|
||||
if (formatFn) return formatFn(v);
|
||||
return Slider::getTextFromValue(v);
|
||||
}
|
||||
};
|
||||
|
||||
class TrommelkisteEditor : public juce::AudioProcessorEditor, private juce::Timer
|
||||
{
|
||||
public:
|
||||
TrommelkisteEditor(TrommelkisteProcessor&);
|
||||
~TrommelkisteEditor() override;
|
||||
|
||||
void paint(juce::Graphics&) override;
|
||||
void resized() override;
|
||||
void mouseDown(const juce::MouseEvent&) override;
|
||||
void timerCallback() override { repaint(); }
|
||||
|
||||
private:
|
||||
TrommelkisteProcessor& proc;
|
||||
TrommelkisteLookAndFeel lnf;
|
||||
|
||||
juce::ComboBox scaleCombo;
|
||||
float uiScale = 1.5f;
|
||||
|
||||
int scaled(int v) const { return (int)(v * uiScale + 0.5f); }
|
||||
void scaleChanged();
|
||||
|
||||
struct TrackUI
|
||||
{
|
||||
using SliderAtt = juce::AudioProcessorValueTreeState::SliderAttachment;
|
||||
using ComboAtt = juce::AudioProcessorValueTreeState::ComboBoxAttachment;
|
||||
|
||||
juce::TextButton prevBtn{"<"};
|
||||
juce::TextButton nextBtn{">"};
|
||||
KnobSlider level, length, velocity, pitch, tone, pan;
|
||||
juce::ComboBox decay;
|
||||
juce::Label fileLabel;
|
||||
|
||||
std::unique_ptr<SliderAtt> levelAtt, lengthAtt, velAtt, pitchAtt, toneAtt, panAtt;
|
||||
std::unique_ptr<ComboAtt> decayAtt;
|
||||
};
|
||||
|
||||
TrackUI ui[NUM_TRACKS];
|
||||
juce::FileChooser chooser{"Load Sample", juce::File{}, "*.wav;*.aif;*.aiff;*.flac"};
|
||||
|
||||
// Preset selector
|
||||
juce::ComboBox presetCombo;
|
||||
juce::TextButton presetPrevBtn{"<"};
|
||||
juce::TextButton presetNextBtn{">"};
|
||||
|
||||
void loadForTrack(int idx);
|
||||
void updateFileLabel(int idx);
|
||||
void refreshPresetCombo();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TrommelkisteEditor)
|
||||
};
|
||||
560
Source/PluginProcessor.cpp
Normal file
560
Source/PluginProcessor.cpp
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
#include "PluginProcessor.h"
|
||||
#include "PluginEditor.h"
|
||||
#include <BinaryData.h>
|
||||
#include <SampleLibrary.h>
|
||||
|
||||
using namespace juce;
|
||||
|
||||
TrommelkisteProcessor::TrommelkisteProcessor()
|
||||
: AudioProcessor(BusesProperties()
|
||||
.withOutput("Output", juce::AudioChannelSet::stereo(), true)),
|
||||
apvts(*this, nullptr, "Parameters", createParameterLayout())
|
||||
{
|
||||
formatManager.registerBasicFormats();
|
||||
scanSamples();
|
||||
loadKit(0);
|
||||
}
|
||||
|
||||
TrommelkisteProcessor::~TrommelkisteProcessor() {}
|
||||
|
||||
std::optional<juce::String> TrommelkisteProcessor::getNameForMidiNoteNumber(int note, int)
|
||||
{
|
||||
static const char* names[] = {"BD", "RS", "SN", "CL", "CH", "PH", "OH", "RD", "LT", "MT"};
|
||||
int t = note - BASE_NOTE;
|
||||
if (t >= 0 && t < NUM_TRACKS)
|
||||
return juce::String(names[t]);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::prepareToPlay(double sr, int)
|
||||
{
|
||||
currentSampleRate = sr;
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::releaseResources() {}
|
||||
|
||||
void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midi)
|
||||
{
|
||||
juce::ScopedNoDenormals nd;
|
||||
const int numSamples = buffer.getNumSamples();
|
||||
buffer.clear();
|
||||
|
||||
for (int i = 0; i < NUM_TRACKS; ++i)
|
||||
trackActive[i] = false;
|
||||
|
||||
for (const auto metadata : midi)
|
||||
{
|
||||
auto msg = metadata.getMessage();
|
||||
if (msg.isNoteOn())
|
||||
{
|
||||
int t = msg.getNoteNumber() - BASE_NOTE;
|
||||
if (t >= 0 && t < NUM_TRACKS)
|
||||
handleNoteOn(t, msg.getFloatVelocity());
|
||||
}
|
||||
else if (msg.isNoteOff())
|
||||
{
|
||||
int t = msg.getNoteNumber() - BASE_NOTE;
|
||||
if (t >= 0 && t < NUM_TRACKS)
|
||||
handleNoteOff(t);
|
||||
}
|
||||
}
|
||||
|
||||
auto* outL = buffer.getWritePointer(0);
|
||||
auto* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : nullptr;
|
||||
|
||||
for (auto& v : voices)
|
||||
{
|
||||
if (!v.active)
|
||||
continue;
|
||||
|
||||
const int ti = v.trackIndex;
|
||||
trackActive[ti] = true;
|
||||
auto& track = tracks[ti];
|
||||
const juce::SpinLock::ScopedLockType sl(track.lock);
|
||||
|
||||
const int bufLen = track.buffer.getNumSamples();
|
||||
if (bufLen == 0)
|
||||
{
|
||||
v.active = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const juce::String p = "t" + juce::String(ti) + "_";
|
||||
const float level = apvts.getRawParameterValue(p + "level")->load();
|
||||
const float length = apvts.getRawParameterValue(p + "length")->load();
|
||||
const float velAmt = apvts.getRawParameterValue(p + "velocity")->load();
|
||||
const float pitchSemitones = apvts.getRawParameterValue(p + "pitch")->load();
|
||||
const float toneHz = apvts.getRawParameterValue(p + "tone")->load();
|
||||
const float pan = apvts.getRawParameterValue(p + "pan")->load();
|
||||
const int decayType = (int)apvts.getRawParameterValue(p + "decay")->load();
|
||||
|
||||
float pitchRatio = std::pow(2.0f, pitchSemitones / 12.0f)
|
||||
* (float)track.fileSampleRate / (float)currentSampleRate;
|
||||
|
||||
const float maxSamples = length * (float)currentSampleRate;
|
||||
const float toneA = (toneHz < 19000.0f)
|
||||
? std::exp(-2.0f * juce::MathConstants<float>::pi * toneHz / (float)currentSampleRate)
|
||||
: 0.0f;
|
||||
|
||||
const float effVel = 1.0f - velAmt + velAmt * v.noteVelocity;
|
||||
const float gain = level * effVel;
|
||||
|
||||
const float panAngle = (pan + 1.0f) * 0.25f * juce::MathConstants<float>::pi;
|
||||
const float panL = std::cos(panAngle) * juce::Decibels::decibelsToGain(-3.0f);
|
||||
const float panR = std::sin(panAngle) * juce::Decibels::decibelsToGain(-3.0f);
|
||||
|
||||
const float* sData = track.buffer.getReadPointer(0);
|
||||
const bool hasR = track.buffer.getNumChannels() > 1;
|
||||
const float* sDataR = hasR ? track.buffer.getReadPointer(1) : nullptr;
|
||||
|
||||
for (int s = 0; s < numSamples; ++s)
|
||||
{
|
||||
if (!v.active)
|
||||
break;
|
||||
|
||||
float env = 1.0f;
|
||||
if (decayType == 0)
|
||||
{
|
||||
env = 1.0f - (float)v.samplesPlayed / maxSamples;
|
||||
if (env <= 0.0f) { v.active = false; break; }
|
||||
}
|
||||
else
|
||||
{
|
||||
if (v.samplesPlayed >= (int)maxSamples) { v.active = false; break; }
|
||||
}
|
||||
|
||||
int pos0 = (int)v.position;
|
||||
if (pos0 >= bufLen) { v.active = false; break; }
|
||||
const float frac = v.position - (float)pos0;
|
||||
int pos1 = pos0 + 1;
|
||||
if (pos1 >= bufLen) pos1 = bufLen - 1;
|
||||
|
||||
float smplL = sData[pos0] + (sData[pos1] - sData[pos0]) * frac;
|
||||
float smplR = hasR ? (sDataR[pos0] + (sDataR[pos1] - sDataR[pos0]) * frac) : smplL;
|
||||
|
||||
if (toneA > 0.0f)
|
||||
{
|
||||
v.filterStateL = (1.0f - toneA) * smplL + toneA * v.filterStateL;
|
||||
smplL = v.filterStateL;
|
||||
v.filterStateR = (1.0f - toneA) * smplR + toneA * v.filterStateR;
|
||||
smplR = v.filterStateR;
|
||||
}
|
||||
|
||||
smplL *= env * gain;
|
||||
smplR *= env * gain;
|
||||
|
||||
outL[s] += smplL * panL;
|
||||
if (outR)
|
||||
outR[s] += smplR * panR;
|
||||
|
||||
v.position += pitchRatio;
|
||||
++v.samplesPlayed;
|
||||
|
||||
if (v.position >= (float)bufLen)
|
||||
v.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int TrommelkisteProcessor::findFreeVoice(int trackIndex)
|
||||
{
|
||||
for (int i = 0; i < MAX_VOICES; ++i)
|
||||
if (!voices[i].active)
|
||||
return i;
|
||||
|
||||
int oldest = 0, bestAge = -1;
|
||||
for (int i = 0; i < MAX_VOICES; ++i)
|
||||
{
|
||||
if (voices[i].trackIndex == trackIndex && voices[i].samplesPlayed > bestAge)
|
||||
{
|
||||
bestAge = voices[i].samplesPlayed;
|
||||
oldest = i;
|
||||
}
|
||||
}
|
||||
if (bestAge >= 0)
|
||||
return oldest;
|
||||
|
||||
oldest = 0;
|
||||
bestAge = -1;
|
||||
for (int i = 0; i < MAX_VOICES; ++i)
|
||||
{
|
||||
if (voices[i].samplesPlayed > bestAge)
|
||||
{
|
||||
bestAge = voices[i].samplesPlayed;
|
||||
oldest = i;
|
||||
}
|
||||
}
|
||||
return oldest;
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::handleNoteOn(int trackIndex, float velocity)
|
||||
{
|
||||
int idx = findFreeVoice(trackIndex);
|
||||
auto& v = voices[idx];
|
||||
v.active = true;
|
||||
v.trackIndex = trackIndex;
|
||||
v.position = 0.0f;
|
||||
v.samplesPlayed = 0;
|
||||
v.filterStateL = 0.0f;
|
||||
v.filterStateR = 0.0f;
|
||||
v.noteVelocity = velocity;
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::handleNoteOff(int trackIndex)
|
||||
{
|
||||
for (auto& v : voices)
|
||||
{
|
||||
if (v.active && v.trackIndex == trackIndex)
|
||||
{
|
||||
const int decayType = (int)apvts.getRawParameterValue(
|
||||
"t" + juce::String(trackIndex) + "_decay")->load();
|
||||
if (decayType == 1)
|
||||
v.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::loadSample(int trackIndex, const juce::File& file)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
||||
return;
|
||||
|
||||
std::unique_ptr<juce::AudioFormatReader> reader(formatManager.createReaderFor(file));
|
||||
if (reader == nullptr)
|
||||
return;
|
||||
|
||||
juce::AudioBuffer<float> newBuf(reader->numChannels, (int)reader->lengthInSamples);
|
||||
reader->read(&newBuf, 0, (int)reader->lengthInSamples, 0, true, true);
|
||||
|
||||
{
|
||||
const juce::SpinLock::ScopedLockType sl(tracks[trackIndex].lock);
|
||||
tracks[trackIndex].buffer = std::move(newBuf);
|
||||
tracks[trackIndex].fileSampleRate = (int)reader->sampleRate;
|
||||
tracks[trackIndex].filePath = file.getFullPathName();
|
||||
}
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::clearSample(int trackIndex)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
||||
return;
|
||||
const juce::SpinLock::ScopedLockType sl(tracks[trackIndex].lock);
|
||||
tracks[trackIndex].buffer.setSize(1, 0);
|
||||
tracks[trackIndex].filePath = "";
|
||||
}
|
||||
|
||||
static void readAudioFromBuffer(juce::AudioFormatManager& fmtMgr,
|
||||
juce::AudioBuffer<float>& dest,
|
||||
int& destSampleRate,
|
||||
const char* data, int size)
|
||||
{
|
||||
auto memStream = std::make_unique<juce::MemoryInputStream>(data, size, false);
|
||||
std::unique_ptr<juce::AudioFormatReader> reader(fmtMgr.createReaderFor(std::move(memStream)));
|
||||
if (reader == nullptr)
|
||||
return;
|
||||
dest.setSize((int)reader->numChannels, (int)reader->lengthInSamples);
|
||||
reader->read(&dest, 0, (int)reader->lengthInSamples, 0, true, true);
|
||||
destSampleRate = (int)reader->sampleRate;
|
||||
}
|
||||
|
||||
// ── Sample library ─────────────────────────────────────────────
|
||||
|
||||
static bool matchesPrefix(const juce::String& name, const juce::String& prefix)
|
||||
{
|
||||
return name.toUpperCase().startsWith(prefix.toUpperCase());
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::scanSamples()
|
||||
{
|
||||
static const char* prefixes[][8] = {
|
||||
{ "BT", nullptr }, // 0 BD
|
||||
{ "RIM", nullptr }, // 1 RS
|
||||
{ "ST", "STAT", nullptr }, // 2 SN
|
||||
{ "HANDCLP", nullptr }, // 3 CL
|
||||
{ "HHCD", nullptr }, // 4 CH
|
||||
{ "CLOP", nullptr }, // 5 PH
|
||||
{ "HHOD", nullptr }, // 6 OH
|
||||
{ "RIDE", nullptr }, // 7 RD
|
||||
{ "LT", nullptr }, // 8 LT
|
||||
{ "MT", nullptr }, // 9 MT
|
||||
};
|
||||
|
||||
for (int t = 0; t < NUM_TRACKS; ++t)
|
||||
{
|
||||
trackSamples[t].clear();
|
||||
currentSampleIndex[t] = -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < numEmbeddedSamples; ++i)
|
||||
{
|
||||
const auto& es = embeddedSamples[i];
|
||||
juce::String name(es.name);
|
||||
|
||||
for (int t = 0; t < NUM_TRACKS; ++t)
|
||||
{
|
||||
for (int p = 0; prefixes[t][p] != nullptr; ++p)
|
||||
{
|
||||
if (matchesPrefix(name, prefixes[t][p]))
|
||||
{
|
||||
SampleRef ref;
|
||||
ref.name = name;
|
||||
ref.data = es.data;
|
||||
ref.dataSize = es.size;
|
||||
trackSamples[t].add(ref);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SampleRefSorter
|
||||
{
|
||||
int compareElements(const SampleRef& a, const SampleRef& b) const { return a.name.compare(b.name); }
|
||||
};
|
||||
SampleRefSorter sorter;
|
||||
for (int t = 0; t < NUM_TRACKS; ++t)
|
||||
trackSamples[t].sort(sorter);
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::loadSampleByIndex(int trackIndex, int sampleIndex)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
||||
return;
|
||||
if (sampleIndex < 0 || sampleIndex >= trackSamples[trackIndex].size())
|
||||
return;
|
||||
|
||||
currentSampleIndex[trackIndex] = sampleIndex;
|
||||
const auto& ref = trackSamples[trackIndex][sampleIndex];
|
||||
|
||||
if (ref.data != nullptr && ref.dataSize > 0)
|
||||
{
|
||||
const juce::SpinLock::ScopedLockType sl(tracks[trackIndex].lock);
|
||||
readAudioFromBuffer(formatManager, tracks[trackIndex].buffer,
|
||||
tracks[trackIndex].fileSampleRate,
|
||||
ref.data, ref.dataSize);
|
||||
tracks[trackIndex].filePath = "";
|
||||
}
|
||||
else if (ref.file.existsAsFile())
|
||||
{
|
||||
loadSample(trackIndex, ref.file);
|
||||
}
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::nextSample(int trackIndex)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
||||
return;
|
||||
const int n = trackSamples[trackIndex].size();
|
||||
if (n == 0) return;
|
||||
|
||||
int idx = currentSampleIndex[trackIndex] + 1;
|
||||
if (idx >= n) idx = 0;
|
||||
loadSampleByIndex(trackIndex, idx);
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::prevSample(int trackIndex)
|
||||
{
|
||||
if (trackIndex < 0 || trackIndex >= NUM_TRACKS)
|
||||
return;
|
||||
const int n = trackSamples[trackIndex].size();
|
||||
if (n == 0) return;
|
||||
|
||||
int idx = currentSampleIndex[trackIndex] - 1;
|
||||
if (idx < 0) idx = n - 1;
|
||||
loadSampleByIndex(trackIndex, idx);
|
||||
}
|
||||
|
||||
// ── Kit / Preset system ───────────────────────────────────────
|
||||
|
||||
static const TrommelkisteProcessor::Kit kits[] = {
|
||||
{ "Classic", {
|
||||
"BT0AADA", "RIM63", "ST0T0SA", "HANDCLP2", "HHCD6",
|
||||
"CLOP2", "HHOD6", "RIDED6", "LT0DA", "MT0DA" } },
|
||||
{ "Punchy", {
|
||||
"BT7A0D3", "RIM127", "ST7T7S3", "HANDCLP1", "HHCD2",
|
||||
"CLOP1", "HHOD2", "RIDED2", "LT7D3", "MT7D3" } },
|
||||
{ "Soft", {
|
||||
"BT0A0D7", "RIM63", "ST0T0S7", "HANDCLP2", "HHCD8",
|
||||
"CLOP4", "HHOD8", "RIDED8", "LT0D7", "MT0D7" } },
|
||||
{ "Raw", {
|
||||
"BTAAADA", "RIM127", "STATASA", "HANDCLP1", "HHCDA",
|
||||
"CLOP3", "HHODA", "RIDEDA", "LTADA", "MTADA" } },
|
||||
};
|
||||
|
||||
const TrommelkisteProcessor::Kit* TrommelkisteProcessor::getKitList() { return kits; }
|
||||
int TrommelkisteProcessor::getNumKits() const { return 4; }
|
||||
juce::String TrommelkisteProcessor::getCurrentKitName() const
|
||||
{
|
||||
if (currentKitIndex >= 0 && currentKitIndex < getNumKits())
|
||||
return kits[currentKitIndex].name;
|
||||
return {};
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::loadKit(int index)
|
||||
{
|
||||
if (index < 0 || index >= getNumKits())
|
||||
return;
|
||||
|
||||
currentKitIndex = index;
|
||||
const auto& kit = kits[index];
|
||||
|
||||
for (int t = 0; t < NUM_TRACKS; ++t)
|
||||
{
|
||||
if (kit.samples[t] == nullptr || kit.samples[t][0] == '\0')
|
||||
{
|
||||
currentSampleIndex[t] = -1;
|
||||
clearSample(t);
|
||||
continue;
|
||||
}
|
||||
|
||||
juce::String target(kit.samples[t]);
|
||||
int found = -1;
|
||||
for (int s = 0; s < trackSamples[t].size(); ++s)
|
||||
{
|
||||
if (trackSamples[t][s].name.equalsIgnoreCase(target))
|
||||
{
|
||||
found = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found >= 0)
|
||||
loadSampleByIndex(t, found);
|
||||
else
|
||||
clearSample(t);
|
||||
}
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::nextKit()
|
||||
{
|
||||
int idx = currentKitIndex + 1;
|
||||
if (idx >= getNumKits()) idx = 0;
|
||||
loadKit(idx);
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::prevKit()
|
||||
{
|
||||
int idx = currentKitIndex - 1;
|
||||
if (idx < 0) idx = getNumKits() - 1;
|
||||
loadKit(idx);
|
||||
}
|
||||
|
||||
juce::AudioProcessorEditor* TrommelkisteProcessor::createEditor()
|
||||
{
|
||||
return new TrommelkisteEditor(*this);
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::getStateInformation(juce::MemoryBlock& destData)
|
||||
{
|
||||
auto state = apvts.copyState();
|
||||
|
||||
state.setProperty("kitIndex", currentKitIndex, nullptr);
|
||||
|
||||
juce::ValueTree samples("Samples");
|
||||
for (int i = 0; i < NUM_TRACKS; ++i)
|
||||
{
|
||||
juce::ValueTree tr("Track" + juce::String(i));
|
||||
tr.setProperty("path", tracks[i].filePath, nullptr);
|
||||
tr.setProperty("sampleIndex", currentSampleIndex[i], nullptr);
|
||||
if (currentSampleIndex[i] >= 0 && currentSampleIndex[i] < trackSamples[i].size())
|
||||
tr.setProperty("sampleName", trackSamples[i][currentSampleIndex[i]].name, nullptr);
|
||||
samples.appendChild(tr, nullptr);
|
||||
}
|
||||
state.appendChild(samples, nullptr);
|
||||
|
||||
std::unique_ptr<juce::XmlElement> xml(state.createXml());
|
||||
copyXmlToBinary(*xml, destData);
|
||||
}
|
||||
|
||||
void TrommelkisteProcessor::setStateInformation(const void* data, int sizeInBytes)
|
||||
{
|
||||
std::unique_ptr<juce::XmlElement> xml(getXmlFromBinary(data, sizeInBytes));
|
||||
if (xml == nullptr)
|
||||
return;
|
||||
|
||||
auto state = juce::ValueTree::fromXml(*xml);
|
||||
apvts.replaceState(state);
|
||||
|
||||
currentKitIndex = (int)state.getProperty("kitIndex", 0);
|
||||
|
||||
auto samples = state.getChildWithName("Samples");
|
||||
if (samples.isValid())
|
||||
{
|
||||
for (int i = 0; i < NUM_TRACKS; ++i)
|
||||
{
|
||||
auto tr = samples.getChildWithName("Track" + juce::String(i));
|
||||
if (!tr.isValid())
|
||||
continue;
|
||||
|
||||
auto path = tr.getProperty("path", "").toString();
|
||||
auto sampleName = tr.getProperty("sampleName", "").toString();
|
||||
|
||||
if (path.isNotEmpty())
|
||||
{
|
||||
loadSample(i, juce::File(path));
|
||||
currentSampleIndex[i] = (int)tr.getProperty("sampleIndex", -1);
|
||||
}
|
||||
else if (sampleName.isNotEmpty())
|
||||
{
|
||||
int found = -1;
|
||||
for (int s = 0; s < trackSamples[i].size(); ++s)
|
||||
{
|
||||
if (trackSamples[i][s].name.equalsIgnoreCase(sampleName))
|
||||
{
|
||||
found = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found >= 0)
|
||||
loadSampleByIndex(i, found);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout TrommelkisteProcessor::createParameterLayout()
|
||||
{
|
||||
AudioProcessorValueTreeState::ParameterLayout layout;
|
||||
const StringArray decayTypes = {"Saw", "Gate"};
|
||||
|
||||
for (int i = 0; i < NUM_TRACKS; ++i)
|
||||
{
|
||||
const String p = "t" + String(i) + "_";
|
||||
const String tn = "T" + String(i + 1) + " ";
|
||||
|
||||
layout.add(std::make_unique<AudioParameterFloat>(
|
||||
p + "level", tn + "Level",
|
||||
NormalisableRange<float>(0.0f, 1.0f), 0.8f));
|
||||
|
||||
layout.add(std::make_unique<AudioParameterFloat>(
|
||||
p + "length", tn + "Length",
|
||||
NormalisableRange<float>(0.01f, 2.0f, 0.001f, 0.45f), 0.5f));
|
||||
|
||||
layout.add(std::make_unique<AudioParameterFloat>(
|
||||
p + "velocity", tn + "Velocity",
|
||||
NormalisableRange<float>(0.0f, 1.0f), 1.0f));
|
||||
|
||||
layout.add(std::make_unique<AudioParameterFloat>(
|
||||
p + "pitch", tn + "Pitch",
|
||||
NormalisableRange<float>(-12.0f, 12.0f, 0.1f), 0.0f));
|
||||
|
||||
layout.add(std::make_unique<AudioParameterFloat>(
|
||||
p + "tone", tn + "Tone",
|
||||
NormalisableRange<float>(20.0f, 20000.0f, 1.0f, 0.25f), 20000.0f));
|
||||
|
||||
layout.add(std::make_unique<AudioParameterFloat>(
|
||||
p + "pan", tn + "Pan",
|
||||
NormalisableRange<float>(-1.0f, 1.0f), 0.0f));
|
||||
|
||||
layout.add(std::make_unique<AudioParameterChoice>(
|
||||
p + "decay", tn + "Decay",
|
||||
decayTypes, 0));
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
AudioProcessor* JUCE_CALLTYPE createPluginFilter()
|
||||
{
|
||||
return new TrommelkisteProcessor();
|
||||
}
|
||||
107
Source/PluginProcessor.h
Normal file
107
Source/PluginProcessor.h
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
#pragma once
|
||||
#include <juce_audio_processors/juce_audio_processors.h>
|
||||
#include <juce_audio_formats/juce_audio_formats.h>
|
||||
|
||||
static constexpr int NUM_TRACKS = 10;
|
||||
static constexpr int MAX_VOICES = 32;
|
||||
static constexpr int BASE_NOTE = 60;
|
||||
|
||||
class TrommelkisteProcessor : public juce::AudioProcessor
|
||||
{
|
||||
public:
|
||||
TrommelkisteProcessor();
|
||||
~TrommelkisteProcessor() override;
|
||||
|
||||
void prepareToPlay(double sampleRate, int samplesPerBlock) override;
|
||||
void releaseResources() override;
|
||||
void processBlock(juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
|
||||
|
||||
juce::AudioProcessorEditor* createEditor() override;
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
const juce::String getName() const override { return "Trommelkiste"; }
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return false; }
|
||||
bool isMidiEffect() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram(int) override {}
|
||||
const juce::String getProgramName(int) override { return {}; }
|
||||
void changeProgramName(int, const juce::String&) override {}
|
||||
|
||||
void getStateInformation(juce::MemoryBlock& destData) override;
|
||||
void setStateInformation(const void* data, int sizeInBytes) override;
|
||||
|
||||
std::optional<juce::String> getNameForMidiNoteNumber(int note, int) override;
|
||||
|
||||
void loadSample(int trackIndex, const juce::File& file);
|
||||
void clearSample(int trackIndex);
|
||||
|
||||
struct Track
|
||||
{
|
||||
juce::AudioBuffer<float> buffer;
|
||||
int fileSampleRate = 44100;
|
||||
juce::SpinLock lock;
|
||||
juce::String filePath;
|
||||
};
|
||||
|
||||
Track tracks[NUM_TRACKS];
|
||||
bool trackActive[NUM_TRACKS] = {};
|
||||
juce::AudioProcessorValueTreeState apvts;
|
||||
|
||||
// Sample library
|
||||
struct SampleRef
|
||||
{
|
||||
juce::String name;
|
||||
juce::File file;
|
||||
const char* data = nullptr;
|
||||
int dataSize = 0;
|
||||
};
|
||||
|
||||
juce::Array<SampleRef> trackSamples[NUM_TRACKS];
|
||||
int currentSampleIndex[NUM_TRACKS] = {};
|
||||
void scanSamples();
|
||||
void loadSampleByIndex(int trackIndex, int sampleIndex);
|
||||
void nextSample(int trackIndex);
|
||||
void prevSample(int trackIndex);
|
||||
|
||||
// Kit / Preset system
|
||||
struct Kit
|
||||
{
|
||||
juce::String name;
|
||||
const char* samples[NUM_TRACKS];
|
||||
};
|
||||
|
||||
int currentKitIndex = 0;
|
||||
void loadKit(int index);
|
||||
void nextKit();
|
||||
void prevKit();
|
||||
int getNumKits() const;
|
||||
juce::String getCurrentKitName() const;
|
||||
static const Kit* getKitList();
|
||||
|
||||
private:
|
||||
struct Voice
|
||||
{
|
||||
bool active = false;
|
||||
int trackIndex = -1;
|
||||
float position = 0.0f;
|
||||
int samplesPlayed = 0;
|
||||
float filterStateL = 0.0f;
|
||||
float filterStateR = 0.0f;
|
||||
float noteVelocity = 1.0f;
|
||||
};
|
||||
|
||||
Voice voices[MAX_VOICES];
|
||||
juce::AudioFormatManager formatManager;
|
||||
double currentSampleRate = 44100.0;
|
||||
|
||||
int findFreeVoice(int trackIndex);
|
||||
void handleNoteOn(int trackIndex, float velocity);
|
||||
void handleNoteOff(int trackIndex);
|
||||
juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout();
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TrommelkisteProcessor)
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue