From 52f51a8d23cdcf17b8a183d519f6519e16c1afab Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 09:16:10 +0300 Subject: [PATCH 1/8] add bitcrusher class w downsampling function definition and declarations for other features --- Source/BitCrusher.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++ Source/BitCrusher.h | 32 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 Source/BitCrusher.cpp create mode 100644 Source/BitCrusher.h diff --git a/Source/BitCrusher.cpp b/Source/BitCrusher.cpp new file mode 100644 index 0000000..f4e3cb9 --- /dev/null +++ b/Source/BitCrusher.cpp @@ -0,0 +1,56 @@ +#include "BitCrusher.h" + +BitCrusher::BitCrusher(float bitDepth, float downsampleRate) + : m_bitDepth(bitDepth), m_downsampleRate(downsampleRate) +{ +} + +void BitCrusher::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +{ + m_sampleRate = static_cast(sampleRate); + setRatio(); +} + +void BitCrusher::reset() noexcept +{ + m_currentSample = 0.0f; + m_accumulator = 0.0f; +} + +float BitCrusher::processSample(const float& in) +{ + return downsample(in); +} + +float BitCrusher::downsample(const float& sample) +{ + m_accumulator += m_ratio; + if (m_accumulator >= 1.0f) { + // DBG("Accumulator before: " << m_accumulator); + m_accumulator -= 1.0f; + m_currentSample = sample; + // DBG("Current sample: " << m_currentSample); + // DBG("Accumulator after: " << m_accumulator); + + } + return m_currentSample; +} + +void BitCrusher::update(const Parameters& params) noexcept +{ + //setBitDepth(params.getBitCrusherBitDepth()); + setDownsampleRate(params.bitCrusherSampleRate()); +} + +void BitCrusher::setDownsampleRate(float downsampleRate) +{ + if (!juce::approximatelyEqual(downsampleRate, m_downsampleRate)) { + m_downsampleRate = downsampleRate; + setRatio(); + } +} + +void BitCrusher::setRatio() +{ + m_ratio = m_downsampleRate / m_sampleRate; +} \ No newline at end of file diff --git a/Source/BitCrusher.h b/Source/BitCrusher.h new file mode 100644 index 0000000..5ebc529 --- /dev/null +++ b/Source/BitCrusher.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include "Parameters.h" + +class BitCrusher +{ +public: + BitCrusher(float bitDepth = 16.0f, float downsampleRate = 44100.0f); + ~BitCrusher() = default; + + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; + float processSample(const float& in); + float downsample(const float& sample); + void update(const Parameters& params) noexcept; + void setBitDepth(float bitDepth); + void setDownsampleRate(float downsampleRate); + void setRatio(); + void setMix(float mix) { m_mix = mix; } + +private: + float m_sampleRate = 0.0f; + float m_bitDepth = 0.0f; + float m_downsampleRate = 0.0f; + float m_ratio = 0.0f; + float m_currentSample = 0.0f; + float m_mix = 0.0f; + float m_accumulator = 0.0f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BitCrusher) +}; + From 26b7dedf0c013a7754d74a80abe604c1e67dcc76 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 10:18:43 +0300 Subject: [PATCH 2/8] add bit reduction to bitcrusher --- Source/BitCrusher.cpp | 31 ++++++++++++++++++++++++------- Source/BitCrusher.h | 7 +++++-- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Source/BitCrusher.cpp b/Source/BitCrusher.cpp index f4e3cb9..8ac8e72 100644 --- a/Source/BitCrusher.cpp +++ b/Source/BitCrusher.cpp @@ -5,10 +5,11 @@ BitCrusher::BitCrusher(float bitDepth, float downsampleRate) { } -void BitCrusher::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +void BitCrusher::prepareToPlay(double sampleRate) noexcept { m_sampleRate = static_cast(sampleRate); setRatio(); + setSteps(); } void BitCrusher::reset() noexcept @@ -19,27 +20,24 @@ void BitCrusher::reset() noexcept float BitCrusher::processSample(const float& in) { - return downsample(in); + float out = bitReduce(in); + return downsample(out); } float BitCrusher::downsample(const float& sample) { m_accumulator += m_ratio; if (m_accumulator >= 1.0f) { - // DBG("Accumulator before: " << m_accumulator); m_accumulator -= 1.0f; m_currentSample = sample; - // DBG("Current sample: " << m_currentSample); - // DBG("Accumulator after: " << m_accumulator); - } return m_currentSample; } void BitCrusher::update(const Parameters& params) noexcept { - //setBitDepth(params.getBitCrusherBitDepth()); setDownsampleRate(params.bitCrusherSampleRate()); + setBitDepth(params.bitCrusherBitDepth()); } void BitCrusher::setDownsampleRate(float downsampleRate) @@ -50,6 +48,25 @@ void BitCrusher::setDownsampleRate(float downsampleRate) } } +float BitCrusher::bitReduce(const float& sample) +{ + float rounded = std::ceil(sample * m_steps); + return rounded / m_steps; +} + +void BitCrusher::setBitDepth(float bitDepth) +{ + if (!juce::approximatelyEqual(bitDepth, m_bitDepth)) { + m_bitDepth = bitDepth; + setSteps(); + } +} + +void BitCrusher::setSteps() +{ + m_steps = std::floor((std::pow(2.0f, m_bitDepth))); +} + void BitCrusher::setRatio() { m_ratio = m_downsampleRate / m_sampleRate; diff --git a/Source/BitCrusher.h b/Source/BitCrusher.h index 5ebc529..89f0a5a 100644 --- a/Source/BitCrusher.h +++ b/Source/BitCrusher.h @@ -5,17 +5,19 @@ class BitCrusher { public: - BitCrusher(float bitDepth = 16.0f, float downsampleRate = 44100.0f); + BitCrusher(float bitDepth = 12.0f, float downsampleRate = 22050.0f); ~BitCrusher() = default; - void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void prepareToPlay(double sampleRate) noexcept; void reset() noexcept; float processSample(const float& in); + float bitReduce(const float& sample); float downsample(const float& sample); void update(const Parameters& params) noexcept; void setBitDepth(float bitDepth); void setDownsampleRate(float downsampleRate); void setRatio(); + void setSteps(); void setMix(float mix) { m_mix = mix; } private: @@ -26,6 +28,7 @@ class BitCrusher float m_currentSample = 0.0f; float m_mix = 0.0f; float m_accumulator = 0.0f; + float m_steps = 0.0f; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BitCrusher) }; From 39db4f4cb03ad5198fa9722494182629724cd3e1 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 11:04:03 +0300 Subject: [PATCH 3/8] refactor BitCrusher class to a dsp primitive to be used by LofiEngine class --- Source/BitCrusher.cpp | 47 ++++++++++++++++--------------------------- Source/BitCrusher.h | 9 ++------- 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/Source/BitCrusher.cpp b/Source/BitCrusher.cpp index 8ac8e72..795c29f 100644 --- a/Source/BitCrusher.cpp +++ b/Source/BitCrusher.cpp @@ -5,11 +5,11 @@ BitCrusher::BitCrusher(float bitDepth, float downsampleRate) { } -void BitCrusher::prepareToPlay(double sampleRate) noexcept +void BitCrusher::prepareToPlay(float sampleRate) noexcept { - m_sampleRate = static_cast(sampleRate); - setRatio(); - setSteps(); + m_sampleRate = sampleRate; + setBitDepth(m_bitDepth); + setDownsampleRate(m_downsampleRate); } void BitCrusher::reset() noexcept @@ -24,6 +24,12 @@ float BitCrusher::processSample(const float& in) return downsample(out); } +float BitCrusher::bitReduce(const float& sample) +{ + float rounded = std::ceil(sample * m_steps); + return rounded / m_steps; +} + float BitCrusher::downsample(const float& sample) { m_accumulator += m_ratio; @@ -34,40 +40,21 @@ float BitCrusher::downsample(const float& sample) return m_currentSample; } -void BitCrusher::update(const Parameters& params) noexcept -{ - setDownsampleRate(params.bitCrusherSampleRate()); - setBitDepth(params.bitCrusherBitDepth()); -} - void BitCrusher::setDownsampleRate(float downsampleRate) { - if (!juce::approximatelyEqual(downsampleRate, m_downsampleRate)) { - m_downsampleRate = downsampleRate; - setRatio(); + m_downsampleRate = downsampleRate; + if (m_sampleRate > 0.0f) { + m_ratio = m_downsampleRate / m_sampleRate; + } + else { + m_ratio = 0.0f; } } -float BitCrusher::bitReduce(const float& sample) -{ - float rounded = std::ceil(sample * m_steps); - return rounded / m_steps; -} void BitCrusher::setBitDepth(float bitDepth) { - if (!juce::approximatelyEqual(bitDepth, m_bitDepth)) { - m_bitDepth = bitDepth; - setSteps(); - } -} - -void BitCrusher::setSteps() -{ + m_bitDepth = bitDepth; m_steps = std::floor((std::pow(2.0f, m_bitDepth))); } -void BitCrusher::setRatio() -{ - m_ratio = m_downsampleRate / m_sampleRate; -} \ No newline at end of file diff --git a/Source/BitCrusher.h b/Source/BitCrusher.h index 89f0a5a..d8382e2 100644 --- a/Source/BitCrusher.h +++ b/Source/BitCrusher.h @@ -5,20 +5,16 @@ class BitCrusher { public: - BitCrusher(float bitDepth = 12.0f, float downsampleRate = 22050.0f); + BitCrusher(float bitDepth = 24.0f, float downsampleRate = 44100.0f); ~BitCrusher() = default; - void prepareToPlay(double sampleRate) noexcept; + void prepareToPlay(float sampleRate) noexcept; void reset() noexcept; float processSample(const float& in); float bitReduce(const float& sample); float downsample(const float& sample); - void update(const Parameters& params) noexcept; void setBitDepth(float bitDepth); void setDownsampleRate(float downsampleRate); - void setRatio(); - void setSteps(); - void setMix(float mix) { m_mix = mix; } private: float m_sampleRate = 0.0f; @@ -26,7 +22,6 @@ class BitCrusher float m_downsampleRate = 0.0f; float m_ratio = 0.0f; float m_currentSample = 0.0f; - float m_mix = 0.0f; float m_accumulator = 0.0f; float m_steps = 0.0f; From e6733f2b1c79556b80fe00c512876580f2b42bde Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 11:27:19 +0300 Subject: [PATCH 4/8] use round instead of trunc in bit reduction to avoid feedback self oscillation due to dc offset --- Source/BitCrusher.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/BitCrusher.cpp b/Source/BitCrusher.cpp index 795c29f..dc16fae 100644 --- a/Source/BitCrusher.cpp +++ b/Source/BitCrusher.cpp @@ -26,7 +26,7 @@ float BitCrusher::processSample(const float& in) float BitCrusher::bitReduce(const float& sample) { - float rounded = std::ceil(sample * m_steps); + float rounded = std::round(sample * m_steps); return rounded / m_steps; } From 8116f148998130912d56ffa1b57d356f85ea9e3a Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 11:30:05 +0300 Subject: [PATCH 5/8] add and implement lofiengine class, add lofi control parameters --- CMakeLists.txt | 2 ++ Source/LofiEngine.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++ Source/LofiEngine.h | 29 ++++++++++++++++++++++ Source/Parameters.cpp | 41 +++++++++++++++++++++++++++++++ Source/Parameters.h | 22 +++++++++++++++-- 5 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 Source/LofiEngine.cpp create mode 100644 Source/LofiEngine.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0aa96b0..61bb47e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,6 +68,8 @@ get_property(project_targets DIRECTORY "${PROJECT_SOURCE_DIR}" PROPERTY BUILDSYS set_target_properties(${project_targets} PROPERTIES CXX_EXTENSIONS OFF) target_sources(viive PRIVATE + Source/BitCrusher.cpp + Source/LofiEngine.cpp Source/FilterEngine.cpp Source/Tempo.cpp Source/Selector.cpp diff --git a/Source/LofiEngine.cpp b/Source/LofiEngine.cpp new file mode 100644 index 0000000..561eba3 --- /dev/null +++ b/Source/LofiEngine.cpp @@ -0,0 +1,56 @@ +#include "LofiEngine.h" + +LofiEngine::LofiEngine() +{ +} + +void LofiEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +{ + m_sampleRate = static_cast(sampleRate); + m_bitCrusherL.prepareToPlay(m_sampleRate); + m_bitCrusherR.prepareToPlay(m_sampleRate); +} + +void LofiEngine::reset() noexcept +{ + m_bitCrusherL.reset(); + m_bitCrusherR.reset(); +} + +void LofiEngine::update(const Parameters& params) +{ + setBitDepth(params.lofiBitDepth()); + setDownsampleRate(params.lofiSampleRate()); + setMixLevel(params.lofiMixLevel()); +} + +void LofiEngine::processSample(const float& inL, const float& inR, float& outL, float& outR) +{ + float processedL = m_bitCrusherL.processSample(inL); + float processedR = m_bitCrusherR.processSample(inR); + outL = inL * (1.0f - m_mixLevel) + processedL * m_mixLevel; + outR = inR * (1.0f - m_mixLevel) + processedR * m_mixLevel; +} + +void LofiEngine::setBitDepth(float bitDepth) +{ + if (!juce::approximatelyEqual(bitDepth, m_bitDepth)) { + m_bitDepth = bitDepth; + m_bitCrusherL.setBitDepth(m_bitDepth); + m_bitCrusherR.setBitDepth(m_bitDepth); + } +} + +void LofiEngine::setDownsampleRate(float downsampleRate) +{ + if (!juce::approximatelyEqual(downsampleRate, m_downsampleRate)) { + m_downsampleRate = downsampleRate; + m_bitCrusherL.setDownsampleRate(m_downsampleRate); + m_bitCrusherR.setDownsampleRate(m_downsampleRate); + } +} + +void LofiEngine::setMixLevel(float mixLevel) +{ + m_mixLevel = mixLevel; +} \ No newline at end of file diff --git a/Source/LofiEngine.h b/Source/LofiEngine.h new file mode 100644 index 0000000..6b78a3a --- /dev/null +++ b/Source/LofiEngine.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include "BitCrusher.h" + +class LofiEngine +{ +public: + LofiEngine(); + ~LofiEngine() = default; + + void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void reset() noexcept; + void processSample(const float& inL, const float& inR, float& outL, float& outR); + void update(const Parameters& params); + +private: + BitCrusher m_bitCrusherL{ 12.0f, 22050.0f }; + BitCrusher m_bitCrusherR{ 12.0f, 22050.0f }; + + float m_sampleRate = 0.0f; + float m_mixLevel = 1.0f; + float m_bitDepth = 0.0f; + float m_downsampleRate = 0.0f; + + void setBitDepth(float bitDepth); + void setDownsampleRate(float downsampleRate); + void setMixLevel(float mixLevel); +}; \ No newline at end of file diff --git a/Source/Parameters.cpp b/Source/Parameters.cpp index 66f181d..668869d 100644 --- a/Source/Parameters.cpp +++ b/Source/Parameters.cpp @@ -166,6 +166,9 @@ Parameters::Parameters(juce::AudioProcessorValueTreeState& apvts) castParameter(apvts, highCutModInvertParamID, m_highCutModInvertParam); castParameter(apvts, chorusModRateParamID, m_chorusModRateParam); castParameter(apvts, chorusModDepthParamID, m_chorusModDepthParam); + castParameter(apvts, lofiSampleRateParamID, m_lofiSampleRateParam); + castParameter(apvts, lofiBitDepthParamID, m_lofiBitDepthParam); + castParameter(apvts, lofiMixLevelParamID, m_lofiMixLevelParam); } juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterLayout() @@ -393,6 +396,29 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL juce::NormalisableRange(.5f, 6.0f, 0.01f), defaultChorusModDepth )); + layout.add(std::make_unique( + lofiSampleRateParamID.getParamID(), + "Lofi Sample Rate", + juce::NormalisableRange(minLofiSampleRate, maxLofiSampleRate, 1.0f), + maxLofiSampleRate, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromHertz) + .withValueFromStringFunction(hzFromString) + )); + layout.add(std::make_unique( + lofiBitDepthParamID.getParamID(), + "Lofi Bit Depth", + juce::NormalisableRange(minLofiBitDepth, maxLofiBitDepth, 1.0f), + maxLofiBitDepth + )); + layout.add(std::make_unique( + lofiMixLevelParamID.getParamID(), + "Lofi Mix Level", + juce::NormalisableRange(0.0f, 100.0f, 1.0f), + 100.0f, + juce::AudioParameterFloatAttributes() + .withStringFromValueFunction(stringFromPercent) + )); return layout; } @@ -417,6 +443,9 @@ void Parameters::prepareToPlay(double sampleRate) noexcept m_highCutModPhaseSmoother.reset(sampleRate, duration); m_chorusModRateSmoother.reset(sampleRate, duration); m_chorusModDepthSmoother.reset(sampleRate, duration); + m_lofiSampleRateSmoother.reset(sampleRate, duration); + m_lofiBitDepthSmoother.reset(sampleRate, duration); + m_lofiMixLevelSmoother.reset(sampleRate, duration); } void Parameters::reset() noexcept @@ -441,6 +470,9 @@ void Parameters::reset() noexcept m_highCutModPhase = 0.0f; m_chorusModRate = defaultChorusModRate; m_chorusModDepth = defaultChorusModDepth; + m_lofiSampleRate = maxLofiSampleRate; + m_lofiBitDepth = maxLofiBitDepth; + m_lofiMixLevel = 1.0f; m_gainSmoother.setCurrentAndTargetValue(juce::Decibels::decibelsToGain(m_gainParam->get())); m_mixSmoother.setCurrentAndTargetValue(m_mixParam->get() * 0.01f); m_feedbackSmoother.setCurrentAndTargetValue(m_feedbackParam->get() * 0.01f); @@ -458,6 +490,9 @@ void Parameters::reset() noexcept m_highCutModPhaseSmoother.setCurrentAndTargetValue(m_highCutModPhaseParam->get()); m_chorusModRateSmoother.setCurrentAndTargetValue(m_chorusModRateParam->get()); m_chorusModDepthSmoother.setCurrentAndTargetValue(m_chorusModDepthParam->get()); + m_lofiSampleRateSmoother.setCurrentAndTargetValue(m_lofiSampleRateParam->get()); + m_lofiBitDepthSmoother.setCurrentAndTargetValue(m_lofiBitDepthParam->get()); + m_lofiMixLevelSmoother.setCurrentAndTargetValue(m_lofiMixLevelParam->get() * 0.01f); } void Parameters::update(const Tempo& tempo) noexcept @@ -480,6 +515,9 @@ void Parameters::update(const Tempo& tempo) noexcept m_highCutModPhaseSmoother.setTargetValue(m_highCutModPhaseParam->get()); m_chorusModRateSmoother.setTargetValue(m_chorusModRateParam->get()); m_chorusModDepthSmoother.setTargetValue(m_chorusModDepthParam->get()); + m_lofiSampleRateSmoother.setTargetValue(m_lofiSampleRateParam->get()); + m_lofiBitDepthSmoother.setTargetValue(m_lofiBitDepthParam->get()); + m_lofiMixLevelSmoother.setTargetValue(m_lofiMixLevelParam->get() * 0.01f); m_tempoSyncL = m_tempoSyncLParam->get(); m_tempoSyncR = m_tempoSyncRParam->get(); m_delayNoteL = m_delayNoteLParam->getIndex(); @@ -532,4 +570,7 @@ void Parameters::smoothen() noexcept m_highCutModPhase = m_highCutModPhaseSmoother.getNextValue(); m_chorusModRate = m_chorusModRateSmoother.getNextValue(); m_chorusModDepth = m_chorusModDepthSmoother.getNextValue(); + m_lofiSampleRate = m_lofiSampleRateSmoother.getNextValue(); + m_lofiBitDepth = m_lofiBitDepthSmoother.getNextValue(); + m_lofiMixLevel = m_lofiMixLevelSmoother.getNextValue(); } \ No newline at end of file diff --git a/Source/Parameters.h b/Source/Parameters.h index f9dfbc4..49b9e12 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -34,6 +34,9 @@ const juce::ParameterID highCutModInvertParamID{ "highCutModInvert", 1 }; const juce::ParameterID chorusIntensityParamID{ "chorusIntensity", 1 }; const juce::ParameterID chorusModRateParamID{ "chorusModRate", 1 }; const juce::ParameterID chorusModDepthParamID{ "chorusModDepth", 1 }; +const juce::ParameterID lofiSampleRateParamID{ "lofiSampleRate", 1 }; +const juce::ParameterID lofiBitDepthParamID{ "lofiBitDepth", 1 }; +const juce::ParameterID lofiMixLevelParamID{ "lofiMixLevel", 1 }; class Parameters { public: @@ -51,6 +54,10 @@ class Parameters { 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 constexpr float maxLofiSampleRate = 22025.0f; + static constexpr float minLofiSampleRate = 100.0f; + static constexpr float maxLofiBitDepth = 12.0f; + static constexpr float minLofiBitDepth = 1.0f; static const juce::StringArray delayModes; static const juce::StringArray delayNoteLengths; static const juce::StringArray modNoteLengths; @@ -93,8 +100,10 @@ class Parameters { float chorusIntensity() const { return m_chorusIntensity; } float chorusModRate() const { return m_chorusModRate; } float chorusModDepth() const { return m_chorusModDepth; } - -private: + float lofiSampleRate() const { return m_lofiSampleRate; } + float lofiBitDepth() const { return m_lofiBitDepth; } + float lofiMixLevel() const { return m_lofiMixLevel; } + private: juce::AudioParameterFloat* m_gainParam; juce::AudioParameterFloat* m_delayTimeLParam; juce::AudioParameterFloat* m_delayTimeRParam; @@ -127,6 +136,9 @@ class Parameters { juce::AudioParameterFloat* m_chorusIntensityParam; juce::AudioParameterFloat* m_chorusModRateParam; juce::AudioParameterFloat* m_chorusModDepthParam; + juce::AudioParameterFloat* m_lofiSampleRateParam; + juce::AudioParameterFloat* m_lofiBitDepthParam; + juce::AudioParameterFloat* m_lofiMixLevelParam; juce::LinearSmoothedValue m_gainSmoother; juce::LinearSmoothedValue m_mixSmoother; @@ -146,6 +158,9 @@ class Parameters { juce::LinearSmoothedValue m_chorusIntensitySmoother; juce::LinearSmoothedValue m_chorusModRateSmoother; juce::LinearSmoothedValue m_chorusModDepthSmoother; + juce::LinearSmoothedValue m_lofiSampleRateSmoother; + juce::LinearSmoothedValue m_lofiBitDepthSmoother; + juce::LinearSmoothedValue m_lofiMixLevelSmoother; float m_gain = 0.0f; float m_delayTimeL = 0.0f; @@ -179,6 +194,9 @@ class Parameters { float m_chorusIntensity = 0.0f; float m_chorusModRate = defaultChorusModRate; float m_chorusModDepth = defaultChorusModDepth; + float m_lofiSampleRate = maxLofiSampleRate; + float m_lofiBitDepth = maxLofiBitDepth; + float m_lofiMixLevel = 1.0f; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(Parameters) }; \ No newline at end of file From 8a121646d64e4118a79c32ece9daa1e7b73e5d09 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 14:18:12 +0300 Subject: [PATCH 6/8] change bitcrusher processing order: first downsample, then bitreduce --- Source/BitCrusher.cpp | 4 ++-- Source/LofiEngine.h | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/BitCrusher.cpp b/Source/BitCrusher.cpp index dc16fae..12685e8 100644 --- a/Source/BitCrusher.cpp +++ b/Source/BitCrusher.cpp @@ -20,8 +20,8 @@ void BitCrusher::reset() noexcept float BitCrusher::processSample(const float& in) { - float out = bitReduce(in); - return downsample(out); + float out = downsample(in); + return bitReduce(out); } float BitCrusher::bitReduce(const float& sample) diff --git a/Source/LofiEngine.h b/Source/LofiEngine.h index 6b78a3a..f948ace 100644 --- a/Source/LofiEngine.h +++ b/Source/LofiEngine.h @@ -26,4 +26,6 @@ class LofiEngine void setBitDepth(float bitDepth); void setDownsampleRate(float downsampleRate); void setMixLevel(float mixLevel); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BitCrusher) }; \ No newline at end of file From 0395bf97a2746dcd57eeafde76f0c33402d85888 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 14:27:24 +0300 Subject: [PATCH 7/8] fix copypaste fail in LofiEngine.h, update changelog --- CHANGELOG.md | 10 ++++++++++ Source/LofiEngine.h | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e36365..3f373b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## \[Unreleased] + +### Added + +* BitCrusher DSP primitive + * Performs first downsampling and then bit reduction +* LofiEngine class which handles Left and Right channel BitCrushers and parameters +* Lofi Mix, SampleRate and BitDepth parameters + + ## \[0.2.0] - 2026-05-18 ### Added diff --git a/Source/LofiEngine.h b/Source/LofiEngine.h index f948ace..c046058 100644 --- a/Source/LofiEngine.h +++ b/Source/LofiEngine.h @@ -27,5 +27,5 @@ class LofiEngine void setDownsampleRate(float downsampleRate); void setMixLevel(float mixLevel); - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BitCrusher) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LofiEngine) }; \ No newline at end of file From 28710a48290d31a03daf8c09c43af9477c9c7846 Mon Sep 17 00:00:00 2001 From: Otto Andelin Date: Wed, 20 May 2026 14:47:32 +0300 Subject: [PATCH 8/8] fix max samplerate parameter typo, make bitreduce and downsample functions private, remove samplersperblock param from lofiengine prepare function --- CHANGELOG.md | 1 - Source/BitCrusher.cpp | 1 - Source/BitCrusher.h | 5 +++-- Source/LofiEngine.cpp | 2 +- Source/LofiEngine.h | 2 +- Source/Parameters.h | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f373b9..0ea3cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,6 @@ All notable changes to this project will be documented in this file. * LofiEngine class which handles Left and Right channel BitCrushers and parameters * Lofi Mix, SampleRate and BitDepth parameters - ## \[0.2.0] - 2026-05-18 ### Added diff --git a/Source/BitCrusher.cpp b/Source/BitCrusher.cpp index 12685e8..3bb1fc1 100644 --- a/Source/BitCrusher.cpp +++ b/Source/BitCrusher.cpp @@ -51,7 +51,6 @@ void BitCrusher::setDownsampleRate(float downsampleRate) } } - void BitCrusher::setBitDepth(float bitDepth) { m_bitDepth = bitDepth; diff --git a/Source/BitCrusher.h b/Source/BitCrusher.h index d8382e2..3c05a4a 100644 --- a/Source/BitCrusher.h +++ b/Source/BitCrusher.h @@ -11,8 +11,6 @@ class BitCrusher void prepareToPlay(float sampleRate) noexcept; void reset() noexcept; float processSample(const float& in); - float bitReduce(const float& sample); - float downsample(const float& sample); void setBitDepth(float bitDepth); void setDownsampleRate(float downsampleRate); @@ -25,6 +23,9 @@ class BitCrusher float m_accumulator = 0.0f; float m_steps = 0.0f; + float bitReduce(const float& sample); + float downsample(const float& sample); + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(BitCrusher) }; diff --git a/Source/LofiEngine.cpp b/Source/LofiEngine.cpp index 561eba3..d6eb4d0 100644 --- a/Source/LofiEngine.cpp +++ b/Source/LofiEngine.cpp @@ -4,7 +4,7 @@ LofiEngine::LofiEngine() { } -void LofiEngine::prepareToPlay(double sampleRate, int samplesPerBlock) noexcept +void LofiEngine::prepareToPlay(double sampleRate) noexcept { m_sampleRate = static_cast(sampleRate); m_bitCrusherL.prepareToPlay(m_sampleRate); diff --git a/Source/LofiEngine.h b/Source/LofiEngine.h index c046058..3469ec8 100644 --- a/Source/LofiEngine.h +++ b/Source/LofiEngine.h @@ -9,7 +9,7 @@ class LofiEngine LofiEngine(); ~LofiEngine() = default; - void prepareToPlay(double sampleRate, int samplesPerBlock) noexcept; + void prepareToPlay(double sampleRate) noexcept; void reset() noexcept; void processSample(const float& inL, const float& inR, float& outL, float& outR); void update(const Parameters& params); diff --git a/Source/Parameters.h b/Source/Parameters.h index 49b9e12..995465f 100644 --- a/Source/Parameters.h +++ b/Source/Parameters.h @@ -54,7 +54,7 @@ class Parameters { 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 constexpr float maxLofiSampleRate = 22025.0f; + static constexpr float maxLofiSampleRate = 22050.0f; static constexpr float minLofiSampleRate = 100.0f; static constexpr float maxLofiBitDepth = 12.0f; static constexpr float minLofiBitDepth = 1.0f;