Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 19 additions & 5 deletions .github/workflows/Validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@ 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"
}
- {
name: 'Windows Latest',
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"
}

Expand All @@ -46,7 +48,7 @@ jobs:
uses: actions/checkout@v4

- name: Enable ccache
uses: hendrikmuhs/[email protected]
uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
with:
key: ${{ runner.os }}-${{ env.BUILD_TYPE }}

Expand All @@ -63,15 +65,27 @@ 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
env:
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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
13 changes: 12 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,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
Expand All @@ -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/`.

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