justasample/Source/Components/ModulationSection.cpp

328 lines
13 KiB
C++
Raw Normal View History

2026-07-24 18:03:15 +02:00
/*
==============================================================================
ModulationSection.cpp
Created: 24 Jul 2026
Author: Armin
==============================================================================
*/
#include <JuceHeader.h>
#include "ModulationSection.h"
ModulationSection::ModulationSection(JustaSampleAudioProcessor& processor) :
apvts(processor.APVTS()),
filterLabel("", "Filter"),
lfoLabel("", "LFO"),
phaserLabel("", "Phaser"),
ringModLabel("", "Ring Mod"),
filterTypeAttachment(apvts, PluginParameters::MOD_FILTER_TYPE, filterTypeBox),
filterCutoffAttachment(apvts, PluginParameters::MOD_FILTER_CUTOFF, filterCutoff),
filterResonanceAttachment(apvts, PluginParameters::MOD_FILTER_RESONANCE, filterResonance),
lfoWaveformAttachment(apvts, PluginParameters::MOD_LFO_WAVEFORM, lfoWaveformBox),
lfoRateAttachment(apvts, PluginParameters::MOD_LFO_RATE, lfoRate),
lfoSyncAttachment(apvts, PluginParameters::MOD_LFO_SYNC, lfoSyncButton),
lfoSyncDivAttachment(apvts, PluginParameters::MOD_LFO_SYNC_DIV, lfoSyncDivBox),
lfoFilterDepthAttachment(apvts, PluginParameters::MOD_LFO_FILTER_DEPTH, lfoFilterDepth),
lfoSpeedDepthAttachment(apvts, PluginParameters::MOD_LFO_SPEED_DEPTH, lfoSpeedDepth),
phaserEnabledAttachment(apvts, PluginParameters::MOD_PHASER_ENABLED, phaserEnabled),
phaserDepthAttachment(apvts, PluginParameters::MOD_PHASER_DEPTH, phaserDepth),
phaserFeedbackAttachment(apvts, PluginParameters::MOD_PHASER_FEEDBACK, phaserFeedback),
phaserStagesAttachment(apvts, PluginParameters::MOD_PHASER_STAGES, phaserStages),
ringModEnabledAttachment(apvts, PluginParameters::MOD_RINGMOD_ENABLED, ringModEnabled),
ringModMixAttachment(apvts, PluginParameters::MOD_RINGMOD_MIX, ringModMix)
{
// Labels
auto setupLabel = [this](juce::Label& label)
{
label.setJustificationType(juce::Justification::centred);
label.setInterceptsMouseClicks(false, false);
addAndMakeVisible(label);
};
setupLabel(filterLabel);
setupLabel(lfoLabel);
setupLabel(phaserLabel);
setupLabel(ringModLabel);
// Filter type combo
filterTypeBox.addItemList(PluginParameters::MOD_FILTER_TYPE_LABELS, 1);
filterTypeBox.setJustificationType(juce::Justification::centred);
addAndMakeVisible(filterTypeBox);
// Filter rotaries
auto setupRotary = [this](CustomRotary& rotary, const juce::String& unit)
{
rotary.setSliderStyle(juce::Slider::RotaryVerticalDrag);
rotary.setMouseDragSensitivity(150);
rotary.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 0, 0);
rotary.getProperties().set(ComponentProps::ROTARY_UNIT, unit);
addAndMakeVisible(rotary);
rotaries.add(&rotary);
};
setupRotary(filterCutoff, PluginParameters::FREQUENCY_UNIT);
setupRotary(filterResonance, "");
// LFO waveform combo
lfoWaveformBox.addItemList(PluginParameters::MOD_LFO_WAVEFORM_LABELS, 1);
lfoWaveformBox.setJustificationType(juce::Justification::centred);
addAndMakeVisible(lfoWaveformBox);
setupRotary(lfoRate, PluginParameters::FREQUENCY_UNIT);
lfoSyncButton.useShape(juce::Path());
lfoSyncButton.setButtonText("Sync");
addAndMakeVisible(lfoSyncButton);
lfoSyncDivBox.addItemList(PluginParameters::MOD_LFO_SYNC_DIV_LABELS, 1);
lfoSyncDivBox.setJustificationType(juce::Justification::centred);
addAndMakeVisible(lfoSyncDivBox);
setupRotary(lfoFilterDepth, "");
setupRotary(lfoSpeedDepth, "");
// Phaser
phaserEnabled.setButtonText("On");
addAndMakeVisible(phaserEnabled);
setupRotary(phaserDepth, "");
setupRotary(phaserFeedback, "");
phaserStages.getProperties().set(ComponentProps::ROTARY_UNIT, "stg");
setupRotary(phaserStages, "stg");
// Ring mod
ringModEnabled.setButtonText("On");
addAndMakeVisible(ringModEnabled);
setupRotary(ringModMix, "%");
}
void ModulationSection::paint(juce::Graphics& g)
{
auto colors = getTheme();
auto bounds = getLocalBounds().toFloat();
g.setColour(colors.foreground);
g.fillRect(bounds);
// Draw section dividers
g.setColour(colors.slate.withAlpha(0.2f));
float dividerY1 = bounds.getY() + bounds.getHeight() * 0.125f;
float dividerY2 = bounds.getY() + bounds.getHeight() * 0.875f;
// Dividers between Filter | LFO | Phaser | Ring Mod
float filterEnd = scalef(380.f);
float lfoEnd = filterEnd + scalef(600.f);
float phaserEnd = lfoEnd + scalef(530.f);
g.drawVerticalLine(int(filterEnd), dividerY1, dividerY2);
g.drawVerticalLine(int(lfoEnd), dividerY1, dividerY2);
g.drawVerticalLine(int(phaserEnd), dividerY1, dividerY2);
}
void ModulationSection::resized()
{
auto bounds = getLocalBounds().toFloat();
// Section labels
auto filterSection = bounds.removeFromLeft(scalef(380.f));
auto lfoSection = bounds.removeFromLeft(scalef(600.f));
auto phaserSection = bounds.removeFromLeft(scalef(530.f));
auto ringModSection = bounds;
// Reduce all sections vertically
filterSection.reduce(scalef(16.f), scalef(8.f));
lfoSection.reduce(scalef(16.f), scalef(8.f));
phaserSection.reduce(scalef(16.f), scalef(8.f));
ringModSection.reduce(scalef(16.f), scalef(8.f));
// === Filter section ===
auto filterTitleBounds = filterSection.removeFromTop(scalef(28.f));
filterLabel.setBounds(filterTitleBounds.toNearestInt());
filterLabel.setFont(getInriaSans().withHeight(scalef(22.f)));
filterSection.removeFromTop(scalef(4.f));
auto filterTypeBounds = filterSection.removeFromTop(scalef(28.f)).reduced(scalef(20.f), 0.f);
filterTypeBox.setBounds(filterTypeBounds.toNearestInt());
filterSection.removeFromTop(scalef(6.f));
float filterRotarySize = scalef(50.f);
float filterRotaryPad = filterRotarySize * Layout::rotaryPadding;
auto filterCutoffBounds = filterSection.removeFromLeft(filterRotarySize + 2 * filterRotaryPad);
filterCutoff.setBounds(filterCutoffBounds.toNearestInt());
filterCutoff.sendLookAndFeelChange();
filterSection.removeFromLeft(scalef(16.f));
auto filterResBounds = filterSection.removeFromLeft(filterRotarySize + 2 * filterRotaryPad);
filterResonance.setBounds(filterResBounds.toNearestInt());
filterResonance.sendLookAndFeelChange();
// === LFO section ===
auto lfoTitleBounds = lfoSection.removeFromTop(scalef(28.f));
lfoLabel.setBounds(lfoTitleBounds.toNearestInt());
lfoLabel.setFont(getInriaSans().withHeight(scalef(22.f)));
lfoSection.removeFromTop(scalef(4.f));
auto lfoWaveformBounds = lfoSection.removeFromTop(scalef(28.f)).reduced(scalef(20.f), 0.f);
lfoWaveformBox.setBounds(lfoWaveformBounds.toNearestInt());
lfoSection.removeFromTop(scalef(4.f));
auto lfoTopRow = lfoSection.removeFromTop(scalef(56.f));
auto lfoBottomRow = lfoSection;
float lfoRotarySize = scalef(48.f);
float lfoRotaryPad = lfoRotarySize * Layout::rotaryPadding;
// Top row: Rate, Sync button, Sync div
auto lfoRateBounds = lfoTopRow.removeFromLeft(lfoRotarySize + 2 * lfoRotaryPad);
lfoRate.setBounds(lfoRateBounds.toNearestInt());
lfoRate.sendLookAndFeelChange();
lfoTopRow.removeFromLeft(scalef(8.f));
auto lfoSyncBounds = lfoTopRow.removeFromLeft(scalef(50.f));
lfoSyncButton.setBounds(lfoSyncBounds.toNearestInt());
lfoSyncButton.setBorder(scalef(2.f), scalef(4.f));
lfoTopRow.removeFromLeft(scalef(8.f));
auto lfoSyncDivBounds = lfoTopRow.removeFromLeft(scalef(70.f));
lfoSyncDivBox.setBounds(lfoSyncDivBounds.toNearestInt());
// Bottom row: Filter depth, Speed depth
auto lfoFilterDepthBounds = lfoBottomRow.removeFromLeft(lfoRotarySize + 2 * lfoRotaryPad);
lfoFilterDepth.setBounds(lfoFilterDepthBounds.toNearestInt());
lfoFilterDepth.sendLookAndFeelChange();
lfoBottomRow.removeFromLeft(scalef(8.f));
auto lfoSpeedDepthBounds = lfoBottomRow.removeFromLeft(lfoRotarySize + 2 * lfoRotaryPad);
lfoSpeedDepth.setBounds(lfoSpeedDepthBounds.toNearestInt());
lfoSpeedDepth.sendLookAndFeelChange();
// === Phaser section ===
auto phaserTitleBounds = phaserSection.removeFromTop(scalef(28.f));
phaserLabel.setBounds(phaserTitleBounds.toNearestInt());
phaserLabel.setFont(getInriaSans().withHeight(scalef(22.f)));
phaserSection.removeFromTop(scalef(4.f));
auto phaserEnableBounds = phaserSection.removeFromTop(scalef(24.f)).reduced(scalef(20.f), 0.f);
phaserEnabled.setBounds(phaserEnableBounds.toNearestInt());
phaserSection.removeFromTop(scalef(4.f));
float phaserRotarySize = scalef(48.f);
float phaserRotaryPad = phaserRotarySize * Layout::rotaryPadding;
auto phaserDepthBounds = phaserSection.removeFromLeft(phaserRotarySize + 2 * phaserRotaryPad);
phaserDepth.setBounds(phaserDepthBounds.toNearestInt());
phaserDepth.sendLookAndFeelChange();
phaserSection.removeFromLeft(scalef(8.f));
auto phaserFbBounds = phaserSection.removeFromLeft(phaserRotarySize + 2 * phaserRotaryPad);
phaserFeedback.setBounds(phaserFbBounds.toNearestInt());
phaserFeedback.sendLookAndFeelChange();
phaserSection.removeFromLeft(scalef(8.f));
auto phaserStagesBounds = phaserSection.removeFromLeft(phaserRotarySize + 2 * phaserRotaryPad);
phaserStages.setBounds(phaserStagesBounds.toNearestInt());
phaserStages.sendLookAndFeelChange();
// === Ring Mod section ===
auto ringModTitleBounds = ringModSection.removeFromTop(scalef(28.f));
ringModLabel.setBounds(ringModTitleBounds.toNearestInt());
ringModLabel.setFont(getInriaSans().withHeight(scalef(22.f)));
ringModSection.removeFromTop(scalef(4.f));
auto ringModEnableBounds = ringModSection.removeFromTop(scalef(24.f)).reduced(scalef(20.f), 0.f);
ringModEnabled.setBounds(ringModEnableBounds.toNearestInt());
ringModSection.removeFromTop(scalef(4.f));
float ringModRotarySize = scalef(48.f);
float ringModRotaryPad = ringModRotarySize * Layout::rotaryPadding;
auto ringModMixBounds = ringModSection.removeFromLeft(ringModRotarySize + 2 * ringModRotaryPad);
ringModMix.setBounds(ringModMixBounds.toNearestInt());
ringModMix.sendLookAndFeelChange();
}
void ModulationSection::lookAndFeelChanged()
{
auto colors = getTheme();
filterLabel.setColour(juce::Label::textColourId, colors.dark);
lfoLabel.setColour(juce::Label::textColourId, colors.dark);
phaserLabel.setColour(juce::Label::textColourId, colors.dark);
ringModLabel.setColour(juce::Label::textColourId, colors.dark);
phaserEnabled.setColour(juce::ToggleButton::textColourId, colors.dark);
ringModEnabled.setColour(juce::ToggleButton::textColourId, colors.dark);
filterTypeBox.setColour(juce::ComboBox::backgroundColourId, colors.darkerSlate);
filterTypeBox.setColour(juce::ComboBox::textColourId, colors.light);
filterTypeBox.setColour(juce::ComboBox::arrowColourId, colors.dark);
lfoWaveformBox.setColour(juce::ComboBox::backgroundColourId, colors.darkerSlate);
lfoWaveformBox.setColour(juce::ComboBox::textColourId, colors.light);
lfoWaveformBox.setColour(juce::ComboBox::arrowColourId, colors.dark);
lfoSyncButton.setColors(colors.darkerSlate, colors.highlight, colors.dark.withAlpha(0.25f));
lfoSyncDivBox.setColour(juce::ComboBox::backgroundColourId, colors.darkerSlate);
lfoSyncDivBox.setColour(juce::ComboBox::textColourId, colors.light);
lfoSyncDivBox.setColour(juce::ComboBox::arrowColourId, colors.dark);
}
void ModulationSection::enablementChanged()
{
bool enabled = isEnabled();
filterTypeBox.setEnabled(enabled);
filterCutoff.setEnabled(enabled);
filterResonance.setEnabled(enabled);
lfoWaveformBox.setEnabled(enabled);
lfoRate.setEnabled(enabled);
lfoSyncButton.setEnabled(enabled);
lfoSyncDivBox.setEnabled(enabled);
lfoFilterDepth.setEnabled(enabled);
lfoSpeedDepth.setEnabled(enabled);
phaserEnabled.setEnabled(enabled);
phaserDepth.setEnabled(enabled);
phaserFeedback.setEnabled(enabled);
phaserStages.setEnabled(enabled);
ringModEnabled.setEnabled(enabled);
ringModMix.setEnabled(enabled);
repaint();
}
void ModulationSection::mouseDown(const juce::MouseEvent& event)
{
auto parent = event.originalComponent->getParentComponent();
for (auto* rotary : rotaries)
if (parent == rotary)
rotary->mouseDown(event.getEventRelativeTo(rotary));
}
void ModulationSection::mouseUp(const juce::MouseEvent& event)
{
auto parent = event.originalComponent->getParentComponent();
for (auto* rotary : rotaries)
if (parent == rotary)
rotary->mouseUp(event.getEventRelativeTo(rotary));
}
void ModulationSection::mouseDrag(const juce::MouseEvent& event)
{
auto parent = event.originalComponent->getParentComponent();
for (auto* rotary : rotaries)
if (parent == rotary)
rotary->mouseDrag(event.getEventRelativeTo(rotary));
}