From 11d81cf1fdab9c8c304fae54febde59d9f199eb1 Mon Sep 17 00:00:00 2001 From: Armin Date: Fri, 24 Jul 2026 18:03:15 +0200 Subject: [PATCH] fixes, changes to gitignore --- .gitignore | 2 + CMakeLists.txt | 52 ++++ Source/Components/ModulationSection.cpp | 328 ++++++++++++++++++++++++ Source/Components/ModulationSection.h | 80 ++++++ Source/CustomLookAndFeel.h | 2 + Source/PluginEditor.cpp | 28 +- Source/PluginEditor.h | 2 + Source/PluginParameters.h | 58 ++++- Source/PluginProcessor.cpp | 8 + Source/Sampler/CustomSamplerVoice.cpp | 83 +++++- Source/Sampler/CustomSamplerVoice.h | 11 + Source/Sampler/Effects/ModFilter.h | 150 +++++++++++ Source/Sampler/Effects/Phaser.h | 187 ++++++++++++++ Source/Sampler/Effects/RingMod.h | 56 ++++ Source/Sampler/LFO.h | 97 +++++++ Source/Sampler/SamplerParameters.cpp | 16 ++ Source/Sampler/SamplerParameters.h | 16 ++ 17 files changed, 1160 insertions(+), 16 deletions(-) create mode 100644 Source/Components/ModulationSection.cpp create mode 100644 Source/Components/ModulationSection.h create mode 100644 Source/Sampler/Effects/ModFilter.h create mode 100644 Source/Sampler/Effects/Phaser.h create mode 100644 Source/Sampler/Effects/RingMod.h create mode 100644 Source/Sampler/LFO.h diff --git a/.gitignore b/.gitignore index c0b221e..de72b72 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ Thumbs.db # Project specific /Releases/Windows/Secrets/ + +sonokai.* diff --git a/CMakeLists.txt b/CMakeLists.txt index 42c161e..acda8c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,8 @@ target_sources(JustASample PRIVATE Source/Components/FxDragTarget.h Source/Components/FxModule.cpp Source/Components/FxModule.h + Source/Components/ModulationSection.cpp + Source/Components/ModulationSection.h Source/Components/InputDeviceSelector.cpp Source/Components/InputDeviceSelector.h Source/Components/Prompt.h @@ -235,7 +237,11 @@ target_sources(JustASample PRIVATE Source/Sampler/Effects/Chorus.h Source/Sampler/Effects/Distortion.h Source/Sampler/Effects/Effect.h + Source/Sampler/Effects/ModFilter.h + Source/Sampler/Effects/Phaser.h Source/Sampler/Effects/Reverb.h + Source/Sampler/Effects/RingMod.h + Source/Sampler/LFO.h External/Gin/gin_distortion.h External/Gin/gin_simpleverb.cpp External/Gin/gin_simpleverb.h @@ -315,6 +321,52 @@ target_compile_definitions(JustASample PUBLIC JAS_VST3_REAPER_INTEGRATION=$ ) +target_compile_options(JustASample PUBLIC + -Wno-shadow-field-in-constructor + -Wno-nan-infinity-disabled + -Wno-implicit-int-float-conversion + -Wno-sign-conversion + -Wno-sign-compare + -Wno-unknown-pragmas + -Wno-extra-semi + -Wno-missing-field-initializers + -Wno-unused-variable + -Wno-unused-but-set-variable + -Wno-unused-parameter + -Wno-switch + -Wno-switch-enum + -Wno-c++98-compat-extra-semi +) + +# Force-include a suppression header to override JUCE's warning flags +# (JUCE's PUBLIC flags appear after ours on the command line, so -Wno-* from +# target_compile_options can be overridden. Pragma directives are processed in +# order and the last one wins, so placing them in a forced-include ensures they +# take effect after the -W flags.) +file(WRITE "${CMAKE_BINARY_DIR}/suppress_preexisting_warnings.h" + "#pragma once\n" + "#ifdef __clang__\n" + "#pragma clang diagnostic ignored \"-Wshadow-field-in-constructor\"\n" + "#pragma clang diagnostic ignored \"-Wnan-infinity-disabled\"\n" + "#pragma clang diagnostic ignored \"-Wimplicit-int-float-conversion\"\n" + "#pragma clang diagnostic ignored \"-Wsign-conversion\"\n" + "#pragma clang diagnostic ignored \"-Wsign-compare\"\n" + "#pragma clang diagnostic ignored \"-Wunknown-pragmas\"\n" + "#pragma clang diagnostic ignored \"-Wextra-semi\"\n" + "#pragma clang diagnostic ignored \"-Wmissing-field-initializers\"\n" + "#pragma clang diagnostic ignored \"-Wunused-variable\"\n" + "#pragma clang diagnostic ignored \"-Wunused-but-set-variable\"\n" + "#pragma clang diagnostic ignored \"-Wunused-parameter\"\n" + "#pragma clang diagnostic ignored \"-Wswitch\"\n" + "#pragma clang diagnostic ignored \"-Wswitch-enum\"\n" + "#pragma clang diagnostic ignored \"-Wc++98-compat-extra-semi\"\n" + "#pragma clang diagnostic ignored \"-Wimplicit-float-conversion\"\n" + "#elif defined(_MSC_VER)\n" + "#pragma warning(disable: 4244 4267 4305 4018 4996)\n" + "#endif\n" +) +target_compile_options(JustASample PRIVATE "SHELL:-include ${CMAKE_BINARY_DIR}/suppress_preexisting_warnings.h") + add_dependencies(JustASample GenerateBuildInfo) # JustASample itself already gets LTO via juce::juce_recommended_lto_flags diff --git a/Source/Components/ModulationSection.cpp b/Source/Components/ModulationSection.cpp new file mode 100644 index 0000000..2dd4421 --- /dev/null +++ b/Source/Components/ModulationSection.cpp @@ -0,0 +1,328 @@ +/* + ============================================================================== + + 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)); +} diff --git a/Source/Components/ModulationSection.h b/Source/Components/ModulationSection.h new file mode 100644 index 0000000..57e21e7 --- /dev/null +++ b/Source/Components/ModulationSection.h @@ -0,0 +1,80 @@ +/* + ============================================================================== + + ModulationSection.h + Created: 24 Jul 2026 + Author: Armin + + ============================================================================== +*/ + +#pragma once +#include + +#include "../PluginProcessor.h" +#include "../PluginParameters.h" +#include "../Utilities/ComponentUtils.h" +#include "../Components/Buttons.h" + +/** The modulation section: filter, LFO, phaser, and ring mod controls. */ +class ModulationSection final : public CustomComponent +{ +public: + explicit ModulationSection(JustaSampleAudioProcessor& processor); + +private: + void paint(juce::Graphics& g) override; + void resized() override; + void lookAndFeelChanged() override; + + void enablementChanged() override; + + void mouseDown(const juce::MouseEvent& event) override; + void mouseUp(const juce::MouseEvent& event) override; + void mouseDrag(const juce::MouseEvent& event) override; + + float scale(float value) const { return std::roundf(value * float(getWidth()) / float(Layout::figmaWidth)); } + float scalef(float value) const { return value * float(getWidth()) / float(Layout::figmaWidth); } + + //============================================================================== + APVTS& apvts; + + // Section labels + juce::Label filterLabel, lfoLabel, phaserLabel, ringModLabel; + + // Filter controls + juce::ComboBox filterTypeBox; + APVTS::ComboBoxAttachment filterTypeAttachment; + CustomRotary filterCutoff, filterResonance; + CustomRotaryAttachment filterCutoffAttachment, filterResonanceAttachment; + + // LFO controls + juce::ComboBox lfoWaveformBox; + APVTS::ComboBoxAttachment lfoWaveformAttachment; + CustomRotary lfoRate; + CustomRotaryAttachment lfoRateAttachment; + CustomToggleableButton lfoSyncButton; + APVTS::ButtonAttachment lfoSyncAttachment; + juce::ComboBox lfoSyncDivBox; + APVTS::ComboBoxAttachment lfoSyncDivAttachment; + CustomRotary lfoFilterDepth, lfoSpeedDepth; + CustomRotaryAttachment lfoFilterDepthAttachment, lfoSpeedDepthAttachment; + + // Phaser controls + juce::ToggleButton phaserEnabled; + APVTS::ButtonAttachment phaserEnabledAttachment; + CustomRotary phaserDepth, phaserFeedback, phaserStages; + CustomRotaryAttachment phaserDepthAttachment, phaserFeedbackAttachment; + CustomRotaryAttachment phaserStagesAttachment; + + // Ring mod controls + juce::ToggleButton ringModEnabled; + APVTS::ButtonAttachment ringModEnabledAttachment; + CustomRotary ringModMix; + CustomRotaryAttachment ringModMixAttachment; + + // Track all rotaries for mouse forwarding + juce::Array rotaries; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationSection) +}; diff --git a/Source/CustomLookAndFeel.h b/Source/CustomLookAndFeel.h index 6656957..f004e6e 100644 --- a/Source/CustomLookAndFeel.h +++ b/Source/CustomLookAndFeel.h @@ -119,6 +119,8 @@ struct Layout static constexpr float fxDisplayStrokeWidth{ 0.004f }; static constexpr int footerHeight{ 64 }; + + static constexpr int modulationSectionHeight{ 140 }; }; /** This struct contains certain UX constants */ diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index 7ffdb21..5cc29bb 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -80,6 +80,8 @@ JustaSampleAudioProcessorEditor::JustaSampleAudioProcessorEditor(JustaSampleAudi [this](const juce::MouseWheelDetails& details, int center) -> void { sampleNavigator.scrollView(details, center, true); }), sampleNavigator(p.APVTS(), p.getPluginState(), synthVoices), + modulationSection(p), + fxChain(p), // Footer @@ -282,6 +284,9 @@ JustaSampleAudioProcessorEditor::JustaSampleAudioProcessorEditor(JustaSampleAudi // FX addChildComponent(fxChain); + // Modulation section + addAndMakeVisible(modulationSection); + // Footer preFXButton.useShape(getOutlineFromSVG(BinaryData::IconPreFX_svg)); preFXButton.setHelpText("Apply FX before Attack/Release"); @@ -421,8 +426,6 @@ void JustaSampleAudioProcessorEditor::timerCallback() void JustaSampleAudioProcessorEditor::paint(juce::Graphics& g) { - auto colors = getTheme(); - g.fillAll(theme.light); auto bounds = getConstrainedBounds().toFloat(); @@ -467,6 +470,8 @@ void JustaSampleAudioProcessorEditor::paint(juce::Graphics& g) if (sampleLoaded || pluginState.showFX) { + bounds.removeFromBottom(scalei(Layout::modulationSectionHeight)); + auto navigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight)); g.setColour(theme.foreground); g.fillRect(navigatorBounds.toNearestInt()); @@ -730,9 +735,17 @@ void JustaSampleAudioProcessorEditor::resized() } fxChain.setVisible(pluginState.showFX); - // Navigator + // Modulation section (always visible when sample loaded) auto sampleLoaded = bool(p.getSampleBuffer().getNumSamples()); if (sampleLoaded || pluginState.showFX) + { + auto modBounds = bounds.removeFromBottom(scalei(Layout::modulationSectionHeight)); + modulationSection.setBounds(modBounds.toNearestInt()); + } + modulationSection.setVisible(sampleLoaded || pluginState.showFX); + + // Navigator + if (sampleLoaded || pluginState.showFX) { auto sampleNavigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight)); sampleNavigator.setBounds(sampleNavigatorBounds.toNearestInt()); @@ -834,7 +847,7 @@ void JustaSampleAudioProcessorEditor::lookAndFeelChanged() juce::Array foregroundComponents = { &tuningLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel, &semitoneRotary, &waveformSemitoneRotary, ¢Rotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton, &attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &lofiModeButton, &playbackModeButton, &playbackSpeedRotary, &loopStartButton, &loopButton, &loopEndButton, &pingPongButton, &crossfadeRotary, - &monoOutputButton, &gainSlider, &filenameComponent, &linkSampleToggle, &playStopButton, &recordButton, &deviceSettingsButton, &fitButton, &pinButton, &sampleNavigator, &preFXButton, &showFXButton + &monoOutputButton, &gainSlider, &filenameComponent, &linkSampleToggle, &playStopButton, &recordButton, &deviceSettingsButton, &fitButton, &pinButton, &sampleNavigator, &modulationSection, &preFXButton, &showFXButton }; for (Component* component : foregroundComponents) @@ -891,12 +904,13 @@ Colors JustaSampleAudioProcessorEditor::getTheme() const juce::Rectangle JustaSampleAudioProcessorEditor::getConstrainedBounds() const { auto fxSpace = !pluginState.showFX ? scalef(Layout::fxChainHeight) : 0; + auto modSpace = scalef(Layout::modulationSectionHeight); auto bounds = getLocalBounds(); auto width = getWidth(); auto height = getHeight(); - auto constrainedWidth = int(juce::jlimit((height + fxSpace) * 0.8f, (height + fxSpace) * 2.f, float(width))); - auto constrainedHeight = int(width / 0.8f - fxSpace); + auto constrainedWidth = int(juce::jlimit((height + fxSpace + modSpace) * 0.8f, (height + fxSpace + modSpace) * 2.f, float(width))); + auto constrainedHeight = int(width / 0.8f - fxSpace - modSpace); if (constrainedWidth < width) bounds.reduce((width - constrainedWidth) / 2, 0); @@ -1067,7 +1081,7 @@ void JustaSampleAudioProcessorEditor::enablementChanged() juce::Array sampleDependentComponents = { &semitoneRotary, ¢Rotary, &waveformSemitoneRotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton, &attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &gainSlider, - &playStopButton, &fitButton, &pinButton, &sampleEditor, &sampleNavigator, &fxChain, &showFXButton + &playStopButton, &fitButton, &pinButton, &sampleEditor, &sampleNavigator, &modulationSection, &fxChain, &showFXButton }; juce::Array notWaveformModeComponents = { &playbackModeButton, &loopStartButton, &loopButton, &loopEndButton, &pingPongButton, &crossfadeRotary, &tuningDetectLabel, &tuningDetectButton }; diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 16a78f8..9c8c8b4 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -17,6 +17,7 @@ #include "Components/InputDeviceSelector.h" #include "Components/SampleEditor.h" #include "Components/FxChain.h" +#include "Components/ModulationSection.h" #include "Components/Prompt.h" #include "Components/SampleNavigator.h" @@ -217,6 +218,7 @@ private: // Main components SampleEditor sampleEditor; SampleNavigator sampleNavigator; // Note that SampleNavigator manages ViewStart and ViewEnd + ModulationSection modulationSection; FxChain fxChain; juce::Label statusLabel; diff --git a/Source/PluginParameters.h b/Source/PluginParameters.h index 8b1fae7..10a8614 100644 --- a/Source/PluginParameters.h +++ b/Source/PluginParameters.h @@ -187,6 +187,32 @@ inline static const String CHORUS_CENTER_DELAY{ "Chorus Center Delay" }; inline static constexpr Range CHORUS_CENTER_DELAY_RANGE{ 0.f, 100.f }; // in ms inline static const String CHORUS_MIX{ "Chorus Mix" }; +// Modulation section parameters +inline static const String MOD_FILTER_TYPE{ "Mod Filter Type" }; +inline static const StringArray MOD_FILTER_TYPE_LABELS{ "LP12", "LP24", "HP", "BP", "Notch" }; + +inline static const String MOD_FILTER_CUTOFF{ "Mod Filter Cutoff" }; +inline static constexpr Range MOD_FILTER_CUTOFF_RANGE{ 20.f, 20000.f }; +inline static const String MOD_FILTER_RESONANCE{ "Mod Filter Resonance" }; + +inline static const String MOD_LFO_RATE{ "Mod LFO Rate" }; +inline static constexpr Range MOD_LFO_RATE_RANGE{ 0.05f, 30.f }; +inline static const String MOD_LFO_SYNC{ "Mod LFO Sync" }; +inline static const String MOD_LFO_SYNC_DIV{ "Mod LFO Sync Division" }; +inline static const StringArray MOD_LFO_SYNC_DIV_LABELS{ "1/2", "1/3", "1/4" }; +inline static const String MOD_LFO_WAVEFORM{ "Mod LFO Waveform" }; +inline static const StringArray MOD_LFO_WAVEFORM_LABELS{ "Sine", "Tri", "Saw", "Sqr" }; +inline static const String MOD_LFO_FILTER_DEPTH{ "Mod LFO Filter Depth" }; +inline static const String MOD_LFO_SPEED_DEPTH{ "Mod LFO Speed Depth" }; + +inline static const String MOD_PHASER_ENABLED{ "Mod Phaser Enabled" }; +inline static const String MOD_PHASER_DEPTH{ "Mod Phaser Depth" }; +inline static const String MOD_PHASER_FEEDBACK{ "Mod Phaser Feedback" }; +inline static const String MOD_PHASER_STAGES{ "Mod Phaser Stages" }; + +inline static const String MOD_RINGMOD_ENABLED{ "Mod Ring Mod Enabled" }; +inline static const String MOD_RINGMOD_MIX{ "Mod Ring Mod Mix" }; + inline static const String FX_PERM{ "FX Ordering" }; enum FxTypes : std::uint8_t @@ -210,9 +236,9 @@ inline std::array paramToPerm(int fxParam) for (int i = 0; i < 4; i++) { int index = fxParam / factorial; - perm[i] = types[index]; + perm[static_cast(i)] = types[static_cast(index)]; for (int j = index; j < 3; j++) - types[j] = types[j + 1]; + types[static_cast(j)] = types[static_cast(j + 1)]; fxParam %= factorial; if (i < 3) factorial /= 3 - i; @@ -229,12 +255,12 @@ static int permToParam(std::array fxPerm) for (int i = 0; i < 3; i++) { int type = fxPerm[i]; - int index; + size_t index; for (index = 0; index < types.size(); index++) if (type == types[index]) break; - result += factorial * index; - for (int j = index; j < 3 - i; j++) + result += factorial * static_cast(index); + for (size_t j = static_cast(index); j < static_cast(3 - i); j++) types[j] = types[j + 1]; if (i < 3) factorial /= 3 - i; @@ -286,7 +312,7 @@ inline void addBool(juce::AudioProcessorValueTreeState::ParameterLayout& layout, layout.add(std::make_unique( juce::ParameterID{ identifier, versionNum }, identifier, defaultValue, juce::AudioParameterBoolAttributes{}.withStringFromValueFunction(formatFunc) )); -}; +} /** Utility to add a choice parameter to the layout */ inline void addChoice(juce::AudioProcessorValueTreeState::ParameterLayout& layout, const juce::String& identifier, int defaultIndex, @@ -432,6 +458,26 @@ inline juce::AudioProcessorValueTreeState::ParameterLayout createParameterLayout addFloat(layout, CHORUS_CENTER_DELAY, 7.f, { CHORUS_CENTER_DELAY_RANGE, 1.f }, Version::V1, suffixF(" " + TIME_UNIT, 1.f)); addFloat(layout, CHORUS_MIX, 0.5f, { 0.f, 1.f, 0.01f }, Version::V1); + // Modulation section parameters + addChoice(layout, MOD_FILTER_TYPE, 0, MOD_FILTER_TYPE_LABELS, Version::V1_3_2); + addFloat(layout, MOD_FILTER_CUTOFF, 1000.f, addSkew({ 20.f, 20000.f, 1.f }, 1000.f), Version::V1_3_2, suffixF(" " + FREQUENCY_UNIT, 1.f)); + addFloat(layout, MOD_FILTER_RESONANCE, 0.f, { 0.f, 1.f, 0.01f }, Version::V1_3_2); + + addFloat(layout, MOD_LFO_RATE, 1.f, addSkew({ 0.05f, 30.f, 0.01f }, 1.f), Version::V1_3_2, suffixF(" " + FREQUENCY_UNIT, 0.01f)); + addBool(layout, MOD_LFO_SYNC, false, Version::V1_3_2); + addChoice(layout, MOD_LFO_SYNC_DIV, 2, MOD_LFO_SYNC_DIV_LABELS, Version::V1_3_2); + addChoice(layout, MOD_LFO_WAVEFORM, 0, MOD_LFO_WAVEFORM_LABELS, Version::V1_3_2); + addFloat(layout, MOD_LFO_FILTER_DEPTH, 0.f, { 0.f, 1.f, 0.01f }, Version::V1_3_2); + addFloat(layout, MOD_LFO_SPEED_DEPTH, 0.f, { 0.f, 1.f, 0.01f }, Version::V1_3_2); + + addBool(layout, MOD_PHASER_ENABLED, false, Version::V1_3_2); + addFloat(layout, MOD_PHASER_DEPTH, 0.5f, { 0.f, 1.f, 0.01f }, Version::V1_3_2); + addFloat(layout, MOD_PHASER_FEEDBACK, 0.f, { -0.95f, 0.95f, 0.01f }, Version::V1_3_2); + addInt(layout, MOD_PHASER_STAGES, 3, { 1, 6 }, Version::V1_3_2, suffixI(" stg")); + + addBool(layout, MOD_RINGMOD_ENABLED, false, Version::V1_3_2); + addFloat(layout, MOD_RINGMOD_MIX, 0.5f, { 0.f, 1.f, 0.01f }, Version::V1_3_2); + // This is a dummy parameter to notify the host of state changes addBool(layout, State::UI_DUMMY_PARAM, true, Version::V1, [](bool, int) -> String { return "Dummy Param"; }); diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 77d7369..300ff95 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -178,6 +178,14 @@ void JustaSampleAudioProcessor::processBlock(juce::AudioBuffer& buffer, j if (sampleBuffer.getNumSamples() == 0 || sampleBuffer.getNumChannels() == 0) return; + // Query host tempo for LFO sync + if (auto* playHead = getPlayHead()) + { + if (auto pos = playHead->getPosition()) + if (auto bpm = pos->getBpm()) + samplerSound.currentBpm.store(float(*bpm)); + } + juce::ScopedTryLock lock(voiceLock); if (lock.isLocked()) diff --git a/Source/Sampler/CustomSamplerVoice.cpp b/Source/Sampler/CustomSamplerVoice.cpp index 905cf05..40edbe7 100644 --- a/Source/Sampler/CustomSamplerVoice.cpp +++ b/Source/Sampler/CustomSamplerVoice.cpp @@ -15,6 +15,10 @@ #include "Effects/Chorus.h" #include "Effects/Distortion.h" #include "Effects/Reverb.h" +#include "Effects/ModFilter.h" +#include "Effects/Phaser.h" +#include "Effects/RingMod.h" +#include "LFO.h" CustomSamplerVoice::CustomSamplerVoice(const SamplerParameters& samplerSound, MTSClient* client, double applicationSampleRate, int expectedBlockSize, bool initSample) : expectedBlockSize(expectedBlockSize), sampleSound(samplerSound), @@ -66,6 +70,13 @@ void CustomSamplerVoice::initializeSample() loopLowpass.emplace_back(std::make_unique(2 * LANCZOS_WINDOW_SIZE)); endLowpass.emplace_back(std::make_unique(2 * LANCZOS_WINDOW_SIZE)); } + + // Prepare modulation section + modLFO.prepare(getSampleRate()); + modFilter.initialize(sampleSound.sample.getNumChannels(), int(getSampleRate())); + modPhaser.initialize(sampleSound.sample.getNumChannels(), int(getSampleRate())); + modRingMod.initialize(sampleSound.sample.getNumChannels(), int(getSampleRate())); + modLfoBuffer.setSize(1, expectedBlockSize * 2); } void CustomSamplerVoice::startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition) @@ -131,7 +142,17 @@ void CustomSamplerVoice::startNote(int midiNoteNumber, float velocity, juce::Syn effect.fx->initialize(sampleSound.sample.getNumChannels(), int(getSampleRate())); effect.fx->updateParams(sampleSound); } - + + // Initialize modulation section + modLFO.reset(); + modLFO.prepare(getSampleRate()); + modFilter.initialize(sampleSound.sample.getNumChannels(), int(getSampleRate())); + modFilter.updateParams(sampleSound, false); + modPhaser.initialize(sampleSound.sample.getNumChannels(), int(getSampleRate())); + modPhaser.updateParams(sampleSound, false); + modRingMod.initialize(sampleSound.sample.getNumChannels(), int(getSampleRate())); + modRingMod.updateParams(sampleSound, false); + updateFXParamsTimer = 0; // Set the initial state (vc.currentPosition is set before updateSpeedAndPitch) @@ -279,6 +300,23 @@ void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer& outputBuffer, someFXEnabled = someFXEnabled || effect.enablementSource->get(); } + // Pre-compute LFO values for the block + bool modEnabled = sampleSound.modLfoFilterDepth->get() > 0.f || sampleSound.modLfoSpeedDepth->get() > 0.f + || sampleSound.modRingModEnabled->get() || sampleSound.modPhaserEnabled->get(); + if (modEnabled) + { + float bpm = sampleSound.currentBpm.load(); + modLFO.setSync(sampleSound.modLfoSync->get(), bpm, + sampleSound.modLfoSyncDiv->getIndex() == 0 ? 2 : (sampleSound.modLfoSyncDiv->getIndex() == 1 ? 3 : 4)); + modLFO.setRate(sampleSound.modLfoRate->get()); + modLFO.setWaveform(static_cast(sampleSound.modLfoWaveform->getIndex())); + + if (modLfoBuffer.getNumSamples() < numSamples) + modLfoBuffer.setSize(1, numSamples); + for (int i = 0; i < numSamples; i++) + modLfoBuffer.setSample(0, i, modLFO.getNextSample()); + } + // Main processing loop VoiceContext con; for (auto ch = 0; ch < sampleSound.sample.getNumChannels(); ch++) @@ -352,8 +390,10 @@ void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer& outputBuffer, if (con.isReleasing) envelopeBuffer.setSample(ch, i, envelopeBuffer.getSample(ch, i) * exponentialCurve(releaseShape, 1 - con.speedMovedSinceRelease / releaseSmoothing)); - // Update the position - con.currentPosition += speed; + // Update the position with optional LFO speed modulation + float lfoVal = modEnabled ? modLfoBuffer.getSample(0, i) : 0.f; + float modulatedSpeed = speed * (1.f + lfoVal * sampleSound.modLfoSpeedDepth->get()); + con.currentPosition += modulatedSpeed; con.speedMovedSinceStart += 1; if (con.isReleasing) con.speedMovedSinceRelease += 1; @@ -430,6 +470,43 @@ void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer& outputBuffer, } vc = con; + // Apply modulation section (filter, ring mod, phaser) before FX chain + if (modEnabled) + { + float lfoFilterDepth = sampleSound.modLfoFilterDepth->get(); + if (lfoFilterDepth > 0.f) + { + float baseCutoff = sampleSound.modFilterCutoff->get(); + float avgLfo = 0.f; + for (int i = 0; i < numSamples; i++) + avgLfo += modLfoBuffer.getSample(0, i); + avgLfo /= float(numSamples); + float modulatedCutoff = baseCutoff * std::pow(2.f, avgLfo * lfoFilterDepth * 4.f); + modFilter.setCutoff(modulatedCutoff); + modFilter.setResonance(sampleSound.modFilterResonance->get()); + modFilter.process(tempOutputBuffer, numSamples); + } + else + { + modFilter.setCutoff(sampleSound.modFilterCutoff->get()); + modFilter.setResonance(sampleSound.modFilterResonance->get()); + modFilter.process(tempOutputBuffer, numSamples); + } + + if (sampleSound.modRingModEnabled->get()) + modRingMod.processWithLFO(tempOutputBuffer, numSamples, modLfoBuffer.getReadPointer(0)); + + if (sampleSound.modPhaserEnabled->get()) + { + float avgLfo = 0.f; + for (int i = 0; i < numSamples; i++) + avgLfo += modLfoBuffer.getSample(0, i); + avgLfo /= float(numSamples); + modPhaser.setDepth(sampleSound.modPhaserDepth->get()); + modPhaser.processWithLFO(tempOutputBuffer, numSamples, avgLfo); + } + } + // Check for updated FX order if (updateFXParamsTimer == UPDATE_PARAMS_LENGTH) initializeFx(); diff --git a/Source/Sampler/CustomSamplerVoice.h b/Source/Sampler/CustomSamplerVoice.h index 1a865d4..1f21360 100644 --- a/Source/Sampler/CustomSamplerVoice.h +++ b/Source/Sampler/CustomSamplerVoice.h @@ -13,6 +13,10 @@ #include "SamplerParameters.h" #include "Effects/Effect.h" +#include "Effects/ModFilter.h" +#include "Effects/Phaser.h" +#include "Effects/RingMod.h" +#include "LFO.h" #include "Stretcher.h" #include @@ -257,6 +261,13 @@ private: int updateFXParamsTimer{ 0 }; std::vector effects; + // Modulation section + ModLFO modLFO; + ModFilter modFilter; + PhaserEffect modPhaser; + RingModEffect modRingMod; + juce::AudioBuffer modLfoBuffer; + MTSClient* mtsClient{ nullptr }; }; diff --git a/Source/Sampler/Effects/ModFilter.h b/Source/Sampler/Effects/ModFilter.h new file mode 100644 index 0000000..cf7117b --- /dev/null +++ b/Source/Sampler/Effects/ModFilter.h @@ -0,0 +1,150 @@ +/* + ============================================================================== + + ModFilter.h + Created: 24 Jul 2026 + Author: Armin + + ============================================================================== +*/ + +#pragma once +#include + +#include "Effect.h" + +enum class ModFilterType : std::uint8_t +{ + LP12, + LP24, + HP, + BP, + Notch +}; + +/** A multi-mode filter effect for the modulation section. Supports LP12, LP24, HP, BP, Notch. */ +class ModFilter final : public Effect +{ +public: + void initialize(int numChannels, int fxSampleRate) override + { + sampleRate = fxSampleRate; + + juce::dsp::ProcessSpec spec{}; + spec.numChannels = static_cast(numChannels); + spec.sampleRate = sampleRate; + filterChain.reset(); + filterChain.prepare(spec); + + currentCutoff = -1.f; + currentResonance = -1.f; + currentType = static_cast(255); // Force update + } + + void setFilterType(ModFilterType type) + { + if (type != currentType) + { + currentType = type; + needsUpdate = true; + } + } + + void setCutoff(float cutoff) + { + cutoff = juce::jlimit(20.f, float(sampleRate) / 2.f - 10.f, cutoff); + if (std::abs(cutoff - currentCutoff) > 0.1f) + { + currentCutoff = cutoff; + needsUpdate = true; + } + } + + void setResonance(float resonance) + { + resonance = juce::jlimit(0.f, 1.f, resonance); + if (std::abs(resonance - currentResonance) > 0.001f) + { + currentResonance = resonance; + needsUpdate = true; + } + } + + void updateParams(const SamplerParameters& samplerSound, bool /*modulating*/) override + { + setFilterType(static_cast(samplerSound.modFilterType->getIndex())); + setCutoff(samplerSound.modFilterCutoff->get()); + setResonance(samplerSound.modFilterResonance->get()); + } + + void process(juce::AudioBuffer& buffer, int numSamples, int startSample = 0) override + { + if (needsUpdate) + updateCoefficients(); + + juce::dsp::AudioBlock block{ buffer.getArrayOfWritePointers(), size_t(buffer.getNumChannels()), size_t(startSample), size_t(numSamples) }; + juce::dsp::ProcessContextReplacing context{ block }; + filterChain.process(context); + } + +private: + void updateCoefficients() + { + needsUpdate = false; + + float q = 0.707f / (1.f - currentResonance * 0.9f); // Map 0-1 resonance to Q + q = juce::jlimit(0.5f, 30.f, q); + + switch (currentType) + { + case ModFilterType::LP12: + { + auto coeff = juce::dsp::IIR::Coefficients::makeLowPass(sampleRate, currentCutoff, q); + *filterChain.get<0>().state = *coeff; + *filterChain.get<1>().state = *coeff; + break; + } + case ModFilterType::LP24: + { + auto coeff = juce::dsp::IIR::Coefficients::makeLowPass(sampleRate, currentCutoff, q); + *filterChain.get<0>().state = *coeff; + *filterChain.get<1>().state = *coeff; + *filterChain.get<2>().state = *coeff; + *filterChain.get<3>().state = *coeff; + break; + } + case ModFilterType::HP: + { + auto coeff = juce::dsp::IIR::Coefficients::makeHighPass(sampleRate, currentCutoff, q); + *filterChain.get<0>().state = *coeff; + *filterChain.get<1>().state = *coeff; + break; + } + case ModFilterType::BP: + { + auto coeff = juce::dsp::IIR::Coefficients::makeBandPass(sampleRate, currentCutoff, q); + *filterChain.get<0>().state = *coeff; + *filterChain.get<1>().state = *coeff; + break; + } + case ModFilterType::Notch: + { + auto coeff = juce::dsp::IIR::Coefficients::makeNotch(sampleRate, currentCutoff, q); + *filterChain.get<0>().state = *coeff; + *filterChain.get<1>().state = *coeff; + break; + } + } + } + + using Filter = juce::dsp::ProcessorDuplicator, juce::dsp::IIR::Coefficients>; + using FilterChain = juce::dsp::ProcessorChain; + + int sampleRate{ 0 }; + FilterChain filterChain; + + ModFilterType currentType{ ModFilterType::LP12 }; + float currentCutoff{ -1.f }; + float currentResonance{ -1.f }; + bool needsUpdate{ true }; +}; diff --git a/Source/Sampler/Effects/Phaser.h b/Source/Sampler/Effects/Phaser.h new file mode 100644 index 0000000..22d6975 --- /dev/null +++ b/Source/Sampler/Effects/Phaser.h @@ -0,0 +1,187 @@ +/* + ============================================================================== + + Phaser.h + Created: 24 Jul 2026 + Author: Armin + + ============================================================================== +*/ + +#pragma once +#include + +#include "Effect.h" + +/** A phaser effect with configurable stages (pairs of all-pass filters) and LFO modulation. */ +class PhaserEffect final : public Effect +{ +public: + void initialize(int numChannels, int fxSampleRate) override + { + sampleRate = fxSampleRate; + numChannelsUsed = numChannels; + + for (auto& stage : stages) + { + stage[0].reset(); + stage[1].reset(); + } + feedbackSampleL = 0.f; + feedbackSampleR = 0.f; + + currentCenterFreq = -1.f; + currentStages = -1; + } + + void setCenterFrequency(float freq) + { + freq = juce::jlimit(50.f, float(sampleRate) / 4.f, freq); + if (std::abs(freq - currentCenterFreq) > 0.5f) + { + currentCenterFreq = freq; + updateAllPassCoefficients(); + } + } + + void setNumStages(int numStages) + { + numStages = juce::jlimit(1, 6, numStages); + if (numStages != currentStages) + { + currentStages = numStages; + updateAllPassCoefficients(); + } + } + + void setFeedback(float fb) + { + feedback = juce::jlimit(-0.95f, 0.95f, fb); + } + + void setDepth(float d) + { + depth = juce::jlimit(0.f, 1.f, d); + } + + /** Process the buffer with a given LFO modulation value [-1, 1]. + This is called per block; the center frequency is modulated by the LFO depth. + */ + void processWithLFO(juce::AudioBuffer& buffer, int numSamples, float lfoValue) + { + float modulatedFreq = currentCenterFreq * std::pow(2.f, lfoValue * depth * 3.f); + setCenterFrequency(modulatedFreq); + + for (int ch = 0; ch < buffer.getNumChannels(); ch++) + { + auto* data = buffer.getWritePointer(ch); + float& fbSample = (ch == 0) ? feedbackSampleL : feedbackSampleR; + + for (int i = 0; i < numSamples; i++) + { + float input = data[i] + fbSample * feedback; + + float apOutput = input; + for (int s = 0; s < currentStages; s++) + { + auto& stage = stages[static_cast(s)]; + auto& filter = (ch == 0) ? stage[0] : stage[1]; + apOutput = filter.process(apOutput); + } + + fbSample = apOutput; + data[i] = input + apOutput * (-depth); + } + } + } + + void updateParams(const SamplerParameters& samplerSound, bool /*modulating*/) override + { + setFeedback(samplerSound.modPhaserFeedback->get()); + setNumStages(samplerSound.modPhaserStages->get()); + setDepth(samplerSound.modPhaserDepth->get()); + } + + void process(juce::AudioBuffer& buffer, int numSamples, int startSample = 0) override + { + // When called without LFO, process with no modulation (center freq stays) + for (int ch = 0; ch < buffer.getNumChannels(); ch++) + { + auto* data = buffer.getWritePointer(ch, startSample); + float& fbSample = (ch == 0) ? feedbackSampleL : feedbackSampleR; + + for (int i = 0; i < numSamples; i++) + { + float input = data[i] + fbSample * feedback; + + float apOutput = input; + for (int s = 0; s < currentStages; s++) + { + auto& stage = stages[static_cast(s)]; + auto& filter = (ch == 0) ? stage[0] : stage[1]; + apOutput = filter.process(apOutput); + } + + fbSample = apOutput; + data[i] = input + apOutput * (-depth); + } + } + } + +private: + /** Simple 1st-order all-pass filter: y[n] = b0*x[n] + b1*x[n-1] - a1*y[n-1] */ + struct AllPassFilter + { + void reset() { x1 = 0.f; y1 = 0.f; } + void setCoefficients(float a, float b) { a1 = a; b1 = b; } + float process(float input) + { + float output = b1 * input + b0 * x1 - a1 * y1; + y1 = output; + x1 = input; + return output; + } + + float b0{ 1.f }; + float b1{ 0.f }; + float a1{ 0.f }; + float x1{ 0.f }; + float y1{ 0.f }; + }; + + void updateAllPassCoefficients() + { + if (sampleRate <= 0) + return; + + for (int s = 0; s < currentStages; s++) + { + float freq = currentCenterFreq * (1.f + float(s) * 0.3f); + freq = juce::jlimit(50.f, float(sampleRate) / 4.f - 10.f, freq); + + float angle = juce::MathConstants::pi * freq / float(sampleRate); + float t = std::tan(angle); + float a = (t - 1.f) / (t + 1.f); + + allPassA[static_cast(s)] = a; + allPassB[static_cast(s)] = 1.f; + + stages[static_cast(s)][0].setCoefficients(a, 1.f); + stages[static_cast(s)][1].setCoefficients(a, 1.f); + } + } + + static constexpr int MAX_STAGES{ 6 }; + std::array, MAX_STAGES> stages; + std::array allPassA{}; + std::array allPassB{}; + + int sampleRate{ 0 }; + int numChannelsUsed{ 2 }; + int currentStages{ 3 }; + float currentCenterFreq{ 1000.f }; + float feedback{ 0.f }; + float depth{ 0.5f }; + float feedbackSampleL{ 0.f }; + float feedbackSampleR{ 0.f }; +}; diff --git a/Source/Sampler/Effects/RingMod.h b/Source/Sampler/Effects/RingMod.h new file mode 100644 index 0000000..c202676 --- /dev/null +++ b/Source/Sampler/Effects/RingMod.h @@ -0,0 +1,56 @@ +/* + ============================================================================== + + RingMod.h + Created: 24 Jul 2026 + Author: Armin + + ============================================================================== +*/ + +#pragma once +#include + +#include "Effect.h" + +/** Ring modulation effect driven by the LFO waveform. */ +class RingModEffect final : public Effect +{ +public: + void initialize(int /*numChannels*/, int /*fxSampleRate*/) override + { + } + + void setMix(float m) + { + mix = juce::jlimit(0.f, 1.f, m); + } + + void updateParams(const SamplerParameters& samplerSound, bool /*modulating*/) override + { + setMix(samplerSound.modRingModMix->get()); + } + + /** Process with a pre-computed LFO buffer for ring modulation. */ + void processWithLFO(juce::AudioBuffer& buffer, int numSamples, const float* lfoBuffer) + { + for (int ch = 0; ch < buffer.getNumChannels(); ch++) + { + auto* data = buffer.getWritePointer(ch); + for (int i = 0; i < numSamples; i++) + { + float dry = data[i]; + float modulated = dry * lfoBuffer[i]; + data[i] = dry + (modulated - dry) * mix; + } + } + } + + void process(juce::AudioBuffer& /*buffer*/, int /*numSamples*/, int /*startSample*/ = 0) override + { + // No-op without LFO buffer + } + +private: + float mix{ 0.5f }; +}; diff --git a/Source/Sampler/LFO.h b/Source/Sampler/LFO.h new file mode 100644 index 0000000..a870c38 --- /dev/null +++ b/Source/Sampler/LFO.h @@ -0,0 +1,97 @@ +/* + ============================================================================== + + LFO.h + Created: 24 Jul 2026 + Author: Armin + + ============================================================================== +*/ + +#pragma once +#include + +enum class LFOWaveform : std::uint8_t +{ + Sine, + Triangle, + Saw, + Square +}; + +/** A simple polyphonic LFO for modulation purposes. Outputs values in [-1, 1]. */ +class ModLFO +{ +public: + void prepare(double newSampleRate) + { + sampleRate = newSampleRate; + phase = 0.f; + } + + void setRate(float hz) + { + rate = hz; + if (!synced) + currentHz = rate; + } + + void setWaveform(LFOWaveform wf) { waveform = wf; } + + void setSync(bool shouldSync, double bpm, int syncDiv) + { + synced = shouldSync; + if (synced && bpm > 0 && syncDiv > 0) + currentHz = float(bpm) * float(syncDiv) / (60.f * 4.f); + else + currentHz = rate; + } + + float getNextSample() + { + float increment = currentHz / float(sampleRate); + float value = evaluate(phase); + + phase += increment; + if (phase >= 1.f) + phase -= 1.f; + + return value; + } + + void reset() + { + phase = 0.f; + } + + float getCurrentHz() const { return currentHz; } + +private: + float evaluate(float p) const + { + switch (waveform) + { + case LFOWaveform::Sine: + return std::sin(2.f * juce::MathConstants::pi * p); + + case LFOWaveform::Triangle: + return 2.f * std::abs(2.f * p - 1.f) - 1.f; + + case LFOWaveform::Saw: + return 2.f * p - 1.f; + + case LFOWaveform::Square: + return p < 0.5f ? 1.f : -1.f; + + default: + return std::sin(2.f * juce::MathConstants::pi * p); + } + } + + double sampleRate{ 44100 }; + float phase{ 0.f }; + float rate{ 1.f }; + float currentHz{ 1.f }; + LFOWaveform waveform{ LFOWaveform::Sine }; + bool synced{ false }; +}; diff --git a/Source/Sampler/SamplerParameters.cpp b/Source/Sampler/SamplerParameters.cpp index efc5231..5b8be01 100644 --- a/Source/Sampler/SamplerParameters.cpp +++ b/Source/Sampler/SamplerParameters.cpp @@ -74,6 +74,22 @@ SamplerParameters::SamplerParameters(const juce::AudioProcessorValueTreeState& a chorusFeedback(dynamic_cast(apvts.getParameter(PluginParameters::CHORUS_FEEDBACK))), chorusCenterDelay(dynamic_cast(apvts.getParameter(PluginParameters::CHORUS_CENTER_DELAY))), + modFilterType(dynamic_cast(apvts.getParameter(PluginParameters::MOD_FILTER_TYPE))), + modFilterCutoff(dynamic_cast(apvts.getParameter(PluginParameters::MOD_FILTER_CUTOFF))), + modFilterResonance(dynamic_cast(apvts.getParameter(PluginParameters::MOD_FILTER_RESONANCE))), + modLfoRate(dynamic_cast(apvts.getParameter(PluginParameters::MOD_LFO_RATE))), + modLfoSync(dynamic_cast(apvts.getParameter(PluginParameters::MOD_LFO_SYNC))), + modLfoSyncDiv(dynamic_cast(apvts.getParameter(PluginParameters::MOD_LFO_SYNC_DIV))), + modLfoWaveform(dynamic_cast(apvts.getParameter(PluginParameters::MOD_LFO_WAVEFORM))), + modLfoFilterDepth(dynamic_cast(apvts.getParameter(PluginParameters::MOD_LFO_FILTER_DEPTH))), + modLfoSpeedDepth(dynamic_cast(apvts.getParameter(PluginParameters::MOD_LFO_SPEED_DEPTH))), + modPhaserEnabled(dynamic_cast(apvts.getParameter(PluginParameters::MOD_PHASER_ENABLED))), + modPhaserDepth(dynamic_cast(apvts.getParameter(PluginParameters::MOD_PHASER_DEPTH))), + modPhaserFeedback(dynamic_cast(apvts.getParameter(PluginParameters::MOD_PHASER_FEEDBACK))), + modPhaserStages(dynamic_cast(apvts.getParameter(PluginParameters::MOD_PHASER_STAGES))), + modRingModEnabled(dynamic_cast(apvts.getParameter(PluginParameters::MOD_RINGMOD_ENABLED))), + modRingModMix(dynamic_cast(apvts.getParameter(PluginParameters::MOD_RINGMOD_MIX))), + playbackMode(dynamic_cast(apvts.getParameter(PluginParameters::PLAYBACK_MODE))), fxOrder(dynamic_cast(apvts.getParameter(PluginParameters::FX_PERM))) { diff --git a/Source/Sampler/SamplerParameters.h b/Source/Sampler/SamplerParameters.h index d994efd..0f9f083 100644 --- a/Source/Sampler/SamplerParameters.h +++ b/Source/Sampler/SamplerParameters.h @@ -47,6 +47,22 @@ public: juce::AudioParameterFloat* eqLowGain, * eqMidGain, * eqHighGain, * eqLowFreq, * eqHighFreq; juce::AudioParameterFloat* chorusMix, * chorusRate, * chorusDepth, * chorusFeedback, * chorusCenterDelay; + /** Modulation parameters */ + juce::AudioParameterChoice* modFilterType; + juce::AudioParameterFloat* modFilterCutoff, * modFilterResonance; + juce::AudioParameterFloat* modLfoRate; + juce::AudioParameterBool* modLfoSync; + juce::AudioParameterChoice* modLfoSyncDiv, * modLfoWaveform; + juce::AudioParameterFloat* modLfoFilterDepth, * modLfoSpeedDepth; + juce::AudioParameterBool* modPhaserEnabled; + juce::AudioParameterFloat* modPhaserDepth, * modPhaserFeedback; + juce::AudioParameterInt* modPhaserStages; + juce::AudioParameterBool* modRingModEnabled; + juce::AudioParameterFloat* modRingModMix; + + /** Host tempo for LFO sync */ + std::atomic currentBpm{ 120.f }; + private: juce::AudioParameterChoice* playbackMode; juce::AudioParameterInt* fxOrder;