diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e36365..0ea3cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ 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/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/BitCrusher.cpp b/Source/BitCrusher.cpp new file mode 100644 index 0000000..3bb1fc1 --- /dev/null +++ b/Source/BitCrusher.cpp @@ -0,0 +1,59 @@ +#include "BitCrusher.h" + +BitCrusher::BitCrusher(float bitDepth, float downsampleRate) + : m_bitDepth(bitDepth), m_downsampleRate(downsampleRate) +{ +} + +void BitCrusher::prepareToPlay(float sampleRate) noexcept +{ + m_sampleRate = sampleRate; + setBitDepth(m_bitDepth); + setDownsampleRate(m_downsampleRate); +} + +void BitCrusher::reset() noexcept +{ + m_currentSample = 0.0f; + m_accumulator = 0.0f; +} + +float BitCrusher::processSample(const float& in) +{ + float out = downsample(in); + return bitReduce(out); +} + +float BitCrusher::bitReduce(const float& sample) +{ + float rounded = std::round(sample * m_steps); + return rounded / m_steps; +} + +float BitCrusher::downsample(const float& sample) +{ + m_accumulator += m_ratio; + if (m_accumulator >= 1.0f) { + m_accumulator -= 1.0f; + m_currentSample = sample; + } + return m_currentSample; +} + +void BitCrusher::setDownsampleRate(float downsampleRate) +{ + m_downsampleRate = downsampleRate; + if (m_sampleRate > 0.0f) { + m_ratio = m_downsampleRate / m_sampleRate; + } + else { + m_ratio = 0.0f; + } +} + +void BitCrusher::setBitDepth(float bitDepth) +{ + m_bitDepth = bitDepth; + m_steps = std::floor((std::pow(2.0f, m_bitDepth))); +} + diff --git a/Source/BitCrusher.h b/Source/BitCrusher.h new file mode 100644 index 0000000..3c05a4a --- /dev/null +++ b/Source/BitCrusher.h @@ -0,0 +1,31 @@ +#pragma once +#include +#include "Parameters.h" + +class BitCrusher +{ +public: + BitCrusher(float bitDepth = 24.0f, float downsampleRate = 44100.0f); + ~BitCrusher() = default; + + void prepareToPlay(float sampleRate) noexcept; + void reset() noexcept; + float processSample(const float& in); + void setBitDepth(float bitDepth); + void setDownsampleRate(float downsampleRate); + +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_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 new file mode 100644 index 0000000..d6eb4d0 --- /dev/null +++ b/Source/LofiEngine.cpp @@ -0,0 +1,56 @@ +#include "LofiEngine.h" + +LofiEngine::LofiEngine() +{ +} + +void LofiEngine::prepareToPlay(double sampleRate) 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..3469ec8 --- /dev/null +++ b/Source/LofiEngine.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "BitCrusher.h" + +class LofiEngine +{ +public: + LofiEngine(); + ~LofiEngine() = default; + + 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); + +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); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LofiEngine) +}; \ 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..995465f 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 = 22050.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