mirror of
https://codeberg.org/armin/justasample.git
synced 2026-09-01 04:10:48 +02:00
fixes, changes to gitignore
This commit is contained in:
parent
2ab046489f
commit
11d81cf1fd
17 changed files with 1160 additions and 16 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -36,3 +36,5 @@ Thumbs.db
|
||||||
|
|
||||||
# Project specific
|
# Project specific
|
||||||
/Releases/Windows/Secrets/
|
/Releases/Windows/Secrets/
|
||||||
|
|
||||||
|
sonokai.*
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,8 @@ target_sources(JustASample PRIVATE
|
||||||
Source/Components/FxDragTarget.h
|
Source/Components/FxDragTarget.h
|
||||||
Source/Components/FxModule.cpp
|
Source/Components/FxModule.cpp
|
||||||
Source/Components/FxModule.h
|
Source/Components/FxModule.h
|
||||||
|
Source/Components/ModulationSection.cpp
|
||||||
|
Source/Components/ModulationSection.h
|
||||||
Source/Components/InputDeviceSelector.cpp
|
Source/Components/InputDeviceSelector.cpp
|
||||||
Source/Components/InputDeviceSelector.h
|
Source/Components/InputDeviceSelector.h
|
||||||
Source/Components/Prompt.h
|
Source/Components/Prompt.h
|
||||||
|
|
@ -235,7 +237,11 @@ target_sources(JustASample PRIVATE
|
||||||
Source/Sampler/Effects/Chorus.h
|
Source/Sampler/Effects/Chorus.h
|
||||||
Source/Sampler/Effects/Distortion.h
|
Source/Sampler/Effects/Distortion.h
|
||||||
Source/Sampler/Effects/Effect.h
|
Source/Sampler/Effects/Effect.h
|
||||||
|
Source/Sampler/Effects/ModFilter.h
|
||||||
|
Source/Sampler/Effects/Phaser.h
|
||||||
Source/Sampler/Effects/Reverb.h
|
Source/Sampler/Effects/Reverb.h
|
||||||
|
Source/Sampler/Effects/RingMod.h
|
||||||
|
Source/Sampler/LFO.h
|
||||||
External/Gin/gin_distortion.h
|
External/Gin/gin_distortion.h
|
||||||
External/Gin/gin_simpleverb.cpp
|
External/Gin/gin_simpleverb.cpp
|
||||||
External/Gin/gin_simpleverb.h
|
External/Gin/gin_simpleverb.h
|
||||||
|
|
@ -315,6 +321,52 @@ target_compile_definitions(JustASample PUBLIC
|
||||||
JAS_VST3_REAPER_INTEGRATION=$<BOOL:${JAS_VST3_REAPER_INTEGRATION}>
|
JAS_VST3_REAPER_INTEGRATION=$<BOOL:${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)
|
add_dependencies(JustASample GenerateBuildInfo)
|
||||||
|
|
||||||
# JustASample itself already gets LTO via juce::juce_recommended_lto_flags
|
# JustASample itself already gets LTO via juce::juce_recommended_lto_flags
|
||||||
|
|
|
||||||
328
Source/Components/ModulationSection.cpp
Normal file
328
Source/Components/ModulationSection.cpp
Normal file
|
|
@ -0,0 +1,328 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
80
Source/Components/ModulationSection.h
Normal file
80
Source/Components/ModulationSection.h
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
ModulationSection.h
|
||||||
|
Created: 24 Jul 2026
|
||||||
|
Author: Armin
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#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<CustomRotary*> rotaries;
|
||||||
|
|
||||||
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationSection)
|
||||||
|
};
|
||||||
|
|
@ -119,6 +119,8 @@ struct Layout
|
||||||
static constexpr float fxDisplayStrokeWidth{ 0.004f };
|
static constexpr float fxDisplayStrokeWidth{ 0.004f };
|
||||||
|
|
||||||
static constexpr int footerHeight{ 64 };
|
static constexpr int footerHeight{ 64 };
|
||||||
|
|
||||||
|
static constexpr int modulationSectionHeight{ 140 };
|
||||||
};
|
};
|
||||||
|
|
||||||
/** This struct contains certain UX constants */
|
/** This struct contains certain UX constants */
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,8 @@ JustaSampleAudioProcessorEditor::JustaSampleAudioProcessorEditor(JustaSampleAudi
|
||||||
[this](const juce::MouseWheelDetails& details, int center) -> void { sampleNavigator.scrollView(details, center, true); }),
|
[this](const juce::MouseWheelDetails& details, int center) -> void { sampleNavigator.scrollView(details, center, true); }),
|
||||||
sampleNavigator(p.APVTS(), p.getPluginState(), synthVoices),
|
sampleNavigator(p.APVTS(), p.getPluginState(), synthVoices),
|
||||||
|
|
||||||
|
modulationSection(p),
|
||||||
|
|
||||||
fxChain(p),
|
fxChain(p),
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
|
|
@ -282,6 +284,9 @@ JustaSampleAudioProcessorEditor::JustaSampleAudioProcessorEditor(JustaSampleAudi
|
||||||
// FX
|
// FX
|
||||||
addChildComponent(fxChain);
|
addChildComponent(fxChain);
|
||||||
|
|
||||||
|
// Modulation section
|
||||||
|
addAndMakeVisible(modulationSection);
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
preFXButton.useShape(getOutlineFromSVG(BinaryData::IconPreFX_svg));
|
preFXButton.useShape(getOutlineFromSVG(BinaryData::IconPreFX_svg));
|
||||||
preFXButton.setHelpText("Apply FX before Attack/Release");
|
preFXButton.setHelpText("Apply FX before Attack/Release");
|
||||||
|
|
@ -421,8 +426,6 @@ void JustaSampleAudioProcessorEditor::timerCallback()
|
||||||
|
|
||||||
void JustaSampleAudioProcessorEditor::paint(juce::Graphics& g)
|
void JustaSampleAudioProcessorEditor::paint(juce::Graphics& g)
|
||||||
{
|
{
|
||||||
auto colors = getTheme();
|
|
||||||
|
|
||||||
g.fillAll(theme.light);
|
g.fillAll(theme.light);
|
||||||
|
|
||||||
auto bounds = getConstrainedBounds().toFloat();
|
auto bounds = getConstrainedBounds().toFloat();
|
||||||
|
|
@ -467,6 +470,8 @@ void JustaSampleAudioProcessorEditor::paint(juce::Graphics& g)
|
||||||
|
|
||||||
if (sampleLoaded || pluginState.showFX)
|
if (sampleLoaded || pluginState.showFX)
|
||||||
{
|
{
|
||||||
|
bounds.removeFromBottom(scalei(Layout::modulationSectionHeight));
|
||||||
|
|
||||||
auto navigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight));
|
auto navigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight));
|
||||||
g.setColour(theme.foreground);
|
g.setColour(theme.foreground);
|
||||||
g.fillRect(navigatorBounds.toNearestInt());
|
g.fillRect(navigatorBounds.toNearestInt());
|
||||||
|
|
@ -730,9 +735,17 @@ void JustaSampleAudioProcessorEditor::resized()
|
||||||
}
|
}
|
||||||
fxChain.setVisible(pluginState.showFX);
|
fxChain.setVisible(pluginState.showFX);
|
||||||
|
|
||||||
// Navigator
|
// Modulation section (always visible when sample loaded)
|
||||||
auto sampleLoaded = bool(p.getSampleBuffer().getNumSamples());
|
auto sampleLoaded = bool(p.getSampleBuffer().getNumSamples());
|
||||||
if (sampleLoaded || pluginState.showFX)
|
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));
|
auto sampleNavigatorBounds = bounds.removeFromBottom(scalei(Layout::sampleNavigatorHeight));
|
||||||
sampleNavigator.setBounds(sampleNavigatorBounds.toNearestInt());
|
sampleNavigator.setBounds(sampleNavigatorBounds.toNearestInt());
|
||||||
|
|
@ -834,7 +847,7 @@ void JustaSampleAudioProcessorEditor::lookAndFeelChanged()
|
||||||
juce::Array<Component*> foregroundComponents = {
|
juce::Array<Component*> foregroundComponents = {
|
||||||
&tuningLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel, &semitoneRotary, &waveformSemitoneRotary, ¢Rotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
|
&tuningLabel, &attackLabel, &releaseLabel, &playbackLabel, &loopingLabel, &masterLabel, &semitoneRotary, &waveformSemitoneRotary, ¢Rotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
|
||||||
&attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &lofiModeButton, &playbackModeButton, &playbackSpeedRotary, &loopStartButton, &loopButton, &loopEndButton, &pingPongButton, &crossfadeRotary,
|
&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)
|
for (Component* component : foregroundComponents)
|
||||||
|
|
@ -891,12 +904,13 @@ Colors JustaSampleAudioProcessorEditor::getTheme() const
|
||||||
juce::Rectangle<int> JustaSampleAudioProcessorEditor::getConstrainedBounds() const
|
juce::Rectangle<int> JustaSampleAudioProcessorEditor::getConstrainedBounds() const
|
||||||
{
|
{
|
||||||
auto fxSpace = !pluginState.showFX ? scalef(Layout::fxChainHeight) : 0;
|
auto fxSpace = !pluginState.showFX ? scalef(Layout::fxChainHeight) : 0;
|
||||||
|
auto modSpace = scalef(Layout::modulationSectionHeight);
|
||||||
|
|
||||||
auto bounds = getLocalBounds();
|
auto bounds = getLocalBounds();
|
||||||
auto width = getWidth();
|
auto width = getWidth();
|
||||||
auto height = getHeight();
|
auto height = getHeight();
|
||||||
auto constrainedWidth = int(juce::jlimit<float>((height + fxSpace) * 0.8f, (height + fxSpace) * 2.f, float(width)));
|
auto constrainedWidth = int(juce::jlimit<float>((height + fxSpace + modSpace) * 0.8f, (height + fxSpace + modSpace) * 2.f, float(width)));
|
||||||
auto constrainedHeight = int(width / 0.8f - fxSpace);
|
auto constrainedHeight = int(width / 0.8f - fxSpace - modSpace);
|
||||||
|
|
||||||
if (constrainedWidth < width)
|
if (constrainedWidth < width)
|
||||||
bounds.reduce((width - constrainedWidth) / 2, 0);
|
bounds.reduce((width - constrainedWidth) / 2, 0);
|
||||||
|
|
@ -1067,7 +1081,7 @@ void JustaSampleAudioProcessorEditor::enablementChanged()
|
||||||
juce::Array<Component*> sampleDependentComponents = {
|
juce::Array<Component*> sampleDependentComponents = {
|
||||||
&semitoneRotary, ¢Rotary, &waveformSemitoneRotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
|
&semitoneRotary, ¢Rotary, &waveformSemitoneRotary, &waveformCentRotary, &tuningDetectLabel, &tuningDetectButton,
|
||||||
&attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &gainSlider,
|
&attackTimeRotary, &attackCurve, &releaseTimeRotary, &releaseCurve, &gainSlider,
|
||||||
&playStopButton, &fitButton, &pinButton, &sampleEditor, &sampleNavigator, &fxChain, &showFXButton
|
&playStopButton, &fitButton, &pinButton, &sampleEditor, &sampleNavigator, &modulationSection, &fxChain, &showFXButton
|
||||||
};
|
};
|
||||||
|
|
||||||
juce::Array<Component*> notWaveformModeComponents = { &playbackModeButton, &loopStartButton, &loopButton, &loopEndButton, &pingPongButton, &crossfadeRotary, &tuningDetectLabel, &tuningDetectButton };
|
juce::Array<Component*> notWaveformModeComponents = { &playbackModeButton, &loopStartButton, &loopButton, &loopEndButton, &pingPongButton, &crossfadeRotary, &tuningDetectLabel, &tuningDetectButton };
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include "Components/InputDeviceSelector.h"
|
#include "Components/InputDeviceSelector.h"
|
||||||
#include "Components/SampleEditor.h"
|
#include "Components/SampleEditor.h"
|
||||||
#include "Components/FxChain.h"
|
#include "Components/FxChain.h"
|
||||||
|
#include "Components/ModulationSection.h"
|
||||||
#include "Components/Prompt.h"
|
#include "Components/Prompt.h"
|
||||||
#include "Components/SampleNavigator.h"
|
#include "Components/SampleNavigator.h"
|
||||||
|
|
||||||
|
|
@ -217,6 +218,7 @@ private:
|
||||||
// Main components
|
// Main components
|
||||||
SampleEditor sampleEditor;
|
SampleEditor sampleEditor;
|
||||||
SampleNavigator sampleNavigator; // Note that SampleNavigator manages ViewStart and ViewEnd
|
SampleNavigator sampleNavigator; // Note that SampleNavigator manages ViewStart and ViewEnd
|
||||||
|
ModulationSection modulationSection;
|
||||||
FxChain fxChain;
|
FxChain fxChain;
|
||||||
|
|
||||||
juce::Label statusLabel;
|
juce::Label statusLabel;
|
||||||
|
|
|
||||||
|
|
@ -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 constexpr Range CHORUS_CENTER_DELAY_RANGE{ 0.f, 100.f }; // in ms
|
||||||
inline static const String CHORUS_MIX{ "Chorus Mix" };
|
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" };
|
inline static const String FX_PERM{ "FX Ordering" };
|
||||||
|
|
||||||
enum FxTypes : std::uint8_t
|
enum FxTypes : std::uint8_t
|
||||||
|
|
@ -210,9 +236,9 @@ inline std::array<FxTypes, 4> paramToPerm(int fxParam)
|
||||||
for (int i = 0; i < 4; i++)
|
for (int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
int index = fxParam / factorial;
|
int index = fxParam / factorial;
|
||||||
perm[i] = types[index];
|
perm[static_cast<size_t>(i)] = types[static_cast<size_t>(index)];
|
||||||
for (int j = index; j < 3; j++)
|
for (int j = index; j < 3; j++)
|
||||||
types[j] = types[j + 1];
|
types[static_cast<size_t>(j)] = types[static_cast<size_t>(j + 1)];
|
||||||
fxParam %= factorial;
|
fxParam %= factorial;
|
||||||
if (i < 3)
|
if (i < 3)
|
||||||
factorial /= 3 - i;
|
factorial /= 3 - i;
|
||||||
|
|
@ -229,12 +255,12 @@ static int permToParam(std::array<FxTypes, 4> fxPerm)
|
||||||
for (int i = 0; i < 3; i++)
|
for (int i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
int type = fxPerm[i];
|
int type = fxPerm[i];
|
||||||
int index;
|
size_t index;
|
||||||
for (index = 0; index < types.size(); index++)
|
for (index = 0; index < types.size(); index++)
|
||||||
if (type == types[index])
|
if (type == types[index])
|
||||||
break;
|
break;
|
||||||
result += factorial * index;
|
result += factorial * static_cast<int>(index);
|
||||||
for (int j = index; j < 3 - i; j++)
|
for (size_t j = static_cast<size_t>(index); j < static_cast<size_t>(3 - i); j++)
|
||||||
types[j] = types[j + 1];
|
types[j] = types[j + 1];
|
||||||
if (i < 3)
|
if (i < 3)
|
||||||
factorial /= 3 - i;
|
factorial /= 3 - i;
|
||||||
|
|
@ -286,7 +312,7 @@ inline void addBool(juce::AudioProcessorValueTreeState::ParameterLayout& layout,
|
||||||
layout.add(std::make_unique<juce::AudioParameterBool>(
|
layout.add(std::make_unique<juce::AudioParameterBool>(
|
||||||
juce::ParameterID{ identifier, versionNum }, identifier, defaultValue, juce::AudioParameterBoolAttributes{}.withStringFromValueFunction(formatFunc)
|
juce::ParameterID{ identifier, versionNum }, identifier, defaultValue, juce::AudioParameterBoolAttributes{}.withStringFromValueFunction(formatFunc)
|
||||||
));
|
));
|
||||||
};
|
}
|
||||||
|
|
||||||
/** Utility to add a choice parameter to the layout */
|
/** Utility to add a choice parameter to the layout */
|
||||||
inline void addChoice(juce::AudioProcessorValueTreeState::ParameterLayout& layout, const juce::String& identifier, int defaultIndex,
|
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_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);
|
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
|
// 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"; });
|
addBool(layout, State::UI_DUMMY_PARAM, true, Version::V1, [](bool, int) -> String { return "Dummy Param"; });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,14 @@ void JustaSampleAudioProcessor::processBlock(juce::AudioBuffer<float>& buffer, j
|
||||||
if (sampleBuffer.getNumSamples() == 0 || sampleBuffer.getNumChannels() == 0)
|
if (sampleBuffer.getNumSamples() == 0 || sampleBuffer.getNumChannels() == 0)
|
||||||
return;
|
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);
|
juce::ScopedTryLock lock(voiceLock);
|
||||||
|
|
||||||
if (lock.isLocked())
|
if (lock.isLocked())
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,10 @@
|
||||||
#include "Effects/Chorus.h"
|
#include "Effects/Chorus.h"
|
||||||
#include "Effects/Distortion.h"
|
#include "Effects/Distortion.h"
|
||||||
#include "Effects/Reverb.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) :
|
CustomSamplerVoice::CustomSamplerVoice(const SamplerParameters& samplerSound, MTSClient* client, double applicationSampleRate, int expectedBlockSize, bool initSample) :
|
||||||
expectedBlockSize(expectedBlockSize), sampleSound(samplerSound),
|
expectedBlockSize(expectedBlockSize), sampleSound(samplerSound),
|
||||||
|
|
@ -66,6 +70,13 @@ void CustomSamplerVoice::initializeSample()
|
||||||
loopLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
|
loopLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
|
||||||
endLowpass.emplace_back(std::make_unique<LowpassStream>(2 * LANCZOS_WINDOW_SIZE));
|
endLowpass.emplace_back(std::make_unique<LowpassStream>(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)
|
void CustomSamplerVoice::startNote(int midiNoteNumber, float velocity, juce::SynthesiserSound* sound, int currentPitchWheelPosition)
|
||||||
|
|
@ -132,6 +143,16 @@ void CustomSamplerVoice::startNote(int midiNoteNumber, float velocity, juce::Syn
|
||||||
effect.fx->updateParams(sampleSound);
|
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;
|
updateFXParamsTimer = 0;
|
||||||
|
|
||||||
// Set the initial state (vc.currentPosition is set before updateSpeedAndPitch)
|
// Set the initial state (vc.currentPosition is set before updateSpeedAndPitch)
|
||||||
|
|
@ -279,6 +300,23 @@ void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer<float>& outputBuffer,
|
||||||
someFXEnabled = someFXEnabled || effect.enablementSource->get();
|
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<LFOWaveform>(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
|
// Main processing loop
|
||||||
VoiceContext con;
|
VoiceContext con;
|
||||||
for (auto ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
|
for (auto ch = 0; ch < sampleSound.sample.getNumChannels(); ch++)
|
||||||
|
|
@ -352,8 +390,10 @@ void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer<float>& outputBuffer,
|
||||||
if (con.isReleasing)
|
if (con.isReleasing)
|
||||||
envelopeBuffer.setSample(ch, i, envelopeBuffer.getSample(ch, i) * exponentialCurve(releaseShape, 1 - con.speedMovedSinceRelease / releaseSmoothing));
|
envelopeBuffer.setSample(ch, i, envelopeBuffer.getSample(ch, i) * exponentialCurve(releaseShape, 1 - con.speedMovedSinceRelease / releaseSmoothing));
|
||||||
|
|
||||||
// Update the position
|
// Update the position with optional LFO speed modulation
|
||||||
con.currentPosition += speed;
|
float lfoVal = modEnabled ? modLfoBuffer.getSample(0, i) : 0.f;
|
||||||
|
float modulatedSpeed = speed * (1.f + lfoVal * sampleSound.modLfoSpeedDepth->get());
|
||||||
|
con.currentPosition += modulatedSpeed;
|
||||||
con.speedMovedSinceStart += 1;
|
con.speedMovedSinceStart += 1;
|
||||||
if (con.isReleasing)
|
if (con.isReleasing)
|
||||||
con.speedMovedSinceRelease += 1;
|
con.speedMovedSinceRelease += 1;
|
||||||
|
|
@ -430,6 +470,43 @@ void CustomSamplerVoice::renderNextBlock(juce::AudioBuffer<float>& outputBuffer,
|
||||||
}
|
}
|
||||||
vc = con;
|
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
|
// Check for updated FX order
|
||||||
if (updateFXParamsTimer == UPDATE_PARAMS_LENGTH)
|
if (updateFXParamsTimer == UPDATE_PARAMS_LENGTH)
|
||||||
initializeFx();
|
initializeFx();
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@
|
||||||
|
|
||||||
#include "SamplerParameters.h"
|
#include "SamplerParameters.h"
|
||||||
#include "Effects/Effect.h"
|
#include "Effects/Effect.h"
|
||||||
|
#include "Effects/ModFilter.h"
|
||||||
|
#include "Effects/Phaser.h"
|
||||||
|
#include "Effects/RingMod.h"
|
||||||
|
#include "LFO.h"
|
||||||
#include "Stretcher.h"
|
#include "Stretcher.h"
|
||||||
#include <libMTSClient.h>
|
#include <libMTSClient.h>
|
||||||
|
|
||||||
|
|
@ -257,6 +261,13 @@ private:
|
||||||
int updateFXParamsTimer{ 0 };
|
int updateFXParamsTimer{ 0 };
|
||||||
std::vector<Fx> effects;
|
std::vector<Fx> effects;
|
||||||
|
|
||||||
|
// Modulation section
|
||||||
|
ModLFO modLFO;
|
||||||
|
ModFilter modFilter;
|
||||||
|
PhaserEffect modPhaser;
|
||||||
|
RingModEffect modRingMod;
|
||||||
|
juce::AudioBuffer<float> modLfoBuffer;
|
||||||
|
|
||||||
MTSClient* mtsClient{ nullptr };
|
MTSClient* mtsClient{ nullptr };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
150
Source/Sampler/Effects/ModFilter.h
Normal file
150
Source/Sampler/Effects/ModFilter.h
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
ModFilter.h
|
||||||
|
Created: 24 Jul 2026
|
||||||
|
Author: Armin
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#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<juce::uint32>(numChannels);
|
||||||
|
spec.sampleRate = sampleRate;
|
||||||
|
filterChain.reset();
|
||||||
|
filterChain.prepare(spec);
|
||||||
|
|
||||||
|
currentCutoff = -1.f;
|
||||||
|
currentResonance = -1.f;
|
||||||
|
currentType = static_cast<ModFilterType>(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<ModFilterType>(samplerSound.modFilterType->getIndex()));
|
||||||
|
setCutoff(samplerSound.modFilterCutoff->get());
|
||||||
|
setResonance(samplerSound.modFilterResonance->get());
|
||||||
|
}
|
||||||
|
|
||||||
|
void process(juce::AudioBuffer<float>& 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<float>::makeLowPass(sampleRate, currentCutoff, q);
|
||||||
|
*filterChain.get<0>().state = *coeff;
|
||||||
|
*filterChain.get<1>().state = *coeff;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ModFilterType::LP24:
|
||||||
|
{
|
||||||
|
auto coeff = juce::dsp::IIR::Coefficients<float>::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<float>::makeHighPass(sampleRate, currentCutoff, q);
|
||||||
|
*filterChain.get<0>().state = *coeff;
|
||||||
|
*filterChain.get<1>().state = *coeff;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ModFilterType::BP:
|
||||||
|
{
|
||||||
|
auto coeff = juce::dsp::IIR::Coefficients<float>::makeBandPass(sampleRate, currentCutoff, q);
|
||||||
|
*filterChain.get<0>().state = *coeff;
|
||||||
|
*filterChain.get<1>().state = *coeff;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ModFilterType::Notch:
|
||||||
|
{
|
||||||
|
auto coeff = juce::dsp::IIR::Coefficients<float>::makeNotch(sampleRate, currentCutoff, q);
|
||||||
|
*filterChain.get<0>().state = *coeff;
|
||||||
|
*filterChain.get<1>().state = *coeff;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using Filter = juce::dsp::ProcessorDuplicator<juce::dsp::IIR::Filter<float>, juce::dsp::IIR::Coefficients<float>>;
|
||||||
|
using FilterChain = juce::dsp::ProcessorChain<Filter, Filter, Filter, Filter>;
|
||||||
|
|
||||||
|
int sampleRate{ 0 };
|
||||||
|
FilterChain filterChain;
|
||||||
|
|
||||||
|
ModFilterType currentType{ ModFilterType::LP12 };
|
||||||
|
float currentCutoff{ -1.f };
|
||||||
|
float currentResonance{ -1.f };
|
||||||
|
bool needsUpdate{ true };
|
||||||
|
};
|
||||||
187
Source/Sampler/Effects/Phaser.h
Normal file
187
Source/Sampler/Effects/Phaser.h
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
Phaser.h
|
||||||
|
Created: 24 Jul 2026
|
||||||
|
Author: Armin
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#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<float>& 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<size_t>(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<float>& 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<size_t>(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<float>::pi * freq / float(sampleRate);
|
||||||
|
float t = std::tan(angle);
|
||||||
|
float a = (t - 1.f) / (t + 1.f);
|
||||||
|
|
||||||
|
allPassA[static_cast<size_t>(s)] = a;
|
||||||
|
allPassB[static_cast<size_t>(s)] = 1.f;
|
||||||
|
|
||||||
|
stages[static_cast<size_t>(s)][0].setCoefficients(a, 1.f);
|
||||||
|
stages[static_cast<size_t>(s)][1].setCoefficients(a, 1.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr int MAX_STAGES{ 6 };
|
||||||
|
std::array<std::array<AllPassFilter, 2>, MAX_STAGES> stages;
|
||||||
|
std::array<float, MAX_STAGES> allPassA{};
|
||||||
|
std::array<float, MAX_STAGES> 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 };
|
||||||
|
};
|
||||||
56
Source/Sampler/Effects/RingMod.h
Normal file
56
Source/Sampler/Effects/RingMod.h
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
RingMod.h
|
||||||
|
Created: 24 Jul 2026
|
||||||
|
Author: Armin
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
#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<float>& 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<float>& /*buffer*/, int /*numSamples*/, int /*startSample*/ = 0) override
|
||||||
|
{
|
||||||
|
// No-op without LFO buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
float mix{ 0.5f };
|
||||||
|
};
|
||||||
97
Source/Sampler/LFO.h
Normal file
97
Source/Sampler/LFO.h
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
LFO.h
|
||||||
|
Created: 24 Jul 2026
|
||||||
|
Author: Armin
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <JuceHeader.h>
|
||||||
|
|
||||||
|
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<float>::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<float>::pi * p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double sampleRate{ 44100 };
|
||||||
|
float phase{ 0.f };
|
||||||
|
float rate{ 1.f };
|
||||||
|
float currentHz{ 1.f };
|
||||||
|
LFOWaveform waveform{ LFOWaveform::Sine };
|
||||||
|
bool synced{ false };
|
||||||
|
};
|
||||||
|
|
@ -74,6 +74,22 @@ SamplerParameters::SamplerParameters(const juce::AudioProcessorValueTreeState& a
|
||||||
chorusFeedback(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_FEEDBACK))),
|
chorusFeedback(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_FEEDBACK))),
|
||||||
chorusCenterDelay(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_CENTER_DELAY))),
|
chorusCenterDelay(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::CHORUS_CENTER_DELAY))),
|
||||||
|
|
||||||
|
modFilterType(dynamic_cast<juce::AudioParameterChoice*>(apvts.getParameter(PluginParameters::MOD_FILTER_TYPE))),
|
||||||
|
modFilterCutoff(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_FILTER_CUTOFF))),
|
||||||
|
modFilterResonance(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_FILTER_RESONANCE))),
|
||||||
|
modLfoRate(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_LFO_RATE))),
|
||||||
|
modLfoSync(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::MOD_LFO_SYNC))),
|
||||||
|
modLfoSyncDiv(dynamic_cast<juce::AudioParameterChoice*>(apvts.getParameter(PluginParameters::MOD_LFO_SYNC_DIV))),
|
||||||
|
modLfoWaveform(dynamic_cast<juce::AudioParameterChoice*>(apvts.getParameter(PluginParameters::MOD_LFO_WAVEFORM))),
|
||||||
|
modLfoFilterDepth(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_LFO_FILTER_DEPTH))),
|
||||||
|
modLfoSpeedDepth(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_LFO_SPEED_DEPTH))),
|
||||||
|
modPhaserEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::MOD_PHASER_ENABLED))),
|
||||||
|
modPhaserDepth(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_PHASER_DEPTH))),
|
||||||
|
modPhaserFeedback(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_PHASER_FEEDBACK))),
|
||||||
|
modPhaserStages(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::MOD_PHASER_STAGES))),
|
||||||
|
modRingModEnabled(dynamic_cast<juce::AudioParameterBool*>(apvts.getParameter(PluginParameters::MOD_RINGMOD_ENABLED))),
|
||||||
|
modRingModMix(dynamic_cast<juce::AudioParameterFloat*>(apvts.getParameter(PluginParameters::MOD_RINGMOD_MIX))),
|
||||||
|
|
||||||
playbackMode(dynamic_cast<juce::AudioParameterChoice*>(apvts.getParameter(PluginParameters::PLAYBACK_MODE))),
|
playbackMode(dynamic_cast<juce::AudioParameterChoice*>(apvts.getParameter(PluginParameters::PLAYBACK_MODE))),
|
||||||
fxOrder(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::FX_PERM)))
|
fxOrder(dynamic_cast<juce::AudioParameterInt*>(apvts.getParameter(PluginParameters::FX_PERM)))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,22 @@ public:
|
||||||
juce::AudioParameterFloat* eqLowGain, * eqMidGain, * eqHighGain, * eqLowFreq, * eqHighFreq;
|
juce::AudioParameterFloat* eqLowGain, * eqMidGain, * eqHighGain, * eqLowFreq, * eqHighFreq;
|
||||||
juce::AudioParameterFloat* chorusMix, * chorusRate, * chorusDepth, * chorusFeedback, * chorusCenterDelay;
|
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<float> currentBpm{ 120.f };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
juce::AudioParameterChoice* playbackMode;
|
juce::AudioParameterChoice* playbackMode;
|
||||||
juce::AudioParameterInt* fxOrder;
|
juce::AudioParameterInt* fxOrder;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue