Skip to content
Merged
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions Source/BitCrusher.cpp
Original file line number Diff line number Diff line change
@@ -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)));
}

31 changes: 31 additions & 0 deletions Source/BitCrusher.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once
#include <JuceHeader.h>
#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)
};

56 changes: 56 additions & 0 deletions Source/LofiEngine.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#include "LofiEngine.h"

LofiEngine::LofiEngine()
{
}

void LofiEngine::prepareToPlay(double sampleRate) noexcept
{
m_sampleRate = static_cast<float>(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;
}
31 changes: 31 additions & 0 deletions Source/LofiEngine.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once

#include <JuceHeader.h>
#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)
};
41 changes: 41 additions & 0 deletions Source/Parameters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -393,6 +396,29 @@ juce::AudioProcessorValueTreeState::ParameterLayout Parameters::createParameterL
juce::NormalisableRange<float>(.5f, 6.0f, 0.01f),
defaultChorusModDepth
));
layout.add(std::make_unique<juce::AudioParameterFloat>(
lofiSampleRateParamID.getParamID(),
"Lofi Sample Rate",
juce::NormalisableRange<float>(minLofiSampleRate, maxLofiSampleRate, 1.0f),
maxLofiSampleRate,
juce::AudioParameterFloatAttributes()
.withStringFromValueFunction(stringFromHertz)
.withValueFromStringFunction(hzFromString)
));
layout.add(std::make_unique<juce::AudioParameterFloat>(
lofiBitDepthParamID.getParamID(),
"Lofi Bit Depth",
juce::NormalisableRange<float>(minLofiBitDepth, maxLofiBitDepth, 1.0f),
maxLofiBitDepth
));
layout.add(std::make_unique<juce::AudioParameterFloat>(
lofiMixLevelParamID.getParamID(),
"Lofi Mix Level",
juce::NormalisableRange<float>(0.0f, 100.0f, 1.0f),
100.0f,
juce::AudioParameterFloatAttributes()
.withStringFromValueFunction(stringFromPercent)
));
return layout;
}

Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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();
}
22 changes: 20 additions & 2 deletions Source/Parameters.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<float> m_gainSmoother;
juce::LinearSmoothedValue<float> m_mixSmoother;
Expand All @@ -146,6 +158,9 @@ class Parameters {
juce::LinearSmoothedValue<float> m_chorusIntensitySmoother;
juce::LinearSmoothedValue<float> m_chorusModRateSmoother;
juce::LinearSmoothedValue<float> m_chorusModDepthSmoother;
juce::LinearSmoothedValue<float> m_lofiSampleRateSmoother;
juce::LinearSmoothedValue<float> m_lofiBitDepthSmoother;
juce::LinearSmoothedValue<float> m_lofiMixLevelSmoother;

float m_gain = 0.0f;
float m_delayTimeL = 0.0f;
Expand Down Expand Up @@ -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)
};
Loading