feat: add presets, Randomize button, menu highlight, finer LFO depth, preset-switch fade-out

- PresetManager: add ~40 new presets including a new Techno category with
  LFO-to-OSC-pitch movement, plus Bass/Lead/Pad/Keys/Brass/Strings/Wind/
  Arp/Pluck/FX expansions
- UI: add RANDOM button; highlight the currently selected entry in all
  dropdown menus (incl. ticking the active preset in the preset browser)
- LFO depth: finer 0.001 step with a skew curve so the lower range resolves
  much finer than the upper range (shared by LFO1 and LFO2)
- DSP: fade out active voices over 250ms on preset change / randomize so long
  release tails from the previous sound are silenced
- Update bg.png
This commit is contained in:
Armin 2026-07-14 13:54:17 +02:00
commit 6f63b403e3
7 changed files with 881 additions and 7 deletions

View file

@ -118,7 +118,7 @@ void ComboBoxLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height,
void ComboBoxLookAndFeel::drawPopupMenuItem(juce::Graphics& g, const juce::Rectangle<int>& area,
bool isSeparator, bool /*isActive*/,
bool isHighlighted, bool /*isTicked*/,
bool isHighlighted, bool isTicked,
bool /*hasSubMenu*/, const juce::String& text,
const juce::String& /*shortcutKeyText*/,
const juce::Drawable* /*icon*/,
@ -130,12 +130,19 @@ void ComboBoxLookAndFeel::drawPopupMenuItem(juce::Graphics& g, const juce::Recta
return;
}
// Persistent highlight for the currently selected (ticked) entry
if (isTicked && !isHighlighted) {
g.setColour(juce::Colour(0xffccaa44).withAlpha(0.3f));
g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f);
}
if (isHighlighted) {
g.setColour(juce::Colour(0xffccaa44));
g.fillRoundedRectangle(area.reduced(2).toFloat(), 3.0f);
}
g.setColour(isHighlighted ? juce::Colour(0xff1a1a1a) : juce::Colour(0xffcccccc));
g.setColour(isHighlighted ? juce::Colour(0xff1a1a1a)
: (isTicked ? juce::Colour(0xffccaa44) : juce::Colour(0xffcccccc)));
g.setFont(juce::Font(13.0f));
g.drawText(text, area.reduced(8, 0), juce::Justification::centredLeft, true);
}
@ -537,6 +544,12 @@ MainContentComponent::MainContentComponent(ChromaFlockProcessor& p)
applyCurrentPreset();
};
randomizeButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0xff2a2a2a));
randomizeButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffccaa44));
randomizeButton.setLookAndFeel(&comboLaf);
randomizeButton.onClick = [this]() { randomizeParameters(); };
addAndMakeVisible(randomizeButton);
auto& presets = processorRef.presetManager.getPresets();
if (!presets.empty()) {
currentPresetIndex = 0;
@ -645,12 +658,19 @@ void MainContentComponent::showPresetMenu() {
std::vector<juce::String> menuOrder;
auto categories = processorRef.presetManager.getCategories();
juce::String currentName;
auto& allPresets = processorRef.presetManager.getPresets();
if (currentPresetIndex >= 0 && currentPresetIndex < static_cast<int>(allPresets.size()))
currentName = allPresets[currentPresetIndex].name;
for (auto& cat : categories) {
juce::PopupMenu subMenu;
auto presets = processorRef.presetManager.getPresetsInCategory(cat);
for (auto& preset : presets) {
menuOrder.push_back(preset.name);
subMenu.addItem(static_cast<int>(menuOrder.size()), preset.name);
bool isCurrent = (preset.name == currentName);
subMenu.addItem(static_cast<int>(menuOrder.size()), preset.name, true, isCurrent);
}
menu.addSubMenu(cat, subMenu);
}
@ -675,6 +695,7 @@ void MainContentComponent::applyCurrentPreset() {
auto& presets = processorRef.presetManager.getPresets();
if (presets.empty()) return;
currentPresetIndex = juce::jlimit(0, static_cast<int>(presets.size()) - 1, currentPresetIndex);
processorRef.fadeOutActiveVoices(0.25f);
processorRef.presetManager.applyPreset(presets[currentPresetIndex].name, processorRef.apvts);
patchLCD.setText(presets[currentPresetIndex].name);
@ -685,6 +706,30 @@ void MainContentComponent::applyCurrentPreset() {
setupSyncKnob(delayTimeKnob, delayTimeAttach, delayBeatAttach, "delayTime", "delayBeat", delaySyncBox);
}
void MainContentComponent::randomizeParameters() {
juce::Random rand;
processorRef.fadeOutActiveVoices(0.25f);
for (auto* p : processorRef.getParameters()) {
auto* rap = dynamic_cast<juce::RangedAudioParameter*>(p);
if (rap == nullptr)
continue;
const auto& id = rap->paramID;
if (id == "octaveTranspose" || id == "semitoneTranspose")
continue;
float v = rand.nextFloat();
if (id == "masterLevel")
v = 0.4f + 0.6f * v;
rap->setValueNotifyingHost(v);
}
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);
}
void MainContentComponent::paint(juce::Graphics& g) {
g.fillAll(juce::Colour(0xff1a1a1a));
@ -915,6 +960,7 @@ void MainContentComponent::resized() {
// --- PRESET + SCALE ---
presetButton.setBounds(20, 8, 90, 28);
randomizeButton.setBounds(20, 42, 90, 28);
patchLCD.setBounds(118, 8, 360, 28);
prevPresetButton.setBounds(486, 8, 28, 28);
nextPresetButton.setBounds(518, 8, 28, 28);