diff --git a/CMakeLists.txt b/CMakeLists.txt index 374991f199d..efe9b091ab8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -205,6 +205,8 @@ set(SURGE_SHARED_SOURCES src/common/dsp/effect/VocoderEffect.cpp src/common/dsp/effect/airwindows/AirWindowsEffect.cpp src/common/dsp/effect/airwindows/AirWindowsEffect.h + src/common/dsp/effect/chowdsp/Neuron.cpp + src/common/dsp/effect/chowdsp/shared/DelayLine.cpp src/common/dsp/filters/VintageLadders.cpp src/common/dsp/filters/Obxd.cpp src/common/dsp/filters/K35.cpp diff --git a/doc/AddingAnFX.md b/doc/AddingAnFX.md new file mode 100644 index 00000000000..26c2d10d634 --- /dev/null +++ b/doc/AddingAnFX.md @@ -0,0 +1,26 @@ +# How to add a effect + +This tutorial will demonstrate how to add an example effect called `MyEffect`. + +1. Create `src/common/dsp/effect/MyEffect.h` and `MyEffect.cpp`. Look at Flanger as an example. + - The parameters for your effect should be placed in an enum. + - Override `init_ctrltypes()` to initialise the parameter names and types, and `init_default_values()` to set the default parameter values. + - The guts of your signal processing should go in the `process()` function. + +2. In `Effect.cpp` add the newly created header file to the list of includes at the top. Then in `spawn_effect()` add a case to spawn your new effect: +```cpp +case fxt_myeffect: + return new MyEffect( storage, fxdata, pd ); +``` + +Note that we have chosen the effect ID `fxt_myeffect` for our example effect. + +3. In SurgeStorage.h: +* add an enum to `fx_type` with your effect ID. +* add the name of your effect to `fx_type_names`. + +4. In `resources/data/configuration.xml` add your effect to the `fx` XML group. Later, you may also add "snapshots" for presets of your effect. + +Note that the configuration file DOES NOT automatically reload when you compile and run the plugin! Whenever you make changes to this file, you must copy it to the correct "installed" location where the plugin is expecting to find it. For Windows,this is `C:/ProgramData/Surge/configuration.xml`. + +Then you can finish coding up your effect! \ No newline at end of file diff --git a/resources/data/configuration.xml b/resources/data/configuration.xml index 92944acddff..9b279ce4b6a 100644 --- a/resources/data/configuration.xml +++ b/resources/data/configuration.xml @@ -246,6 +246,10 @@ + + + + diff --git a/src/common/SurgeStorage.h b/src/common/SurgeStorage.h index 547f27e4683..4ee55a3c5d9 100644 --- a/src/common/SurgeStorage.h +++ b/src/common/SurgeStorage.h @@ -280,6 +280,7 @@ enum fx_type fxt_flanger, fxt_ringmod, fxt_airwindows, + fxt_neuron, n_fx_types, }; @@ -300,6 +301,7 @@ const char fx_type_names[n_fx_types][16] = "Flanger", "Ring Mod", "Airwindows", + "Neuron", }; enum fx_bypass diff --git a/src/common/dsp/effect/Effect.cpp b/src/common/dsp/effect/Effect.cpp index 7cf4ec66b61..d332b1b2f54 100644 --- a/src/common/dsp/effect/Effect.cpp +++ b/src/common/dsp/effect/Effect.cpp @@ -12,6 +12,7 @@ #include "FlangerEffect.h" #include "RingModulatorEffect.h" #include "airwindows/AirWindowsEffect.h" +#include "chowdsp/Neuron.h" #include "DebugHelpers.h" using namespace std; @@ -50,6 +51,8 @@ Effect* spawn_effect(int id, SurgeStorage* storage, FxStorage* fxdata, pdata* pd return new RingModulatorEffect(storage, fxdata, pd); case fxt_airwindows: return new AirWindowsEffect( storage, fxdata, pd ); + case fxt_neuron: + return new chowdsp::Neuron(storage, fxdata, pd); default: return 0; }; diff --git a/src/common/dsp/effect/chowdsp/Neuron.cpp b/src/common/dsp/effect/chowdsp/Neuron.cpp new file mode 100644 index 00000000000..bcf27c7f905 --- /dev/null +++ b/src/common/dsp/effect/chowdsp/Neuron.cpp @@ -0,0 +1,235 @@ +/* +** Surge Synthesizer is Free and Open Source Software +** +** Surge is made available under the Gnu General Public License, v3.0 +** https://www.gnu.org/licenses/gpl-3.0.en.html +** +** Copyright 2004-2020 by various individuals as described by the Git transaction log +** +** All source at: https://github.com/surge-synthesizer/surge.git +** +** Surge was a commercial product from 2004-2018, with Copyright and ownership +** in that period held by Claes Johanson at Vember Audio. Claes made Surge +** open source in September 2018. +*/ + +#include "Neuron.h" + +namespace chowdsp +{ + +Neuron::Neuron(SurgeStorage* storage, FxStorage* fxdata, pdata* pd) + : Effect(storage, fxdata, pd) +{ + dc_blocker.setBlockSize(BLOCK_SIZE); + makeup.set_blocksize(BLOCK_SIZE); + outgain.set_blocksize(BLOCK_SIZE); +} + +Neuron::~Neuron() +{} + +void Neuron::init() +{ + Wf.reset(numSteps); + Wh.reset(numSteps); + Uf.reset(numSteps); + Uh.reset(numSteps); + bf.reset(numSteps); + + delay1Smooth.reset(numSteps); + delay2Smooth.reset(numSteps); + + os.reset(); + + delay1.prepare(dsamplerate * os.getOSRatio(), BLOCK_SIZE, 2); + delay2.prepare(dsamplerate * os.getOSRatio(), BLOCK_SIZE, 2); + delay1.setDelay(0.0f); + delay2.setDelay(0.0f); + + y1[0] = 0.0f; + y1[1] = 0.0f; + + dc_blocker.suspend(); + dc_blocker.coeff_HP(35.0f / samplerate, 0.707); + dc_blocker.coeff_instantize(); + + width.instantize(); + + makeup.set_target(1.0f); + outgain.set_target(0.0f); +} + +void Neuron::process(float* dataL, float* dataR) +{ + set_params(); + + os.upsample(dataL, dataR); + process_internal(os.leftUp, os.rightUp, os.getUpBlockSize()); + os.downsample(dataL, dataR); + + dc_blocker.process_block(dataL, dataR); + makeup.multiply_2_blocks(dataL, dataR, BLOCK_SIZE_QUAD); + + // scale width + float M alignas(16)[BLOCK_SIZE], S alignas(16)[BLOCK_SIZE]; + + encodeMS(dataL, dataR, M, S, BLOCK_SIZE_QUAD); + width.multiply_block(S, BLOCK_SIZE_QUAD); + decodeMS(M, S, dataL, dataR, BLOCK_SIZE_QUAD); + + outgain.multiply_2_blocks(dataL, dataR, BLOCK_SIZE_QUAD); +} + +void Neuron::process_internal(float* dataL, float* dataR, const int numSamples) +{ + for(int k = 0; k < numSamples; k++) + { + dataL[k] = processSample(dataL[k], y1[0]); + dataR[k] = processSample(dataR[k], y1[1]); + + delay1.setDelay(delay1Smooth.getNextValue()); + delay2.setDelay(delay2Smooth.getNextValue()); + delay1.pushSample(0, dataL[k]); + delay2.pushSample(1, dataR[k]); + + y1[0] = delay1.popSample(0); + y1[1] = delay2.popSample(1); + } +} + +void Neuron::set_params() +{ + auto bf_clamped = limit_range(*f[neuron_bias_bf], 0.0f, 1.0f); + + Wf.setTargetValue(*f[neuron_squash_wf] * 20.0f); + Wh.setTargetValue(db_to_linear(*f[neuron_drive_wh])); + Uf.setTargetValue(*f[neuron_stab_uf] * 5.0f); + Uh.setTargetValue(*f[neuron_asym_uh] * 0.9f); + bf.setTargetValue(bf_clamped * 6.0f - 1.0f); + + // tune delay length + auto freqHz1 = (2 * 3.14159265358979323846) * 440 * + storage->note_to_pitch_ignoring_tuning(*f[neuron_comb_freq]); + auto freqHz2 = + (2 * 3.14159265358979323846) * 440 * + storage->note_to_pitch_ignoring_tuning(*f[neuron_comb_freq] + *f[neuron_comb_sep]); + auto delayTimeSec1 = 1.0f / (float) freqHz1; + auto delayTimeSec2 = 1.0f / (float) freqHz2; + + delay1Smooth.setTargetValue(delayTimeSec1 * 0.5f * samplerate * os.getOSRatio()); + delay2Smooth.setTargetValue(delayTimeSec2 * 0.5f * samplerate * os.getOSRatio()); + + // calc makeup gain + auto drive_makeup = [](float wh) -> float + { + return std::exp(-0.11898f * wh) + 1.0f; + }; + + auto bias_makeup = [](float bf) -> float + { + return 6.0f * std::pow(bf, 7.5f) + 0.9f; + }; + + const auto makeupGain = drive_makeup(*f[neuron_drive_wh]) * bias_makeup(bf_clamped); + + makeup.set_target_smoothed(makeupGain); + + width.set_target_smoothed(db_to_linear(*f[neuron_width])); + outgain.set_target_smoothed(db_to_linear(*f[neuron_gain])); +} + + +void Neuron::suspend() +{ + init(); +} + +const char* Neuron::group_label(int id) +{ + switch (id) + { + case 0: + return "Distortion"; + case 1: + return "Comb"; + case 2: + return "Output"; + } + + return 0; +} + +int Neuron::group_label_ypos(int id) +{ + switch (id) + { + case 0: + return 1; + case 1: + return 13; + case 2: + return 19; + } + return 0; +} + +void Neuron::init_ctrltypes() +{ + Effect::init_ctrltypes(); + + fxdata->p[neuron_drive_wh].set_name("Drive"); + fxdata->p[neuron_drive_wh].set_type(ct_decibel_narrow); + fxdata->p[neuron_drive_wh].posy_offset = 1; + + fxdata->p[neuron_squash_wf].set_name("Squash"); + fxdata->p[neuron_squash_wf].set_type(ct_percent); + fxdata->p[neuron_squash_wf].posy_offset = 1; + fxdata->p[neuron_squash_wf].val_default.f = 0.5f; + + fxdata->p[neuron_stab_uf].set_name("Stab"); + fxdata->p[neuron_stab_uf].set_type(ct_percent); + fxdata->p[neuron_stab_uf].posy_offset = 1; + fxdata->p[neuron_stab_uf].val_default.f = 0.5f; + + fxdata->p[neuron_asym_uh].set_name("Asymmetry"); + fxdata->p[neuron_asym_uh].set_type(ct_percent); + fxdata->p[neuron_asym_uh].posy_offset = 1; + fxdata->p[neuron_asym_uh].val_default.f = 1.0f; + + fxdata->p[neuron_bias_bf].set_name("Bias"); + fxdata->p[neuron_bias_bf].set_type(ct_percent); + fxdata->p[neuron_bias_bf].posy_offset = 1; + + fxdata->p[neuron_comb_freq].set_name("Frequency"); + fxdata->p[neuron_comb_freq].set_type(ct_freq_audible); + fxdata->p[neuron_comb_freq].posy_offset = 3; + fxdata->p[neuron_comb_freq].val_default.f = 70.0f; + + fxdata->p[neuron_comb_sep].set_name("Separation"); + fxdata->p[neuron_comb_sep].set_type(ct_freq_mod); + fxdata->p[neuron_comb_sep].posy_offset = 3; + + fxdata->p[neuron_width].set_name("Width"); + fxdata->p[neuron_width].set_type(ct_decibel_narrow); + fxdata->p[neuron_width].posy_offset = 5; + + fxdata->p[neuron_gain].set_name("Gain"); + fxdata->p[neuron_gain].set_type(ct_decibel_narrow); + fxdata->p[neuron_gain].posy_offset = 5; +} + +void Neuron::init_default_values() +{ + fxdata->p[neuron_drive_wh].val.f = 0.0f; + fxdata->p[neuron_squash_wf].val.f = 0.5f; + fxdata->p[neuron_stab_uf].val.f = 0.5f; + fxdata->p[neuron_asym_uh].val.f = 1.0f; + fxdata->p[neuron_bias_bf].val.f = 0.0f; + fxdata->p[neuron_comb_freq].val.f = 70.0f; + fxdata->p[neuron_comb_sep].val.f = 0.5f; + fxdata->p[neuron_width].val.f = 0.0f; + fxdata->p[neuron_gain].val.f = 0.0f; +} + +} // namespace chowdsp diff --git a/src/common/dsp/effect/chowdsp/Neuron.h b/src/common/dsp/effect/chowdsp/Neuron.h new file mode 100644 index 00000000000..270ec2099c5 --- /dev/null +++ b/src/common/dsp/effect/chowdsp/Neuron.h @@ -0,0 +1,110 @@ +/* +** Surge Synthesizer is Free and Open Source Software +** +** Surge is made available under the Gnu General Public License, v3.0 +** https://www.gnu.org/licenses/gpl-3.0.en.html +** +** Copyright 2004-2020 by various individuals as described by the Git transaction log +** +** All source at: https://github.com/surge-synthesizer/surge.git +** +** Surge was a commercial product from 2004-2018, with Copyright and ownership +** in that period held by Claes Johanson at Vember Audio. Claes made Surge +** open source in September 2018. +*/ + +#pragma once + +#include "../Effect.h" +#include "BiquadFilter.h" +#include "shared/DelayLine.h" +#include "shared/Oversampling.h" +#include "shared/SmoothedValue.h" + +#include + +namespace chowdsp +{ + +/* +** CHOWDSP Neuron: +** Neuron is a distortion effect based on the Gated Recurrent Unit, +** a sort of "neuron" model commonly used in recurrent neural networks. +** +** For more details on the original idea and implementation of this effect, +** please see this Medium article: https://jatinchowdhury18.medium.com/complex-nonlinearities-episode-10-gated-recurrent-distortion-6d60948323cf +*/ +class Neuron : public Effect +{ +public: + enum neuron_params + { + neuron_drive_wh = 0, + neuron_squash_wf, + neuron_stab_uf, + neuron_asym_uh, + neuron_bias_bf, + + neuron_comb_freq, + neuron_comb_sep, + + neuron_width, + neuron_gain, + + neuron_num_params, + }; + + Neuron(SurgeStorage* storage, FxStorage* fxdata, pdata* pd); + virtual ~Neuron(); + + virtual const char* get_effectname() override + { + return "Neuron"; + } + + virtual void init() override; + virtual void process(float* dataL, float* dataR) override; + virtual void suspend() override; + + virtual void init_ctrltypes() override; + virtual void init_default_values() override; + virtual const char* group_label(int id) override; + virtual int group_label_ypos(int id) override; + +private: + void set_params(); + void process_internal(float* dataL, float* dataR, const int numSamples); + + inline float processSample (float x, float yPrev) noexcept + { + float f = sigmoid (Wf.getNextValue()*x + Uf.getNextValue()*yPrev + bf.getNextValue()); + return f*yPrev + (1.0f-f) * std::tanh (Wh.getNextValue()*x + Uh.getNextValue()*f*yPrev); + } + + inline float sigmoid (float x) const noexcept + { + return 1.0f / (1.0f + std::exp (-x)); + } + + enum { + numSteps = 200, + }; + + SmoothedValue Wf = 0.5f; + SmoothedValue Wh = 0.5f; + SmoothedValue Uf = 0.5f; + SmoothedValue Uh = 0.5f; + SmoothedValue bf = 0.0f; + SmoothedValue delay1Smooth = 0.0f; + SmoothedValue delay2Smooth = 0.0f; + + float y1[2] = {0.0f, 0.0f}; + + BiquadFilter dc_blocker; + lipol_ps makeup alignas(16), width alignas(16), outgain alignas(16); + chowdsp::DelayLine delay1{1 << 18}; + chowdsp::DelayLine delay2{1 << 18}; + Oversampling<2, BLOCK_SIZE> os; +}; + +} // namespace chowdsp diff --git a/src/common/dsp/effect/chowdsp/shared/DelayInterpolation.h b/src/common/dsp/effect/chowdsp/shared/DelayInterpolation.h new file mode 100644 index 00000000000..a0067a19f67 --- /dev/null +++ b/src/common/dsp/effect/chowdsp/shared/DelayInterpolation.h @@ -0,0 +1,240 @@ +#pragma once + +#include + +namespace chowdsp +{ + +/** + A collection of structs to pass as the template argument when setting the + interpolation type for the DelayLine class. +*/ +namespace DelayLineInterpolationTypes +{ + /** + No interpolation between successive samples in the delay line will be + performed. This is useful when the delay is a constant integer or to + create lo-fi audio effects. + */ + struct None + { + void reset (int newTotalSize) { totalSize = newTotalSize; } + + template + void updateInternalVariables (int& /*delayIntOffset*/, T& /*delayFrac*/) {} + + template + inline T call (const T* buffer, int delayInt, T /*delayFrac*/, const T& /*state*/) + { + return buffer[delayInt % totalSize]; + } + + int totalSize; + }; + + /** + Successive samples in the delay line will be linearly interpolated. This + type of interpolation has a low compuational cost where the delay can be + modulated in real time, but it also introduces a low-pass filtering effect + into your audio signal. + */ + struct Linear + { + void reset (int newTotalSize) { totalSize = newTotalSize; } + + template + void updateInternalVariables (int& /*delayIntOffset*/, T& /*delayFrac*/) {} + + template + inline T call (const T* buffer, int delayInt, T delayFrac, const T& /*state*/) + { + auto index1 = delayInt; + auto index2 = index1 + 1; + + if (index2 >= totalSize) + { + index1 %= totalSize; + index2 %= totalSize; + } + + auto value1 = buffer[index1]; + auto value2 = buffer[index2]; + + return value1 + delayFrac * (value2 - value1); + } + + int totalSize; + }; + + /** + Successive samples in the delay line will be interpolated using a 3rd order + Lagrange interpolator. This method incurs more computational overhead than + linear interpolation but reduces the low-pass filtering effect whilst + remaining amenable to real time delay modulation. + */ + struct Lagrange3rd + { + void reset (int newTotalSize) { totalSize = newTotalSize; } + + template + void updateInternalVariables (int& delayIntOffset, T& delayFrac) + { + if (delayIntOffset >= 1) + { + delayFrac++; + delayIntOffset--; + } + } + + template + inline T call (const T* buffer, int delayInt, T delayFrac, const T& /*state*/) + { + auto index1 = delayInt; + auto index2 = index1 + 1; + auto index3 = index2 + 1; + auto index4 = index3 + 1; + + if (index4 >= totalSize) + { + index1 %= totalSize; + index2 %= totalSize; + index3 %= totalSize; + index4 %= totalSize; + } + + auto value1 = buffer[index1]; + auto value2 = buffer[index2]; + auto value3 = buffer[index3]; + auto value4 = buffer[index4]; + + auto d1 = delayFrac - (T) 1.0; + auto d2 = delayFrac - (T) 2.0; + auto d3 = delayFrac - (T) 3.0; + + auto c1 = -d1 * d2 * d3 / (T) 6.0; + auto c2 = d2 * d3 * (T) 0.5; + auto c3 = -d1 * d3 * (T) 0.5; + auto c4 = d1 * d2 / (T) 6.0; + + return value1 * c1 + delayFrac * (value2 * c2 + value3 * c3 + value4 * c4); + } + + int totalSize; + }; + + /** + Successive samples in the delay line will be interpolated using a 5th order + Lagrange interpolator. This method incurs more computational overhead than + linear interpolation. + */ + struct Lagrange5th + { + void reset (int newTotalSize) { totalSize = newTotalSize; } + + template + void updateInternalVariables (int& delayIntOffset, T& delayFrac) + { + if (delayIntOffset >= 2) + { + delayFrac += (T) 2; + delayIntOffset -= 2; + } + } + + template + inline T call (const T* buffer, int delayInt, T delayFrac, const T& /*state*/) + { + auto index1 = delayInt; + auto index2 = index1 + 1; + auto index3 = index2 + 1; + auto index4 = index3 + 1; + auto index5 = index4 + 1; + auto index6 = index5 + 1; + + if (index6 >= totalSize) + { + index1 %= totalSize; + index2 %= totalSize; + index3 %= totalSize; + index4 %= totalSize; + index5 %= totalSize; + index6 %= totalSize; + } + + auto value1 = buffer[index1]; + auto value2 = buffer[index2]; + auto value3 = buffer[index3]; + auto value4 = buffer[index4]; + auto value5 = buffer[index5]; + auto value6 = buffer[index6]; + + auto d1 = delayFrac - (T) 1.0; + auto d2 = delayFrac - (T) 2.0; + auto d3 = delayFrac - (T) 3.0; + auto d4 = delayFrac - (T) 4.0; + auto d5 = delayFrac - (T) 5.0; + + auto c1 = -d1 * d2 * d3 * d4 * d5 / (T) 120.0; + auto c2 = d2 * d3 * d4 * d5 / (T) 24.0; + auto c3 = -d1 * d3 * d4 * d5 / (T) 12.0; + auto c4 = d1 * d2 * d4 * d5 / (T) 12.0; + auto c5 = -d1 * d2 * d3 * d5 / (T) 24.0; + auto c6 = d1 * d2 * d3 * d4 / (T) 120.0; + + return value1 * c1 + delayFrac * (value2 * c2 + + value3 * c3 + value4 * c4 + value5 * c5 + value6 * c6); + } + + int totalSize; + }; + + /** + Successive samples in the delay line will be interpolated using 1st order + Thiran interpolation. This method is very efficient, and features a flat + amplitude frequency response in exchange for less accuracy in the phase + response. This interpolation method is stateful so is unsuitable for + applications requiring fast delay modulation. + */ + struct Thiran + { + void reset (int newTotalSize) { totalSize = newTotalSize; } + + template + void updateInternalVariables (int& delayIntOffset, T& delayFrac) + { + if (delayFrac < (T) 0.618 && delayIntOffset >= 1) + { + delayFrac++; + delayIntOffset--; + } + + alpha = double ((1 - delayFrac) / (1 + delayFrac)); + } + + template + inline T call (const T* buffer, int delayInt, T delayFrac, T& state) + { + auto index1 = delayInt; + auto index2 = index1 + 1; + + if (index2 >= totalSize) + { + index1 %= totalSize; + index2 %= totalSize; + } + + auto value1 = buffer[index1]; + auto value2 = buffer[index2]; + + auto output = delayFrac == 0 ? value1 : value2 + (T) alpha * (value1 - state); + state = output; + + return output; + } + + int totalSize; + double alpha = 0.0; + }; +} + +} // chowdsp diff --git a/src/common/dsp/effect/chowdsp/shared/DelayLine.cpp b/src/common/dsp/effect/chowdsp/shared/DelayLine.cpp new file mode 100644 index 00000000000..9f5bccf9437 --- /dev/null +++ b/src/common/dsp/effect/chowdsp/shared/DelayLine.cpp @@ -0,0 +1,129 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include +#include +#include "DelayLine.h" + +namespace chowdsp +{ + +//============================================================================== +template +DelayLine::DelayLine() + : DelayLine (0) +{ +} + +template +DelayLine::DelayLine (size_t maximumDelayInSamples) +{ + totalSize = std::max ((size_t) 4, maximumDelayInSamples + 1); +} + +//============================================================================== +template +void DelayLine::setDelay (SampleType newDelayInSamples) +{ + auto upperLimit = (SampleType) (totalSize - 1); + + delay = limit_range (newDelayInSamples, (SampleType) 0, upperLimit); + delayInt = static_cast (std::floor (delay)); + delayFrac = delay - (SampleType) delayInt; + + interpolator.updateInternalVariables (delayInt, delayFrac); +} + +template +SampleType DelayLine::getDelay() const +{ + return delay; +} + +//============================================================================== +template +void DelayLine::prepare (double sampleRate, size_t blockSize, size_t numChannels) +{ + this->bufferData.clear(); + for (size_t ch = 0; ch < numChannels; ++ch) + this->bufferData.push_back (std::vector (totalSize, (SampleType) 0)); + + this->writePos.resize (numChannels); + this->readPos.resize (numChannels); + + this->v.resize (numChannels); + interpolator.reset (totalSize); + + reset(); +} + +template +void DelayLine::reset() +{ + for (auto vec : { &this->writePos, &this->readPos }) + std::fill (vec->begin(), vec->end(), 0); + + std::fill (this->v.begin(), this->v.end(), static_cast (0)); + + for (size_t ch = 0; ch < this->bufferData.size(); ++ch) + std::fill (this->bufferData[ch].begin(), this->bufferData[ch].end(), static_cast (0)); +} + +//============================================================================== +template +void DelayLine::pushSample (size_t channel, SampleType sample) +{ + this->bufferData[channel][this->writePos[channel]] = sample; + this->writePos[channel] = (this->writePos[channel] + totalSize - 1) % totalSize; +} + +template +SampleType DelayLine::popSample (size_t channel, SampleType delayInSamples, bool updateReadPointer) +{ + if (delayInSamples >= 0) + setDelay(delayInSamples); + + auto result = interpolateSample (channel); + + if (updateReadPointer) + this->readPos[channel] = (this->readPos[channel] + totalSize - 1) % totalSize; + + return result; +} + +//============================================================================== +template class DelayLine; +template class DelayLine; +template class DelayLine; +template class DelayLine; +template class DelayLine; +template class DelayLine; +template class DelayLine; +template class DelayLine; +template class DelayLine; +template class DelayLine; + +} // namespace chowdsp diff --git a/src/common/dsp/effect/chowdsp/shared/DelayLine.h b/src/common/dsp/effect/chowdsp/shared/DelayLine.h new file mode 100644 index 00000000000..2d527c418de --- /dev/null +++ b/src/common/dsp/effect/chowdsp/shared/DelayLine.h @@ -0,0 +1,168 @@ +#pragma once + +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include +#include "DelayInterpolation.h" + +namespace chowdsp +{ + +/** Base class for delay lines with any interpolation type */ +template +class DelayLineBase +{ +public: + DelayLineBase() = default; + + virtual void setDelay (SampleType /* newDelayInSamples */) = 0; + virtual SampleType getDelay() const = 0; + + virtual void prepare (double sampleRate, size_t blockSize, size_t nChannels) = 0; + virtual void reset() = 0; + + virtual void pushSample (size_t /* channel */, SampleType /* sample */) = 0; + virtual SampleType popSample (size_t /* channel */, SampleType /* delayInSamples */, bool /* updateReadPointer */) = 0; + + void copyState (const DelayLineBase& other) + { + for (size_t ch = 0; ch < bufferData.size(); ++ch) + std::copy (other.bufferData[ch].begin(), other.bufferData[ch].end(), bufferData[ch].begin()); + + std::copy (other.v.begin(), other.v.end(), v.begin()); + std::copy (other.writePos.begin(), other.writePos.end(), writePos.begin()); + std::copy (other.readPos.begin(), other.readPos.end(), readPos.begin()); + } + +protected: + std::vector> bufferData; + std::vector v; + std::vector writePos, readPos; +}; + +//============================================================================== +/** + A delay line processor featuring several algorithms for the fractional delay + calculation, block processing, and sample-by-sample processing useful when + modulating the delay in real time or creating a standard delay effect with + feedback. + + This implementation has been modified from the original JUCE implementation + to include 5th-order Lagrange Interpolation. + + Note: If you intend to change the delay in real time, you may want to smooth + changes to the delay systematically using either a ramp or a low-pass filter. + + @see SmoothedValue, FirstOrderTPTFilter +*/ +template +class DelayLine : public DelayLineBase +{ +public: + //============================================================================== + /** Default constructor. */ + DelayLine(); + + /** Constructor. */ + explicit DelayLine (size_t maximumDelayInSamples); + + //============================================================================== + /** Sets the delay in samples. */ + void setDelay (SampleType newDelayInSamples) override; + + /** Returns the current delay in samples. */ + SampleType getDelay() const override; + + //============================================================================== + /** Initialises the processor. */ + void prepare (double sampleRate, size_t blockSize, size_t nChannels) override; + + /** Resets the internal state variables of the processor. */ + void reset() override; + + //============================================================================== + /** Pushes a single sample into one channel of the delay line. + + Use this function and popSample instead of process if you need to modulate + the delay in real time instead of using a fixed delay value, or if you want + to code a delay effect with a feedback loop. + + @see setDelay, popSample, process + */ + void pushSample (size_t channel, SampleType sample) override; + + /** Pops a single sample from one channel of the delay line. + + Use this function to modulate the delay in real time or implement standard + delay effects with feedback. + + @param channel the target channel for the delay line. + + @param delayInSamples sets the wanted fractional delay in samples, or -1 + to use the value being used before or set with + setDelay function. + + @param updateReadPointer should be set to true if you use the function + once for each sample, or false if you need + multi-tap delay capabilities. + + @see setDelay, pushSample, process + */ + SampleType popSample (size_t channel, SampleType delayInSamples = -1, bool updateReadPointer = true) override; + + //============================================================================== + /** Processes the input and output samples supplied in the processing context. + + Can be used for block processing when the delay is not going to change + during processing. The delay must first be set by calling setDelay. + + @see setDelay + */ + void process (const SampleType* inputSamples, SampleType* outputSamples, const size_t numSamples, size_t channel) + { + for (size_t i = 0; i < numSamples; ++i) + { + pushSample (channel, inputSamples[i]); + outputSamples[i] = popSample (channel); + } + } + +private: + inline SampleType interpolateSample (size_t channel) noexcept + { + auto index = (this->readPos[channel] + delayInt); + return interpolator.call (this->bufferData[channel].data(), + index, delayFrac, this->v[channel]); + } + + //============================================================================== + InterpolationType interpolator; + SampleType delay = 0.0, delayFrac = 0.0; + int delayInt = 0; + size_t totalSize = 4; +}; + +} // namespace chowdsp diff --git a/src/common/dsp/effect/chowdsp/shared/Oversampling.h b/src/common/dsp/effect/chowdsp/shared/Oversampling.h new file mode 100644 index 00000000000..ee7a2f88dba --- /dev/null +++ b/src/common/dsp/effect/chowdsp/shared/Oversampling.h @@ -0,0 +1,117 @@ +/* +** Surge Synthesizer is Free and Open Source Software +** +** Surge is made available under the Gnu General Public License, v3.0 +** https://www.gnu.org/licenses/gpl-3.0.en.html +** +** Copyright 2004-2020 by various individuals as described by the Git transaction log +** +** All source at: https://github.com/surge-synthesizer/surge.git +** +** Surge was a commercial product from 2004-2018, with Copyright and ownership +** in that period held by Claes Johanson at Vember Audio. Claes made Surge +** open source in September 2018. +*/ + +#pragma once +#include +#include +#include +#include + +namespace chowdsp +{ + +/* +** Generalised oversampling class for use in effects processing. +** Uses anti-imaging filters for upsampling, and anti-aliasing +** filters for downsampling. Most of the oversampling parameters +** are set as template parameters: +** +** @param: OSFactor sets the oversampling ratio as a power of two. i.e. Ratio = 2^OSFactor +** @param: block_size size of the blocks of audio before upsampling +** @param: FilterOrd sets the order of the anti-imaging/anti-aliasing filters +** @param: steep sets whether to use the filters in "steep" mode (see vt_dsp/halfratefilter.h) +** +** The class should be used in the processign callback as follows: +** Then use the following code to process samples: +** @code +** Ovsampling<2, BLOCK_SIZE> os; +** void process(float* dataL, float* dataR) +** { +** os.upsample(dataL, dataR); +** process_samples(os.leftUp, os.rightUp, os.getUpBLockSize()); +** os.downsample(dataL, dataR); +** } +** @endcode +*/ +template +class Oversampling +{ + std::unique_ptr hr_filts_up alignas(16)[OSFactor]; + std::unique_ptr hr_filts_down alignas(16)[OSFactor]; + + static constexpr size_t osRatio = 1 << OSFactor; + static constexpr size_t up_block_size = block_size * osRatio; + static constexpr size_t block_size_quad = block_size / 4; + +public: + Oversampling() + { + for (size_t i = 0; i < OSFactor; ++i) + { + hr_filts_up[i] = std::make_unique (FilterOrd, steep); + hr_filts_down[i] = std::make_unique (FilterOrd, steep); + } + } + + /** Resets the processing pipeline */ + void reset() + { + for(size_t i = 0; i < OSFactor; ++i) + { + hr_filts_up[i]->reset(); + hr_filts_down[i]->reset(); + } + + std::fill(leftUp, &leftUp[up_block_size], 0.0f); + std::fill(rightUp, &rightUp[up_block_size], 0.0f); + } + + /** Upsamples the audio in the input arrays, and stores the upsampled audio internally */ + inline void upsample(float* leftIn, float* rightIn) noexcept + { + copy_block(leftIn, leftUp, block_size_quad); + copy_block(rightIn, rightUp, block_size_quad); + + for (size_t i = 0; i < OSFactor; ++i) + { + auto numSamples = block_size * (1 << (i + 1)); + hr_filts_up[i]->process_block_U2(leftUp, rightUp, leftUp, rightUp, numSamples); + } + } + + /** Downsamples that audio in the internal buffers, and stores the downsampled audio in the input arrays */ + inline void downsample(float* leftOut, float* rightOut) noexcept + { + for (size_t i = OSFactor; i > 0; --i) + { + auto numSamples = block_size * (1 << i); + hr_filts_down[i - 1]->process_block_D2 (leftUp, rightUp, numSamples); + } + + copy_block(leftUp, leftOut, block_size_quad); + copy_block(rightUp, rightOut, block_size_quad); + } + + /** Returns the size of the upsampled blocks */ + inline constexpr size_t getUpBlockSize() const noexcept { return up_block_size; } + + /** Returns the oversampling ratio */ + inline constexpr size_t getOSRatio() const noexcept { return osRatio; } + + float leftUp alignas(16)[up_block_size]; + float rightUp alignas(16)[up_block_size]; +}; + +} // namespace chowdsp diff --git a/src/common/dsp/effect/chowdsp/shared/SmoothedValue.h b/src/common/dsp/effect/chowdsp/shared/SmoothedValue.h new file mode 100644 index 00000000000..01a08a50a60 --- /dev/null +++ b/src/common/dsp/effect/chowdsp/shared/SmoothedValue.h @@ -0,0 +1,333 @@ +#pragma once + +/* + ============================================================================== + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + JUCE is an open source library subject to commercial or open-source + licensing. + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + ============================================================================== +*/ + +#include "basic_dsp.h" + +namespace chowdsp +{ + +//============================================================================== +/** + A base class for the smoothed value classes. + This class is used to provide common functionality to the SmoothedValue and + dsp::LogRampedValue classes. + @tags{Audio} +*/ +template +class SmoothedValueBase +{ +private: + //============================================================================== + template struct FloatTypeHelper; + + template