rework arpeggiator with blank notes and progressions

This commit is contained in:
Armin 2026-08-13 02:09:39 +02:00
commit d034139b5e
5 changed files with 219 additions and 104 deletions

View file

@ -489,8 +489,75 @@ void MidiLed::paint(juce::Graphics& g) {
g.fillEllipse(centre.x - radius * 0.75f, centre.y - radius * 0.75f, radius * 1.5f, radius * 1.5f);
}
// --- Clickable state LED (on/off, direction) ---
LedToggle::LedToggle(juce::AudioProcessorValueTreeState& apvts, const juce::String& paramId,
const juce::String& onText, const juce::String& offText)
: apvts(apvts), paramId(paramId), onText(onText), offText(offText) {
startTimerHz(30);
setMouseCursor(juce::MouseCursor::PointingHandCursor);
}
void LedToggle::timerCallback() {
bool v = apvts.getRawParameterValue(paramId)->load() > 0.5f;
if (v != lit) {
lit = v;
repaint();
if (onChange)
onChange();
}
}
void LedToggle::mouseDown(const juce::MouseEvent&) {
auto* param = apvts.getParameter(paramId);
float newVal = param->getValue() > 0.5f ? 0.0f : 1.0f;
param->beginChangeGesture();
param->setValueNotifyingHost(newVal);
param->endChangeGesture();
}
void LedToggle::mouseEnter(const juce::MouseEvent&) {
setMouseCursor(juce::MouseCursor::PointingHandCursor);
}
void LedToggle::paint(juce::Graphics& g) {
auto b = getLocalBounds().toFloat();
float ledD = juce::jmin(22.0f, b.getHeight() - 8.0f);
float radius = ledD * 0.5f;
auto centre = juce::Point<float>(b.getX() + radius + 4.0f, b.getCentreY());
if (lit) {
float auraR = radius * 1.9f;
float maxAura = juce::jmin(centre.x - b.getX(), b.getRight() - centre.x,
centre.y - b.getY(), b.getBottom() - centre.y) - 1.0f;
auraR = juce::jmin(auraR, maxAura);
g.setColour(juce::Colour(0xffccaa44).withAlpha(0.2f));
g.fillEllipse(centre.x - auraR, centre.y - auraR, auraR * 2.0f, auraR * 2.0f);
}
g.setColour(juce::Colour(0xff1a1200));
g.fillEllipse(centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f);
g.setColour(juce::Colour(0xff555555));
g.drawEllipse(centre.x - radius, centre.y - radius, radius * 2.0f, radius * 2.0f, 1.5f);
g.setColour(lit ? juce::Colour(0xffeebb55) : juce::Colour(0xff3a2a06));
g.fillEllipse(centre.x - radius * 0.78f, centre.y - radius * 0.78f, radius * 1.56f, radius * 1.56f);
g.setFont(juce::Font(juce::FontOptions(10.0f).withStyle("Bold")));
g.setColour(lit ? juce::Colour(0xffeebb55) : juce::Colour(0xff666666));
g.drawText(lit ? onText : offText,
juce::Rectangle<float>(b.getX() + ledD + 12.0f, b.getY(),
b.getWidth() - ledD - 12.0f, b.getHeight()),
juce::Justification::centredLeft);
}
MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
: processorRef(p), vuMeter(p), waveformDisplay(p), pianoRoll(p), midiLed(p) {
: processorRef(p), vuMeter(p), waveformDisplay(p), pianoRoll(p), midiLed(p),
arpEnabledLed(p.apvts, "arpEnabled", "ON", "OFF"),
arpDirectionLed(p.apvts, "arpDirection", "ON", "OFF"),
delayPingPongLed(p.apvts, "delayPingPong", "ON", "OFF"),
lfo1SyncLed(p.apvts, "lfo1Sync", "SYNC ON", "SYNC OFF"),
lfo2SyncLed(p.apvts, "lfo2Sync", "SYNC ON", "SYNC OFF"),
delaySyncLed(p.apvts, "delaySync", "SYNC ON", "SYNC OFF") {
auto setupParam = [&](juce::Slider& knob, std::unique_ptr<SliderAttachment>& attach,
const juce::String& paramId, const juce::String& name) {
@ -571,32 +638,32 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
setupParam(lfo1DepthKnob, lfo1DepthAttach, "lfo1Depth", "DEPTH");
setupCB(lfo1ShapeBox, lfo1ShapeAttach, "lfo1Shape", {"Sine", "Triangle", "Saw", "Square"});
setupCB(lfo1DestBox, lfo1DestAttach, "lfo1Dest", {"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"});
setupCB(lfo1SyncBox, lfo1SyncAttach, "lfo1Sync", {"Sync Off", "Sync On"});
lfo1SyncBox.onChange = [this]() {
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox);
addAndMakeVisible(lfo1SyncLed);
lfo1SyncLed.onChange = [this]() {
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
};
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox);
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
setupKnob(lfo2RateKnob, "RATE");
setupParam(lfo2DepthKnob, lfo2DepthAttach, "lfo2Depth", "DEPTH");
setupCB(lfo2ShapeBox, lfo2ShapeAttach, "lfo2Shape", {"Sine", "Triangle", "Saw", "Square"});
setupCB(lfo2DestBox, lfo2DestAttach, "lfo2Dest", {"Filter", "OSC1", "OSC2", "Both OSC", "OSC2 Phase"});
setupCB(lfo2SyncBox, lfo2SyncAttach, "lfo2Sync", {"Sync Off", "Sync On"});
lfo2SyncBox.onChange = [this]() {
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox);
addAndMakeVisible(lfo2SyncLed);
lfo2SyncLed.onChange = [this]() {
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
};
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox);
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
// FX2
setupKnob(delayTimeKnob, "TIME");
setupCB(delaySyncBox, delaySyncAttach, "delaySync", {"Sync Off", "Sync On"});
delaySyncBox.onChange = [this]() {
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
addAndMakeVisible(delaySyncLed);
delaySyncLed.onChange = [this]() {
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
};
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
setupParam(delayFbKnob, delayFbAttach, "delayFeedback", "FBACK");
setupParam(delayMixKnob, delayMixAttach, "delayMix", "MIX");
setupParam(delayPingPongKnob, delayPingPongAttach, "delayPingPong", "PING");
addAndMakeVisible(delayPingPongLed);
setupParam(reverbSizeKnob, reverbSizeAttach, "reverbSize", "SIZE");
setupParam(reverbDampKnob, reverbDampAttach, "reverbDamping", "DAMP");
setupParam(reverbMixKnob, reverbMixAttach, "reverbMix", "MIX");
@ -691,20 +758,21 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
label.setJustificationType(juce::Justification::centred);
addAndMakeVisible(label);
};
setupArpLabel(arpEnabledLabel, "ON");
setupArpLabel(arpEnabledLabel, "ON/OFF");
setupArpLabel(arpPatternLabel, "PATTERN");
setupArpLabel(arpOctavesLabel, "OCTAVES");
setupArpLabel(arpDirectionLabel, "DIR");
setupArpLabel(arpDirectionLabel, "INVERT");
setupArpLabel(arpRateLabel, "RATE");
setupCB(arpEnabledBox, arpEnabledAttach, "arpEnabled", {"Off", "On"});
addAndMakeVisible(arpEnabledLed);
addAndMakeVisible(arpDirectionLed);
setupCB(arpPatternBox, arpPatternAttach, "arpPattern",
{"Up / Minor", "Down / Minor", "Up & Down / Major", "Down & Up / Major", "Random",
"As Played", "Chord", "Up & Down X", "Down & Up X", "Random Once", "Octave Up",
"Octave Down", "Pinky Up", "Pinky Down", "Up / Major", "Down / Major",
"Up & Down / Minor", "C3/C4 1", "C3/C4 2", "C3/C4 3"});
"Up & Down / Minor", "C3/C4 1", "C3/C4 2", "C3/C4 3", "C3/C4 4", "C3/C4 5"});
setupCB(arpOctavesBox, arpOctavesAttach, "arpOctaves", {"1", "2", "3"});
setupCB(arpDirectionBox, arpDirectionAttach, "arpDirection", {"Up", "Down"});
setupCB(arpRateBox, arpRateAttach, "arpRate", {"1/32", "1/16", "1/8", "1/4", "1/2", "1", "2"});
setupCombo(uiScaleBox);
@ -762,11 +830,10 @@ void MainContentComponent::setupSyncKnob(juce::Slider& knob,
std::unique_ptr<SliderAttachment>& beatAttach,
const juce::String& rateId,
const juce::String& beatId,
juce::ComboBox& syncBox) {
bool on = syncBox.getSelectedId() == 2;
bool syncOn) {
rateAttach.reset();
beatAttach.reset();
if (on)
if (syncOn)
beatAttach = std::make_unique<SliderAttachment>(processorRef.apvts, beatId, knob);
else
rateAttach = std::make_unique<SliderAttachment>(processorRef.apvts, rateId, knob);
@ -916,9 +983,9 @@ void MainContentComponent::loadPresetFile() {
currentPresetIndex = -1;
patchLCD.setText(file.getFileNameWithoutExtension());
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox);
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox);
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
});
}
@ -930,11 +997,11 @@ void MainContentComponent::applyCurrentPreset() {
processorRef.presetManager.applyPreset(presets[currentPresetIndex].name, processorRef.apvts);
patchLCD.setText(presets[currentPresetIndex].name);
// The ComboBoxAttachment updates the sync boxes without firing onChange,
// so re-sync the rate/beat knob attachments to match.
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox);
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox);
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
// The LedToggle updates itself via its timer, so re-sync the rate/beat
// knob attachments to match.
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
}
void MainContentComponent::randomizeParameters() {
@ -957,9 +1024,9 @@ void MainContentComponent::randomizeParameters() {
patchLCD.setText("RANDOM");
// Re-sync the rate/beat knob attachments to match the randomized sync states.
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncBox);
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncBox);
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
setupSyncKnob(lfo1RateKnob, lfo1RateAttach, lfo1BeatAttach, "lfo1Rate", "lfo1Beat", lfo1SyncLed.isLit());
setupSyncKnob(lfo2RateKnob, lfo2RateAttach, lfo2BeatAttach, "lfo2Rate", "lfo2Beat", lfo2SyncLed.isLit());
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncLed.isLit());
}
void MainContentComponent::paint(juce::Graphics& g) {
@ -1219,14 +1286,14 @@ void MainContentComponent::resized() {
lfo1DepthKnob.setBounds(132, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo1ShapeBox.setBounds(242, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo1DestBox.setBounds(357, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo1SyncBox.setBounds(22, lfoKnobY + lfoKnobSize + 6, 200, 28);
lfo1SyncLed.setBounds(22, lfoKnobY + lfoKnobSize + 6, 200, 28);
// LFO 2 sub-section (X=560, Y=642, W=530, H=110)
lfo2RateKnob.setBounds(568, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo2DepthKnob.setBounds(678, lfoKnobY, lfoKnobSize, lfoKnobSize);
lfo2ShapeBox.setBounds(788, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo2DestBox.setBounds(903, lfoKnobY + (lfoKnobSize - 28) / 2, 105, 28);
lfo2SyncBox.setBounds(568, lfoKnobY + lfoKnobSize + 6, 200, 28);
lfo2SyncLed.setBounds(568, lfoKnobY + lfoKnobSize + 6, 200, 28);
// --- FX2 SECTION ---
int fx2KnobY = lfoKnobY;
@ -1237,8 +1304,8 @@ void MainContentComponent::resized() {
delayTimeKnob.setBounds(dx, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayFbKnob.setBounds(dx + dGap, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayMixKnob.setBounds(dx + dGap * 2, fx2KnobY, lfoKnobSize, lfoKnobSize);
delayPingPongKnob.setBounds(dx + dGap * 3, fx2KnobY, lfoKnobSize, lfoKnobSize);
delaySyncBox.setBounds(dx, fx2KnobY + lfoKnobSize + 6, 250, 28);
delayPingPongLed.setBounds(dx + dGap * 3, fx2KnobY, lfoKnobSize, lfoKnobSize);
delaySyncLed.setBounds(dx, fx2KnobY + lfoKnobSize + 6, 250, 28);
// Reverb sub-section: SIZE, DAMP, MIX (right edge aligned to 1650)
{
@ -1297,7 +1364,7 @@ void MainContentComponent::resized() {
int x = arpX + (arpW - totalW) / 2;
arpEnabledLabel.setBounds(x, labelY, widths[0], labelH);
arpEnabledBox.setBounds(x, comboY, widths[0], comboH);
arpEnabledLed.setBounds(x, comboY, widths[0], comboH);
x += widths[0] + gap;
arpPatternLabel.setBounds(x, labelY, widths[1], labelH);
@ -1309,7 +1376,7 @@ void MainContentComponent::resized() {
x += widths[2] + gap;
arpDirectionLabel.setBounds(x, labelY, widths[3], labelH);
arpDirectionBox.setBounds(x, comboY, widths[3], comboH);
arpDirectionLed.setBounds(x, comboY, widths[3], comboH);
x += widths[3] + gap;
arpRateLabel.setBounds(x, labelY, widths[4], labelH);