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
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@ All notable changes to this project will be documented in this file.
* Ranges finetuned for usability and musicality, subject to change
* Insert effect selector
* Allows user to switch between chorus and lofi effect in the feedback loop

* Feedback loop kill
* fades wet signal out and clears delay line buffers, feedback and any audio processor states
* Automatable param
* Button in GUI
* Triggered with position rewind when playback stop (ie pressing stop twice during playback)

### Fixed
* Delay time offset bug where offset never truly returned to zero once modified

## \[0.2.0] - 2026-05-18

### Added
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ target_sources(viive PRIVATE
Source/FilterEngine.cpp
Source/Tempo.cpp
Source/Selector.cpp
Source/KillButton.cpp
Source/SyncButton.cpp
Source/StereoDelay.cpp
Source/StereoFilter.cpp
Expand Down
6 changes: 6 additions & 0 deletions Source/Chorus.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ void Chorus::reset() noexcept
m_modDepthMs = -1.0f;
}

void Chorus::kill()
{
m_delayLine.reset();
m_lpState = 0.0f;
}

void Chorus::setBaseDelay(float delayInMs)
{
if (!juce::approximatelyEqual(delayInMs, m_baseDelayMs)) {
Expand Down
2 changes: 2 additions & 0 deletions Source/Chorus.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class Chorus
void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept;
void reset() noexcept;
void update();
void kill();

float processSample(const float& in);
void setBaseDelay(float delayInMs);
void setModRate(float rate) { m_lfo.setFrequency(rate); }
Expand Down
11 changes: 11 additions & 0 deletions Source/ChorusEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ void ChorusEngine::reset() noexcept
m_modDepth = -1.0f;
}

void ChorusEngine::kill()
{
m_chorus1L.kill();
m_chorus1R.kill();
m_chorus2L.kill();
m_chorus2R.kill();
m_highpass.reset();
m_dcBlockStateL = 0.0f;
m_dcBlockStateR = 0.0f;
}

void ChorusEngine::setModRate(float newRate)
{
if (!juce::approximatelyEqual(newRate, m_modRate)) {
Expand Down
1 change: 1 addition & 0 deletions Source/ChorusEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ChorusEngine : public InsertEffect

void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept override;
void reset() noexcept override;
void kill() override;

void processSample(const float& inL, const float& inR, float& outL, float& outR) override;
void update(const Parameters& params) override;
Expand Down
43 changes: 39 additions & 4 deletions Source/DelayEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ void DelayEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept
m_filterEngine.prepareToPlay(sampleRate, samplesPerBlock);
m_feedbackHighpass.prepare(spec);
m_stereoDelay.prepareToPlay(sampleRate, samplesPerBlock);
m_coeff = onePoleLowpassCoeff(100.0f, static_cast<float>(sampleRate));
m_coeff = onePoleLowpassCoeff(100.0f, m_sampleRate);
m_killCoeff = onePoleLowpassCoeff(10.0f, m_sampleRate);
}

void DelayEngine::reset() noexcept
Expand Down Expand Up @@ -82,20 +83,53 @@ void DelayEngine::update(const Parameters& params)
setWidthLevel(params.stereo());
setOffsetMs(params.offset());
setDelayTimes(params.delayTimeL(), params.delayTimeR());
setKill(params.feedbackKill());
m_filterEngine.update(params);
m_insertEffectSelector.update(params);
}

void DelayEngine::setKill(bool shouldKill)
{
bool risingEdge = shouldKill && !m_killButtonHeld;
m_killButtonHeld = shouldKill;
if (risingEdge)
triggerKill();
}

void DelayEngine::triggerKill()
{
m_isKilling = true;
}

void DelayEngine::resetAfterKill()
{
m_stereoDelay.kill();
m_insertEffectSelector.kill();
m_filterEngine.kill();
m_feedbackHighpass.reset();
m_feedbackCompressor.reset();
m_feedbackL = 0.0f;
m_feedbackR = 0.0f;
m_killGain = 1.0f;
m_isKilling = false;
}

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)
void DelayEngine::processSample(const float& inL, const float& inR, float& outL, float& outR)
{
float dryL = inL;
float dryR = inR;

if (m_isKilling) {
m_killGain = onePoleLowpass(0.0f, m_killGain, m_killCoeff);
if (m_killGain < 0.001f)
resetAfterKill();
}

float mono = (dryL + dryR) * .5f;
float delayInL = 0.0f, delayInR = 0.0f, wetL = 0.0f, wetR = 0.0f;
switch (m_delayMode) {
Expand All @@ -107,7 +141,7 @@ void DelayEngine::processSample(const float& inL, const float& inR, float& outL,
delayInL = m_feedbackR;
delayInR = mono + m_feedbackL;
break;
case DelayMode::Cross:
case DelayMode::Cross:
delayInL = dryL + m_feedbackR;
delayInR = dryR + m_feedbackL;
break;
Expand All @@ -122,7 +156,8 @@ void DelayEngine::processSample(const float& inL, const float& inR, float& outL,

m_stereoDelay.processSample(delayInL, wetL, Channel::Left);
m_stereoDelay.processSample(delayInR, wetR, Channel::Right);

wetL *= m_killGain;
wetR *= m_killGain;
m_insertEffectSelector.processSample(wetL, wetR, wetL, wetR);

m_filterEngine.processSample(wetL, wetR, wetL, wetR);
Expand Down
11 changes: 10 additions & 1 deletion Source/DelayEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class DelayEngine
void reset() noexcept;

void update(const Parameters& params);
void processSample(const float& inL, const float& inR, float& outL, float& outR, const Parameters& params);
void processSample(const float& inL, const float& inR, float& outL, float& outR);
void setDelayTimes(const float delayInMsL, const float delayInMsR);
void setFeedbackLevel(const float value) { m_feedbackLevel = value; }
void setMixLevel(const float value) { m_mixLevel = value; }
Expand All @@ -30,6 +30,8 @@ class DelayEngine
void setOffsetMs(const float value) { m_offsetMs = value * .5f; }
void setDelayMode(const int modeIndex);
void setInsertEffect(const int effectIndex);
void setKill(bool shouldKill);
void triggerKill();
void updateSyncedModulators(const double ppqPosition);

std::atomic<float>& getLowCutLfoValue() { return m_filterEngine.getLowCutLfoValue(); }
Expand All @@ -52,6 +54,7 @@ class DelayEngine

float m_coeff = 0.0f;

float m_killCoeff = 0.0f;
float m_feedbackL = 0.0f;
float m_feedbackR = 0.0f;

Expand All @@ -64,6 +67,12 @@ class DelayEngine
float m_gainLevel = 1.0f;
float m_widthLevel = 1.0f;
float m_offsetMs = 0.0f;
float m_killGain = 1.0f;

bool m_isKilling = false;
bool m_killButtonHeld = false;

void resetAfterKill();

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(DelayEngine)
};
6 changes: 6 additions & 0 deletions Source/FilterEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ void FilterEngine::reset() noexcept
m_highCutSmoothed[0] = m_highCutSmoothed[1] = Parameters::maxFilterFreq;
}

void FilterEngine::kill()
{
m_lowCut.reset();
m_highCut.reset();
}

float FilterEngine::modulatedCutoff(float base, float lfoValue, float modDepth)
{
return juce::jlimit(Parameters::minFilterFreq, Parameters::maxFilterFreq, base * std::pow(2.0f, lfoValue * modDepth));
Expand Down
2 changes: 1 addition & 1 deletion Source/FilterEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class FilterEngine

void update(const Parameters& params);
void processSample(const float& inL, const float& inR, float& outL, float& outR);

void kill();
void updateLFOs(const double ppqPosition) noexcept;

std::atomic<float>& getLowCutLfoValue() { return m_lowCutLfoValue; }
Expand Down
1 change: 1 addition & 0 deletions Source/InsertEffect.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ class InsertEffect
virtual void reset() noexcept = 0;
virtual void processSample(const float& inL, const float& inR, float& outL, float& outR) = 0;
virtual void update(const Parameters& params) = 0;
virtual void kill() = 0;
};
6 changes: 6 additions & 0 deletions Source/InsertEffectSelector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ void InsertEffectSelector::update(const Parameters& params)
m_active->update(params);
}

void InsertEffectSelector::kill()
{
if (m_active)
m_active->kill();
}

void InsertEffectSelector::select(InsertEffectType type)
{
switch(type) {
Expand Down
1 change: 1 addition & 0 deletions Source/InsertEffectSelector.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class InsertEffectSelector
void processSample(const float& inL, const float& inR, float& outL, float& outR);
void update(const Parameters& params);
void select(InsertEffectType type);
void kill();

private:
ChorusEngine m_chorus;
Expand Down
40 changes: 40 additions & 0 deletions Source/KillButton.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#include <JuceHeader.h>
#include "KillButton.h"

KillButton::KillButton(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID, int width, int height)
: m_apvts(apvts), m_paramID(parameterID.getParamID())
{
m_button.setButtonText(text);
m_button.setClickingTogglesState(false);
m_button.setColour(juce::TextButton::ColourIds::buttonColourId, juce::Colours::transparentWhite);
m_button.setColour(juce::TextButton::ColourIds::buttonOnColourId, juce::Colours::orange);
m_button.setColour(juce::TextButton::ColourIds::textColourOnId, juce::Colours::black);
m_button.setBounds(0, 0, width, height);
m_button.addMouseListener(this, false);
addAndMakeVisible(m_button);
setSize(width, height);
}

KillButton::~KillButton()
{
m_button.removeMouseListener(this);
}

void KillButton::resized()
{
m_button.setTopLeftPosition(0, 0);
}

void KillButton::mouseDown(const juce::MouseEvent&)
{
m_button.setToggleState(true, juce::dontSendNotification);
if (auto* param = m_apvts.getParameter(m_paramID))
param->setValueNotifyingHost(1.0f);
}

void KillButton::mouseUp(const juce::MouseEvent&)
{
m_button.setToggleState(false, juce::dontSendNotification);
if (auto* param = m_apvts.getParameter(m_paramID))
param->setValueNotifyingHost(0.0f);
}
21 changes: 21 additions & 0 deletions Source/KillButton.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#pragma once

#include <JuceHeader.h>

class KillButton : public juce::Component
{
public:
KillButton(const juce::String& text, juce::AudioProcessorValueTreeState& apvts, const juce::ParameterID& parameterID, int width = 60, int height = 25);
~KillButton() override;

void resized() override;
void mouseDown(const juce::MouseEvent& e) override;
void mouseUp(const juce::MouseEvent& e) override;

private:
juce::TextButton m_button;
juce::AudioProcessorValueTreeState& m_apvts;
juce::String m_paramID;

JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(KillButton)
};
10 changes: 10 additions & 0 deletions Source/LofiEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ void LofiEngine::reset() noexcept
m_envR = 0.0f;
}

void LofiEngine::kill()
{
m_bitCrusherL.reset();
m_bitCrusherR.reset();
m_highpass.reset();
m_lowpass.reset();
m_envL = 0.0f;
m_envR = 0.0f;
}

void LofiEngine::update(const Parameters& params)
{
setResampleFreq(params.lofiResampleFreq());
Expand Down
1 change: 1 addition & 0 deletions Source/LofiEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class LofiEngine : public InsertEffect
void reset() noexcept override;
void processSample(const float& inL, const float& inR, float& outL, float& outR) override;
void update(const Parameters& params) override;
void kill () override;

private:
BitCrusher m_bitCrusherL{ 8.0f, 22050.0f };
Expand Down
7 changes: 7 additions & 0 deletions Source/Parameters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts)
castParameter(apvts, delayModeParamID, m_delayModeParam);
castParameter(apvts, mixParamID, m_mixParam);
castParameter(apvts, feedbackParamID, m_feedbackParam);
castParameter(apvts, feedbackKillParamID, m_feedbackKillParam);
castParameter(apvts, stereoParamID, m_stereoParam);
castParameter(apvts, offsetParamID, m_offsetParam);
castParameter(apvts, insertEffectTypeParamID, m_insertEffectTypeParam);
Expand Down Expand Up @@ -444,6 +445,11 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL
"Lofi Noise",
true
));
layout.add(std::make_unique<juce::AudioParameterBool>(
feedbackKillParamID.getParamID(),
"Feedback Kill",
false
));
return layout;
}

Expand Down Expand Up @@ -544,6 +550,7 @@ void Parameters::update(const Tempo& tempo) noexcept
m_lofiMixLevelSmoother.setTargetValue(m_lofiMixLevelParam->get() * 0.01f);
m_lofiDampenFreqSmoother.setTargetValue(m_lofiDampenFreqParam->get());
m_lofiNoiseEnabled = m_lofiNoiseEnabledParam->get();
m_feedbackKill = m_feedbackKillParam->get();
m_tempoSyncL = m_tempoSyncLParam->get();
m_tempoSyncR = m_tempoSyncRParam->get();
m_delayNoteL = m_delayNoteLParam->getIndex();
Expand Down
Loading
Loading