Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6831138
add filterengine and stereofilter classes
trsctr Apr 22, 2026
ab4df7a
update cmakelists, wire filterengine to delayengine, create types.h f…
trsctr Apr 22, 2026
321b8ab
add parameters for filter modulation
trsctr Apr 22, 2026
03f67d1
add mod param update to filterengine, smoothen cutoff with onepolelp,…
trsctr Apr 22, 2026
0cba639
switch to octave based filter modulation, fix lfo phase bug
trsctr Apr 22, 2026
59f355f
add lfoscope component + buffer for debug purposes, add it to editor …
trsctr Apr 22, 2026
486a224
finetune filter mod range and value display
trsctr Apr 22, 2026
ff662dc
add square to lfo shapes
trsctr Apr 22, 2026
b870c7c
flip lfo phase for high cut
trsctr Apr 22, 2026
bcdc072
clean up old code from delay engine
trsctr Apr 22, 2026
c4f14c6
add tempo lfo tempo sync + parameters
trsctr Apr 22, 2026
f10dc35
add size options and custom look and feel for small rotary knob
trsctr Apr 23, 2026
13d46e8
add filter mod depth controls to gui
trsctr Apr 23, 2026
6c0bdd9
display phase in degrees
trsctr Apr 23, 2026
e17e141
add saw up/down shapes to lfo
trsctr Apr 24, 2026
d22b397
add parameter for highcut mod invert toggle
trsctr Apr 24, 2026
64e57ff
add params for setting filter lfo shape
trsctr Apr 24, 2026
ecc33a8
sync tempo synced lfo to song position
trsctr Apr 24, 2026
fd62362
add sample & hold modulation
trsctr Apr 24, 2026
9c84129
redefine lfo tempo sync note values, set max mod rate to 8hz
trsctr May 5, 2026
6600b7f
set max filter q to 1.5
trsctr May 5, 2026
3fd84e0
update changelog
trsctr May 5, 2026
63e0947
add selector component and lfo waveform gui selector element
trsctr May 5, 2026
b54070b
fix tempo sync bug in s&h lfo
trsctr May 5, 2026
259e1d8
add lfo value indicators to filter mod section
trsctr May 5, 2026
28497bb
add high cut lfo invert toggle to gui
trsctr May 5, 2026
87f6e57
move delay mode selector label to left side of menu
trsctr May 5, 2026
7624f1b
update changelog, readme, cmakelists version number
trsctr May 6, 2026
d11b883
code cleanup
trsctr May 6, 2026
853c4c9
fix version number string in cmakelists, hardcode beta version number…
trsctr May 6, 2026
1c2ef85
remove unused variables from Parameters.h
trsctr May 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

All notable changes to this project will be documented in this file.

## \[0.2.0-beta.1] - 2026-05-06

### 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

### Known limitations

* Tempo synced LFOs might sync incorrectly to song playback positions in DAW on extreme settings (very high or very low BPMs)

## \[0.1.0] - 2026-04-17

### Added
Expand Down
9 changes: 7 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.22)

project(viive VERSION 0.1.0)
project(viive VERSION 0.2.0)


### Dependency versions ###
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -31,8 +32,11 @@ 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 untested and unsigned. May be blocked by Gatekeeper on
newer macOS versions and cannot be guaranteed to work without
code signing and notarization.

## 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
Expand All @@ -57,6 +61,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/`.

Expand Down
62 changes: 15 additions & 47 deletions Source/DelayEngine.cpp
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -19,8 +19,7 @@ void DelayEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept
spec.numChannels = 2;
m_sampleRate = static_cast<float>(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);
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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;
Expand All @@ -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);
Expand Down
24 changes: 13 additions & 11 deletions Source/DelayEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<float>;

Expand All @@ -19,24 +23,26 @@ 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; }
void setGainLevel(const float value) { m_gainLevel = value; }
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<float>& getLowCutLfoValue() { return m_filterEngine.getLowCutLfoValue(); }
std::atomic<float>& getHighCutLfoValue() { return m_filterEngine.getHighCutLfoValue(); }

#if JUCE_DEBUG
LFOScopeBuffer& getLFOScopeBuffer() { return m_filterEngine.m_scopeBuffer; }
#endif

private:
//juce::dsp::DelayLine<float, juce::dsp::DelayLineInterpolationTypes::Linear> 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<float> m_feedbackCompressor;
ChorusEngine m_chorusEngine;
Expand All @@ -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)
};
Loading
Loading