diff --git a/.github/workflows/Validation.yml b/.github/workflows/Validation.yml index 63dd436..6f76f48 100644 --- a/.github/workflows/Validation.yml +++ b/.github/workflows/Validation.yml @@ -29,7 +29,8 @@ jobs: gen: 'make', cmake_config_flags: "-D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -D CMAKE_OSX_ARCHITECTURES=arm64\\;x86_64 -D CMAKE_OSX_DEPLOYMENT_TARGET=10.15", cmake_build_flags: "--parallel 6", - pluginval_link: "https://github.com/Tracktion/pluginval/releases/latest/download/pluginval_macOS.zip", + pluginval_link: "https://github.com/Tracktion/pluginval/releases/download/v1.0.4/pluginval_macOS.zip", + pluginval_sha256: "3c4c533bda0c5059eea3ddaea752d757ee2025041f0f47e6bcb0e87f6082b29f", pluginval_path: "pluginval.app/Contents/MacOS/pluginval" } - { @@ -37,7 +38,8 @@ jobs: os: windows-latest, gen: 'MSVC', cmake_config_flags: "-D CMAKE_C_COMPILER_LAUNCHER=ccache -D CMAKE_CXX_COMPILER_LAUNCHER=ccache", - pluginval_link: "https://github.com/Tracktion/pluginval/releases/latest/download/pluginval_Windows.zip", + pluginval_link: "https://github.com/Tracktion/pluginval/releases/download/v1.0.4/pluginval_Windows.zip", + pluginval_sha256: "c08e61ce3b96db41636f8ec7e76f4c7e2c13ebdac7fa1b5a1f52b4f32ec715ab", pluginval_path: "./pluginval.exe" } @@ -46,7 +48,7 @@ jobs: uses: actions/checkout@v4 - name: Enable ccache - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23 with: key: ${{ runner.os }}-${{ env.BUILD_TYPE }} @@ -63,6 +65,13 @@ jobs: - name: Build run: cmake --build Build --config ${{ env.BUILD_TYPE }} ${{ matrix.config.cmake_build_flags }} + - name: Copy VST3 for validation + shell: bash + working-directory: Build + run: | + mkdir -p pluginval-input + cp -r *_artefacts/${{ env.BUILD_TYPE }}/VST3/*.vst3 pluginval-input/ + - name: Validate shell: bash working-directory: Build @@ -70,8 +79,13 @@ jobs: VALIDATE_IN_PROCESS: true run: | curl -LsS ${{ matrix.config.pluginval_link }} -o pluginval.zip + if [[ "$RUNNER_OS" == "macOS" ]]; then + echo "${{ matrix.config.pluginval_sha256 }} pluginval.zip" | shasum -a 256 -c || { echo "pluginval checksum verification failed"; exit 1; } + else + echo "${{ matrix.config.pluginval_sha256 }} pluginval.zip" | sha256sum -c || { echo "pluginval checksum verification failed"; exit 1; } + fi unzip -q pluginval - ${{ matrix.config.pluginval_path }} --validate *_artefacts/${{ env.BUILD_TYPE }}/VST3/*.vst3 + ${{ matrix.config.pluginval_path }} --validate pluginval-input/*.vst3 - name: Get safe ref name shell: bash @@ -109,7 +123,7 @@ jobs: done - name: Create Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3 with: files: "**/*.zip" body_path: CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a32176c..7e36365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. +## \[0.2.0] - 2026-05-18 + +### Added + +* Filter modulation for High and Low cuts with independent LFOs + * LFO rate control + * LFO Tempo sync linked to song position during playback + * LFO phase offset per filter + * LFO Shape selection + * Filter modulation depth control + * Phase invert switch for high cut lfo (on by default) +* Square, Saw Up/Down and Sample&Hold LFO Shapes +* Indicator lights for both LFOs to get visual idea of its current value + +### Changed + +* Refactored filters to StereoFilter primitive and FilterEngine class +* Set maximum resonance for low and high cut filters to 1.5 + ## \[0.1.0] - 2026-04-17 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e81347..0aa96b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.22) -project(viive VERSION 0.1.0) +project(viive VERSION 0.2.0) ### Dependency versions ### @@ -41,6 +41,7 @@ set(VST3_DEV_COPY_DIR "" CACHE PATH "If set, copies the Debug VST3 build here af ### Plugin Project Setup ### juce_add_plugin(viive + VERSION 0.2.0 FORMATS VST3 Standalone PRODUCT_NAME "viive" COMPANY_NAME "trsctr" @@ -67,9 +68,12 @@ get_property(project_targets DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY BUILDSYS set_target_properties(${project_targets} PROPERTIES CXX_EXTENSIONS OFF) target_sources(viive PRIVATE + Source/FilterEngine.cpp Source/Tempo.cpp + Source/Selector.cpp Source/SyncButton.cpp Source/StereoDelay.cpp + Source/StereoFilter.cpp Source/ChorusEngine.cpp Source/LFO.cpp Source/Chorus.cpp @@ -78,7 +82,8 @@ target_sources(viive PRIVATE Source/DelayEngine.cpp Source/Parameters.cpp Source/PluginProcessor.cpp - Source/PluginEditor.cpp) + Source/PluginEditor.cpp + Source/LFOScope.cpp) juce_generate_juce_header(viive) diff --git a/README.md b/README.md index 60ac2d1..4543b48 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Currently developed and tested on Windows (JUCE AudioPluginHost, Ableton Live Su - Mix and feedback controls with feedback stabilization (0-140%) - Stereo width control with mid/side processing - Sculpting low/high-cut filters with adjustable resonance on delayed signal / feedback loop +- Filter modulation section with separate LFOs for high and low cut filters with tempo sync, adjustable rate, modulation depth, stereo phase offset and selectable shapes: Sine, Triangle, Square, Saw Up/Down, Sample & Hold - Dual-voice chorus effect in feedback loop with intensity, modulation rate, and modulation depth controls - Feedback loop protection: HPF, compressor, soft clipping @@ -31,8 +32,16 @@ Download the zip for your platform, extract it, and copy `viive.vst3` to your VS Then rescan plugins in your DAW. +NOTE: macOS build is unsigned and unnotarized. If Gatekeeper blocks the plugin, run the following commands in Terminal after copying it to your VST3 folder: + +```bash +xattr -dr com.apple.quarantine ~/Library/Audio/Plug-Ins/VST3/viive.vst3 +codesign --force --deep --sign - ~/Library/Audio/Plug-Ins/VST3/viive.vst3 +``` + +Then rescan plugins in your DAW. + ## Potential future features -- LFO/modulation for general parameters (such as filter cutoffs) - Additional effects in feedback loop (bitcrush, phaser) - Preset manager - Single/dual delay mode switch @@ -57,6 +66,8 @@ cmake --build Build --config Release JUCE 8.0.11 is downloaded automatically during configuration. +NOTE: Linux builds are reportedly possible but untested and undocumented. + ### Output The VST3 will be produced under `Build/viive_artefacts/Release/VST3/`. diff --git a/Source/DelayEngine.cpp b/Source/DelayEngine.cpp index ac537ff..d8b4a32 100644 --- a/Source/DelayEngine.cpp +++ b/Source/DelayEngine.cpp @@ -1,9 +1,9 @@ #include "DelayEngine.h" #include "DSP.h" +#include "Types.h" + DelayEngine::DelayEngine() { - m_lowCutFilter.setType(juce::dsp::StateVariableTPTFilterType::highpass); - m_highCutFilter.setType(juce::dsp::StateVariableTPTFilterType::lowpass); m_feedbackHighpass.setType(juce::dsp::StateVariableTPTFilterType::highpass); m_feedbackCompressor.setAttack(.1f); m_feedbackCompressor.setRelease(50.f); @@ -19,8 +19,7 @@ void DelayEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept spec.numChannels = 2; m_sampleRate = static_cast(sampleRate); m_feedbackCompressor.prepare(spec); - m_lowCutFilter.prepare(spec); - m_highCutFilter.prepare(spec); + m_filterEngine.prepareToPlay(sampleRate, samplesPerBlock); m_feedbackHighpass.prepare(spec); m_chorusEngine.prepareToPlay(sampleRate, samplesPerBlock); m_stereoDelay.prepareToPlay(sampleRate, samplesPerBlock); @@ -31,19 +30,14 @@ void DelayEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept void DelayEngine::reset() noexcept { m_stereoDelay.reset(); - m_lowCutFilter.reset(); - m_highCutFilter.reset(); m_chorusEngine.reset(); + m_filterEngine.reset(); m_feedbackHighpass.reset(); m_feedbackHighpass.setCutoffFrequency(60.0f); // just to remove rumble from fb loop m_feedbackHighpass.setResonance(Parameters::defaultFilterQ); m_feedbackCompressor.reset(); m_feedbackL = 0.0f; m_feedbackR = 0.0f; - m_lowCutFreq = -1.0f; - m_lowCutQ = -1.0f; - m_highCutFreq = -1.0f; - m_highCutQ = -1.0f; m_baseDelayTimeMs = -1.0f; m_mixLevel = 0.5f; m_feedbackLevel = 0.0f; @@ -58,31 +52,6 @@ void DelayEngine::setDelayMode(const int modeIndex) } } -void DelayEngine::setLowCut(const Parameters& params) { - setFilterFreq(params.lowCutFreq(), m_lowCutFreq, m_lowCutFilter); - setFilterQ(params.lowCutQ(), m_lowCutQ, m_lowCutFilter); -} - -void DelayEngine::setHighCut(const Parameters& params) { - setFilterFreq(params.highCutFreq(), m_highCutFreq, m_highCutFilter); - setFilterQ(params.highCutQ(), m_highCutQ, m_highCutFilter); -} - -void DelayEngine::setFilterFreq(const float freq, float& currentFreq, Filter& filter) { - if (!juce::approximatelyEqual(freq, currentFreq)) { - filter.setCutoffFrequency(freq); - currentFreq = freq; - } -} - -void DelayEngine::setFilterQ(const float q, float& currentQ, Filter& filter) { - float newQ = std::min(q, Parameters::maxFilterQ); - if (!juce::approximatelyEqual(newQ, currentQ)) { - filter.setResonance(newQ); - currentQ = newQ; - } -} - void DelayEngine::setDelayTimes(const float targetL, const float targetR) { float targetLeftMs = targetL - m_offsetMs; float targetRightMs = targetR + m_offsetMs; @@ -98,14 +67,18 @@ void DelayEngine::setDelayTimes(const float targetL, const float targetR) { void DelayEngine::update(const Parameters& params) { - setLowCut(params); - setHighCut(params); setMixLevel(params.mix()); setGainLevel(params.gain()); setFeedbackLevel(params.feedback()); setWidthLevel(params.stereo()); setOffsetMs(params.offset()); setDelayTimes(params.delayTimeL(), params.delayTimeR()); + m_filterEngine.update(params); +} + +void DelayEngine::updateSyncedModulators(const double ppqPosition) +{ + m_filterEngine.updateLFOs(ppqPosition); } void DelayEngine::processSample(const float& inL, const float& inR, float& outL, float& outR, const Parameters& params) @@ -114,7 +87,7 @@ void DelayEngine::processSample(const float& inL, const float& inR, float& outL, float dryR = inR; float mono = (dryL + dryR) * .5f; - float delayInL = 0.0f, delayInR = 0.0f, popL = 0.0f, popR = 0.0f, wetL = 0.0f, wetR = 0.0f; + float delayInL = 0.0f, delayInR = 0.0f, wetL = 0.0f, wetR = 0.0f; switch (m_delayMode) { case DelayMode::PingPongLR: delayInL = mono + m_feedbackR; @@ -137,17 +110,12 @@ void DelayEngine::processSample(const float& inL, const float& inR, float& outL, delayInR = dryR; } - m_stereoDelay.processSample(delayInL, popL, Channel::Left); - m_stereoDelay.processSample(delayInR, popR, Channel::Right); + m_stereoDelay.processSample(delayInL, wetL, Channel::Left); + m_stereoDelay.processSample(delayInR, wetR, Channel::Right); - m_chorusEngine.processSample(popL, popR, wetL, wetR, params); - - // sculpting filters, first left and then right channel - wetL = m_lowCutFilter.processSample(0, wetL); - wetL = m_highCutFilter.processSample(0, wetL); + m_chorusEngine.processSample(wetL, wetR, wetL, wetR, params); - wetR = m_lowCutFilter.processSample(1, wetR); - wetR = m_highCutFilter.processSample(1, wetR); + m_filterEngine.processSample(wetL, wetR, wetL, wetR); // this removes rumble generated by low cut filter on higher q values wetL = m_feedbackHighpass.processSample(0, wetL); diff --git a/Source/DelayEngine.h b/Source/DelayEngine.h index f7f4004..f360c71 100644 --- a/Source/DelayEngine.h +++ b/Source/DelayEngine.h @@ -3,6 +3,10 @@ #include "Parameters.h" #include "ChorusEngine.h" #include "StereoDelay.h" +#include "FilterEngine.h" +#if JUCE_DEBUG +#include "LFOScopeBuffer.h" +#endif using Filter = juce::dsp::StateVariableTPTFilter; @@ -19,10 +23,6 @@ class DelayEngine void update(const Parameters& params); void processSample(const float& inL, const float& inR, float& outL, float& outR, const Parameters& params); - void setLowCut(const Parameters& params); - void setHighCut(const Parameters& params); - void setFilterFreq(const float freq, float& currentFreq, Filter& filter); - void setFilterQ(const float q, float& currentQ, Filter& filter); void setDelayTimes(const float delayInMsL, const float delayInMsR); void setFeedbackLevel(const float value) { m_feedbackLevel = value; } void setMixLevel(const float value) { m_mixLevel = value; } @@ -30,13 +30,19 @@ class DelayEngine void setWidthLevel(const float value) { m_widthLevel = value; } void setOffsetMs(const float value) { m_offsetMs = value * .5f; } void setDelayMode(const int modeIndex); + void updateSyncedModulators(const double ppqPosition); + + std::atomic& getLowCutLfoValue() { return m_filterEngine.getLowCutLfoValue(); } + std::atomic& getHighCutLfoValue() { return m_filterEngine.getHighCutLfoValue(); } + +#if JUCE_DEBUG + LFOScopeBuffer& getLFOScopeBuffer() { return m_filterEngine.m_scopeBuffer; } +#endif private: - //juce::dsp::DelayLine m_delayLine; StereoDelay m_stereoDelay; DelayMode m_delayMode = DelayMode::Stereo; - Filter m_lowCutFilter; - Filter m_highCutFilter; + FilterEngine m_filterEngine; Filter m_feedbackHighpass; juce::dsp::Compressor m_feedbackCompressor; ChorusEngine m_chorusEngine; @@ -57,10 +63,6 @@ class DelayEngine float m_gainLevel = 1.0f; float m_widthLevel = 1.0f; float m_offsetMs = 0.0f; - float m_lowCutFreq = -1.0f; - float m_lowCutQ = -1.0f; - float m_highCutFreq = -1.0f; - float m_highCutQ = -1.0f; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DelayEngine) }; \ No newline at end of file diff --git a/Source/FilterEngine.cpp b/Source/FilterEngine.cpp new file mode 100644 index 0000000..ad29844 --- /dev/null +++ b/Source/FilterEngine.cpp @@ -0,0 +1,170 @@ +#include "FilterEngine.h" +#include "Sync.h" + +FilterEngine::FilterEngine() +{ + int lowCutSeed = juce::Random::getSystemRandom().nextInt(); + int highCutSeed = juce::Random::getSystemRandom().nextInt(); + for (int i = 0; i < 2; i++) { + m_lowCutLfos[i].setSeed(lowCutSeed); + m_highCutLfos[i].setSeed(highCutSeed); + } +} + +void FilterEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +{ + m_lowCut.prepareToPlay(sampleRate, samplesPerBlock); + m_highCut.prepareToPlay(sampleRate, samplesPerBlock); + for (auto& lfo : m_lowCutLfos) + lfo.prepareToPlay(static_cast(sampleRate)); + for (auto& lfo : m_highCutLfos) + lfo.prepareToPlay(static_cast(sampleRate)); + m_cutoffSmoothCoeff = onePoleLowpassCoeff(5.0f, static_cast(sampleRate)); +} + +void FilterEngine::reset() noexcept +{ + m_lowCut.reset(); + m_highCut.reset(); + for (auto& lfo : m_lowCutLfos) + lfo.reset(); + for (auto& lfo : m_highCutLfos) + lfo.reset(); + m_lowCutBase = -1.0f; + m_lowCutQ = -1.0f; + m_highCutBase = -1.0f; + m_highCutQ = -1.0f; + m_lowCutSmoothed[0] = m_lowCutSmoothed[1] = Parameters::minFilterFreq; + m_highCutSmoothed[0] = m_highCutSmoothed[1] = Parameters::maxFilterFreq; +} + +float FilterEngine::modulatedCutoff(float base, float lfoValue, float modDepth) +{ + return juce::jlimit(Parameters::minFilterFreq, Parameters::maxFilterFreq, base * std::pow(2.0f, lfoValue * modDepth)); +} + +void FilterEngine::setFilterFreq(float freq, float& current) +{ + if (!juce::approximatelyEqual(freq, current)) + current = freq; +} + +void FilterEngine::setFilterQ(float q, float& current, StereoFilter& filter) +{ + float newQ = std::min(q, Parameters::maxFilterQ); + if (!juce::approximatelyEqual(newQ, current)) { + current = newQ; + filter.setQ(current); + } +} + +void FilterEngine::setFilterModDepth(float depth, float& current) +{ + if (!juce::approximatelyEqual(depth, current)) + current = depth; +} + +void FilterEngine::setFilterModRate(float rate, float& current, LFO* lfos) +{ + if (!juce::approximatelyEqual(rate, current)) { + current = rate; + for (int i = 0; i < 2; i++) + lfos[i].setFrequency(current); + } +} + +void FilterEngine::setFilterModPhase(float phase, float& current, LFO* lfos) +{ + if (!juce::approximatelyEqual(phase, current)) { + current = phase; + float normalized = std::fmod(std::fmod(phase, 360.0f) + 360.0f, 360.0f) / 360.0f; + lfos[Channel::Right].setOffset(normalized); + } +} + +void FilterEngine::setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfos) +{ + LFOShape newShape = static_cast(shapeIndex); + if (newShape != current) { + current = newShape; + for (int i = 0; i < 2; i++) + lfos[i].setShape(current); + } +} + +void FilterEngine::setLowCutFilter(const Parameters& params) +{ + setFilterFreq(params.lowCutFreq(), m_lowCutBase); + setFilterQ(params.lowCutQ(), m_lowCutQ, m_lowCut); + setFilterModRate(params.lowCutModRate(), m_lowCutModRate, m_lowCutLfos); + setFilterModDepth(params.lowCutModDepth(), m_lowCutModDepth); + setFilterModPhase(params.lowCutModPhase(), m_lowCutModPhase, m_lowCutLfos); + setFilterModShape(params.lowCutModShape(), m_lowCutModShape, m_lowCutLfos); + m_lowCutModTempoSync = params.lowCutModTempoSync(); + m_lowCutModNote = params.lowCutModNote(); +} + +void FilterEngine::setHighCutFilter(const Parameters& params) +{ + setFilterFreq(params.highCutFreq(), m_highCutBase); + setFilterQ(params.highCutQ(), m_highCutQ, m_highCut); + setFilterModRate(params.highCutModRate(), m_highCutModRate, m_highCutLfos); + setFilterModDepth(params.highCutModDepth(), m_highCutModDepth); + setFilterModPhase(params.highCutModPhase(), m_highCutModPhase, m_highCutLfos); + setFilterModShape(params.highCutModShape(), m_highCutModShape, m_highCutLfos); + m_highCutModTempoSync = params.highCutModTempoSync(); + m_highCutModNote = params.highCutModNote(); +} + +void FilterEngine::update(const Parameters& params) +{ + setLowCutFilter(params); + setHighCutFilter(params); + m_highCutModInvert = params.highCutModInvert(); +} + +void FilterEngine::updateLFOs(const double ppqPosition) noexcept +{ + if (m_lowCutModTempoSync) + { + float phase = getSyncedPhase(m_lowCutModNote, ppqPosition); + m_lowCutLfos[Channel::Left].setPhase(phase); + m_lowCutLfos[Channel::Right].setPhase(phase); + } + if (m_highCutModTempoSync) + { + float phase = getSyncedPhase(m_highCutModNote, ppqPosition); + m_highCutLfos[Channel::Left].setPhase(phase); + m_highCutLfos[Channel::Right].setPhase(phase); + } +} + +void FilterEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) +{ + float lcL = m_lowCutLfos[Channel::Left].process(); + float lcR = m_lowCutLfos[Channel::Right].process(); + float hcL = m_highCutLfos[Channel::Left].process(); + float hcR = m_highCutLfos[Channel::Right].process(); + + hcL = m_highCutModInvert ? -hcL : hcL; + hcR = m_highCutModInvert ? -hcR : hcR; + + m_lowCutLfoValue.store(lcL, std::memory_order_relaxed); + m_highCutLfoValue.store(hcL, std::memory_order_relaxed); + +#if JUCE_DEBUG + if (++m_scopeCounter >= scopeDownsample) { + m_scopeCounter = 0; + m_scopeBuffer.push(lcL, lcR, hcL, hcR); + } +#endif + + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcL, m_lowCutModDepth), m_lowCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_lowCut.setCutoff(onePoleLowpass(modulatedCutoff(m_lowCutBase, lcR, m_lowCutModDepth), m_lowCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcL, m_highCutModDepth), m_highCutSmoothed[Channel::Left], m_cutoffSmoothCoeff), Channel::Left); + m_highCut.setCutoff(onePoleLowpass(modulatedCutoff(m_highCutBase, hcR, m_highCutModDepth), m_highCutSmoothed[Channel::Right], m_cutoffSmoothCoeff), Channel::Right); + + float lowL, lowR; + m_lowCut.processSample(inL, inR, lowL, lowR); + m_highCut.processSample(lowL, lowR, outL, outR); +} diff --git a/Source/FilterEngine.h b/Source/FilterEngine.h new file mode 100644 index 0000000..7597fcc --- /dev/null +++ b/Source/FilterEngine.h @@ -0,0 +1,85 @@ +#pragma once +#include +#include "Parameters.h" +#include "LFO.h" +#include "StereoFilter.h" +#include "Types.h" +#include "DSP.h" +#if JUCE_DEBUG +#include "LFOScopeBuffer.h" +#endif + +class FilterEngine +{ +public: + FilterEngine(); + ~FilterEngine() = default; + + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; + + void update(const Parameters& params); + void processSample(const float& inL, const float& inR, float& outL, float& outR); + + void updateLFOs(const double ppqPosition) noexcept; + + std::atomic& getLowCutLfoValue() { return m_lowCutLfoValue; } + std::atomic& getHighCutLfoValue() { return m_highCutLfoValue; } + +#if JUCE_DEBUG + LFOScopeBuffer m_scopeBuffer; +#endif + +private: + StereoFilter m_lowCut{ FilterType::highpass }; + StereoFilter m_highCut{ FilterType::lowpass }; + juce::Random m_random; + + LFO m_lowCutLfos[2]; + LFO m_highCutLfos[2]; + + float m_lowCutBase = -1.0f; + float m_lowCutQ = -1.0f; + float m_lowCutModDepth = 0.0f; + float m_lowCutModRate = 0.0f; + float m_lowCutModPhase = 0.0f; + LFOShape m_lowCutModShape = LFOShape::Sine; + bool m_lowCutModTempoSync = false; + int m_lowCutModNote = 0; + + float m_highCutBase = -1.0f; + float m_highCutQ = -1.0f; + float m_highCutModDepth = 0.0f; + float m_highCutModRate = 0.0f; + float m_highCutModPhase = 0.0f; + LFOShape m_highCutModShape = LFOShape::Sine; + bool m_highCutModTempoSync = false; + int m_highCutModNote = 0; + + bool m_highCutModInvert = false; + + std::atomic m_lowCutLfoValue { 0.0f }; + std::atomic m_highCutLfoValue { 0.0f }; + + float m_lowCutSmoothed[2] = { Parameters::minFilterFreq, Parameters::minFilterFreq }; + float m_highCutSmoothed[2] = { Parameters::maxFilterFreq, Parameters::maxFilterFreq }; + float m_cutoffSmoothCoeff = 0.0f; + +#if JUCE_DEBUG + static constexpr int scopeDownsample = 64; + int m_scopeCounter = 0; +#endif + + void setLowCutFilter(const Parameters& params); + void setHighCutFilter(const Parameters& params); + + float modulatedCutoff(float base, float lfoValue, float modDepth); + void setFilterFreq(float freq, float& current); + void setFilterQ(float q, float& current, StereoFilter& filter); + void setFilterModDepth(float depth, float& current); + void setFilterModRate(float rate, float& current, LFO* lfos); + void setFilterModPhase(float phase, float& current, LFO* lfos); + void setFilterModShape(int shapeIndex, LFOShape& current, LFO* lfos); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(FilterEngine) +}; diff --git a/Source/LFO.cpp b/Source/LFO.cpp index d63d220..e577899 100644 --- a/Source/LFO.cpp +++ b/Source/LFO.cpp @@ -29,6 +29,11 @@ void LFO::setShape(LFOShape shape) { m_shape = shape; } +void LFO::setPhase(float phase) { + m_phase = std::fmod(phase, 1.0f); + m_lastOffsetPhase = std::fmod(m_phase + m_offset, 1.0f); +} + void LFO::setIncrement() { if (m_sampleRate > 0.0f) { m_increment = m_frequency / m_sampleRate; @@ -40,12 +45,22 @@ void LFO::setIncrement() { float LFO::process() { m_phase += m_increment; m_phase = std::fmod(m_phase, 1.0f); + m_nextSample = (m_phase < m_lastPhase) ? m_random.nextFloat() * 2.0f - 1.0f : m_nextSample; + m_lastPhase = m_phase; float offsetPhase = std::fmod(m_phase + m_offset, 1.0f); switch (m_shape) { case (LFOShape::Sine): return processSine(offsetPhase); case (LFOShape::Triangle): return processTriangle(offsetPhase); + case (LFOShape::Square): + return processSquare(offsetPhase); + case (LFOShape::SawDown): + return processSawDown(offsetPhase); + case (LFOShape::SawUp): + return processSawUp(offsetPhase); + case (LFOShape::SampleAndHold): + return processSampleAndHold(offsetPhase); default: return 0.0f; } @@ -57,4 +72,22 @@ float LFO::processSine(float phase) { float LFO::processTriangle(float phase) { return 1.0f - 4.0f * std::abs(phase - 0.5f); +} + +float LFO::processSquare(float phase) { + return (phase < 0.5f) ? 1.0f : -1.0f; +} + +float LFO::processSawUp(float phase) { + return phase * 2.0f - 1.0f; +} + +float LFO::processSawDown(float phase) { + return phase * -2.0f + 1.0f; +} + +float LFO::processSampleAndHold(float phase) { + float out = (phase < m_lastOffsetPhase) ? m_currentSample = m_nextSample : m_currentSample; + m_lastOffsetPhase = phase; + return out; } \ No newline at end of file diff --git a/Source/LFO.h b/Source/LFO.h index 9e91150..e827f5c 100644 --- a/Source/LFO.h +++ b/Source/LFO.h @@ -1,7 +1,7 @@ #pragma once #include +#include "Types.h" -enum class LFOShape { Sine, Triangle }; class LFO { public: LFO(float frequency = 1.0f, float offset = 0.0f, LFOShape shape = LFOShape::Sine); @@ -12,10 +12,13 @@ class LFO { void setFrequency(float frequency); void setOffset(float offset); void setShape(LFOShape shape); + void setPhase(float phase); + void setSeed(int seed) { m_random.setSeed(seed); } float process(); private: - LFOShape m_shape = LFOShape::Sine; + LFOShape m_shape; + juce::Random m_random; float m_sampleRate = 0.0f; @@ -23,8 +26,17 @@ class LFO { float m_phase = 0.0f; float m_offset = 0.0f; float m_increment = 0.0f; + float m_lastPhase = 0.0f; + float m_lastOffsetPhase = 0.0f; + float m_currentSample = 0.0f; + float m_nextSample = 0.0f; + void setIncrement(); float processSine(float phase); float processTriangle(float phase); + float processSquare(float phase); + float processSawUp(float phase); + float processSawDown(float phase); + float processSampleAndHold(float phase); }; \ No newline at end of file diff --git a/Source/LFOIndicator.h b/Source/LFOIndicator.h new file mode 100644 index 0000000..c84a6fd --- /dev/null +++ b/Source/LFOIndicator.h @@ -0,0 +1,35 @@ +#pragma once +#include + +class LFOIndicator : public juce::Component, private juce::Timer +{ +public: + LFOIndicator(std::atomic& value) : m_value(value) + { + startTimerHz(60); + } + + ~LFOIndicator() override { stopTimer(); } + + void paint(juce::Graphics& g) override + { + auto bounds = getLocalBounds().toFloat().reduced(1.0f); + float v = m_value.load(); + juce::Colour fill; + if (v >= 0.0f) + fill = juce::Colours::black.interpolatedWith(juce::Colours::green, v); + else + fill = juce::Colours::black.interpolatedWith(juce::Colours::red, -v); + + g.setColour(juce::Colours::darkgrey.darker(0.5f)); + g.drawEllipse(bounds, 1.0f); + g.setColour(fill); + g.fillEllipse(bounds.reduced(1.0f)); + } + +private: + std::atomic& m_value; + void timerCallback() override { repaint(); } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOIndicator) +}; diff --git a/Source/LFOScope.cpp b/Source/LFOScope.cpp new file mode 100644 index 0000000..24c472e --- /dev/null +++ b/Source/LFOScope.cpp @@ -0,0 +1,71 @@ +#include "LFOScope.h" + +LFOScope::LFOScope(LFOScopeBuffer& buffer) : m_buffer(buffer) +{ + startTimerHz(s_refreshRate); +} + +LFOScope::~LFOScope() +{ + stopTimer(); +} + +void LFOScope::resized() {} + +void LFOScope::paint(juce::Graphics& g) +{ + auto bounds = getLocalBounds(); + int w = bounds.getWidth(); + int h = bounds.getHeight(); + + g.fillAll(juce::Colour(0xff1a1a1a)); + + g.setColour(juce::Colour(0xff333333)); + g.drawHorizontalLine(h / 2, 0.0f, (float)w); + g.drawHorizontalLine(h / 4, 0.0f, (float)w); + g.drawHorizontalLine(3 * h / 4, 0.0f, (float)w); + g.drawRect(bounds); + + int numSamples = std::min(w, LFOScopeBuffer::size); + int writeIdx = m_buffer.writeIndex.load(std::memory_order_acquire); + + const juce::Colour colours[LFOScopeBuffer::numChannels] = { + juce::Colour(0xff4fc3f7), // lowcut L — light blue + juce::Colour(0xff0288d1), // lowcut R — dark blue + juce::Colour(0xffffb74d), // highcut L — amber + juce::Colour(0xfff57c00), // highcut R — dark amber + }; + + const char* labels[LFOScopeBuffer::numChannels] = { + "LC L", "LC R", "HC L", "HC R" + }; + + for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) + drawChannel(g, ch, colours[ch], writeIdx, numSamples); + + g.setFont(11.0f); + for (int ch = 0; ch < LFOScopeBuffer::numChannels; ++ch) { + g.setColour(colours[ch]); + g.drawText(labels[ch], 4 + ch * 48, 4, 44, 14, juce::Justification::left); + } +} + +void LFOScope::drawChannel(juce::Graphics& g, int channel, juce::Colour colour, int writeIdx, int numSamples) +{ + int h = getHeight(); + juce::Path path; + + for (int x = 0; x < numSamples; ++x) { + int bufIdx = (writeIdx - numSamples + x + LFOScopeBuffer::size) & (LFOScopeBuffer::size - 1); + float value = m_buffer.data[channel][bufIdx]; + float y = juce::jmap(value, -1.0f, 1.0f, (float)(h - 1), 0.0f); + + if (x == 0) + path.startNewSubPath(0.0f, y); + else + path.lineTo((float)x, y); + } + + g.setColour(colour); + g.strokePath(path, juce::PathStrokeType(1.5f)); +} diff --git a/Source/LFOScope.h b/Source/LFOScope.h new file mode 100644 index 0000000..bc239e7 --- /dev/null +++ b/Source/LFOScope.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include "LFOScopeBuffer.h" + +class LFOScope : public juce::Component, private juce::Timer +{ +public: + LFOScope(LFOScopeBuffer& buffer); + ~LFOScope() override; + + void paint(juce::Graphics&) override; + void resized() override; + +private: + LFOScopeBuffer& m_buffer; + + static constexpr int s_refreshRate = 30; + + void timerCallback() override { repaint(); } + void drawChannel(juce::Graphics& g, int channel, juce::Colour colour, int writeIdx, int numSamples); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LFOScope) +}; diff --git a/Source/LFOScopeBuffer.h b/Source/LFOScopeBuffer.h new file mode 100644 index 0000000..3002e03 --- /dev/null +++ b/Source/LFOScopeBuffer.h @@ -0,0 +1,19 @@ +#pragma once +#include + +struct LFOScopeBuffer { + static constexpr int numChannels = 4; + static constexpr int size = 4096; + + float data[numChannels][size] = {}; + std::atomic writeIndex { 0 }; + + void push(float lcL, float lcR, float hcL, float hcR) noexcept { + int idx = writeIndex.load(std::memory_order_relaxed); + data[0][idx] = lcL; + data[1][idx] = lcR; + data[2][idx] = hcL; + data[3][idx] = hcR; + writeIndex.store((idx + 1) & (size - 1), std::memory_order_release); + } +}; diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 3f4c9e9..66f181d 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -33,7 +33,7 @@ static juce::String stringFromPercent(float value, int) static juce::String stringFromHertz(float value, int) { - if (value < 10) { + if (value < 20) { return juce::String(value, 2) + " Hz"; } if (value < 1000.0f) { @@ -47,6 +47,11 @@ static juce::String stringFromHertz(float value, int) } } +static juce::String stringFromDegrees(float value, int) +{ + return juce::String(int(value)) + "°"; +} + static float millisecondsFromString(const juce::String& text) { float value = text.getFloatValue(); @@ -68,6 +73,11 @@ static float hzFromString(const juce::String& text) return value; } +static float degreesFromString(const juce::String& text) +{ + return text.getFloatValue(); +} + const juce::StringArray Parameters::delayModes = { "Stereo", "Cross Fb", @@ -75,7 +85,7 @@ const juce::StringArray Parameters::delayModes = { "Ping Pong RL", }; -const juce::StringArray Parameters::noteLengths = { +const juce::StringArray Parameters::delayNoteLengths = { "1/32", "1/16 trip", "1/32 dot", @@ -94,6 +104,33 @@ const juce::StringArray Parameters::noteLengths = { "1/1", }; +const juce::StringArray Parameters::modNoteLengths = { + "1/8", + "1/8 dot", + "1/4", + "1/4 dot", + "1/2", + "1/2 dot", + "1/1", + "1/1 dot", + "2/1", + "3/1", + "4/1", + "5/1", + "6/1", + "7/1", + "8/1", +}; + +const juce::StringArray Parameters::lfoShapes = { + "SIN", + "TRI", + "SQR", + "SW+", + "SW-", + "S&H", +}; + Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) { @@ -112,8 +149,21 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) castParameter(apvts, chorusIntensityParamID, m_chorusIntensityParam); castParameter(apvts, lowCutFreqParamID, m_lowCutFreqParam); castParameter(apvts, lowCutQParamID, m_lowCutQParam); + castParameter(apvts, lowCutModRateParamID, m_lowCutModRateParam); + castParameter(apvts, lowCutModDepthParamID, m_lowCutModDepthParam); + castParameter(apvts, lowCutModPhaseParamID, m_lowCutModPhaseParam); + castParameter(apvts, lowCutModTempoSyncParamID, m_lowCutModTempoSyncParam); + castParameter(apvts, lowCutModNoteParamID, m_lowCutModNoteParam); + castParameter(apvts, lowCutModShapeParamID, m_lowCutModShapeParam); castParameter(apvts, highCutFreqParamID, m_highCutFreqParam); castParameter(apvts, highCutQParamID, m_highCutQParam); + castParameter(apvts, highCutModRateParamID, m_highCutModRateParam); + castParameter(apvts, highCutModDepthParamID, m_highCutModDepthParam); + castParameter(apvts, highCutModPhaseParamID, m_highCutModPhaseParam); + castParameter(apvts, highCutModTempoSyncParamID, m_highCutModTempoSyncParam); + castParameter(apvts, highCutModNoteParamID, m_highCutModNoteParam); + castParameter(apvts, highCutModShapeParamID, m_highCutModShapeParam); + castParameter(apvts, highCutModInvertParamID, m_highCutModInvertParam); castParameter(apvts, chorusModRateParamID, m_chorusModRateParam); castParameter(apvts, chorusModDepthParamID, m_chorusModDepthParam); } @@ -148,13 +198,13 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL layout.add(std::make_unique( delayNoteLParamID, "Delay Note L", - noteLengths, + delayNoteLengths, 9 )); layout.add(std::make_unique( delayNoteRParamID, "Delay Note R", - noteLengths, + delayNoteLengths, 9 )); layout.add(std::make_unique( @@ -223,6 +273,47 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL juce::NormalisableRange(minFilterQ, maxFilterQ, 0.01f), defaultFilterQ )); + layout.add(std::make_unique( + lowCutModRateParamID.getParamID(), + "Low Cut Mod Rate", + juce::NormalisableRange(0.01f, 8.0f, 0.001f, 0.3f), + 0.05f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromHertz) + .withValueFromStringFunction(hzFromString) + )); + layout.add(std::make_unique( + lowCutModNoteParamID, + "Low Cut Mod Note", + modNoteLengths, + 6 + )); + layout.add(std::make_unique( + lowCutModTempoSyncParamID.getParamID(), + "Low Cut Mod Tempo Sync", + false + )); + layout.add(std::make_unique( + lowCutModDepthParamID.getParamID(), + "Low Cut Mod Depth", + juce::NormalisableRange(0.0f, 2.0f, 0.01f), + 0.0f + )); + layout.add(std::make_unique( + lowCutModPhaseParamID.getParamID(), + "Low Cut Mod Phase", + juce::NormalisableRange(0.0f, 360.0f, 0.1f), + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromDegrees) + .withValueFromStringFunction(degreesFromString) + )); + layout.add(std::make_unique( + lowCutModShapeParamID, + "Low Cut Mod Shape", + lfoShapes, + 0 + )); layout.add(std::make_unique( highCutFreqParamID.getParamID(), "High Cut Frequency", @@ -238,6 +329,52 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL juce::NormalisableRange(minFilterQ, maxFilterQ, 0.01f), defaultFilterQ )); + layout.add(std::make_unique( + highCutModRateParamID.getParamID(), + "High Cut Mod Rate", + juce::NormalisableRange(0.01f, 8.0f, 0.001f, 0.3f), + 0.05f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromHertz) + .withValueFromStringFunction(hzFromString) + )); + layout.add(std::make_unique( + highCutModNoteParamID, + "High Cut Mod Note", + modNoteLengths, + 6 + )); + layout.add(std::make_unique( + highCutModShapeParamID, + "High Cut Mod Shape", + lfoShapes, + 0 + )); + layout.add(std::make_unique( + highCutModTempoSyncParamID.getParamID(), + "High Cut Mod Tempo Sync", + false + )); + layout.add(std::make_unique( + highCutModDepthParamID.getParamID(), + "High Cut Mod Depth", + juce::NormalisableRange(0.0f, 2.0f, 0.01f), + 0.0f + )); + layout.add(std::make_unique( + highCutModPhaseParamID.getParamID(), + "High Cut Mod Phase", + juce::NormalisableRange(0.0f, 360.0f, 0.1f), + 0.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromDegrees) + .withValueFromStringFunction(degreesFromString) + )); + layout.add(std::make_unique( + highCutModInvertParamID.getParamID(), + "High Cut Mod Invert", + true + )); layout.add(std::make_unique( chorusIntensityParamID.getParamID(), "Chorus Intensity", @@ -270,8 +407,14 @@ void Parameters::prepareToPlay(double sampleRate) noexcept m_chorusIntensitySmoother.reset(sampleRate, duration); m_lowCutFreqSmoother.reset(sampleRate, duration); m_lowCutQSmoother.reset(sampleRate, duration); + m_lowCutModRateSmoother.reset(sampleRate, duration); + m_lowCutModDepthSmoother.reset(sampleRate, duration); + m_lowCutModPhaseSmoother.reset(sampleRate, duration); m_highCutFreqSmoother.reset(sampleRate, duration); m_highCutQSmoother.reset(sampleRate, duration); + m_highCutModRateSmoother.reset(sampleRate, duration); + m_highCutModDepthSmoother.reset(sampleRate, duration); + m_highCutModPhaseSmoother.reset(sampleRate, duration); m_chorusModRateSmoother.reset(sampleRate, duration); m_chorusModDepthSmoother.reset(sampleRate, duration); } @@ -288,8 +431,14 @@ void Parameters::reset() noexcept m_chorusIntensity = 0.0f; m_lowCutFreq = 20.0f; m_lowCutQ = defaultFilterQ; + m_lowCutModRate = 0.0f; + m_lowCutModDepth = 0.0f; + m_lowCutModPhase = 0.0f; m_highCutFreq = 20000.0f; m_highCutQ = defaultFilterQ; + m_highCutModRate = 0.0f; + m_highCutModDepth = 0.0f; + m_highCutModPhase = 0.0f; m_chorusModRate = defaultChorusModRate; m_chorusModDepth = defaultChorusModDepth; m_gainSmoother.setCurrentAndTargetValue(juce::Decibels::decibelsToGain(m_gainParam->get())); @@ -299,8 +448,14 @@ void Parameters::reset() noexcept m_chorusIntensitySmoother.setCurrentAndTargetValue(m_chorusIntensityParam->get()); m_lowCutFreqSmoother.setCurrentAndTargetValue(m_lowCutFreqParam->get()); m_lowCutQSmoother.setCurrentAndTargetValue(m_lowCutQParam->get()); + m_lowCutModRateSmoother.setCurrentAndTargetValue(m_lowCutModRateParam->get()); + m_lowCutModDepthSmoother.setCurrentAndTargetValue(m_lowCutModDepthParam->get()); + m_lowCutModPhaseSmoother.setCurrentAndTargetValue(m_lowCutModPhaseParam->get()); m_highCutFreqSmoother.setCurrentAndTargetValue(m_highCutFreqParam->get()); m_highCutQSmoother.setCurrentAndTargetValue(m_highCutQParam->get()); + m_highCutModRateSmoother.setCurrentAndTargetValue(m_highCutModRateParam->get()); + m_highCutModDepthSmoother.setCurrentAndTargetValue(m_highCutModDepthParam->get()); + m_highCutModPhaseSmoother.setCurrentAndTargetValue(m_highCutModPhaseParam->get()); m_chorusModRateSmoother.setCurrentAndTargetValue(m_chorusModRateParam->get()); m_chorusModDepthSmoother.setCurrentAndTargetValue(m_chorusModDepthParam->get()); } @@ -315,8 +470,14 @@ void Parameters::update(const Tempo& tempo) noexcept m_chorusIntensitySmoother.setTargetValue(m_chorusIntensityParam->get()); m_lowCutFreqSmoother.setTargetValue(m_lowCutFreqParam->get()); m_lowCutQSmoother.setTargetValue(m_lowCutQParam->get()); + m_lowCutModRateSmoother.setTargetValue(m_lowCutModRateParam->get()); + m_lowCutModDepthSmoother.setTargetValue(m_lowCutModDepthParam->get()); + m_lowCutModPhaseSmoother.setTargetValue(m_lowCutModPhaseParam->get()); m_highCutFreqSmoother.setTargetValue(m_highCutFreqParam->get()); m_highCutQSmoother.setTargetValue(m_highCutQParam->get()); + m_highCutModRateSmoother.setTargetValue(m_highCutModRateParam->get()); + m_highCutModDepthSmoother.setTargetValue(m_highCutModDepthParam->get()); + m_highCutModPhaseSmoother.setTargetValue(m_highCutModPhaseParam->get()); m_chorusModRateSmoother.setTargetValue(m_chorusModRateParam->get()); m_chorusModDepthSmoother.setTargetValue(m_chorusModDepthParam->get()); m_tempoSyncL = m_tempoSyncLParam->get(); @@ -324,6 +485,21 @@ void Parameters::update(const Tempo& tempo) noexcept m_delayNoteL = m_delayNoteLParam->getIndex(); m_delayNoteR = m_delayNoteRParam->getIndex(); m_delayMode = m_delayModeParam->getIndex(); + m_lowCutModTempoSync = m_lowCutModTempoSyncParam->get(); + m_lowCutModNote = m_lowCutModNoteParam->getIndex(); + m_highCutModTempoSync = m_highCutModTempoSyncParam->get(); + m_highCutModNote = m_highCutModNoteParam->getIndex(); + m_highCutModInvert = m_highCutModInvertParam->get(); + m_lowCutModShape = m_lowCutModShapeParam->getIndex(); + m_highCutModShape = m_highCutModShapeParam->getIndex(); + if (m_lowCutModTempoSync) { + float syncedHz = float(tempo.noteLengthToHz(m_lowCutModNote)); + m_lowCutModRateSmoother.setTargetValue(syncedHz); + } + if (m_highCutModTempoSync) { + float syncedHz = float(tempo.noteLengthToHz(m_highCutModNote)); + m_highCutModRateSmoother.setTargetValue(syncedHz); + } if (m_tempoSyncL) { float syncedMs = juce::jmin(float(tempo.noteLengthToMs(m_delayNoteL)), maxDelayTime); m_delayTimeLParam->setValueNotifyingHost(m_delayTimeLParam->convertTo0to1(syncedMs)); @@ -346,8 +522,14 @@ void Parameters::smoothen() noexcept m_chorusIntensity = m_chorusIntensitySmoother.getNextValue(); m_lowCutFreq = m_lowCutFreqSmoother.getNextValue(); m_lowCutQ = m_lowCutQSmoother.getNextValue(); + m_lowCutModRate = m_lowCutModRateSmoother.getNextValue(); + m_lowCutModDepth = m_lowCutModDepthSmoother.getNextValue(); + m_lowCutModPhase = m_lowCutModPhaseSmoother.getNextValue(); m_highCutFreq = m_highCutFreqSmoother.getNextValue(); m_highCutQ = m_highCutQSmoother.getNextValue(); + m_highCutModRate = m_highCutModRateSmoother.getNextValue(); + m_highCutModDepth = m_highCutModDepthSmoother.getNextValue(); + m_highCutModPhase = m_highCutModPhaseSmoother.getNextValue(); m_chorusModRate = m_chorusModRateSmoother.getNextValue(); m_chorusModDepth = m_chorusModDepthSmoother.getNextValue(); } \ No newline at end of file diff --git a/Source/Parameters.h b/Source/Parameters.h index b0b40e1..f9dfbc4 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -16,8 +16,21 @@ const juce::ParameterID stereoParamID{ "stereo", 1 }; const juce::ParameterID offsetParamID{ "offset", 1 }; const juce::ParameterID lowCutFreqParamID{ "lowCutFreq", 1 }; const juce::ParameterID lowCutQParamID{ "lowCutQ", 1 }; +const juce::ParameterID lowCutModRateParamID{ "lowCutModRate", 1 }; +const juce::ParameterID lowCutModDepthParamID{ "lowCutModDepth", 1 }; +const juce::ParameterID lowCutModPhaseParamID{ "lowCutModPhase", 1 }; +const juce::ParameterID lowCutModTempoSyncParamID{ "lowCutModTempoSync", 1 }; +const juce::ParameterID lowCutModNoteParamID{ "lowCutModNote", 1 }; +const juce::ParameterID lowCutModShapeParamID{ "lowCutModShape", 1 }; const juce::ParameterID highCutFreqParamID{ "highCutFreq", 1 }; const juce::ParameterID highCutQParamID{ "highCutQ", 1 }; +const juce::ParameterID highCutModRateParamID{ "highCutModRate", 1 }; +const juce::ParameterID highCutModDepthParamID{ "highCutModDepth", 1 }; +const juce::ParameterID highCutModPhaseParamID{ "highCutModPhase", 1 }; +const juce::ParameterID highCutModTempoSyncParamID{ "highCutModTempoSync", 1 }; +const juce::ParameterID highCutModNoteParamID{ "highCutModNote", 1 }; +const juce::ParameterID highCutModShapeParamID{ "highCutModShape", 1 }; +const juce::ParameterID highCutModInvertParamID{ "highCutModInvert", 1 }; const juce::ParameterID chorusIntensityParamID{ "chorusIntensity", 1 }; const juce::ParameterID chorusModRateParamID{ "chorusModRate", 1 }; const juce::ParameterID chorusModDepthParamID{ "chorusModDepth", 1 }; @@ -30,14 +43,18 @@ class Parameters { static constexpr float minDelayTime = 5.0f; static constexpr float maxDelayTime = 5000.0f; static constexpr float defaultDelayTime = 100.0f; - static constexpr float maxFilterQ = 2.0f; + static constexpr float minFilterFreq = 20.0f; + static constexpr float maxFilterFreq = 20000.0f; + static constexpr float maxFilterQ = 1.5f; static constexpr float minFilterQ = 0.1f; static constexpr float defaultFilterQ = 0.707f; static constexpr float defaultChorusModRate = 0.2f; static constexpr float defaultChorusModRate2 = 0.45f; // this is not a passed param but default value for 2nd chorus static constexpr float defaultChorusModDepth = 4.0f; static const juce::StringArray delayModes; - static const juce::StringArray noteLengths; + static const juce::StringArray delayNoteLengths; + static const juce::StringArray modNoteLengths; + static const juce::StringArray lfoShapes; void prepareToPlay(double sampleRate) noexcept; void update(const Tempo& tempo) noexcept; @@ -58,8 +75,21 @@ class Parameters { float offset() const { return m_offset; } float lowCutFreq() const { return m_lowCutFreq; } float lowCutQ() const { return m_lowCutQ; } + float lowCutModRate() const { return m_lowCutModRate; } + float lowCutModDepth() const { return m_lowCutModDepth; } + float lowCutModPhase() const { return m_lowCutModPhase; } + bool lowCutModTempoSync() const { return m_lowCutModTempoSync; } + int lowCutModNote() const { return m_lowCutModNote; } + int lowCutModShape() const { return m_lowCutModShape; } float highCutFreq() const { return m_highCutFreq; } float highCutQ() const { return m_highCutQ; } + float highCutModRate() const { return m_highCutModRate; } + float highCutModDepth() const { return m_highCutModDepth; } + float highCutModPhase() const { return m_highCutModPhase; } + bool highCutModTempoSync() const { return m_highCutModTempoSync; } + int highCutModNote() const { return m_highCutModNote; } + int highCutModShape() const { return m_highCutModShape; } + bool highCutModInvert() const { return m_highCutModInvert; } float chorusIntensity() const { return m_chorusIntensity; } float chorusModRate() const { return m_chorusModRate; } float chorusModDepth() const { return m_chorusModDepth; } @@ -79,8 +109,21 @@ class Parameters { juce::AudioParameterFloat* m_offsetParam; juce::AudioParameterFloat* m_lowCutFreqParam; juce::AudioParameterFloat* m_lowCutQParam; + juce::AudioParameterFloat* m_lowCutModRateParam; + juce::AudioParameterFloat* m_lowCutModDepthParam; + juce::AudioParameterFloat* m_lowCutModPhaseParam; + juce::AudioParameterBool* m_lowCutModTempoSyncParam; + juce::AudioParameterChoice* m_lowCutModNoteParam; + juce::AudioParameterChoice* m_lowCutModShapeParam; juce::AudioParameterFloat* m_highCutFreqParam; juce::AudioParameterFloat* m_highCutQParam; + juce::AudioParameterFloat* m_highCutModRateParam; + juce::AudioParameterFloat* m_highCutModDepthParam; + juce::AudioParameterFloat* m_highCutModPhaseParam; + juce::AudioParameterBool* m_highCutModTempoSyncParam; + juce::AudioParameterChoice* m_highCutModNoteParam; + juce::AudioParameterChoice* m_highCutModShapeParam; + juce::AudioParameterBool* m_highCutModInvertParam; juce::AudioParameterFloat* m_chorusIntensityParam; juce::AudioParameterFloat* m_chorusModRateParam; juce::AudioParameterFloat* m_chorusModDepthParam; @@ -92,17 +135,18 @@ class Parameters { juce::LinearSmoothedValue m_offsetSmoother; juce::LinearSmoothedValue m_lowCutFreqSmoother; juce::LinearSmoothedValue m_lowCutQSmoother; + juce::LinearSmoothedValue m_lowCutModRateSmoother; + juce::LinearSmoothedValue m_lowCutModDepthSmoother; + juce::LinearSmoothedValue m_lowCutModPhaseSmoother; juce::LinearSmoothedValue m_highCutFreqSmoother; juce::LinearSmoothedValue m_highCutQSmoother; + juce::LinearSmoothedValue m_highCutModRateSmoother; + juce::LinearSmoothedValue m_highCutModDepthSmoother; + juce::LinearSmoothedValue m_highCutModPhaseSmoother; juce::LinearSmoothedValue m_chorusIntensitySmoother; juce::LinearSmoothedValue m_chorusModRateSmoother; juce::LinearSmoothedValue m_chorusModDepthSmoother; - float m_targetDelayTimeL = 0.0f; - float m_targetDelayTimeR = 0.0f; - - float m_coeff = 0.0f; - float m_gain = 0.0f; float m_delayTimeL = 0.0f; float m_delayTimeR = 0.0f; @@ -117,8 +161,21 @@ class Parameters { float m_offset = 0.0f; float m_lowCutFreq = 20.0f; float m_lowCutQ = defaultFilterQ; + float m_lowCutModRate = 0.0f; + float m_lowCutModDepth = 0.0f; + float m_lowCutModPhase = 0.0f; + bool m_lowCutModTempoSync = false; + int m_lowCutModNote = 0; + int m_lowCutModShape = 0; float m_highCutFreq = 20000.0f; float m_highCutQ = defaultFilterQ; + float m_highCutModRate = 0.0f; + float m_highCutModDepth = 0.0f; + float m_highCutModPhase = 0.0f; + bool m_highCutModTempoSync = false; + int m_highCutModNote = 0; + int m_highCutModShape = 0; + bool m_highCutModInvert = false; float m_chorusIntensity = 0.0f; float m_chorusModRate = defaultChorusModRate; float m_chorusModDepth = defaultChorusModDepth; diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp index a8bd991..e03d99d 100644 --- a/Source/PluginEditor.cpp +++ b/Source/PluginEditor.cpp @@ -17,21 +17,31 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) m_delayGroup.addAndMakeVisible(m_offsetKnob); m_delayGroup.addAndMakeVisible(m_tempoSyncLButton); m_delayGroup.addAndMakeVisible(m_tempoSyncRButton); - addAndMakeVisible(m_delayModeLabel); - m_delayModeLabel.setText("Delay Mode:", juce::NotificationType::dontSendNotification); - m_delayModeLabel.setEditable(false); - m_delayModeLabel.setSize(80, 25); addAndMakeVisible(m_modeSelector); - m_modeSelector.addItemList(Parameters::delayModes, 1); - m_modeSelector.setEditableText(false); - m_modeSelector.setSize(130, 25); - m_modeSelector.onChange = [this] { modeSelectorChanged();}; - auto* param = dynamic_cast(m_audioProcessor.apvts.getParameter(delayModeParamID.getParamID())); - m_modeSelector.setSelectedId(param->getIndex() + 1, juce::dontSendNotification); addAndMakeVisible(m_delayGroup); - m_filterGroup.setText("Filter"); + m_filterModGroup.setText("Modulation"); + m_filterModGroup.setTextLabelPosition(juce::Justification::horizontallyCentred); + m_filterModGroup.setColour(juce::GroupComponent::outlineColourId, juce::Colours::transparentBlack); + m_filterModGroup.addAndMakeVisible(m_lowCutModRateKnob); + m_filterModGroup.addChildComponent(m_lowCutModNoteKnob); + m_filterModGroup.addAndMakeVisible(m_lowCutModDepthKnob); + m_filterModGroup.addAndMakeVisible(m_lowCutModPhaseKnob); + m_filterModGroup.addAndMakeVisible(m_highCutModRateKnob); + m_filterModGroup.addChildComponent(m_highCutModNoteKnob); + m_filterModGroup.addAndMakeVisible(m_highCutModDepthKnob); + m_filterModGroup.addAndMakeVisible(m_highCutModPhaseKnob); + m_filterModGroup.addAndMakeVisible(m_lowCutModTempoSyncButton); + m_filterModGroup.addAndMakeVisible(m_highCutModTempoSyncButton); + m_filterModGroup.addAndMakeVisible(m_lowCutModShapeSelector); + m_filterModGroup.addAndMakeVisible(m_highCutModShapeSelector); + m_filterModGroup.addAndMakeVisible(m_lowCutLfoIndicator); + m_filterModGroup.addAndMakeVisible(m_highCutLfoIndicator); + m_filterModGroup.addAndMakeVisible(m_highCutModInvertButton); + + m_filterGroup.setText("Filter"); m_filterGroup.setTextLabelPosition(juce::Justification::horizontallyCentred); + m_filterGroup.addAndMakeVisible(m_filterModGroup); m_filterGroup.addAndMakeVisible(m_lowCutFreqKnob); m_filterGroup.addAndMakeVisible(m_lowCutQKnob); m_filterGroup.addAndMakeVisible(m_highCutFreqKnob); @@ -54,20 +64,35 @@ ViiveAudioProcessorEditor::ViiveAudioProcessorEditor(ViiveAudioProcessor& p) addAndMakeVisible(m_meter); +#if JUCE_DEBUG + m_lfoScope = std::make_unique(p.getLFOScopeBuffer()); + addAndMakeVisible(*m_lfoScope); +#endif + bool syncL = m_audioProcessor.apvts.getParameter(tempoSyncLParamID.getParamID())->getValue() > 0.5f; bool syncR = m_audioProcessor.apvts.getParameter(tempoSyncRParamID.getParamID())->getValue() > 0.5f; + bool lowCutSync = m_audioProcessor.apvts.getParameter(lowCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; + bool highCutSync = m_audioProcessor.apvts.getParameter(highCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; - updateDelayKnobs(syncL, syncR); + updateSyncedKnobs(syncL, syncR, lowCutSync, highCutSync); m_audioProcessor.apvts.addParameterListener(tempoSyncLParamID.getParamID(), this); m_audioProcessor.apvts.addParameterListener(tempoSyncRParamID.getParamID(), this); - setSize (770, 370); + m_audioProcessor.apvts.addParameterListener(lowCutModTempoSyncParamID.getParamID(), this); + m_audioProcessor.apvts.addParameterListener(highCutModTempoSyncParamID.getParamID(), this); +#if JUCE_DEBUG + setSize(770, 670); +#else + setSize(770, 490); +#endif } ViiveAudioProcessorEditor::~ViiveAudioProcessorEditor() { m_audioProcessor.apvts.removeParameterListener(tempoSyncLParamID.getParamID(), this); m_audioProcessor.apvts.removeParameterListener(tempoSyncRParamID.getParamID(), this); + m_audioProcessor.apvts.removeParameterListener(lowCutModTempoSyncParamID.getParamID(), this); + m_audioProcessor.apvts.removeParameterListener(highCutModTempoSyncParamID.getParamID(), this); } //============================================================================== @@ -77,20 +102,24 @@ void ViiveAudioProcessorEditor::paint (juce::Graphics& g) g.fillAll(juce::Colours::darkgrey); g.setColour(juce::Colours::white); g.setFont(12.0f); - g.drawText(JucePlugin_VersionString, getLocalBounds().reduced(5), juce::Justification::topRight); + g.drawText("viive " JucePlugin_VersionString, getLocalBounds().reduced(5), juce::Justification::topRight); } void ViiveAudioProcessorEditor::resized() { auto bounds = getLocalBounds(); int y = 10; +#ifdef JUCE_DEBUG + int height = bounds.getHeight() - 30 - 180; +#else int height = bounds.getHeight() - 20; - - m_chorusGroup.setBounds(bounds.getWidth() - 360, y + 35, 300, height / 2 - 10); - m_delayGroup.setBounds(10, y + 35, m_chorusGroup.getX() - 20, height / 2 - 10); - m_outputGroup.setBounds(bounds.getWidth() - 360, height / 2 + 45, 300, height / 2 - 30); - m_filterGroup.setBounds(10, height / 2 + 45, m_outputGroup.getX() - 20, height / 2 - 30); - +#endif + m_chorusGroup.setBounds(bounds.getWidth() - 360, y + 35, 300, 165); + m_delayGroup.setBounds(10, y + 35, m_chorusGroup.getX() - 20, 165); + m_outputGroup.setBounds(bounds.getWidth() - 360, m_delayGroup.getBottom() + 10, 300, 155); + m_filterGroup.setBounds(10, m_delayGroup.getBottom() + 10, m_outputGroup.getX() - 20, 265); + m_filterModGroup.setBounds(0, m_lowCutFreqKnob.getBottom() + 30, m_filterGroup.getWidth(), 120); + m_delayTimeLKnob.setTopLeftPosition(20, 20); m_delayTimeRKnob.setTopLeftPosition(m_delayTimeLKnob.getRight() + 20, 20); m_delayNoteLKnob.setTopLeftPosition(m_delayTimeLKnob.getX(), m_delayTimeLKnob.getY()); @@ -99,14 +128,36 @@ void ViiveAudioProcessorEditor::resized() m_tempoSyncRButton.setCentrePosition(m_delayTimeRKnob.getRight() - 35, m_delayTimeRKnob.getBottom() + 15); m_feedbackKnob.setTopLeftPosition(m_delayTimeRKnob.getRight() + 20, 20); - m_delayModeLabel.setTopLeftPosition(10, y); - m_modeSelector.setTopLeftPosition(95, y); + m_modeSelector.setTopLeftPosition(10, y); m_offsetKnob.setTopLeftPosition(m_feedbackKnob.getRight() + 20, 20); m_lowCutFreqKnob.setTopLeftPosition(20, 20); m_lowCutQKnob.setTopLeftPosition(m_lowCutFreqKnob.getRight() + 20, 20); + m_lowCutModRateKnob.setTopLeftPosition(20, 20); + m_lowCutModNoteKnob.setTopLeftPosition(m_lowCutModRateKnob.getX(), m_lowCutModRateKnob.getY()); + m_lowCutModDepthKnob.setTopLeftPosition(m_lowCutModRateKnob.getRight() + 10, 20); + m_lowCutModPhaseKnob.setTopLeftPosition(m_lowCutModDepthKnob.getRight() + 10, 20); + m_lowCutModTempoSyncButton.setTopLeftPosition(20, m_lowCutModRateKnob.getBottom() + 5); + m_lowCutModShapeSelector.setTopLeftPosition(m_lowCutModDepthKnob.getX(), m_lowCutModTempoSyncButton.getY()); + + constexpr int indicatorSize = 8; + m_lowCutLfoIndicator.setSize(indicatorSize, indicatorSize); + m_lowCutLfoIndicator.setCentrePosition(m_lowCutModPhaseKnob.getBounds().getCentreX(), + m_lowCutModPhaseKnob.getBottom() + 15); m_highCutFreqKnob.setTopLeftPosition(m_lowCutQKnob.getRight() + 20, 20); m_highCutQKnob.setTopLeftPosition(m_highCutFreqKnob.getRight() + 20, 20); + m_highCutModPhaseKnob.setTopRightPosition(m_highCutQKnob.getRight(), 20); + m_highCutModDepthKnob.setTopRightPosition(m_highCutModPhaseKnob.getX() - 10, 20); + m_highCutModRateKnob.setTopRightPosition(m_highCutModDepthKnob.getX() - 10, 20); + m_highCutModNoteKnob.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getY()); + m_highCutModTempoSyncButton.setTopLeftPosition(m_highCutModRateKnob.getX(), m_highCutModRateKnob.getBottom() + 5); + m_highCutModShapeSelector.setTopLeftPosition(m_highCutModDepthKnob.getX(), m_highCutModTempoSyncButton.getY()); + + m_highCutLfoIndicator.setSize(indicatorSize, indicatorSize); + m_highCutLfoIndicator.setCentrePosition(m_highCutModPhaseKnob.getBounds().getCentreX(), + m_highCutModPhaseKnob.getBottom() + 15); + m_highCutModInvertButton.setCentrePosition(m_highCutLfoIndicator.getBounds().getCentreX() + 25, + m_highCutLfoIndicator.getBounds().getCentreY()); m_stereoKnob.setTopLeftPosition(20, 20); m_mixKnob.setTopLeftPosition(m_stereoKnob.getRight() + 20, 20); m_gainKnob.setTopLeftPosition(m_mixKnob.getRight() + 20, 20); @@ -116,29 +167,34 @@ void ViiveAudioProcessorEditor::resized() m_meter.setBounds(m_outputGroup.getRight() + 15, y + 45, 35, height - 45); - +#if JUCE_DEBUG + m_lfoScope->setBounds(10, 490, bounds.getWidth() - 20, 170); +#endif } -void ViiveAudioProcessorEditor::updateDelayKnobs(bool syncLActive, bool syncRActive) +void ViiveAudioProcessorEditor::updateSyncedKnobs(bool syncLActive, bool syncRActive, bool lowCutSyncActive, bool highCutSyncActive) { m_delayTimeLKnob.setVisible(!syncLActive); m_delayNoteLKnob.setVisible(syncLActive); m_delayTimeRKnob.setVisible(!syncRActive); m_delayNoteRKnob.setVisible(syncRActive); + m_lowCutModRateKnob.setVisible(!lowCutSyncActive); + m_lowCutModNoteKnob.setVisible(lowCutSyncActive); + m_highCutModRateKnob.setVisible(!highCutSyncActive); + m_highCutModNoteKnob.setVisible(highCutSyncActive); + DBG("Updated synced knobs: " << (syncLActive ? "Sync L active, " : "Sync L inactive, ") << (syncRActive ? "Sync R active, " : "Sync R inactive, ") << (lowCutSyncActive ? "Low Cut Sync active, " : "Low Cut Sync inactive, ") << (highCutSyncActive ? "High Cut Sync active" : "High Cut Sync inactive")); + } void ViiveAudioProcessorEditor::parameterChanged(const juce::String& paramID, float newValue) { - if (paramID == tempoSyncLParamID.getParamID() || paramID == tempoSyncRParamID.getParamID()) { + if (paramID == tempoSyncLParamID.getParamID() || paramID == tempoSyncRParamID.getParamID() || paramID == lowCutModTempoSyncParamID.getParamID() || paramID == highCutModTempoSyncParamID.getParamID()){ juce::MessageManager::callAsync([this] { bool syncL = m_audioProcessor.apvts.getParameter(tempoSyncLParamID.getParamID())->getValue() > 0.5f; bool syncR = m_audioProcessor.apvts.getParameter(tempoSyncRParamID.getParamID())->getValue() > 0.5f; - updateDelayKnobs(syncL, syncR); + bool lowCutSync = m_audioProcessor.apvts.getParameter(lowCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; + bool highCutSync = m_audioProcessor.apvts.getParameter(highCutModTempoSyncParamID.getParamID())->getValue() > 0.5f; + updateSyncedKnobs(syncL, syncR, lowCutSync, highCutSync); }); } } -void ViiveAudioProcessorEditor::modeSelectorChanged() { - int selectedID = m_modeSelector.getSelectedId(); - auto* param = dynamic_cast(m_audioProcessor.apvts.getParameter(delayModeParamID.getParamID())); - param->setValueNotifyingHost(param->convertTo0to1(selectedID - 1)); -} \ No newline at end of file diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h index 508f533..8f83751 100644 --- a/Source/PluginEditor.h +++ b/Source/PluginEditor.h @@ -5,7 +5,12 @@ #include "Parameters.h" #include "RotaryKnob.h" #include "SyncButton.h" +#include "Selector.h" #include "LevelMeter.h" +#include "LFOIndicator.h" +#if JUCE_DEBUG +#include "LFOScope.h" +#endif //============================================================================== /** @@ -36,8 +41,16 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, RotaryKnob m_lowCutFreqKnob{ "Low Cut Freq", m_audioProcessor.apvts, lowCutFreqParamID.getParamID() }; RotaryKnob m_lowCutQKnob{ "Low Cut Q", m_audioProcessor.apvts, lowCutQParamID.getParamID() }; + RotaryKnob m_lowCutModRateKnob{ "Rate", m_audioProcessor.apvts, lowCutModRateParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_lowCutModNoteKnob{ "Note", m_audioProcessor.apvts, lowCutModNoteParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_lowCutModDepthKnob{ "Depth", m_audioProcessor.apvts, lowCutModDepthParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_lowCutModPhaseKnob{ "Phase", m_audioProcessor.apvts, lowCutModPhaseParamID.getParamID(), RotaryKnob::Size::Small }; RotaryKnob m_highCutFreqKnob{ "High Cut Freq", m_audioProcessor.apvts, highCutFreqParamID.getParamID() }; RotaryKnob m_highCutQKnob{ "High Cut Q", m_audioProcessor.apvts, highCutQParamID.getParamID() }; + RotaryKnob m_highCutModRateKnob{ "Rate", m_audioProcessor.apvts, highCutModRateParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_highCutModNoteKnob{ "Note", m_audioProcessor.apvts, highCutModNoteParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_highCutModDepthKnob{ "Depth", m_audioProcessor.apvts, highCutModDepthParamID.getParamID(), RotaryKnob::Size::Small }; + RotaryKnob m_highCutModPhaseKnob{ "Phase", m_audioProcessor.apvts, highCutModPhaseParamID.getParamID(), RotaryKnob::Size::Small }; RotaryKnob m_gainKnob{ "Gain", m_audioProcessor.apvts, gainParamID.getParamID() }; RotaryKnob m_mixKnob{ "Mix", m_audioProcessor.apvts, mixParamID.getParamID() }; @@ -48,17 +61,29 @@ class ViiveAudioProcessorEditor : public juce::AudioProcessorEditor, SyncButton m_tempoSyncLButton{ "Sync", m_audioProcessor.apvts, tempoSyncLParamID.getParamID() }; SyncButton m_tempoSyncRButton{ "Sync", m_audioProcessor.apvts, tempoSyncRParamID.getParamID() }; + SyncButton m_lowCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, lowCutModTempoSyncParamID.getParamID() }; + SyncButton m_highCutModTempoSyncButton{ "Sync", m_audioProcessor.apvts, highCutModTempoSyncParamID.getParamID() }; - juce::Label m_delayModeLabel; - juce::ComboBox m_modeSelector; + Selector m_modeSelector{ "Delay Mode:", m_audioProcessor.apvts, delayModeParamID.getParamID(), Parameters::delayModes, 215, 0.0f, 85, 25 }; + Selector m_lowCutModShapeSelector{ "", m_audioProcessor.apvts, lowCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; + Selector m_highCutModShapeSelector{ "", m_audioProcessor.apvts, highCutModShapeParamID.getParamID(), Parameters::lfoShapes, 60, 12.0f }; + + LFOIndicator m_lowCutLfoIndicator { m_audioProcessor.getLowCutLfoValue() }; + LFOIndicator m_highCutLfoIndicator { m_audioProcessor.getHighCutLfoValue() }; + SyncButton m_highCutModInvertButton{ juce::String::fromUTF8("\xC3\x98"), m_audioProcessor.apvts, highCutModInvertParamID.getParamID(), 20}; + + juce::Label m_modulationLabel; juce::HashMap m_linkedParams; - juce::GroupComponent m_delayGroup, m_filterGroup, m_outputGroup, m_chorusGroup; + juce::GroupComponent m_delayGroup, m_filterGroup, m_outputGroup, m_chorusGroup, m_filterModGroup; + +#if JUCE_DEBUG + std::unique_ptr m_lfoScope; +#endif virtual void parameterChanged(const juce::String& paramID, float newValue) override; - void modeSelectorChanged(); - void updateDelayKnobs(bool syncLActive, bool syncRActive); + void updateSyncedKnobs(bool syncLActive, bool syncRActive, bool lowCutSyncActive, bool highCutSyncActive); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ViiveAudioProcessorEditor) }; diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp index 9834a85..3789f73 100644 --- a/Source/PluginProcessor.cpp +++ b/Source/PluginProcessor.cpp @@ -116,17 +116,10 @@ void ViiveAudioProcessor::processBlock(juce::AudioBuffer& buffer, juce::M m_tempo.update(getPlayHead()); m_params.update(m_tempo); + if (m_tempo.getIsPlaying()) + m_delayEngine.updateSyncedModulators(m_tempo.getPpqPosition()); m_delayEngine.setDelayMode(m_params.delayMode()); - //float syncedTimeL = static_cast(m_tempo.noteLengthToMs(m_params.delayNoteL())); - //float syncedTimeR = static_cast(m_tempo.noteLengthToMs(m_params.delayNoteR())); - //if (syncedTimeL > Parameters::maxDelayTime) - // syncedTimeL = Parameters::maxDelayTime; - //if (syncedTimeR > Parameters::maxDelayTime) - // syncedTimeR = Parameters::maxDelayTime; - - //float sampleRate = float(getSampleRate()); - auto mainInput = getBusBuffer(buffer, true, 0); const float* inputDataL = mainInput.getReadPointer(0); const float* inputDataR = mainInput.getReadPointer(1); diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h index df8009a..b0b35e0 100644 --- a/Source/PluginProcessor.h +++ b/Source/PluginProcessor.h @@ -4,6 +4,9 @@ #include "Parameters.h" #include "DelayEngine.h" #include "Tempo.h" +#if JUCE_DEBUG +#include "LFOScopeBuffer.h" +#endif //============================================================================== /** @@ -43,6 +46,12 @@ class ViiveAudioProcessor : public juce::AudioProcessor std::atomic& getOutputLevelL() { return m_outputLevelL; } std::atomic& getOutputLevelR() { return m_outputLevelR; } + std::atomic& getLowCutLfoValue() { return m_delayEngine.getLowCutLfoValue(); } + std::atomic& getHighCutLfoValue() { return m_delayEngine.getHighCutLfoValue(); } + +#if JUCE_DEBUG + LFOScopeBuffer& getLFOScopeBuffer() { return m_delayEngine.getLFOScopeBuffer(); } +#endif //============================================================================== int getNumPrograms() override; diff --git a/Source/RotaryKnob.cpp b/Source/RotaryKnob.cpp index 6c2b3dd..819aafa 100644 --- a/Source/RotaryKnob.cpp +++ b/Source/RotaryKnob.cpp @@ -1,27 +1,80 @@ #include #include "RotaryKnob.h" +juce::Font RotaryKnob::KnobLookAndFeel::getLabelFont(juce::Label&) +{ + return juce::Font(juce::FontOptions().withHeight(12.0f)); +} + //============================================================================== -RotaryKnob::RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID) +void RotaryKnob::KnobLookAndFeel::drawRotarySlider(juce::Graphics& g, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, float rotaryEndAngle, juce::Slider& slider) +{ + auto bounds = juce::Rectangle(x, y, width, height).toFloat().reduced(4); + auto radius = jmin(bounds.getWidth(), bounds.getHeight()) / 2.0f; + auto toAngle = rotaryStartAngle + sliderPos * (rotaryEndAngle - rotaryStartAngle); + auto lineW = jmin(5.0f, radius * 0.3f); + auto arcRadius = radius - lineW * 0.5f; + + juce::Path backgroundArc; + backgroundArc.addCentredArc(bounds.getCentreX(), bounds.getCentreY(), + arcRadius, arcRadius, 0.0f, + rotaryStartAngle, rotaryEndAngle, true); + g.setColour(slider.findColour(juce::Slider::rotarySliderOutlineColourId)); + g.strokePath(backgroundArc, juce::PathStrokeType(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + + if (slider.isEnabled()) + { + juce::Path valueArc; + valueArc.addCentredArc(bounds.getCentreX(), bounds.getCentreY(), + arcRadius, arcRadius, 0.0f, + rotaryStartAngle, toAngle, true); + g.setColour(slider.findColour(juce::Slider::rotarySliderFillColourId)); + g.strokePath(valueArc, juce::PathStrokeType(lineW, juce::PathStrokeType::curved, juce::PathStrokeType::rounded)); + } + + auto thumbWidth = lineW * 1.6f; // default is lineW * 2.0f + juce::Point thumb( + bounds.getCentreX() + arcRadius * std::cos(toAngle - juce::MathConstants::halfPi), + bounds.getCentreY() + arcRadius * std::sin(toAngle - juce::MathConstants::halfPi)); + g.setColour(slider.findColour(juce::Slider::thumbColourId)); + g.fillEllipse(juce::Rectangle(thumbWidth, thumbWidth).withCentre(thumb)); +} + +//============================================================================== +RotaryKnob::RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID, Size size) : m_attachment(apvts, parameterID.getParamID(), m_slider) { + const bool isSmall = size == Size::Small; + const int w = isSmall ? 45 : 70; + const int textBoxH = isSmall ? 12 : 16; + const int gap = isSmall ? 0 : 2; + const int labelH = isSmall ? 14 : 24; + + if (isSmall) + { + m_slider.setLookAndFeel(&m_laf); + m_slider.setColour(juce::Slider::textBoxOutlineColourId, juce::Colours::transparentBlack); + } m_slider.setSliderStyle(juce::Slider::SliderStyle::RotaryHorizontalVerticalDrag); - m_slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, 70, 16); - m_slider.setBounds(0, 0, 70, 86); + m_slider.setTextBoxStyle(juce::Slider::TextBoxBelow, false, w, textBoxH); + m_slider.setBounds(0, 0, w, w + textBoxH + gap); addAndMakeVisible(m_slider); m_label.setText(text, juce::NotificationType::dontSendNotification); + m_label.setFont(juce::Font(juce::FontOptions().withHeight(isSmall ? 13.0f : 14.0f))); m_label.setJustificationType(juce::Justification::horizontallyCentred); m_label.setBorderSize(juce::BorderSize(0)); m_label.attachToComponent(&m_slider, false); addAndMakeVisible(m_label); - setSize(70, 110); + setSize(w, w + textBoxH + gap + labelH); } RotaryKnob::~RotaryKnob() { + m_slider.setLookAndFeel(nullptr); } void RotaryKnob::resized() { - m_slider.setTopLeftPosition(0, 24); + m_slider.setTopLeftPosition(0, getHeight() - m_slider.getHeight()); } diff --git a/Source/RotaryKnob.h b/Source/RotaryKnob.h index a1ddc35..68ae0ad 100644 --- a/Source/RotaryKnob.h +++ b/Source/RotaryKnob.h @@ -8,7 +8,9 @@ class RotaryKnob : public juce::Component { public: - RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID); + enum class Size { Normal, Small }; + + RotaryKnob(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID, Size size = Size::Normal); ~RotaryKnob() override; void resized() override; @@ -16,6 +18,15 @@ class RotaryKnob : public juce::Component juce::Label& getLabel() { return m_label; } private: + struct KnobLookAndFeel : public juce::LookAndFeel_V4 + { + void drawRotarySlider(juce::Graphics&, int x, int y, int width, int height, + float sliderPos, float rotaryStartAngle, float rotaryEndAngle, + juce::Slider&) override; + juce::Font getLabelFont(juce::Label&) override; + }; + + KnobLookAndFeel m_laf; juce::Slider m_slider; juce::Label m_label; diff --git a/Source/Selector.cpp b/Source/Selector.cpp new file mode 100644 index 0000000..0abccf4 --- /dev/null +++ b/Source/Selector.cpp @@ -0,0 +1,55 @@ +#include "Selector.h" + +Selector::Selector(const juce::String& labelText, + juce::AudioProcessorValueTreeState& apvts, + const juce::ParameterID& parameterID, + const juce::StringArray& items, + int width, + float fontSize, + int leftLabelWidth, + int height) + : m_hasLabel(labelText.isNotEmpty()), m_leftLabelWidth(leftLabelWidth) +{ + if (fontSize > 0.0f) + { + m_laf.fontSize = fontSize; + m_comboBox.setLookAndFeel(&m_laf); + } + + m_comboBox.addItemList(items, 1); + m_comboBox.setEditableText(false); + addAndMakeVisible(m_comboBox); + + m_attachment = std::make_unique( + apvts, parameterID.getParamID(), m_comboBox); + + if (m_hasLabel) + { + m_label.setText(labelText, juce::NotificationType::dontSendNotification); + addAndMakeVisible(m_label); + } + + const int autoH = (m_hasLabel && m_leftLabelWidth == 0 ? s_labelH : 0) + s_comboH; + setSize(width, height > 0 ? height : autoH); +} + +Selector::~Selector() +{ + m_comboBox.setLookAndFeel(nullptr); +} + +void Selector::resized() +{ + if (m_hasLabel && m_leftLabelWidth > 0) + { + m_label.setBounds(0, 0, m_leftLabelWidth, getHeight()); + m_comboBox.setBounds(m_leftLabelWidth, 0, getWidth() - m_leftLabelWidth, getHeight()); + } + else + { + const int comboY = m_hasLabel ? s_labelH : 0; + if (m_hasLabel) + m_label.setBounds(0, 0, getWidth(), s_labelH); + m_comboBox.setBounds(0, comboY, getWidth(), s_comboH); + } +} diff --git a/Source/Selector.h b/Source/Selector.h new file mode 100644 index 0000000..2116ce6 --- /dev/null +++ b/Source/Selector.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +class Selector : public juce::Component +{ +public: + Selector(const juce::String& labelText, + juce::AudioProcessorValueTreeState& apvts, + const juce::ParameterID& parameterID, + const juce::StringArray& items, + int width = 60, + float fontSize = 0.0f, + int leftLabelWidth = 0, + int height = 0); + ~Selector() override; + + void resized() override; + +private: + static constexpr int s_labelH = 14; + static constexpr int s_comboH = 20; + + struct SelectorLookAndFeel : public juce::LookAndFeel_V4 + { + float fontSize = 0.0f; + juce::Font getComboBoxFont(juce::ComboBox&) override + { + return juce::Font(juce::FontOptions().withHeight(fontSize)); + } + }; + + SelectorLookAndFeel m_laf; + juce::Label m_label; + juce::ComboBox m_comboBox; + std::unique_ptr m_attachment; + bool m_hasLabel; + int m_leftLabelWidth = 0; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Selector) +}; diff --git a/Source/StereoDelay.h b/Source/StereoDelay.h index e5cb725..a59e0f7 100644 --- a/Source/StereoDelay.h +++ b/Source/StereoDelay.h @@ -1,7 +1,6 @@ #pragma once #include - -enum Channel { Left = 0, Right = 1 }; +#include "Types.h" class StereoDelay { public: diff --git a/Source/StereoFilter.cpp b/Source/StereoFilter.cpp new file mode 100644 index 0000000..f889824 --- /dev/null +++ b/Source/StereoFilter.cpp @@ -0,0 +1,40 @@ +#include "StereoFilter.h" + +StereoFilter::StereoFilter(FilterType type) +{ + for (auto& f : m_filters) + f.setType(type); +} + +void StereoFilter::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +{ + juce::dsp::ProcessSpec spec; + spec.sampleRate = sampleRate; + spec.maximumBlockSize = juce::uint32(samplesPerBlock); + spec.numChannels = 1; + for (auto& f : m_filters) + f.prepare(spec); +} + +void StereoFilter::reset() noexcept +{ + for (auto& f : m_filters) + f.reset(); +} + +void StereoFilter::setCutoff(float freq, Channel channel) +{ + m_filters[channel].setCutoffFrequency(freq); +} + +void StereoFilter::setQ(float q) +{ + for (auto& f : m_filters) + f.setResonance(q); +} + +void StereoFilter::processSample(const float& inL, const float& inR, float& outL, float& outR) +{ + outL = m_filters[Channel::Left].processSample(0, inL); + outR = m_filters[Channel::Right].processSample(0, inR); +} diff --git a/Source/StereoFilter.h b/Source/StereoFilter.h new file mode 100644 index 0000000..f38ee7e --- /dev/null +++ b/Source/StereoFilter.h @@ -0,0 +1,25 @@ +#pragma once +#include +#include "Types.h" + +using Filter = juce::dsp::StateVariableTPTFilter; +using FilterType = juce::dsp::StateVariableTPTFilterType; + +class StereoFilter +{ +public: + StereoFilter(FilterType type = FilterType::lowpass); + ~StereoFilter() = default; + + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; + + void processSample(const float& inL, const float& inR, float& outL, float& outR); + void setCutoff(float freq, Channel channel); + void setQ(float q); + +private: + Filter m_filters[2]; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(StereoFilter) +}; diff --git a/Source/Sync.h b/Source/Sync.h new file mode 100644 index 0000000..2c46334 --- /dev/null +++ b/Source/Sync.h @@ -0,0 +1,47 @@ +#pragma once +#include + +inline static std::array delayNoteLengthMultipliers = +{ + 0.125, // 0 = 1/32 + 0.5 / 3.0, // 1 = 1/16 triplet + 0.1875, // 2 = 1/32 dotted + 0.25, // 3 = 1/16 + 1.0 / 3.0, // 4 = 1/8 triplet + 0.375, // 5 = 1/16 dotted + 0.5, // 6 = 1/8 + 2.0 / 3.0, // 7 = 1/4 triplet + 0.75, // 8 = 1/8 dotted + 1.0, // 9 = 1/4 + 4.0 / 3.0, // 10 = 1/2 triplet + 1.5, // 11 = 1/4 dotted + 2.0, // 12 = 1/2 + 8.0 / 3.0, // 13 = 1/1 triplet + 3.0, // 14 = 1/2 dotted + 4.0, // 15 = 1/1 +}; + +inline static std::array modNoteLengthMultipliers = +{ + 0.5, // 0 = 1/8 + 0.75, // 1 = 1/8 dotted + 1.0, // 2 = 1/4 + 1.5, // 3 = 1/4 dotted + 2.0, // 4 = 1/2 + 3.0, // 5 = 1/2 dotted + 4.0, // 6 = 1/1 + 6.0, // 7 = 1/1 dotted + 8.0, // 8 = 2/1 + 12.0, // 9 = 3/1 + 16.0, // 10 = 4/1 + 20.0, // 11 = 5/1 + 24.0, // 12 = 6/1 + 28.0, // 13 = 7/1 + 32.0, // 14 = 8/1 +}; + +inline float getSyncedPhase(int index, double ppqPosition) +{ + double cycleLengthInBeats = modNoteLengthMultipliers[size_t(index)]; + return static_cast(std::fmod(ppqPosition, cycleLengthInBeats) / cycleLengthInBeats); +} diff --git a/Source/Tempo.cpp b/Source/Tempo.cpp index 3d785d3..e3281e3 100644 --- a/Source/Tempo.cpp +++ b/Source/Tempo.cpp @@ -1,24 +1,5 @@ #include "Tempo.h" - -static std::array noteLengthMultipliers = -{ - 0.125, // 0 = 1/32 - 0.5 / 3.0, // 1 = 1/16 triplet - 0.1875, // 2 = 1/32 dotted - 0.25, // 3 = 1/16 - 1.0 / 3.0, // 4 = 1/8 triplet - 0.375, // 5 = 1/16 dotted - 0.5, // 6 = 1/8 - 2.0 / 3.0, // 7 = 1/4 triplet - 0.75, // 8 = 1/8 dotted - 1.0, // 9 = 1/4 - 4.0 / 3.0, // 10 = 1/2 triplet - 1.5, // 11 = 1/4 dotted - 2.0, // 12 = 1/2 - 8.0 / 3.0, // 13 = 1/1 triplet - 3.0, // 14 = 1/2 dotted - 4.0, // 15 = 1/1 -}; +#include "Sync.h" void Tempo::reset() noexcept { @@ -41,9 +22,19 @@ void Tempo::update(const juce::AudioPlayHead* playhead) noexcept if (pos.getBpm().hasValue()) m_bpm = *pos.getBpm(); + + m_isPlaying = pos.getIsPlaying(); + + m_ppqPosition = pos.getPpqPosition().hasValue() ? *pos.getPpqPosition() : 0.0; + } double Tempo::noteLengthToMs(int index) const noexcept { - return 60000.0 * noteLengthMultipliers[size_t(index)] / m_bpm; + return 60000.0 * delayNoteLengthMultipliers[size_t(index)] / m_bpm; +} + +double Tempo::noteLengthToHz(int index) const noexcept +{ + return m_bpm / (60.0 * modNoteLengthMultipliers[size_t(index)]); } \ No newline at end of file diff --git a/Source/Tempo.h b/Source/Tempo.h index 0b6425c..b488cf0 100644 --- a/Source/Tempo.h +++ b/Source/Tempo.h @@ -10,9 +10,14 @@ class Tempo void update(const juce::AudioPlayHead* playhead) noexcept; double noteLengthToMs(int index) const noexcept; + double noteLengthToHz(int index) const noexcept; double getTempo() const noexcept { return m_bpm; } + bool getIsPlaying() const noexcept { return m_isPlaying; } + double getPpqPosition() const noexcept { return m_ppqPosition; } private: double m_bpm = 120.0; + bool m_isPlaying = false; + double m_ppqPosition = 0.0; }; \ No newline at end of file diff --git a/Source/Types.h b/Source/Types.h new file mode 100644 index 0000000..e0374c0 --- /dev/null +++ b/Source/Types.h @@ -0,0 +1,5 @@ +#pragma once + +enum Channel { Left = 0, Right = 1 }; + +enum class LFOShape { Sine, Triangle, Square, SawUp, SawDown, SampleAndHold }; diff --git a/viive.png b/viive.png index a3a8a19..cce6370 100644 Binary files a/viive.png and b/viive.png differ