BogaudioModules

BogaudioModules for VCV Rack
Log | Files | Refs | README | LICENSE

commit 1188f4bd42cdcff5dfa5cfc4316cc24ef92d383d
parent 4df3d8b82b616b214e7b05e584b66a00da298e50
Author: Matt Demanett <matt@demanett.net>
Date:   Sun, 22 Apr 2018 18:42:18 -0400

Slew limiter dsp; use to slighlty smooth abrupt CV changes in oscillators, avoid pops.

Diffstat:
Abenchmarks/signal_benchmark.cpp | 42++++++++++++++++++++++++++++++++++++++++++
Msrc/Additator.cpp | 26+++++++++++++++++---------
Msrc/Additator.hpp | 8++++++++
Msrc/FMOp.cpp | 12++++++++----
Msrc/FMOp.hpp | 6++++++
Msrc/Test.cpp | 8++++++++
Msrc/Test.hpp | 7++++++-
Msrc/VCO.cpp | 3++-
Msrc/VCO.hpp | 4+++-
Msrc/XCO.cpp | 47++++++++++++++++++++++++++++++++++-------------
Msrc/XCO.hpp | 17++++++++++++++++-
Msrc/dsp/signal.cpp | 30++++++++++++++++++++++++++++++
Msrc/dsp/signal.hpp | 17+++++++++++++++++
13 files changed, 197 insertions(+), 30 deletions(-)

diff --git a/benchmarks/signal_benchmark.cpp b/benchmarks/signal_benchmark.cpp @@ -0,0 +1,42 @@ + +#include <benchmark/benchmark.h> + +#include "dsp/noise.hpp" +#include "dsp/signal.hpp" + +using namespace bogaudio::dsp; + +static void BM_SlewLimiter_Fast(benchmark::State& state) { + WhiteNoiseGenerator r; + const int n = 256; + float buf[n]; + for (int i = 0; i < n; ++i) { + buf[i] = r.next(); + } + SlewLimiter sl(44100.0, 1.0f); + int i = 0; + for (auto _ : state) { + i = ++i % n; + benchmark::DoNotOptimize(sl.next(buf[i])); + } +} +BENCHMARK(BM_SlewLimiter_Fast); + +static void BM_SlewLimiter_Slow(benchmark::State& state) { + WhiteNoiseGenerator r; + const int n = 256; + float buf[n]; + for (int i = 0; i < n; ++i) { + buf[i] = r.next(); + } + SlewLimiter sl(44100.0, 1.0f); + int i = 0, j = 0; + for (auto _ : state) { + i = ++i % n; + if (i == 0) { + j = ++j % n; + } + benchmark::DoNotOptimize(sl.next(buf[j])); + } +} +BENCHMARK(BM_SlewLimiter_Slow); diff --git a/src/Additator.cpp b/src/Additator.cpp @@ -8,10 +8,18 @@ void Additator::onReset() { } void Additator::onSampleRateChange() { - _oscillator.setSampleRate(engineGetSampleRate()); - _maxFrequency = 0.47f * _oscillator._sampleRate; + float sampleRate = engineGetSampleRate(); + _oscillator.setSampleRate(sampleRate); + _maxFrequency = 0.47f * sampleRate; _steps = modulationSteps; _phase = PHASE_RESET; + _widthSL.setParams(sampleRate, slewLimitTime);; + _oddSkewSL.setParams(sampleRate, slewLimitTime);; + _evenSkewSL.setParams(sampleRate, slewLimitTime);; + _amplitudeNormalizationSL.setParams(sampleRate, slewLimitTime);; + _decaySL.setParams(sampleRate, slewLimitTime);; + _balanceSL.setParams(sampleRate, slewLimitTime);; + _filterSL.setParams(sampleRate, slewLimitTime);; } float Additator::cvValue(Input& cv, bool dc) { @@ -38,9 +46,9 @@ void Additator::step() { if (_steps >= modulationSteps) { _steps = 0; - float width = clamp(params[WIDTH_PARAM].value + (maxWidth / 2.0f) * cvValue(inputs[WIDTH_INPUT]), 0.0f, maxWidth); - float oddSkew = clamp(params[ODD_SKEW_PARAM].value + cvValue(inputs[ODD_SKEW_INPUT]), -maxSkew, maxSkew); - float evenSkew = clamp(params[EVEN_SKEW_PARAM].value + cvValue(inputs[EVEN_SKEW_INPUT]), -maxSkew, maxSkew); + float width = _widthSL.next(clamp(params[WIDTH_PARAM].value + (maxWidth / 2.0f) * cvValue(inputs[WIDTH_INPUT]), 0.0f, maxWidth)); + float oddSkew = _oddSkewSL.next(clamp(params[ODD_SKEW_PARAM].value + cvValue(inputs[ODD_SKEW_INPUT]), -maxSkew, maxSkew)); + float evenSkew = _evenSkewSL.next(clamp(params[EVEN_SKEW_PARAM].value + cvValue(inputs[EVEN_SKEW_INPUT]), -maxSkew, maxSkew)); if ( _width != width || _oddSkew != oddSkew || @@ -65,10 +73,10 @@ void Additator::step() { } int partials = clamp((int)roundf(params[PARTIALS_PARAM].value * cvValue(inputs[PARTIALS_INPUT], true)), 0, maxPartials); - float amplitudeNormalization = clamp(params[GAIN_PARAM].value + ((maxAmplitudeNormalization - minAmplitudeNormalization) / 2.0f) * cvValue(inputs[GAIN_INPUT]), minAmplitudeNormalization, maxAmplitudeNormalization); - float decay = clamp(params[DECAY_PARAM].value + ((maxDecay - minDecay) / 2.0f) * cvValue(inputs[DECAY_INPUT]), minDecay, maxDecay); - float balance = clamp(params[BALANCE_PARAM].value + cvValue(inputs[BALANCE_INPUT]), -1.0f, 1.0f); - float filter = clamp(params[FILTER_PARAM].value + cvValue(inputs[FILTER_INPUT]), minFilter, maxFilter); + float amplitudeNormalization = _amplitudeNormalizationSL.next(clamp(params[GAIN_PARAM].value + ((maxAmplitudeNormalization - minAmplitudeNormalization) / 2.0f) * cvValue(inputs[GAIN_INPUT]), minAmplitudeNormalization, maxAmplitudeNormalization)); + float decay = _decaySL.next(clamp(params[DECAY_PARAM].value + ((maxDecay - minDecay) / 2.0f) * cvValue(inputs[DECAY_INPUT]), minDecay, maxDecay)); + float balance = _balanceSL.next(clamp(params[BALANCE_PARAM].value + cvValue(inputs[BALANCE_INPUT]), -1.0f, 1.0f)); + float filter = _filterSL.next(clamp(params[FILTER_PARAM].value + cvValue(inputs[FILTER_INPUT]), minFilter, maxFilter)); if ( _partials != partials || _amplitudeNormalization != amplitudeNormalization || diff --git a/src/Additator.hpp b/src/Additator.hpp @@ -68,6 +68,7 @@ struct Additator : Module { const float maxDecay = 3.0f; const float minFilter = 0.1; const float maxFilter = 1.9; + const float slewLimitTime = 1.0f; int _steps = 0; int _partials = 0; @@ -82,6 +83,13 @@ struct Additator : Module { float _maxFrequency = 0.0f; SineBankOscillator _oscillator; PositiveZeroCrossing _syncTrigger; + SlewLimiter _widthSL; + SlewLimiter _oddSkewSL; + SlewLimiter _evenSkewSL; + SlewLimiter _amplitudeNormalizationSL; + SlewLimiter _decaySL; + SlewLimiter _balanceSL; + SlewLimiter _filterSL; Additator() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) diff --git a/src/FMOp.cpp b/src/FMOp.cpp @@ -16,6 +16,10 @@ void FMOp::onSampleRateChange() { _sineTable.setSampleRate(sampleRate); _decimator.setParams(sampleRate, oversample); _maxFrequency = 0.47f * sampleRate; + _feedbackSL.setParams(sampleRate, slewLimitTime); + _depthSL.setParams(sampleRate, slewLimitTime); + _levelSL.setParams(sampleRate, slewLimitTime); + _sustainSL.setParams(sampleRate, slewLimitTime / 10.0f); } void FMOp::step() { @@ -73,7 +77,7 @@ void FMOp::step() { } _envelope.setAttack(powf(params[ATTACK_PARAM].value, 2.0f) * 10.f); _envelope.setDecay(powf(params[DECAY_PARAM].value, 2.0f) * 10.f); - _envelope.setSustain(sustain); + _envelope.setSustain(_sustainSL.next(sustain)); _envelope.setRelease(powf(params[RELEASE_PARAM].value, 2.0f) * 10.f); } @@ -100,7 +104,7 @@ void FMOp::step() { envelope = _envelope.next(); } - float feedback = _feedback; + float feedback = _feedbackSL.next(_feedback); if (_feedbackEnvelopeOn) { feedback *= envelope; } @@ -110,13 +114,13 @@ void FMOp::step() { offset = feedback * _feedbackDelayedSample; } if (inputs[FM_INPUT].active) { - offset += inputs[FM_INPUT].value * _depth * 2.0f; + offset += inputs[FM_INPUT].value * _depthSL.next(_depth) * 2.0f; } for (int i = 0; i < oversample; ++i) { _phasor.advancePhase(); _buffer[i] = _sineTable.nextFromPhasor(_phasor, Phasor::radiansToPhase(offset)); } - float out = _level; + float out = _levelSL.next(_level); if (_levelEnvelopeOn) { out *= envelope; } diff --git a/src/FMOp.hpp b/src/FMOp.hpp @@ -4,6 +4,7 @@ #include "dsp/envelope.hpp" #include "dsp/filter.hpp" #include "dsp/oscillator.hpp" +#include "dsp/signal.hpp" using namespace bogaudio::dsp; @@ -52,6 +53,7 @@ struct FMOp : Module { const float amplitude = 5.0f; const int modulationSteps = 100; static constexpr int oversample = 8; + const float slewLimitTime = 1.0f; int _steps = 0; float _feedback = 0.0f; float _feedbackDelayedSample = 0.0f; @@ -67,6 +69,10 @@ struct FMOp : Module { SineTableOscillator _sineTable; LPFDecimator _decimator; SchmittTrigger _gateTrigger; + SlewLimiter _feedbackSL; + SlewLimiter _depthSL; + SlewLimiter _levelSL; + SlewLimiter _sustainSL; FMOp() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) diff --git a/src/Test.cpp b/src/Test.cpp @@ -339,6 +339,14 @@ void Test::step() { _table.setSampleRate(engineGetSampleRate()); _table.setFrequency(oscillatorPitch()); outputs[OUT2_OUTPUT].value = _table.next() * 5.0f; + +#elif SLEW + float ms = params[PARAM1_PARAM].value; + if (inputs[CV1_INPUT].active) { + ms *= clamp(inputs[CV2_INPUT].value, 0.0f, 10.0f) / 10.0f; + } + _slew.setParams(engineGetSampleRate(), powf(ms, 2.0f) * 100.0f); + outputs[OUT_OUTPUT].value = _slew.next(inputs[IN_INPUT].value); #endif } diff --git a/src/Test.hpp b/src/Test.hpp @@ -16,12 +16,13 @@ extern Model* modelTest; // #define OVERSAMPLING 1 // #define OVERSAMPLED_BL 1 // #define ANTIALIASING 1 -#define DECIMATORS 1 +// #define DECIMATORS 1 // #define FM 1 // #define PM 1 // #define FEEDBACK_PM 1 // #define EG 1 // #define TABLES 1 +#define SLEW 1 #include "pitch.hpp" #ifdef LPF @@ -69,6 +70,8 @@ extern Model* modelTest; #include "dsp/envelope.hpp" #elif TABLES #include "dsp/oscillator.hpp" +#elif SLEW +#include "dsp/signal.hpp" #else #error what #endif @@ -178,6 +181,8 @@ struct Test : Module { #elif TABLES SineTableOscillator _sine; TablePhasor _table; +#elif SLEW + SlewLimiter _slew; #endif Test() diff --git a/src/VCO.cpp b/src/VCO.cpp @@ -46,7 +46,7 @@ void VCO::step() { pw *= 1.0f - 2.0f * _square.minPulseWidth; pw *= 0.5f; pw += 0.5f; - _square.setPulseWidth(pw); + _square.setPulseWidth(_squarePulseWidthSL.next(pw)); _fmDepth = params[FM_PARAM].value; } @@ -140,6 +140,7 @@ void VCO::setSampleRate(float sampleRate) { _squareDecimator.setParams(sampleRate, oversample); _sawDecimator.setParams(sampleRate, oversample); _triangleDecimator.setParams(sampleRate, oversample); + _squarePulseWidthSL.setParams(sampleRate, slewLimitTime / 10.0f); } void VCO::setFrequency(float frequency) { diff --git a/src/VCO.hpp b/src/VCO.hpp @@ -46,6 +46,7 @@ struct VCO : Module { const int modulationSteps = 100; const float amplitude = 5.0f; static constexpr int oversample = 8; + const float slewLimitTime = 1.0f; int _modulationStep = 0; float _oversampleThreshold = 0.0f; float _frequency = 0.0f; @@ -54,7 +55,6 @@ struct VCO : Module { bool _slowMode = false; float _fmDepth = 0.0f; bool _fmLinearMode = false; - PositiveZeroCrossing _syncTrigger; Phasor _phasor; BandLimitedSquareOscillator _square; @@ -67,6 +67,8 @@ struct VCO : Module { float _squareBuffer[oversample]; float _sawBuffer[oversample]; float _triangleBuffer[oversample]; + PositiveZeroCrossing _syncTrigger; + SlewLimiter _squarePulseWidthSL; VCO() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { onReset(); diff --git a/src/XCO.cpp b/src/XCO.cpp @@ -47,34 +47,36 @@ void XCO::step() { pw *= 1.0f - 2.0f * _square.minPulseWidth; pw *= 0.5f; pw += 0.5f; - _square.setPulseWidth(pw); + _square.setPulseWidth(_squarePulseWidthSL.next(pw)); float saturation = params[SAW_SATURATION_PARAM].value; if (inputs[SAW_SATURATION_INPUT].active) { saturation *= clamp(inputs[SAW_SATURATION_INPUT].value / 10.0f, 0.0f, 1.0f); } - _saw.setSaturation(saturation * 10.f); + _saw.setSaturation(_sawSaturationSL.next(saturation) * 10.f); - _triangleSampleWidth = params[TRIANGLE_SAMPLE_PARAM].value * Phasor::maxSampleWidth; + float tsw = params[TRIANGLE_SAMPLE_PARAM].value * Phasor::maxSampleWidth; if (inputs[TRIANGLE_SAMPLE_INPUT].active) { - _triangleSampleWidth *= clamp(inputs[TRIANGLE_SAMPLE_INPUT].value / 10.0f, 0.0f, 1.0f); + tsw *= clamp(inputs[TRIANGLE_SAMPLE_INPUT].value / 10.0f, 0.0f, 1.0f); } + _triangleSampleWidth = _triangleSampleWidthSL.next(tsw); _triangle.setSampleWidth(_triangleSampleWidth); - _sineFeedback = params[SINE_FEEDBACK_PARAM].value; + float sfb = params[SINE_FEEDBACK_PARAM].value; if (inputs[SINE_FEEDBACK_INPUT].active) { - _sineFeedback *= clamp(inputs[SINE_FEEDBACK_INPUT].value / 10.0f, 0.0f, 1.0f); + sfb *= clamp(inputs[SINE_FEEDBACK_INPUT].value / 10.0f, 0.0f, 1.0f); } + _sineFeedback = _sineFeedbackSL.next(sfb); _fmDepth = params[FM_DEPTH_PARAM].value; if (inputs[FM_DEPTH_INPUT].active) { _fmDepth *= clamp(inputs[FM_DEPTH_INPUT].value / 10.0f, 0.0f, 1.0f); } - _squarePhaseOffset = phaseOffset(params[SQUARE_PHASE_PARAM], inputs[SQUARE_PHASE_INPUT]); - _sawPhaseOffset = phaseOffset(params[SAW_PHASE_PARAM], inputs[SAW_PHASE_INPUT]); - _trianglePhaseOffset = phaseOffset(params[TRIANGLE_PHASE_PARAM], inputs[TRIANGLE_PHASE_INPUT]); - _sinePhaseOffset = phaseOffset(params[SINE_PHASE_PARAM], inputs[SINE_PHASE_INPUT]); + _squarePhaseOffset = _squarePhaseOffsetSL.next(phaseOffset(params[SQUARE_PHASE_PARAM], inputs[SQUARE_PHASE_INPUT])); + _sawPhaseOffset = _sawPhaseOffsetSL.next(phaseOffset(params[SAW_PHASE_PARAM], inputs[SAW_PHASE_INPUT])); + _trianglePhaseOffset = _trianglePhaseOffsetSL.next(phaseOffset(params[TRIANGLE_PHASE_PARAM], inputs[TRIANGLE_PHASE_INPUT])); + _sinePhaseOffset = _sinePhaseOffsetSL.next(phaseOffset(params[SINE_PHASE_PARAM], inputs[SINE_PHASE_INPUT])); _squareMix = level(params[SQUARE_MIX_PARAM], inputs[SQUARE_MIX_INPUT]); _sawMix = level(params[SAW_MIX_PARAM], inputs[SAW_MIX_INPUT]); @@ -88,8 +90,9 @@ void XCO::step() { float frequency = _baseHz; Phasor::phase_delta_t phaseOffset = 0; - if (inputs[FM_INPUT].active && _fmDepth > 0.01f) { - float fm = inputs[FM_INPUT].value * _fmDepth; + float fmd = _fmDepthSL.next(_fmDepth); + if (inputs[FM_INPUT].active && fmd > 0.01f) { + float fm = inputs[FM_INPUT].value * fmd; if (_fmLinearMode) { phaseOffset = Phasor::radiansToPhase(2.0f * fm); } @@ -189,7 +192,9 @@ void XCO::step() { outputs[SAW_OUTPUT].value = sawOut; outputs[TRIANGLE_OUTPUT].value = triangleOut; outputs[SINE_OUTPUT].value = _sineFeedbackDelayedSample = sineOut; - outputs[MIX_OUTPUT].value = _squareMix * squareOut + _sawMix * sawOut + _triangleMix * triangleOut + _sineMix * sineOut; + if (outputs[MIX_OUTPUT].active) { + outputs[MIX_OUTPUT].value = _squareMixSL.next(_squareMix) * squareOut + _sawMixSL.next(_sawMix) * sawOut + _triangleMixSL.next(_triangleMix) * triangleOut + _sineMixSL.next(_sineMix) * sineOut; + } } Phasor::phase_delta_t XCO::phaseOffset(Param& param, Input& input) { @@ -210,13 +215,29 @@ float XCO::level(Param& param, Input& input) { void XCO::setSampleRate(float sampleRate) { _oversampleThreshold = 0.06f * sampleRate; + _phasor.setSampleRate(sampleRate); _square.setSampleRate(sampleRate); _saw.setSampleRate(sampleRate); + _squareDecimator.setParams(sampleRate, oversample); _sawDecimator.setParams(sampleRate, oversample); _triangleDecimator.setParams(sampleRate, oversample); _sineDecimator.setParams(sampleRate, oversample); + + _fmDepthSL.setParams(sampleRate, slewLimitTime); + _squarePulseWidthSL.setParams(sampleRate, slewLimitTime / 10.0f); + _sawSaturationSL.setParams(sampleRate, slewLimitTime / 10.0f); + _triangleSampleWidthSL.setParams(sampleRate, slewLimitTime / 10.0f); + _sineFeedbackSL.setParams(sampleRate, slewLimitTime / 10.0f); + _squarePhaseOffsetSL.setParams(sampleRate, slewLimitTime / 2.0f); + _sawPhaseOffsetSL.setParams(sampleRate, slewLimitTime / 2.0f); + _trianglePhaseOffsetSL.setParams(sampleRate, slewLimitTime / 2.0f); + _sinePhaseOffsetSL.setParams(sampleRate, slewLimitTime / 2.0f); + _squareMixSL.setParams(sampleRate, slewLimitTime); + _sawMixSL.setParams(sampleRate, slewLimitTime); + _triangleMixSL.setParams(sampleRate, slewLimitTime); + _sineMixSL.setParams(sampleRate, slewLimitTime); } void XCO::setFrequency(float frequency) { diff --git a/src/XCO.hpp b/src/XCO.hpp @@ -70,6 +70,7 @@ struct XCO : Module { const int modulationSteps = 100; const float amplitude = 5.0f; static constexpr int oversample = 8; + const float slewLimitTime = 1.0f; int _modulationStep = 0; float _oversampleThreshold = 0.0f; float _frequency = 0.0f; @@ -89,7 +90,6 @@ struct XCO : Module { float _sawMix = 1.0f; float _triangleMix = 1.0f; float _sineMix = 1.0f; - PositiveZeroCrossing _syncTrigger; Phasor _phasor; BandLimitedSquareOscillator _square; @@ -104,6 +104,21 @@ struct XCO : Module { float _sawBuffer[oversample]; float _triangleBuffer[oversample]; float _sineBuffer[oversample]; + PositiveZeroCrossing _syncTrigger; + + SlewLimiter _fmDepthSL; + SlewLimiter _squarePulseWidthSL; + SlewLimiter _sawSaturationSL; + SlewLimiter _triangleSampleWidthSL; + SlewLimiter _sineFeedbackSL; + SlewLimiter _squarePhaseOffsetSL; + SlewLimiter _sawPhaseOffsetSL; + SlewLimiter _trianglePhaseOffsetSL; + SlewLimiter _sinePhaseOffsetSL; + SlewLimiter _squareMixSL; + SlewLimiter _sawMixSL; + SlewLimiter _triangleMixSL; + SlewLimiter _sineMixSL; XCO() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { onReset(); diff --git a/src/dsp/signal.cpp b/src/dsp/signal.cpp @@ -1,4 +1,6 @@ +#include <assert.h> + #include "signal.hpp" using namespace bogaudio::dsp; @@ -40,3 +42,31 @@ bool PositiveZeroCrossing::next(float sample) { void PositiveZeroCrossing::reset() { _state = NEGATIVE_STATE; } + + +void SlewLimiter::setParams(float sampleRate, float milliseconds) { + assert(sampleRate > 0.0f); + assert(milliseconds >= 0.0f); + _sampleRate = sampleRate; + _milliseconds = milliseconds; + _samples = (_milliseconds / 1000.0f) * _sampleRate; +} + +float SlewLimiter::next(float sample) { + if (_samples < 2 || (sample > _current - 0.01f && sample < _current + 0.01f)) { + _current = sample; + return sample; + } + if (_target != sample) { + _target = sample; + _delta = (sample - _current) / _samples; + _steps = _samples; + } + else if (_steps <= 1) { + _current = sample; + return sample; + } + _current += _delta; + --_steps; + return _current; +} diff --git a/src/dsp/signal.hpp b/src/dsp/signal.hpp @@ -28,5 +28,22 @@ struct PositiveZeroCrossing { void reset(); }; +struct SlewLimiter { + float _sampleRate; + float _milliseconds; + float _samples; + float _current = 0.0f; + float _target = 0.0f; + int _steps = 0; + float _delta = 0.0f; + + SlewLimiter(float sampleRate = 1000.0f, float milliseconds = 1.0f) { + setParams(sampleRate, milliseconds); + } + + void setParams(float sampleRate, float milliseconds); + float next(float sample); +}; + } // namespace dsp } // namespace bogaudio