/* ============================================================================== ModulationSection.cpp Created: 24 Jul 2026 Author: Armin ============================================================================== */ #include #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)); }