mirror of
https://codeberg.org/armin/trommelkiste.git
synced 2026-09-01 04:10:46 +02:00
recent
This commit is contained in:
parent
2e5fe1750f
commit
044e3b7a52
7 changed files with 618 additions and 47 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -8,4 +8,3 @@ Builds/
|
||||||
*.xcuserdata
|
*.xcuserdata
|
||||||
DerivedData/
|
DerivedData/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
JUCE
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ project(Trommelkiste VERSION 1.0.0 LANGUAGES C CXX)
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
add_subdirectory(JUCE)
|
add_subdirectory(build/JUCE)
|
||||||
|
|
||||||
juce_add_plugin(Trommelkiste
|
juce_add_plugin(Trommelkiste
|
||||||
PRODUCT_NAME "Trommelkiste"
|
PRODUCT_NAME "Trommelkiste"
|
||||||
|
|
@ -64,6 +64,7 @@ target_link_libraries(Trommelkiste
|
||||||
juce::juce_audio_utils
|
juce::juce_audio_utils
|
||||||
juce::juce_core
|
juce::juce_core
|
||||||
juce::juce_data_structures
|
juce::juce_data_structures
|
||||||
|
juce::juce_dsp
|
||||||
juce::juce_events
|
juce::juce_events
|
||||||
juce::juce_graphics
|
juce::juce_graphics
|
||||||
juce::juce_gui_basics
|
juce::juce_gui_basics
|
||||||
|
|
|
||||||
5
Makefile
5
Makefile
|
|
@ -1,4 +1,4 @@
|
||||||
JUCE := JUCE
|
JUCE := build/JUCE
|
||||||
NPROC := $(shell sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)
|
NPROC := $(shell sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4)
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
|
|
||||||
|
|
@ -52,7 +52,8 @@ all: build/Makefile
|
||||||
@find $(BUILD_DIR) -type d \( -name "*.vst3" -o -name "*.component" -o -name "*.app" \) 2>/dev/null | while read f; do echo " $$f"; done
|
@find $(BUILD_DIR) -type d \( -name "*.vst3" -o -name "*.component" -o -name "*.app" \) 2>/dev/null | while read f; do echo " $$f"; done
|
||||||
|
|
||||||
build/Makefile: CMakeLists.txt
|
build/Makefile: CMakeLists.txt
|
||||||
@if [ ! -d "$(JUCE)" ]; then \
|
@if [ ! -f "$(JUCE)/CMakeLists.txt" ]; then \
|
||||||
|
rm -rf "$(JUCE)"; \
|
||||||
echo "==> Cloning JUCE..."; \
|
echo "==> Cloning JUCE..."; \
|
||||||
git clone --depth 1 https://github.com/juce-framework/JUCE.git $(JUCE); \
|
git clone --depth 1 https://github.com/juce-framework/JUCE.git $(JUCE); \
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -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 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 ROW_H = 52;
|
||||||
static constexpr int TOP = 44;
|
static constexpr int TOP = 80;
|
||||||
static constexpr int COL_LBL = 8;
|
static constexpr int COL_LBL = 8;
|
||||||
static constexpr int COL_KNOB = 44;
|
static constexpr int COL_KNOB = 44;
|
||||||
static constexpr int KNOB_W = 44;
|
static constexpr int KNOB_W = 44;
|
||||||
static constexpr int KNOB_GAP = 4;
|
static constexpr int KNOB_GAP = 4;
|
||||||
static constexpr int COL_DECAY = COL_KNOB + 6 * (KNOB_W + KNOB_GAP) + 8;
|
static constexpr int COL_DECAY = COL_KNOB + 6 * (KNOB_W + KNOB_GAP) + 8;
|
||||||
static constexpr int COL_FILE = COL_DECAY + 78;
|
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 ──────────────────────────────────────────────────
|
// ── LookAndFeel ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -29,16 +50,58 @@ void TrommelkisteLookAndFeel::drawRotarySlider(Graphics& g, int x, int y, int w,
|
||||||
auto cy = bounds.getCentreY();
|
auto cy = bounds.getCentreY();
|
||||||
auto angle = startAngle + sliderPos * (endAngle - startAngle);
|
auto angle = startAngle + sliderPos * (endAngle - startAngle);
|
||||||
|
|
||||||
g.setColour(Colour(0xFF333333));
|
// Outer shadow
|
||||||
g.fillEllipse(cx - radius, cy - radius, radius * 2.0f, radius * 2.0f);
|
{
|
||||||
|
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;
|
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.setColour(accent);
|
||||||
g.strokePath(arc, PathStrokeType(2.5f, PathStrokeType::curved, PathStrokeType::rounded));
|
g.strokePath(arc, PathStrokeType(2.5f, PathStrokeType::curved, PathStrokeType::rounded));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pointer / indicator line
|
||||||
{
|
{
|
||||||
Path ptr;
|
Path ptr;
|
||||||
ptr.addRectangle(-1.25f, -radius + 2.0f, 2.5f, radius * 0.5f);
|
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.setColour(Colours::white);
|
||||||
g.fillPath(ptr);
|
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&,
|
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));
|
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 ───────────────────────────────────────────────────────
|
// ── Editor ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
|
TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
|
||||||
|
|
@ -93,7 +207,7 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
|
||||||
setLookAndFeel(&lnf);
|
setLookAndFeel(&lnf);
|
||||||
lnf.fontScale = 1.5f;
|
lnf.fontScale = 1.5f;
|
||||||
uiScale = 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);
|
setResizable(false, false);
|
||||||
setWantsKeyboardFocus(false);
|
setWantsKeyboardFocus(false);
|
||||||
|
|
||||||
|
|
@ -118,6 +232,11 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
|
||||||
setupKnob(t.pitch, "pitch");
|
setupKnob(t.pitch, "pitch");
|
||||||
setupKnob(t.tone, "tone");
|
setupKnob(t.tone, "tone");
|
||||||
setupKnob(t.pan, "pan");
|
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.level.formatFn = [](double v) { return String(v, 1); };
|
||||||
t.length.formatFn = [](double v) { return String(v, 1) + "s"; };
|
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);
|
int pct = (int) std::round(std::abs(v) * 100.0);
|
||||||
return (v < 0 ? "L" : "R") + String(pct);
|
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.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.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.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.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.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.setClickingTogglesState(true);
|
||||||
t.decay.setColour(ComboBox::backgroundColourId, Colour(0xFF333333));
|
t.decay.setButtonText("");
|
||||||
t.decay.setColour(ComboBox::textColourId, Colours::white);
|
t.decay.setColour(ToggleButton::textColourId, Colours::white);
|
||||||
t.decay.setColour(ComboBox::outlineColourId, Colour(0xFF555555));
|
t.decay.setColour(ToggleButton::tickColourId, lnf.accent);
|
||||||
addAndMakeVisible(t.decay);
|
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] {
|
t.prevBtn.onClick = [this, i] {
|
||||||
proc.prevSample(i);
|
proc.prevSample(i);
|
||||||
|
|
@ -209,6 +337,88 @@ TrommelkisteEditor::TrommelkisteEditor(TrommelkisteProcessor& p)
|
||||||
scaleCombo.onChange = [this] { scaleChanged(); };
|
scaleCombo.onChange = [this] { scaleChanged(); };
|
||||||
addAndMakeVisible(scaleCombo);
|
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);
|
startTimerHz(30);
|
||||||
resized();
|
resized();
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +428,17 @@ TrommelkisteEditor::~TrommelkisteEditor()
|
||||||
setLookAndFeel(nullptr);
|
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()
|
void TrommelkisteEditor::scaleChanged()
|
||||||
{
|
{
|
||||||
static const float scales[] = { 1.0f, 1.25f, 1.5f, 1.75f, 2.0f };
|
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];
|
uiScale = scales[selId - 1];
|
||||||
lnf.fontScale = uiScale;
|
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.drawText("TROMMELKISTE", scaled(10), scaled(4), scaled(400), scaled(20), Justification::centredLeft);
|
||||||
|
|
||||||
g.setColour(lnf.accent.withAlpha(0.3f));
|
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.setColour(Colours::white.withAlpha(0.9f));
|
||||||
g.setFont(font(9.0f, uiScale));
|
g.setFont(font(9.0f, uiScale));
|
||||||
g.drawText("LVL", scaled(COL_KNOB), scaled(30), scaled(KNOB_W), scaled(12), Justification::centred);
|
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(30), 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(30), 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(30), 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(30), 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(30), 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(30), scaled(68), 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(30), scaled(100), scaled(12), Justification::centredLeft);
|
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)
|
for (int i = 0; i < NUM_TRACKS; ++i)
|
||||||
{
|
{
|
||||||
const int ry = TOP + i * ROW_H;
|
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.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.setColour(Colours::white);
|
||||||
g.setFont(fontBold(12.0f, uiScale));
|
g.setFont(fontBold(12.0f, uiScale));
|
||||||
g.drawText(trackNames[i], scaled(COL_LBL), scaled(ry + 6), scaled(32), scaled(16), Justification::centredLeft);
|
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.setFont(font(9.0f, uiScale));
|
||||||
g.drawText(noteNames[i], scaled(COL_LBL), scaled(ry + 24), scaled(32), scaled(14), Justification::centredLeft);
|
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));
|
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()
|
void TrommelkisteEditor::resized()
|
||||||
{
|
{
|
||||||
// Header: title is painted; position controls in top row
|
// Header: title is painted; position controls in top row
|
||||||
presetPrevBtn.setBounds(scaled(440), scaled(4), scaled(20), scaled(20));
|
presetPrevBtn.setBounds(scaled(150), scaled(14), scaled(20), scaled(20));
|
||||||
presetCombo.setBounds(scaled(462), scaled(4), scaled(110), scaled(20));
|
presetCombo.setBounds(scaled(172), scaled(14), scaled(100), scaled(20));
|
||||||
presetNextBtn.setBounds(scaled(574), scaled(4), scaled(20), scaled(20));
|
presetNextBtn.setBounds(scaled(274), scaled(14), scaled(20), scaled(20));
|
||||||
scaleCombo.setBounds(scaled(598), scaled(4), scaled(56), 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)
|
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));
|
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));
|
t.decay.setBounds(scaled(COL_DECAY), scaled(ry + 16), scaled(68), scaled(22));
|
||||||
|
|
||||||
const int fileX = COL_FILE;
|
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.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.nextBtn.setBounds(scaled(fileEnd - 18), scaled(ry + 16), scaled(18), scaled(22));
|
||||||
t.fileLabel.setBounds(scaled(fileX + 20), scaled(ry + 16),
|
t.fileLabel.setBounds(scaled(fileX + 20), scaled(ry + 16),
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ public:
|
||||||
|
|
||||||
void drawRotarySlider(juce::Graphics&, int x, int y, int w, int h,
|
void drawRotarySlider(juce::Graphics&, int x, int y, int w, int h,
|
||||||
float pos, float start, float end, juce::Slider&) override;
|
float pos, float start, float end, juce::Slider&) override;
|
||||||
|
void drawToggleButton(juce::Graphics&, juce::ToggleButton&, bool, bool) override;
|
||||||
void drawButtonBackground(juce::Graphics&, juce::Button&, const juce::Colour&,
|
void drawButtonBackground(juce::Graphics&, juce::Button&, const juce::Colour&,
|
||||||
bool, bool) override;
|
bool, bool) override;
|
||||||
void drawComboBox(juce::Graphics&, int w, int h, bool down,
|
void drawComboBox(juce::Graphics&, int w, int h, bool down,
|
||||||
|
|
@ -38,7 +39,7 @@ public:
|
||||||
void paint(juce::Graphics&) override;
|
void paint(juce::Graphics&) override;
|
||||||
void resized() override;
|
void resized() override;
|
||||||
void mouseDown(const juce::MouseEvent&) override;
|
void mouseDown(const juce::MouseEvent&) override;
|
||||||
void timerCallback() override { repaint(); }
|
void timerCallback() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TrommelkisteProcessor& proc;
|
TrommelkisteProcessor& proc;
|
||||||
|
|
@ -53,26 +54,46 @@ private:
|
||||||
struct TrackUI
|
struct TrackUI
|
||||||
{
|
{
|
||||||
using SliderAtt = juce::AudioProcessorValueTreeState::SliderAttachment;
|
using SliderAtt = juce::AudioProcessorValueTreeState::SliderAttachment;
|
||||||
using ComboAtt = juce::AudioProcessorValueTreeState::ComboBoxAttachment;
|
using ButtonAtt = juce::AudioProcessorValueTreeState::ButtonAttachment;
|
||||||
|
|
||||||
juce::TextButton prevBtn{"<"};
|
juce::TextButton prevBtn{"<"};
|
||||||
juce::TextButton nextBtn{">"};
|
juce::TextButton nextBtn{">"};
|
||||||
KnobSlider level, length, velocity, pitch, tone, pan;
|
KnobSlider level, length, velocity, pitch, tone, pan;
|
||||||
juce::ComboBox decay;
|
KnobSlider delaySend, reverbSend, ringMod, destruction, shuffle;
|
||||||
|
juce::ToggleButton decay;
|
||||||
juce::Label fileLabel;
|
juce::Label fileLabel;
|
||||||
|
|
||||||
std::unique_ptr<SliderAtt> levelAtt, lengthAtt, velAtt, pitchAtt, toneAtt, panAtt;
|
std::unique_ptr<SliderAtt> levelAtt, lengthAtt, velAtt, pitchAtt, toneAtt, panAtt;
|
||||||
std::unique_ptr<ComboAtt> decayAtt;
|
std::unique_ptr<SliderAtt> delaySendAtt, reverbSendAtt;
|
||||||
|
std::unique_ptr<SliderAtt> ringModAtt, destructionAtt, shuffleAtt;
|
||||||
|
std::unique_ptr<ButtonAtt> decayAtt;
|
||||||
};
|
};
|
||||||
|
|
||||||
TrackUI ui[NUM_TRACKS];
|
TrackUI ui[NUM_TRACKS];
|
||||||
juce::FileChooser chooser{"Load Sample", juce::File{}, "*.wav;*.aif;*.aiff;*.flac"};
|
juce::FileChooser chooser{"Load Sample", juce::File{}, "*.wav;*.aif;*.aiff;*.flac"};
|
||||||
|
double trackActivatedTime[NUM_TRACKS] = {};
|
||||||
|
|
||||||
// Preset selector
|
// Preset selector
|
||||||
juce::ComboBox presetCombo;
|
juce::ComboBox presetCombo;
|
||||||
juce::TextButton presetPrevBtn{"<"};
|
juce::TextButton presetPrevBtn{"<"};
|
||||||
juce::TextButton presetNextBtn{">"};
|
juce::TextButton presetNextBtn{">"};
|
||||||
|
|
||||||
|
// Global effect controls in header
|
||||||
|
KnobSlider globalRingFreq, globalDestruction;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> globalRingFreqAtt, globalDestructionAtt;
|
||||||
|
|
||||||
|
// Master controls in header
|
||||||
|
KnobSlider masterVolume, masterPan, masterDelay, masterReverb;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> masterVolumeAtt, masterPanAtt, masterDelayAtt, masterReverbAtt;
|
||||||
|
|
||||||
|
// Global shuffle controls in header
|
||||||
|
KnobSlider globalShuffleDiv, globalShuffleAmt, globalGroove;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> globalShuffleDivAtt, globalShuffleAmtAtt, globalGrooveAtt;
|
||||||
|
|
||||||
|
// Delay sync control in header
|
||||||
|
KnobSlider delaySyncDiv;
|
||||||
|
std::unique_ptr<juce::AudioProcessorValueTreeState::SliderAttachment> delaySyncDivAtt;
|
||||||
|
|
||||||
void loadForTrack(int idx);
|
void loadForTrack(int idx);
|
||||||
void updateFileLabel(int idx);
|
void updateFileLabel(int idx);
|
||||||
void refreshPresetCombo();
|
void refreshPresetCombo();
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,24 @@ std::optional<juce::String> TrommelkisteProcessor::getNameForMidiNoteNumber(int
|
||||||
void TrommelkisteProcessor::prepareToPlay(double sr, int)
|
void TrommelkisteProcessor::prepareToPlay(double sr, int)
|
||||||
{
|
{
|
||||||
currentSampleRate = sr;
|
currentSampleRate = sr;
|
||||||
|
|
||||||
|
juce::dsp::ProcessSpec spec;
|
||||||
|
spec.sampleRate = sr;
|
||||||
|
spec.maximumBlockSize = 512;
|
||||||
|
spec.numChannels = 2;
|
||||||
|
reverb.prepare(spec);
|
||||||
|
reverbParams.roomSize = 0.5f;
|
||||||
|
reverbParams.damping = 0.5f;
|
||||||
|
reverbParams.wetLevel = 0.33f;
|
||||||
|
reverbParams.dryLevel = 0.8f;
|
||||||
|
reverbParams.width = 1.0f;
|
||||||
|
reverbParams.freezeMode = 0.0f;
|
||||||
|
reverb.setParameters(reverbParams);
|
||||||
|
|
||||||
|
const int delaySamples = DELAY_BUFFER_SECONDS * (int)sr;
|
||||||
|
delayBuffer.setSize(2, delaySamples);
|
||||||
|
delayBuffer.clear();
|
||||||
|
delayWritePos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TrommelkisteProcessor::releaseResources() {}
|
void TrommelkisteProcessor::releaseResources() {}
|
||||||
|
|
@ -42,6 +60,34 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
for (int i = 0; i < NUM_TRACKS; ++i)
|
for (int i = 0; i < NUM_TRACKS; ++i)
|
||||||
trackActive[i] = false;
|
trackActive[i] = false;
|
||||||
|
|
||||||
|
// Shuffle: add ghost notes (bar-quantized)
|
||||||
|
const float globalShuffleAmt = apvts.getRawParameterValue("global_shuffleAmount")->load();
|
||||||
|
const float globalGroove = apvts.getRawParameterValue("global_groove")->load();
|
||||||
|
const int shuffleDivIdx = (int)apvts.getRawParameterValue("global_shuffleDiv")->load();
|
||||||
|
// 0=OFF, 1=every 4 bars, 2=every 8, 3=every 16, 4=every 32
|
||||||
|
static constexpr int shuffleDivBars[] = { 0, 4, 8, 16, 32 };
|
||||||
|
const int divBars = shuffleDivBars[juce::jlimit(0, 4, shuffleDivIdx)];
|
||||||
|
|
||||||
|
// Try to get bar position from play head
|
||||||
|
bool hasTransport = false;
|
||||||
|
int64_t currentBar = 0;
|
||||||
|
if (auto* ph = getPlayHead())
|
||||||
|
{
|
||||||
|
auto info = ph->getPosition();
|
||||||
|
if (info.hasValue() && info->getPpqPosition().hasValue())
|
||||||
|
{
|
||||||
|
hasTransport = true;
|
||||||
|
double ppq = *info->getPpqPosition();
|
||||||
|
double beatsPerBar = 4.0;
|
||||||
|
if (info->getTimeSignature().hasValue())
|
||||||
|
beatsPerBar = (double)(*info->getTimeSignature()).numerator;
|
||||||
|
currentBar = (int64_t)(ppq / beatsPerBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool barAllowed = (divBars == 0) || !hasTransport || (currentBar % divBars == 0);
|
||||||
|
|
||||||
|
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
|
||||||
for (const auto metadata : midi)
|
for (const auto metadata : midi)
|
||||||
{
|
{
|
||||||
auto msg = metadata.getMessage();
|
auto msg = metadata.getMessage();
|
||||||
|
|
@ -49,7 +95,32 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
{
|
{
|
||||||
int t = msg.getNoteNumber() - BASE_NOTE;
|
int t = msg.getNoteNumber() - BASE_NOTE;
|
||||||
if (t >= 0 && t < NUM_TRACKS)
|
if (t >= 0 && t < NUM_TRACKS)
|
||||||
|
{
|
||||||
handleNoteOn(t, msg.getFloatVelocity());
|
handleNoteOn(t, msg.getFloatVelocity());
|
||||||
|
|
||||||
|
const float shuffleAmt = apvts.getRawParameterValue(
|
||||||
|
"t" + juce::String(t) + "_shuffle")->load();
|
||||||
|
const float effShuffle = shuffleAmt * globalShuffleAmt;
|
||||||
|
|
||||||
|
if (effShuffle > 0.0f && barAllowed)
|
||||||
|
{
|
||||||
|
// Random note repeat on same drum
|
||||||
|
if (dist(rng) < effShuffle * 0.5f)
|
||||||
|
handleNoteOn(t, msg.getFloatVelocity() * effShuffle * 0.7f);
|
||||||
|
|
||||||
|
// Ghost note on random other drum
|
||||||
|
if (dist(rng) < effShuffle * 0.9f)
|
||||||
|
{
|
||||||
|
int ghostTrack = t;
|
||||||
|
while (ghostTrack == t)
|
||||||
|
ghostTrack = std::uniform_int_distribution<int>(0, NUM_TRACKS - 1)(rng);
|
||||||
|
handleNoteOn(ghostTrack, msg.getFloatVelocity() * effShuffle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shuffleAmt > 0.0f && globalGroove > 0.0f)
|
||||||
|
handleNoteOn(t, msg.getFloatVelocity() * (1.0f + globalGroove * 0.3f * shuffleAmt));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (msg.isNoteOff())
|
else if (msg.isNoteOff())
|
||||||
{
|
{
|
||||||
|
|
@ -62,6 +133,12 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
auto* outL = buffer.getWritePointer(0);
|
auto* outL = buffer.getWritePointer(0);
|
||||||
auto* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : nullptr;
|
auto* outR = buffer.getNumChannels() > 1 ? buffer.getWritePointer(1) : nullptr;
|
||||||
|
|
||||||
|
// Temporary send buffers
|
||||||
|
juce::AudioBuffer<float> delaySend(2, numSamples);
|
||||||
|
juce::AudioBuffer<float> reverbSend(2, numSamples);
|
||||||
|
delaySend.clear();
|
||||||
|
reverbSend.clear();
|
||||||
|
|
||||||
for (auto& v : voices)
|
for (auto& v : voices)
|
||||||
{
|
{
|
||||||
if (!v.active)
|
if (!v.active)
|
||||||
|
|
@ -86,7 +163,11 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
const float pitchSemitones = apvts.getRawParameterValue(p + "pitch")->load();
|
const float pitchSemitones = apvts.getRawParameterValue(p + "pitch")->load();
|
||||||
const float toneHz = apvts.getRawParameterValue(p + "tone")->load();
|
const float toneHz = apvts.getRawParameterValue(p + "tone")->load();
|
||||||
const float pan = apvts.getRawParameterValue(p + "pan")->load();
|
const float pan = apvts.getRawParameterValue(p + "pan")->load();
|
||||||
const int decayType = (int)apvts.getRawParameterValue(p + "decay")->load();
|
const bool decayGate = apvts.getRawParameterValue(p + "decay")->load() > 0.5f;
|
||||||
|
const float delaySendAmt = apvts.getRawParameterValue(p + "delaySend")->load();
|
||||||
|
const float reverbSendAmt = apvts.getRawParameterValue(p + "reverbSend")->load();
|
||||||
|
const float ringModAmt = apvts.getRawParameterValue(p + "ringMod")->load();
|
||||||
|
const float destructionAmt = apvts.getRawParameterValue(p + "destruction")->load();
|
||||||
|
|
||||||
float pitchRatio = std::pow(2.0f, pitchSemitones / 12.0f)
|
float pitchRatio = std::pow(2.0f, pitchSemitones / 12.0f)
|
||||||
* (float)track.fileSampleRate / (float)currentSampleRate;
|
* (float)track.fileSampleRate / (float)currentSampleRate;
|
||||||
|
|
@ -107,13 +188,17 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
const bool hasR = track.buffer.getNumChannels() > 1;
|
const bool hasR = track.buffer.getNumChannels() > 1;
|
||||||
const float* sDataR = hasR ? track.buffer.getReadPointer(1) : nullptr;
|
const float* sDataR = hasR ? track.buffer.getReadPointer(1) : nullptr;
|
||||||
|
|
||||||
|
const float globalRingFreq = apvts.getRawParameterValue("global_ringFreq")->load();
|
||||||
|
const float globalDestr = apvts.getRawParameterValue("global_destruction")->load();
|
||||||
|
const float ringModInc = 2.0f * juce::MathConstants<float>::pi * globalRingFreq / (float)currentSampleRate;
|
||||||
|
|
||||||
for (int s = 0; s < numSamples; ++s)
|
for (int s = 0; s < numSamples; ++s)
|
||||||
{
|
{
|
||||||
if (!v.active)
|
if (!v.active)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
float env = 1.0f;
|
float env = 1.0f;
|
||||||
if (decayType == 0)
|
if (!decayGate)
|
||||||
{
|
{
|
||||||
env = 1.0f - (float)v.samplesPlayed / maxSamples;
|
env = 1.0f - (float)v.samplesPlayed / maxSamples;
|
||||||
if (env <= 0.0f) { v.active = false; break; }
|
if (env <= 0.0f) { v.active = false; break; }
|
||||||
|
|
@ -140,6 +225,30 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
smplR = v.filterStateR;
|
smplR = v.filterStateR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ring modulation
|
||||||
|
if (ringModAmt > 0.0f)
|
||||||
|
{
|
||||||
|
float mod = std::sin(v.ringModPhase);
|
||||||
|
smplL = smplL * (1.0f - ringModAmt + ringModAmt * mod);
|
||||||
|
smplR = smplR * (1.0f - ringModAmt + ringModAmt * mod);
|
||||||
|
v.ringModPhase += ringModInc;
|
||||||
|
if (v.ringModPhase >= 2.0f * juce::MathConstants<float>::pi)
|
||||||
|
v.ringModPhase -= 2.0f * juce::MathConstants<float>::pi;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destruction (soft clip + bitcrush)
|
||||||
|
const float effDestr = juce::jmin(1.0f, destructionAmt + globalDestr);
|
||||||
|
if (effDestr > 0.0f)
|
||||||
|
{
|
||||||
|
float drive = 1.0f + effDestr * 19.0f;
|
||||||
|
smplL = std::tanh(smplL * drive);
|
||||||
|
smplR = std::tanh(smplR * drive);
|
||||||
|
float bits = juce::jmax(2.0f, 16.0f - effDestr * 14.0f);
|
||||||
|
float levels = std::pow(2.0f, bits);
|
||||||
|
smplL = std::round(smplL * levels) / levels;
|
||||||
|
smplR = std::round(smplR * levels) / levels;
|
||||||
|
}
|
||||||
|
|
||||||
smplL *= env * gain;
|
smplL *= env * gain;
|
||||||
smplR *= env * gain;
|
smplR *= env * gain;
|
||||||
|
|
||||||
|
|
@ -147,6 +256,12 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
if (outR)
|
if (outR)
|
||||||
outR[s] += smplR * panR;
|
outR[s] += smplR * panR;
|
||||||
|
|
||||||
|
// Send to effects
|
||||||
|
delaySend.addSample(0, s, smplL * delaySendAmt);
|
||||||
|
delaySend.addSample(1, s, smplR * delaySendAmt);
|
||||||
|
reverbSend.addSample(0, s, smplL * reverbSendAmt);
|
||||||
|
reverbSend.addSample(1, s, smplR * reverbSendAmt);
|
||||||
|
|
||||||
v.position += pitchRatio;
|
v.position += pitchRatio;
|
||||||
++v.samplesPlayed;
|
++v.samplesPlayed;
|
||||||
|
|
||||||
|
|
@ -154,6 +269,81 @@ void TrommelkisteProcessor::processBlock(juce::AudioBuffer<float>& buffer, juce:
|
||||||
v.active = false;
|
v.active = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read master effect levels
|
||||||
|
const float masterDelayMix = apvts.getRawParameterValue("master_delay")->load();
|
||||||
|
const float masterReverbMix = apvts.getRawParameterValue("master_reverb")->load();
|
||||||
|
|
||||||
|
// Delay sync
|
||||||
|
const int delaySyncDiv = (int)apvts.getRawParameterValue("delay_syncDiv")->load();
|
||||||
|
float delayTimeSec = 0.45f; // default free-running delay time
|
||||||
|
if (delaySyncDiv >= 1 && delaySyncDiv <= 8)
|
||||||
|
{
|
||||||
|
double bpm = 120.0;
|
||||||
|
if (auto* ph = getPlayHead())
|
||||||
|
{
|
||||||
|
auto info = ph->getPosition();
|
||||||
|
if (info.hasValue() && info->getBpm().hasValue())
|
||||||
|
bpm = *info->getBpm();
|
||||||
|
}
|
||||||
|
double beats = delaySyncDiv * 0.25;
|
||||||
|
delayTimeSec = (float)(beats * 60.0 / bpm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process delay
|
||||||
|
const int delaySamples = delayBuffer.getNumSamples();
|
||||||
|
auto* delL = delayBuffer.getWritePointer(0);
|
||||||
|
auto* delR = delayBuffer.getNumChannels() > 1
|
||||||
|
? delayBuffer.getWritePointer(1) : nullptr;
|
||||||
|
const float feedback = 0.4f;
|
||||||
|
const float delayMix = 0.3f;
|
||||||
|
const int delayTimeSamples = (int)(delayTimeSec * currentSampleRate);
|
||||||
|
|
||||||
|
for (int s = 0; s < numSamples; ++s)
|
||||||
|
{
|
||||||
|
const int readPos = (delayWritePos - delayTimeSamples + delaySamples)
|
||||||
|
% delaySamples;
|
||||||
|
const float delOutL = delL[readPos];
|
||||||
|
const float delOutR = delR ? delR[readPos] : delOutL;
|
||||||
|
|
||||||
|
delL[delayWritePos] = delaySend.getSample(0, s) + delOutL * feedback;
|
||||||
|
if (delR)
|
||||||
|
delR[delayWritePos] = delaySend.getSample(1, s) + delOutR * feedback;
|
||||||
|
|
||||||
|
outL[s] += delOutL * delayMix * masterDelayMix;
|
||||||
|
if (outR)
|
||||||
|
outR[s] += delOutR * delayMix * masterDelayMix;
|
||||||
|
|
||||||
|
delayWritePos = (delayWritePos + 1) % delaySamples;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process reverb
|
||||||
|
juce::dsp::AudioBlock<float> reverbBlock(reverbSend);
|
||||||
|
juce::dsp::ProcessContextReplacing<float> reverbCtx(reverbBlock);
|
||||||
|
reverb.process(reverbCtx);
|
||||||
|
|
||||||
|
for (int s = 0; s < numSamples; ++s)
|
||||||
|
{
|
||||||
|
outL[s] += reverbSend.getSample(0, s) * masterReverbMix;
|
||||||
|
if (outR)
|
||||||
|
outR[s] += reverbSend.getSample(1, s) * masterReverbMix;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master volume & pan
|
||||||
|
const float masterVol = apvts.getRawParameterValue("master_volume")->load();
|
||||||
|
const float masterPan = apvts.getRawParameterValue("master_pan")->load();
|
||||||
|
const float mPanAngle = (masterPan + 1.0f) * 0.25f * MathConstants<float>::pi;
|
||||||
|
const float mPanL = std::cos(mPanAngle) * Decibels::decibelsToGain(-3.0f);
|
||||||
|
const float mPanR = std::sin(mPanAngle) * Decibels::decibelsToGain(-3.0f);
|
||||||
|
|
||||||
|
for (int s = 0; s < numSamples; ++s)
|
||||||
|
{
|
||||||
|
float tmpL = outL[s] * mPanL * masterVol;
|
||||||
|
float tmpR = outR ? outR[s] * mPanR * masterVol : tmpL;
|
||||||
|
outL[s] = tmpL;
|
||||||
|
if (outR)
|
||||||
|
outR[s] = tmpR;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int TrommelkisteProcessor::findFreeVoice(int trackIndex)
|
int TrommelkisteProcessor::findFreeVoice(int trackIndex)
|
||||||
|
|
@ -206,9 +396,9 @@ void TrommelkisteProcessor::handleNoteOff(int trackIndex)
|
||||||
{
|
{
|
||||||
if (v.active && v.trackIndex == trackIndex)
|
if (v.active && v.trackIndex == trackIndex)
|
||||||
{
|
{
|
||||||
const int decayType = (int)apvts.getRawParameterValue(
|
const bool decayGate = apvts.getRawParameterValue(
|
||||||
"t" + juce::String(trackIndex) + "_decay")->load();
|
"t" + juce::String(trackIndex) + "_decay")->load() > 0.5f;
|
||||||
if (decayType == 1)
|
if (decayGate)
|
||||||
v.active = false;
|
v.active = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -515,7 +705,6 @@ void TrommelkisteProcessor::setStateInformation(const void* data, int sizeInByte
|
||||||
juce::AudioProcessorValueTreeState::ParameterLayout TrommelkisteProcessor::createParameterLayout()
|
juce::AudioProcessorValueTreeState::ParameterLayout TrommelkisteProcessor::createParameterLayout()
|
||||||
{
|
{
|
||||||
AudioProcessorValueTreeState::ParameterLayout layout;
|
AudioProcessorValueTreeState::ParameterLayout layout;
|
||||||
const StringArray decayTypes = {"Saw", "Gate"};
|
|
||||||
|
|
||||||
for (int i = 0; i < NUM_TRACKS; ++i)
|
for (int i = 0; i < NUM_TRACKS; ++i)
|
||||||
{
|
{
|
||||||
|
|
@ -546,11 +735,71 @@ juce::AudioProcessorValueTreeState::ParameterLayout TrommelkisteProcessor::creat
|
||||||
p + "pan", tn + "Pan",
|
p + "pan", tn + "Pan",
|
||||||
NormalisableRange<float>(-1.0f, 1.0f), 0.0f));
|
NormalisableRange<float>(-1.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
layout.add(std::make_unique<AudioParameterChoice>(
|
layout.add(std::make_unique<AudioParameterBool>(
|
||||||
p + "decay", tn + "Decay",
|
p + "decay", tn + "Decay", false));
|
||||||
decayTypes, 0));
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
p + "delaySend", tn + "Delay",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
p + "reverbSend", tn + "Reverb",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
p + "ringMod", tn + "RingMod",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
p + "destruction", tn + "Destr",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
p + "shuffle", tn + "Shuffle",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global controls
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"master_volume", "Master Volume",
|
||||||
|
NormalisableRange<float>(0.0f, 1.2f, 0.01f), 1.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"master_pan", "Master Pan",
|
||||||
|
NormalisableRange<float>(-1.0f, 1.0f, 0.01f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"master_delay", "Master Delay",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"master_reverb", "Master Reverb",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"delay_syncDiv", "Delay Sync",
|
||||||
|
NormalisableRange<float>(0.0f, 8.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"global_shuffleAmount", "Shuffle Amt",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 1.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"global_groove", "Groove",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f, 0.01f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"global_shuffleDiv", "Shuffle Div",
|
||||||
|
NormalisableRange<float>(0.0f, 4.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"global_ringFreq", "Ring Freq",
|
||||||
|
NormalisableRange<float>(20.0f, 2000.0f, 1.0f, 0.3f), 80.0f));
|
||||||
|
|
||||||
|
layout.add(std::make_unique<AudioParameterFloat>(
|
||||||
|
"global_destruction", "Destruction",
|
||||||
|
NormalisableRange<float>(0.0f, 1.0f), 0.0f));
|
||||||
|
|
||||||
return layout;
|
return layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <juce_audio_processors/juce_audio_processors.h>
|
#include <juce_audio_processors/juce_audio_processors.h>
|
||||||
#include <juce_audio_formats/juce_audio_formats.h>
|
#include <juce_audio_formats/juce_audio_formats.h>
|
||||||
|
#include <juce_dsp/juce_dsp.h>
|
||||||
|
#include <random>
|
||||||
|
|
||||||
static constexpr int NUM_TRACKS = 10;
|
static constexpr int NUM_TRACKS = 10;
|
||||||
static constexpr int MAX_VOICES = 32;
|
static constexpr int MAX_VOICES = 32;
|
||||||
|
|
@ -92,11 +94,20 @@ private:
|
||||||
float filterStateL = 0.0f;
|
float filterStateL = 0.0f;
|
||||||
float filterStateR = 0.0f;
|
float filterStateR = 0.0f;
|
||||||
float noteVelocity = 1.0f;
|
float noteVelocity = 1.0f;
|
||||||
|
float ringModPhase = 0.0f;
|
||||||
};
|
};
|
||||||
|
|
||||||
Voice voices[MAX_VOICES];
|
Voice voices[MAX_VOICES];
|
||||||
juce::AudioFormatManager formatManager;
|
juce::AudioFormatManager formatManager;
|
||||||
double currentSampleRate = 44100.0;
|
double currentSampleRate = 44100.0;
|
||||||
|
std::mt19937 rng{std::random_device{}()};
|
||||||
|
|
||||||
|
// Effects
|
||||||
|
static constexpr int DELAY_BUFFER_SECONDS = 4;
|
||||||
|
juce::AudioBuffer<float> delayBuffer;
|
||||||
|
int delayWritePos = 0;
|
||||||
|
juce::dsp::Reverb reverb;
|
||||||
|
juce::dsp::Reverb::Parameters reverbParams;
|
||||||
|
|
||||||
int findFreeVoice(int trackIndex);
|
int findFreeVoice(int trackIndex);
|
||||||
void handleNoteOn(int trackIndex, float velocity);
|
void handleNoteOn(int trackIndex, float velocity);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue