From f26d89cb8e0b33fb9c21fb98c9e9a37cd8bae0b5 Mon Sep 17 00:00:00 2001 From: Dave French Date: Sun, 23 May 2021 15:58:23 +0100 Subject: [PATCH] 1.4 release (#110) * Zilah initial commit * Zilah, included * Zilah, included * Zilah, smoothing filter, uni and bipolar output * Zilah, #include * Zilah, define uint8_t uint16_t * Zilah, define u_int8_t u_int16_t * Zilah, define uint8_t uint16_t * Remove mac builds, as broken, to stop github spam * Zilah change u_int16_t to int, for windows builds * Zilah add msbLsbPair aggregator * Zilah update plugin.json * Zilah update plugin.json version number for release * Hula Initial commit * Hula prep for internal testing * Hula update readme, change log, plugin.json * Hula, revise std::Array init * Hula, revise std::Array init again, build locally, fail on github actions * Hula, revise std::Array init again, build locally, ok * Hula Initial commit * Hula prep for internal testing * Hula update readme, change log, plugin.json * Hula, revise std::Array init * Hula, revise std::Array init again, build locally, fail on github actions * Hula, revise std::Array init again, build locally, ok * SynthFilter updated to simd * Utility filters, Upsampler * Amburgh Initial commit * update unittests for Synthfilter::nonLinearProcess = [](){}; * Hula, random tuning per poly channel * Hula, balast filters on depth and feedback * Hula adjust noise level in wave table, set fc of final HPF to 10kHz * Hula adjust ratio range to 0.5 25.95 * Hula ratio knob no longer snaps * Release 1.4 Hula Amburgh --- CHANGELOG.md | 7 + README.md | 25 + images/Amburgh.png | Bin 0 -> 44214 bytes images/Hula.png | Bin 0 -> 35962 bytes plugin.json | 24 +- res/Amburgh.svg | 1043 ++++++++++++++++++++++++++++++++++++ res/Hula.svg | 844 +++++++++++++++++++++++++++++ src/composites/Amburgh.h | 256 +++++++++ src/composites/Hula.h | 251 +++++++++ src/composites/Maccomo.h | 7 +- src/dsp/AudioMath.h | 5 + src/dsp/LookupTable.h | 86 ++- src/dsp/SynthFilter.h | 263 +++++---- src/dsp/UtilityFilters.h | 69 +++ src/modules/Amburgh.cpp | 93 ++++ src/modules/Hula.cpp | 69 +++ src/plugin.cpp | 2 + src/plugin.hpp | 4 + test/main.cpp | 12 +- test/perfTest.cpp | 28 +- test/testAudioMath.cpp | 22 + test/testLookupTable.cpp | 15 +- test/testSynthFilter.cpp | 140 ++--- test/testUtilityFilter.cpp | 33 ++ 24 files changed, 3101 insertions(+), 197 deletions(-) create mode 100644 images/Amburgh.png create mode 100644 images/Hula.png create mode 100644 res/Amburgh.svg create mode 100644 res/Hula.svg create mode 100644 src/composites/Amburgh.h create mode 100644 src/composites/Hula.h create mode 100644 src/modules/Amburgh.cpp create mode 100644 src/modules/Hula.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4003db1..8bd5288 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,5 +42,12 @@ Added Iverson and Iverson Jr - Option for rotary encoders added to context menu - Maccomo - Improved Non linear distortion + + ### v1.3 + - Zilah 14 bit midi cc-cv module, initial release + + ### v1.4 + - Hula, small fm oscillator with small random, detune, initial release + diff --git a/README.md b/README.md index 7a3032c..8902fa9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Instructions can be found in the VCV manual https://vcvrack.com/manual/Building# [Wallenda](#wallenda) [Maccomo](#maccomo) + + [Amburg](#amburg) [Massarti](#massarti) @@ -32,6 +34,10 @@ Instructions can be found in the VCV manual https://vcvrack.com/manual/Building# [Zilah](#zilah) + + [Hula](#hula) + + @@ -80,6 +86,12 @@ If the audio input is disconnected, the filter will still run in monophonic mode

+### Amburg + + +Maccomo with some bite. The resonance response has been greatly improved, +and the drive control now really does drive. + ### Massarti @@ -342,3 +354,16 @@ A MIDI 1.0 14 bit CC controller. The MIDI 1.0 standard defines cc 0 - 31 can be resolution than the default 128 steps. Options are given in the context menu as to how the pair of CC values are processed, MSB wait for LSB is my personal favorite, but the MIDI 1.0 option is compliant with the specification. The smoothing filter can be adjusted, and both unipolar and bipolar outputs + + + + +### Hula + + + +A small form fm oscillator, each instance has it's own small detune applied and it's own custom noise floor, hence the lack of a fine tune control. + +It starts emitting a sine wave, turing up the feedback will increase the harmonics. +The fm input can be supplied an audio signal, with controllable depth. There are many guides on the web that will explain the principals of FM synthesis much better than I ever could. + diff --git a/images/Amburgh.png b/images/Amburgh.png new file mode 100644 index 0000000000000000000000000000000000000000..0a27e68df56e211f4fe7c05fd1970fefcd350572 GIT binary patch literal 44214 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 literal 0 HcmV?d00001 diff --git a/images/Hula.png b/images/Hula.png new file mode 100644 index 0000000000000000000000000000000000000000..2ef53634853d7363def5578e194d1ab74df85767 GIT binary patch literal 35962 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 a@0*3vMSrad)>?1CKQfYv5+z~=zW)!U$tR-# literal 0 HcmV?d00001 diff --git a/plugin.json b/plugin.json index 1dfd89c..9d379f0 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "slug": "StudioSixPlusOne", "name": "Studio Six Plus One", - "version": "1.3", + "version": "1.4", "license": "GPL-3.0-or-later", "brand": "Studio Six Plus One", "author": "Studio Six Plus One", @@ -35,6 +35,16 @@ "Filter" ] }, + { + "slug": "Amburgh", + "name": "Amburgh", + "description": "Non Linear Filter self oscillating vcf", + "manualUrl": "https://github.com/StudioSixPlusOne/rack-modules/blob/master/README.md#Amburgh", + "tags": [ + "Polyphonic", + "Filter" + ] + }, { "slug": "PolyShiftRegister", "name": "Tyrant", @@ -92,7 +102,7 @@ { "slug": "Zazel", "name": "Zazel", - "description": "Parameter Controller for fades and LFO", + "description": "Parameter Controller for fades and LFO, Ramp generator", "manualUrl": "https://github.com/StudioSixPlusOne/rack-modules/blob/master/README.md#zazel", "tags": [ "Clock modulator", @@ -131,6 +141,16 @@ "MIDI", "external" ] + }, + { + "slug": "Hula", + "name": "Hula", + "description": "fm VCO unique per instance", + "manualUrl": "https://github.com/StudioSixPlusOne/rack-modules/blob/master/README.md#hula", + "tags": [ + "VCO", + "Polyphonic" + ] } ] } \ No newline at end of file diff --git a/res/Amburgh.svg b/res/Amburgh.svg new file mode 100644 index 0000000..c8e71c6 --- /dev/null +++ b/res/Amburgh.svg @@ -0,0 +1,1043 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/Hula.svg b/res/Hula.svg new file mode 100644 index 0000000..ff01c2c --- /dev/null +++ b/res/Hula.svg @@ -0,0 +1,844 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/composites/Amburgh.h b/src/composites/Amburgh.h new file mode 100644 index 0000000..25bffc6 --- /dev/null +++ b/src/composites/Amburgh.h @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2020 Dave French + * + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#pragma once + +#include "IComposite.h" +#include "SynthFilter.h" +#include "AudioMath.h" +#include "UtilityFilters.h" +#include +#include +#include + +using float_4 = ::rack::simd::float_4; + +namespace rack +{ + namespace engine + { + struct Module; + } +} // namespace rack +using Module = ::rack::engine::Module; +using namespace rack; + +template +class AmburghDescription : public IComposite +{ +public: + Config getParam (int i) override; + int getNumParams() override; +}; + +/** + * Complete Amburgh composite + * + * If TBase is WidgetComposite, this class is used as the implementation part of the KSDelay module. + * If TBase is TestComposite, this class may stand alone for unit tests. + */ + +template +class AmburghComp : public TBase +{ +public: + AmburghComp (Module* module) : TBase (module) + { + } + + AmburghComp() : TBase() + { + } + + virtual ~AmburghComp() + { + } + + /** Implement IComposite + */ + static std::shared_ptr getDescription() + { + return std::make_shared>(); + } + + void setSampleRate (float rate) + { + sampleRate = rate; + sampleTime = 1.0f / rate; + maxFreq = std::min (rate / 2.0f, 20000.0f); + } + + // must be called after setSampleRate + void init() + { + filters.resize (SIMD_MAX_CHANNELS); + for (auto& f : filters) + { + f.setUseNonLinearProcessing (true); + f.setType (sspo::MoogLadderFilter::types()[0]); + f.setUseOversample (true); + float_4 asym = float_4 (1.0f); + f.nonLinearProcess = [asym] (float_4 in, float_4 drive) { + return rack::simd::ifelse (in > float_4 (0), + (atan (drive * in) / atan (drive)), + (atan ((drive * in) / asym) / atan (drive / asym))); + }; //end of lambda + } + + sspo::AudioMath::defaultGenerator.seed (time (NULL)); + } + + enum ParamIds + { + FREQUENCY_PARAM, + FREQUENCY_CV_ATTENUVERTER_PARAM, + RESONANCE_CV_ATTENUVERTER_PARAM, + RESONANCE_PARAM, + DRIVE_CV_ATTENUVERTER_PARAM, + DRIVE_PARAM, + MODE_PARAM, + NUM_PARAMS + }; + + enum InputIds + { + VOCT_INPUT, + RESONANCE_CV_INPUT, + DRIVE_CV_INPUT, + MODE_CV_INPUT, + MAIN_INPUT, + FREQ_CV_INPUT, + NUM_INPUTS + }; + + enum OutputIds + { + MAIN_OUTPUT, + NUM_OUTPUTS + }; + + enum LightIds + { + NUM_LIGHTS + }; + + static constexpr float minFreq = 0.0f; + float maxFreq = 20000.0f; + static constexpr float maxRes = 4.0f; + static constexpr float maxDrive = 30.0f; + static constexpr int SIMD_MAX_CHANNELS = 4; + static constexpr int typeCount = 6; + int currentType = 1; + float sampleRate = 1.0f; + float sampleTime = 1.0f; + std::vector> filters; + void step() override; +}; + +template +inline void AmburghComp::step() +{ + auto channels = std::max (TBase::inputs[MAIN_INPUT].getChannels(), + TBase::inputs[VOCT_INPUT].getChannels()); + channels = std::max (channels, 1); + auto freqParam = TBase::params[FREQUENCY_PARAM].getValue(); + auto resParam = TBase::params[RESONANCE_PARAM].getValue(); + auto driveParam = TBase::params[DRIVE_PARAM].getValue(); + auto modeParam = static_cast (TBase::params[MODE_PARAM].getValue()); + auto freqAttenuverterParam = TBase::params[FREQUENCY_CV_ATTENUVERTER_PARAM].getValue(); + auto resAttenuverterParam = TBase::params[RESONANCE_CV_ATTENUVERTER_PARAM].getValue(); + auto driveAttenuverterParam = TBase::params[DRIVE_CV_ATTENUVERTER_PARAM].getValue(); + + auto noise = float_4 (1e-6f * (2.0f * sspo::AudioMath::rand01() - 1.0f)); + freqParam = freqParam * 10.0f - 5.0f; + + for (auto c = 0; c < channels; c += 4) + { + auto in = TBase::inputs[MAIN_INPUT].template getPolyVoltageSimd (c); + // Add -120dB noise to bootstrap self-oscillation + in += noise; + + auto frequency = float_4 (freqParam); + if (TBase::inputs[VOCT_INPUT].isConnected()) + frequency += float_4 (TBase::inputs[VOCT_INPUT].template getPolyVoltageSimd (c)); + if (TBase::inputs[FREQ_CV_INPUT].isConnected()) + { + frequency += (TBase::inputs[FREQ_CV_INPUT].template getPolyVoltageSimd (c) + * freqAttenuverterParam); + } + frequency = dsp::FREQ_C4 * rack::simd::pow (2.0f, frequency); + + frequency = rack::simd::clamp (frequency, float_4 (0.0f), float_4 (maxFreq)); + + auto resonance = float_4 (resParam); + resonance += (float_4 (TBase::inputs[RESONANCE_CV_INPUT].template getPolyVoltageSimd (c)) / 5.0f) + * resAttenuverterParam * maxRes; + resonance = rack::simd::clamp (resonance, float_4 (0.5f), float_4 (maxRes)); + + auto drive = float_4 (driveParam); + drive += (TBase::inputs[DRIVE_CV_INPUT].template getPolyVoltageSimd (c) / 5.0f) + * driveAttenuverterParam * maxDrive; + drive = rack::simd::clamp (drive, float_4 (1.0f), float_4 (maxDrive)); + + if (currentType != modeParam) + { + currentType = modeParam; + filters[c / 4].setType (sspo::MoogLadderFilter::types()[currentType]); + } + + filters[c / 4].setParameters (frequency, resonance, drive, float_4 (0.5f), sampleRate); + + auto out = filters[c / 4].process (in / 10.0f) * 10.0f; + //out = std::isfinite (out) ? out : 0; + + TBase::outputs[MAIN_OUTPUT].setVoltageSimd (out, c); + } + TBase::outputs[MAIN_OUTPUT].setChannels (channels); +} + +template +int AmburghDescription::getNumParams() +{ + return AmburghComp::NUM_PARAMS; +} + +template +IComposite::Config AmburghDescription::getParam (int i) +{ + auto freqBase = static_cast (std::pow (2, 10.0f)); + auto freqMul = static_cast (dsp::FREQ_C4 / std::pow (2, 5.f)); + IComposite::Config ret = { 0.0f, 1.0f, 0.0f, "Code type", "unit", 0.0f, 1.0f, 0.0f }; + switch (i) + { + case AmburghComp::FREQUENCY_PARAM: + ret = { 0.0f, 1.125f, 0.5f, "Frequency", " Hz", freqBase, freqMul }; + break; + case AmburghComp::FREQUENCY_CV_ATTENUVERTER_PARAM: + ret = { -1.0f, 1.0f, 0.0f, "Frequency CV", " ", 0.0f, 1.0f, 0.0f }; + break; + case AmburghComp::RESONANCE_CV_ATTENUVERTER_PARAM: + ret = { -1.0f, 1.0f, 0.0f, "Resonance CV", " ", 0.0f, 1.0f, 0.0f }; + break; + case AmburghComp::RESONANCE_PARAM: + ret = { 0.5f, AmburghComp::maxRes, 0.707f, "Resonance", " ", 0.0f, 1.0f, 0.0f }; + break; + case AmburghComp::DRIVE_CV_ATTENUVERTER_PARAM: + ret = { -1.0f, 1.0f, 0.0f, "Drive CV", " ", 0.0f, 1.0f, 0.0f }; + break; + case AmburghComp::DRIVE_PARAM: + ret = { 1.0f, AmburghComp::maxDrive, 1.0f, "Drive", " ", 0.0f, 1.0f, 0.0f }; + break; + case AmburghComp::MODE_PARAM: + ret = { 0.0f, AmburghComp::typeCount - 1, 0.0f, "Type", " ", 0.0f, 1.0f, 0.0f }; + break; + default: + assert (false); + } + return ret; +} \ No newline at end of file diff --git a/src/composites/Hula.h b/src/composites/Hula.h new file mode 100644 index 0000000..00021a9 --- /dev/null +++ b/src/composites/Hula.h @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2021 Dave French + * + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#pragma once + +#include "IComposite.h" +#include "LookupTable.h" +#include "AudioMath.h" +#include "dsp/UtilityFilters.h" + +namespace rack +{ + namespace engine + { + struct Module; + } +} // namespace rack +using Module = ::rack::engine::Module; +using namespace rack; +using float_4 = ::rack::simd::float_4; + +using namespace sspo::AudioMath::LookupTable; +using namespace sspo::AudioMath; + +template +class HulaDescription : public IComposite +{ +public: + Config getParam (int i) override; + int getNumParams() override; +}; + +/** + * Complete Hulacomposite + * + * If TBase is WidgetComposite, this class is used as the implementation part of the KSDelay module. + * If TBase is TestComposite, this class may stand alone for unit tests. + */ + +template +class HulaComp : public TBase +{ +public: + HulaComp (Module* module) : TBase (module) + { + } + + HulaComp() : TBase() + { + } + + virtual ~HulaComp() + { + } + + /** Implement IComposite + */ + static std::shared_ptr getDescription() + { + return std::make_shared>(); + } + + void setSampleRate (float rate); + + // must be called after setSampleRate + void init(); + + enum ParamIds + { + RATIO_PARAM, + SEMITONE_PARAM, + OCTAVE_PARAM, + DEPTH_PARAM, + FEEDBACK_PARAM, + NUM_PARAMS + }; + enum InputIds + { + VOCT_INPUT, + DEPTH_CV_INPUT, + FEEDBACK_CV_INPUT, + FM_INPUT, + NUM_INPUTS + }; + enum OutputIds + { + MAIN_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds + { + NUM_LIGHTS + }; + + constexpr static int SIMD_CHANNELS = 4; + float reciprocalSampleRate = 1; + float sampleRate = 1; + std::array lastOuts; + std::array phases; + std::array fineTuneVocts; + + static constexpr int oversampleCount = 4; + static constexpr int oversampleQuality = 1; + + std::array, SIMD_CHANNELS> decimators; + std::array, SIMD_CHANNELS> oversampleBuffers; + std::array, SIMD_CHANNELS> dcOutFilters; + std::array, SIMD_CHANNELS> lpFilters; + + std::array, SIMD_CHANNELS> depthFilters; + std::array, SIMD_CHANNELS> feedbackFilters; + + static constexpr float dcOutCutoff = 5.5f; + + void step() override; +}; + +template +void HulaComp::setSampleRate (float rate) +{ + reciprocalSampleRate = 1 / rate; + sampleRate = rate; + + for (auto& l : lpFilters) + l.setButterworthLp2 (rate, std::min (10e3f, rate * 0.25f)); + + for (auto& dc : dcOutFilters) + dc.setButterworthHp2 (sampleRate, dcOutCutoff); + + /// filter the changes in depth and feedback by fs/40 + for (auto& d : depthFilters) + d.setButterworthLp2 (1000.0f, 25.0f); + + for (auto& f : feedbackFilters) + f.setButterworthLp2 (1000.0f, 25.0f); +} + +template +void HulaComp::init() +{ + // set random detune, += 5 cent; + for (auto& f : fineTuneVocts) + f = float_4 ((rand01() * 2.0f - 1.0f) * 5.0f / (12.0f * 100.0f), + (rand01() * 2.0f - 1.0f) * 5.0f / (12.0f * 100.0f), + (rand01() * 2.0f - 1.0f) * 5.0f / (12.0f * 100.0f), + (rand01() * 2.0f - 1.0f) * 5.0f / (12.0f * 100.0f)); + + for (auto& l : lastOuts) + l = float_4 (0); + + for (auto& p : phases) + p = float_4 (0); +} + +template +inline void HulaComp::step() +{ + auto channels = std::max (1, TBase::inputs[VOCT_INPUT].getChannels()); + + for (auto c = 0; c < channels; c += 4) + { + //calculate frequency + float_4 voct = TBase::inputs[VOCT_INPUT].template getPolyVoltageSimd (c) + fineTuneVocts[c / 4]; + voct += simd::floor (TBase::params[OCTAVE_PARAM].getValue()); + voct += simd::floor (TBase::params[SEMITONE_PARAM].getValue()) * (1.0f / 12.0f); + float_4 freq = dsp::FREQ_C4 * lookup.pow2 (voct); + freq *= TBase::params[RATIO_PARAM].getValue(); + float_4 phaseInc = freq * reciprocalSampleRate / oversampleCount; + + //phase offset as fm is implemented as phase modulation + float_4 phaseOffset = TBase::params[FEEDBACK_PARAM].getValue() * 0.053f * (lastOuts[c / 4]); + if (TBase::inputs[FEEDBACK_CV_INPUT].isConnected()) + { + phaseOffset *= feedbackFilters[c / 4].process (simd::abs (TBase::inputs[FEEDBACK_CV_INPUT].template getPolyVoltageSimd (c) * 0.1f)); + } + float_4 fmIn = TBase::inputs[FM_INPUT].template getPolyVoltageSimd (c) * 0.2f; // scale from +-5 to +=1 + + if (TBase::inputs[DEPTH_CV_INPUT].isConnected()) + { + fmIn *= depthFilters[c / 4].process (simd::abs (TBase::inputs[DEPTH_CV_INPUT].template getPolyVoltageSimd (c) * 0.1f)); + } + + phaseOffset += TBase::params[DEPTH_PARAM].getValue() * fmIn; + + for (auto i = 0; i < oversampleCount; ++i) + { + //generate oversampled signal + phases[c / 4] += phaseInc; + phases[c / 4] = simd::ifelse (phases[c / 4] > float_4 (1.0f), phases[c / 4] - 1.0f, phases[c / 4]); + oversampleBuffers[c / 4][i] = lookup.hulaSin4 ((phases[c / 4] + phaseOffset) * k_2pi); + } + + lastOuts[c / 4] = dcOutFilters[c / 4].process (decimators[c / 4].process (oversampleBuffers[c / 4].data())) * 5.0f; + TBase::outputs[MAIN_OUTPUT].setVoltageSimd (lpFilters[c / 4].process (lastOuts[c / 4]), c); + } + + TBase::outputs[MAIN_OUTPUT].setChannels (channels); +} + +template +int HulaDescription::getNumParams() +{ + return HulaComp::NUM_PARAMS; +} + +template +IComposite::Config HulaDescription::getParam (int i) +{ + IComposite::Config ret = { 0.0f, 1.0f, 0.0f, "Code type", "unit", 0.0f, 1.0f, 0.0f }; + switch (i) + { + //TODO + case HulaComp::RATIO_PARAM: + ret = { 0.5f, 25.95f, 1.0f, "Ratio", " ", 0, 1, 0.0f }; + break; + case HulaComp::SEMITONE_PARAM: + ret = { 0.0f, 12.0f, 0.0f, "Semitone", " ", 0, 1, 0.0f }; + break; + case HulaComp::OCTAVE_PARAM: + ret = { -5.0f, 5.0f, 0.0f, "Octave", " ", 0, 1, 0.0f }; + break; + case HulaComp::DEPTH_PARAM: + ret = { 0.0f, 1.0f, 0.0f, "Depth", " ", 0, 1, 0.0f }; + break; + case HulaComp::FEEDBACK_PARAM: + ret = { 0.0f, 1.0f, 0.0f, "Feedback", " ", 0, 1, 0.0f }; + break; + + default: + assert (false); + } + return ret; +} \ No newline at end of file diff --git a/src/composites/Maccomo.h b/src/composites/Maccomo.h index cb45c4f..f58ef40 100644 --- a/src/composites/Maccomo.h +++ b/src/composites/Maccomo.h @@ -94,7 +94,8 @@ class MaccomoComp : public TBase for (auto& f : filters) { f.setUseNonLinearProcessing (true); - f.setType (sspo::MoogLadderFilter::types()[0]); + f.setType (sspo::MoogLadderFilter::types()[0]); + f.nonLinearProcess = [] (float in, float drive) { return std::tanh (in * drive); }; } sspo::AudioMath::defaultGenerator.seed (time (NULL)); @@ -143,7 +144,7 @@ class MaccomoComp : public TBase static constexpr int typeCount = 6; std::vector currentTypes; - std::vector filters; + std::vector> filters; void step() override; @@ -198,7 +199,7 @@ inline void MaccomoComp::step() if (currentTypes[i] != modeParam + int (TBase::inputs[MODE_CV_INPUT].getPolyVoltage (i))) { currentTypes[i] = clamp (modeParam + int (TBase::inputs[MODE_CV_INPUT].getPolyVoltage (i)), 0, typeCount - 1); - filters[i].setType (sspo::MoogLadderFilter::types()[currentTypes[i]]); + filters[i].setType (sspo::MoogLadderFilter::types()[currentTypes[i]]); } filters[i].setParameters (frequency, resonance, drive, 0, sampleRate); diff --git a/src/dsp/AudioMath.h b/src/dsp/AudioMath.h index 24bb2bb..52f358e 100644 --- a/src/dsp/AudioMath.h +++ b/src/dsp/AudioMath.h @@ -28,6 +28,11 @@ #include #include +#include "simd/functions.hpp" +#include "simd/sse_mathfun.h" +#include "simd/sse_mathfun_extension.h" +#include "simd/vector.hpp" + //#include "LookupTable.h" //#include "lookupTables/SineTable.h" diff --git a/src/dsp/LookupTable.h b/src/dsp/LookupTable.h index dfaf767..ec73480 100644 --- a/src/dsp/LookupTable.h +++ b/src/dsp/LookupTable.h @@ -27,6 +27,13 @@ #include "AudioMath.h" +#include "simd/functions.hpp" +#include "simd/sse_mathfun.h" +#include "simd/sse_mathfun_extension.h" +#include "simd/vector.hpp" + +using float_4 = ::rack::simd::float_4; + namespace sspo { namespace AudioMath @@ -43,7 +50,7 @@ namespace sspo }; template - inline T process (Table& source, const T x) + inline T process (Table& source, const T x) noexcept { assert (source.table.size() != 0 && "Lookup table empty"); assert (source.minX != source.maxX && "Lookup table min equal max"); @@ -54,10 +61,73 @@ namespace sspo auto index = static_cast ((x / source.interval) - source.minX / source.interval); // commented out due to incressing time of lookup by 60% - index = rack::math::clamp (index, 0, static_cast (source.table.size() - 2)); + // index = rack::simd::clamp (index, 0, static_cast (source.table.size() - 2)); T fraction = ((x / source.interval) - source.minX / source.interval) - index; - T ret = linearInterpolate (static_cast (source.table[index]), static_cast (source.table[index + 1]), fraction); - return ret; + return linearInterpolate (static_cast (source.table[index]), static_cast (source.table[index + 1]), fraction); + } + + // original take using slow vector access to read from table + // perf.exe reports 23% of 1% usage + // template + // inline float_4 process (Table& source, float_4 x) + // { + // assert (source.table.size() != 0 && "Lookup table empty"); + // assert (source.minX != source.maxX && "Lookup table min equal max"); + // assert (source.interval != 0 && "Lookup interval 0"); + // + // //x = rack::simd::clamp(x, float_4(source.minX), float_4(source.maxX)); + // //assert(x >= source.minX && "Lookuptable index too low"); + // //assert(x <= source.maxX && "Lookuptable index too greate"); + // + // float_4 index = rack::simd::floor((x / source.interval) - source.minX / source.interval); + // //index = simd::clamp(index, float_4(0), float_4(source.table.size() - 1.0f)); + // + // float_4 fraction = ((x / source.interval) - source.minX / source.interval) - index; + // float_4 lower = float_4 (source.table[index[0]], + // source.table[index[1]], + // source.table[index[2]], + // source.table[index[3]]); + // float_4 upper = float_4 (source.table[index[0] + 1], + // source.table[index[1] + 1], + // source.table[index[2] + 1], + // source.table[index[3] + 1]); + // return linearInterpolate (lower, upper, fraction); + // + // } + + // second attempt without creating new float_4; + // member float_4 are used, this makes Lookup tables no reentrant + // a separate instance is require for each module instance + // dont use lookup.xxx() for simd + // perf.exe reports 23% of 1% usage + + template + inline float_4 process (Table& source, float_4 x) + { + // const float_4 lower = float_4(0.0f, 0.45f, 0.33f, 0.77f); + // const float_4 upper {0.1f, 0.55f, 0.45f, 0.9f}; + float_4 fraction; + float_4 index; + + assert (source.table.size() != 0 && "Lookup table empty"); + assert (source.minX != source.maxX && "Lookup table min equal max"); + assert (source.interval != 0 && "Lookup interval 0"); + + //x = rack::simd::clamp(x, float_4(source.minX), float_4(source.maxX)); + //assert(x >= source.minX && "Lookuptable index too low"); + //assert(x <= source.maxX && "Lookuptable index too greate"); + + index = rack::simd::floor ((x / source.interval) - source.minX / source.interval); + fraction = ((x / source.interval) - source.minX / source.interval) - index; + float_4 lower = float_4 (source.table[index[0]], + source.table[index[1]], + source.table[index[2]], + source.table[index[3]]); + float_4 upper = float_4 (source.table[index[0] + 1], + source.table[index[1] + 1], + source.table[index[2] + 1], + source.table[index[3] + 1]); + return linearInterpolate (lower, upper, fraction); // linearInterpolate (lower, upper, fraction); } template @@ -127,11 +197,13 @@ namespace sspo { Lookup() { - sineTable = sspo::AudioMath::LookupTable::makeTable (-k_2pi - 0.1f, k_2pi + 0.1f, 0.001f, [] (const float x) -> float { return std::sin (x); }); + sineTable = sspo::AudioMath::LookupTable::makeTable (-4 * k_2pi - 0.1f, 4 * k_2pi + 0.1f, 0.001f, [] (const float x) -> float { return std::sin (x); }); pow2Table = LookupTable::makeTable (-10.1f, 10.1f, 0.001f, [] (const float x) -> float { return std::pow (2.0f, x); }); pow10Table = LookupTable::makeTable (-10.1f, 10.1f, 0.001f, [] (const float x) -> float { return std::pow (10.0f, x); }); log10Table = LookupTable::makeTable (0.00001f, 10.1f, 0.001f, [] (const float x) -> float { return std::log10 (x); }); unisonSpreadTable = LookupTable::makeTable (0.0f, 1.1f, 0.01f, [] (const float x) -> float { return unisonSpreadScalar (x); }); + + hulaSineTable = sspo::AudioMath::LookupTable::makeTable (-4 * k_2pi - 0.1f, 4 * k_2pi + 0.1f, 0.001f, [] (const float x) -> float { return std::sin (x) + (rand01() - 0.5f) * 1e-4f; }); } sspo::AudioMath::LookupTable::Table sineTable; @@ -139,12 +211,16 @@ namespace sspo sspo::AudioMath::LookupTable::Table pow10Table; sspo::AudioMath::LookupTable::Table log10Table; sspo::AudioMath::LookupTable::Table unisonSpreadTable; + sspo::AudioMath::LookupTable::Table hulaSineTable; float sin (const float x) { return sspo::AudioMath::LookupTable::process (sineTable, x); } float pow2 (const float x) { return sspo::AudioMath::LookupTable::process (pow2Table, x); } + float_4 pow2 (const float_4 x) { return sspo::AudioMath::LookupTable::process (pow2Table, x); } float pow10 (const float x) { return sspo::AudioMath::LookupTable::process (pow10Table, x); } float log10 (const float x) { return sspo::AudioMath::LookupTable::process (log10Table, x); } float unisonSpread (const float x) { return sspo::AudioMath::LookupTable::process (unisonSpreadTable, x); } + float hulaSin (const float x) { return sspo::AudioMath::LookupTable::process (hulaSineTable, x); } + float_4 hulaSin4 (const float_4 x) { return sspo::AudioMath::LookupTable::process (hulaSineTable, x); } }; } // namespace LookupTable diff --git a/src/dsp/SynthFilter.h b/src/dsp/SynthFilter.h index f751894..6404c34 100644 --- a/src/dsp/SynthFilter.h +++ b/src/dsp/SynthFilter.h @@ -26,25 +26,26 @@ #include #include +#include +#include "AudioMath.h" +#include "UtilityFilters.h" +#include "simd/vector.hpp" +#include "simd/functions.hpp" +#include "simd/sse_mathfun.h" +#include "simd/sse_mathfun_extension.h" namespace sspo { - inline float fastTanh (float x) - { - // return x * (27 + x * x) / (27 + 9 * x * x); - return std::tanh (x); - } - + template class SynthFilter { public: - constexpr static float cutoffMin = 20.0f; - constexpr static float cutoffMax = 20000.0f; - constexpr static float cutoffDefault = 20000.0f; - constexpr static float QDefault = 0.707f; + constexpr static float cutoffMin{ 20.0f }; + constexpr static float cutoffMax = { 20000.0f }; + constexpr static float cutoffDefault{ 20000.0f }; + constexpr static float QDefault{ 0.707f }; constexpr static auto LD_PI = 3.14159265358979323846264338327950288419716939937510L; - constexpr static auto k_pi = static_cast (LD_PI); - constexpr static auto k_2pi = k_pi + k_pi; + constexpr static float k_pi = float (LD_PI); enum class Type { @@ -66,53 +67,60 @@ namespace sspo useNPL = x; } + void setUseOversample (const bool x) + { + useOverSample = x; + } + protected: - float cutoff{ cutoffDefault }; - float Q{ QDefault }; + T cutoff{ T (cutoffDefault) }; + T Q{ T (QDefault) }; float sampleRate{ 1.0f }; - float aux{ 0.0f }; + T aux{ T (0.0f) }; bool useNPL{ false }; - float saturation{ 1.0 }; + bool useOverSample{ false }; + T saturation{ T (1.0) }; Type type{ Type::LPF2 }; }; - class OnePoleFilter : public SynthFilter + template + class OnePoleFilter : public SynthFilter { public: OnePoleFilter() { - type = Type::LPF1; + SynthFilter::type = SynthFilter::Type::LPF1; reset(); } - void setFeedback (float newFeedback) + void setFeedback (T newFeedback) { feedback = newFeedback; } - void setPreGain (float newPreGain) + void setPreGain (T newPreGain) { preGain = newPreGain; } - void setBeta (float newBeta) + void setBeta (T newBeta) { beta = newBeta; } - float getFeedbackOut() + T getFeedbackOut() { return beta * (z1 + feedback * feedbackIn); } - void setFeedForward (float x) + void setFeedForward (T x) { feedforward = x; } - float process (const float in) + T process (const T in) { - if (type != Type::LPF1 && type != Type::HPF1) + if (SynthFilter::type != SynthFilter::Type::LPF1 && SynthFilter::type != SynthFilter::Type::HPF1) return in; auto xn = in; @@ -123,114 +131,136 @@ namespace sspo auto hp = xn - lp; z1 = vn + lp; - if (type == Type::LPF1) + if (SynthFilter::type == SynthFilter::Type::LPF1) return lp; else return hp; } - void setParameters (const float newCutoff, const float newQ, const float newAux, const float newSampleRate) + void setParameters (const T newCutoff, const T newQ, const T newAux, const float newSampleRate) { - cutoff = newCutoff; - Q = newQ; - aux = newAux; - sampleRate = newSampleRate; + SynthFilter::cutoff = newCutoff; + SynthFilter::Q = newQ; + SynthFilter::aux = newAux; + SynthFilter::sampleRate = newSampleRate; calcCoeffs(); } - void setType (Type newType) + void setType (typename SynthFilter::Type newType) { - type = newType; + SynthFilter::type = newType; } void reset() { - z1 = 0.0f; - feedback = 0.0f; + z1 = T (0.0f); + feedback = T (0.0f); } private: void calcCoeffs() { - auto wd = k_2pi * cutoff; - auto T = 1.0f / sampleRate; - auto wa = (2 / T) * std::tan (wd * T / 2); - auto g = wa * T / 2; + auto wd = AudioMath::k_2pi * SynthFilter::cutoff; + auto T1 = 1.0f / SynthFilter::sampleRate; + auto wa = (2 / T1) * rack::simd::tan (wd * T1 / 2); + auto g = wa * T1 / 2; feedforward = g / (1.0f + g); } - float feedforward{ 1.0f }; - float beta{ 0.0f }; - float preGain{ 1.0f }; - float feedbackIn{ 0.0f }; - float feedbackOut{ 0.0f }; - float inputGain{ 1.0f }; - float feedback{ 0.0f }; - float z1{ 0.0f }; + T feedforward{ 1.0f }; + T beta{ 0.0f }; + T preGain{ 1.0f }; + T feedbackIn{ 0.0f }; + T feedbackOut{ 0.0f }; + T inputGain{ 1.0f }; + T feedback{ 0.0f }; + T z1{ 0.0f }; }; - class MoogLadderFilter : public SynthFilter + template + class MoogLadderFilter : public SynthFilter { public: - static std::vector types() + static std::vector::Type> types() { - return { Type::LPF2, Type::LPF4, Type::HPF2, Type::HPF4, Type::BPF2, Type::BPF4 }; + return { SynthFilter::Type::LPF2, + SynthFilter::Type::LPF4, + SynthFilter::Type::HPF2, + SynthFilter::Type::HPF4, + SynthFilter::Type::BPF2, + SynthFilter::Type::BPF4 }; } MoogLadderFilter() { - lpf1.setType (Type::LPF1); - lpf2.setType (Type::LPF1); - lpf3.setType (Type::LPF1); - lpf4.setType (Type::LPF1); + lpf1.setType (SynthFilter::Type::LPF1); + lpf2.setType (SynthFilter::Type::LPF1); + lpf3.setType (SynthFilter::Type::LPF1); + lpf4.setType (SynthFilter::Type::LPF1); - type = Type::LPF4; + SynthFilter::type = SynthFilter::Type::LPF4; reset(); } - ~MoogLadderFilter() - { - } + ~MoogLadderFilter() = default; - void setParameters (const float newCutoff, - const float newQ, - const float newSaturation, - const float newAux, + void setParameters (const T newCutoff, + const T newQ, + const T newSaturation, + const T newAux, const float newSampleRate) { - if ((newCutoff == cutoff) && (newQ == Q) && (newSaturation == saturation) && (newSampleRate == sampleRate)) - return; - - cutoff = newCutoff; - aux = newAux; - Q = newQ; - sampleRate = newSampleRate; - saturation = newSaturation; - lpf1.setParameters (cutoff, 0, 0, sampleRate); - lpf2.setParameters (cutoff, 0, 0, sampleRate); - lpf3.setParameters (cutoff, 0, 0, sampleRate); - lpf4.setParameters (cutoff, 0, 0, sampleRate); - - K = (4.0f) * (Q - 1.0f) / (10.0f - 1.0f); + // if ((newCutoff == SynthFilter::cutoff) && (newQ == SynthFilter::Q) + // && (newSaturation == SynthFilter::saturation) && (newSampleRate == SynthFilter::sampleRate)) + // return; + + SynthFilter::cutoff = newCutoff; + SynthFilter::aux = newAux; + SynthFilter::Q = newQ; + SynthFilter::sampleRate = newSampleRate; + SynthFilter::saturation = newSaturation; + lpf1.setParameters (SynthFilter::cutoff, 0, 0, SynthFilter::sampleRate); + lpf2.setParameters (SynthFilter::cutoff, 0, 0, SynthFilter::sampleRate); + lpf3.setParameters (SynthFilter::cutoff, 0, 0, SynthFilter::sampleRate); + lpf4.setParameters (SynthFilter::cutoff, 0, 0, SynthFilter::sampleRate); + + K = (4.0f) * (SynthFilter::Q - 1.0f) / (10.0f - 1.0f); calcCoeffs(); } - float process (const float in) + //this must be defined in the module, normally as a lambda + std::function nonLinearProcess; + + T process (const T in) { - if (type == Type::BSF2 || type == Type::LPF1 || type == Type ::HPF1) + if (SynthFilter::type == SynthFilter::Type::BSF2 + || SynthFilter::type == SynthFilter::Type::LPF1 + || SynthFilter::type == SynthFilter::Type ::HPF1) return in; auto sigma = lpf1.getFeedbackOut() + lpf2.getFeedbackOut() + lpf3.getFeedbackOut() + lpf4.getFeedbackOut(); auto xn = in; - xn *= 1.0f + aux * K; + xn *= 1.0f + SynthFilter::aux * K; auto U = (xn - K * sigma) * alpha; - if (useNPL) - U = fastTanh (saturation * U); + if (SynthFilter::useNPL) + { + if (SynthFilter::useOverSample) + { + upsampler.process (U, oversampleBuffer); + for (auto i = 0; i < oversampleRate; ++i) + oversampleBuffer[i] = nonLinearProcess (U, SynthFilter::saturation); + U = decimator.process (oversampleBuffer); + } + else + { + U = nonLinearProcess (U, SynthFilter::saturation); + } + } auto f1 = lpf1.process (U); auto f2 = lpf2.process (f1); @@ -244,9 +274,9 @@ namespace sspo + typeCoeffs.E * f4; } - void setType (Type newType) + void setType (typename SynthFilter::Type newType) { - type = newType; + SynthFilter::type = newType; calcCoeffs(); } @@ -265,7 +295,7 @@ namespace sspo { } - OberheimXpander (const float a, const float b, const float c, const float d, const float e) + OberheimXpander (const T a, const T b, const T c, const T d, const T e) { A = a; B = b; @@ -274,19 +304,19 @@ namespace sspo E = e; } - float A{ 0.0f }; - float B{ 0.0f }; - float C{ 0.0f }; - float D{ 0.0f }; - float E{ 0.0f }; + T A{ 0.0f }; + T B{ 0.0f }; + T C{ 0.0f }; + T D{ 0.0f }; + T E{ 0.0f }; } typeCoeffs; void calcCoeffs() { - auto wd = k_2pi * cutoff; - auto T = 1.0f / static_cast (sampleRate); - auto wa = (2 / T) * std::tan (wd * T / 2); - auto g = wa * T / 2; + auto wd = AudioMath::k_2pi * SynthFilter::cutoff; + auto T1 = 1.0f / static_cast (SynthFilter::sampleRate); + auto wa = (2 / T1) * tan (wd * T1 / 2); + auto g = wa * T1 / 2; auto G = g / (1.0f + g); @@ -303,39 +333,44 @@ namespace sspo gamma = G * G * G * G; alpha = 1.0f / (1.0f + K * gamma); - switch (type) + switch (SynthFilter::type) { - case Type::LPF4: - typeCoeffs = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; + case SynthFilter::Type::LPF4: + typeCoeffs = { T (0.0f), T (0.0f), T (0.0f), T (0.0f), T (1.0f) }; break; - case Type::LPF2: - typeCoeffs = { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }; + case SynthFilter::Type::LPF2: + typeCoeffs = { T (0.0f), T (0.0f), T (1.0f), T (0.0f), T (0.0f) }; break; - case Type::BPF4: - typeCoeffs = { 0.0f, 0.0f, 4.0f, -8.0f, 4.0f }; + case SynthFilter::Type::BPF4: + typeCoeffs = { T (0.0f), T (0.0f), T (4.0f), T (-8.0f), T (4.0f) }; break; - case Type::BPF2: - typeCoeffs = { 0.0f, 2.0f, -2.0f, 0.0f, 0.0f }; + case SynthFilter::Type::BPF2: + typeCoeffs = { T (0.0f), T (2.0f), T (-2.0f), T (0.0f), T (0.0f) }; break; - case Type::HPF4: - typeCoeffs = { 1.0f, -4.0f, 6.0f, -4.0f, 1.0f }; + case SynthFilter::Type::HPF4: + typeCoeffs = { T (1.0f), T (-4.0f), T (6.0f), T (-4.0f), T (1.0f) }; break; - case Type::HPF2: - typeCoeffs = { 1.0f, -2.0f, 1.0f, 0.0f, 0.0f }; + case SynthFilter::Type::HPF2: + typeCoeffs = { T (1.0f), T (-2.0f), T (1.0f), T (0.0f), T (0.0f) }; break; default: //lpf4 - typeCoeffs = { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; + typeCoeffs = { T (0.0f), T (0.0f), T (0.0f), T (0.0f), T (1.0f) }; break; } } - OnePoleFilter lpf1; - OnePoleFilter lpf2; - OnePoleFilter lpf3; - OnePoleFilter lpf4; + OnePoleFilter lpf1{}; + OnePoleFilter lpf2{}; + OnePoleFilter lpf3{}; + OnePoleFilter lpf4{}; + + T K{ 0.0f }; + T gamma{ 0.0f }; + T alpha{ 1.0f }; - float K{ 0.0f }; - float gamma{ 0.0f }; - float alpha{ 1.0f }; + static constexpr int oversampleRate = 4; + sspo::Upsampler upsampler; + sspo::Decimator decimator; + T oversampleBuffer[oversampleRate]; }; } // namespace sspo \ No newline at end of file diff --git a/src/dsp/UtilityFilters.h b/src/dsp/UtilityFilters.h index d310916..3ef6e8f 100644 --- a/src/dsp/UtilityFilters.h +++ b/src/dsp/UtilityFilters.h @@ -282,4 +282,73 @@ namespace sspo } }; + /// IIR Decimator + /// oversample, upsample tate + /// quality, number of sequential filters + template + struct Decimator + { + BiQuad filters[quality]; + + Decimator() + { + for (auto i = 0; i < quality; ++i) + { + // the oversample filter has been set at niquist, to remove unwanted + // noise in the audio spectrum + filters[i].setButterworthLp2 (10000.0f, 10000.0f / (1.0f * oversample)); + } + } + + T process (const T* input) + { + T x = 0; + for (auto i = 0; i < oversample; ++i) + { + x = filters[0].process (input[i]); + for (auto j = 1; j < quality; ++j) + { + x = filters[j].process (x); + } + } + // we simply return the last sample + return x; + } + }; + + /// IIR upsample interpolator + /// oversample, upsample rate + /// quality, number of sequential filters + template + struct Upsampler + { + BiQuad filters[quality]; + + Upsampler() + { + for (auto i = 0; i < quality; ++i) + { + // the oversample filter has been set at niquist, to remove unwanted + // noise in the audio spectrum + filters[i].setButterworthLp2 (10000.0f, 10000.0f / (1.0f * oversample)); + } + } + + void process (T in, T* buffer) + { + buffer[0] = doFilter (in); + for (auto i = 1; i < oversample; ++i) + buffer[i] = doFilter (T (0) * oversample); + } + + T doFilter (T in) + { + T x = filters[0].process (in); + for (auto i = 1; i < quality; ++i) + { + x = filters[i].process (x); + } + return x; + } + }; } // namespace sspo \ No newline at end of file diff --git a/src/modules/Amburgh.cpp b/src/modules/Amburgh.cpp new file mode 100644 index 0000000..e2e829b --- /dev/null +++ b/src/modules/Amburgh.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2020 Dave French + * + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "plugin.hpp" +#include "Amburgh.h" +#include "WidgetComposite.h" +#include "ctrl/SqMenuItem.h" +#include "widgets.h" + +using Comp = AmburghComp; + +struct Amburgh : Module +{ + std::shared_ptr ma; + + Amburgh() + { + config (Comp::NUM_PARAMS, Comp::NUM_INPUTS, Comp::NUM_OUTPUTS, Comp::NUM_LIGHTS); + ma = std::make_shared (this); + std::shared_ptr icomp = Comp::getDescription(); + SqHelper::setupParams (icomp, this); + + onSampleRateChange(); + ma->init(); + } + + void onSampleRateChange() override + { + float rate = SqHelper::engineGetSampleRate(); + ma->setSampleRate (rate); + } + + void process (const ProcessArgs& args) override + { + ma->step(); + } +}; + +/***************************************************** +User Interface +*****************************************************/ + +struct AmburghWidget : ModuleWidget +{ + AmburghWidget (Amburgh* module) + { + setModule (module); + std::shared_ptr icomp = Comp::getDescription(); + box.size = Vec (10 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT); + SqHelper::setPanel (this, "res/Amburgh.svg"); + + addChild (createWidget (Vec (RACK_GRID_WIDTH, 0))); + addChild (createWidget (Vec (box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild (createWidget (Vec (RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild (createWidget (Vec (box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam (SqHelper::createParamCentered (icomp, mm2px (Vec (41.01, 25.14 + 1.25)), module, Comp::FREQUENCY_PARAM)); + addParam (SqHelper::createParamCentered (icomp, mm2px (Vec (25.135, 29.10 + 2.5)), module, Comp::FREQUENCY_CV_ATTENUVERTER_PARAM)); + addParam (SqHelper::createParamCentered (icomp, mm2px (Vec (25.135, 47.802 + 2.5)), module, Comp::RESONANCE_CV_ATTENUVERTER_PARAM)); + addParam (SqHelper::createParamCentered (icomp, mm2px (Vec (41.01, 47.802 + 2.5)), module, Comp::RESONANCE_PARAM)); + addParam (SqHelper::createParamCentered (icomp, mm2px (Vec (25.135, 70.292 + 2.5)), module, Comp::DRIVE_CV_ATTENUVERTER_PARAM)); + addParam (SqHelper::createParamCentered (icomp, mm2px (Vec (41.01, 70.292 + 2.5)), module, Comp::DRIVE_PARAM)); + addParam (SqHelper::createParamCentered (icomp, mm2px (Vec (25.135, 92.781)), module, Comp::MODE_PARAM)); + + addInput (createInputCentered (mm2px (Vec (9.26, 21.344)), module, Comp::VOCT_INPUT)); + addInput (createInputCentered (mm2px (Vec (9.26, 47.802 + 2.5)), module, Comp::RESONANCE_CV_INPUT)); + addInput (createInputCentered (mm2px (Vec (9.26, 70.292 + 2.5)), module, Comp::DRIVE_CV_INPUT)); + addInput (createInputCentered (mm2px (Vec (9.26, 112.625)), module, Comp::MAIN_INPUT)); + addInput (createInputCentered (mm2px (Vec (9.26, 29.50 + 2.5)), module, Comp::FREQ_CV_INPUT)); + + addOutput (createOutputCentered (mm2px (Vec (41.01, 112.625)), module, Comp::MAIN_OUTPUT)); + } +}; + +Model* modelAmburgh = createModel ("Amburgh"); \ No newline at end of file diff --git a/src/modules/Hula.cpp b/src/modules/Hula.cpp new file mode 100644 index 0000000..d5ab1e2 --- /dev/null +++ b/src/modules/Hula.cpp @@ -0,0 +1,69 @@ +#include "plugin.hpp" +#include "Hula.h" +#include "WidgetComposite.h" +#include "ctrl/SqMenuItem.h" +#include "widgets.h" +#include +#include +#include "AudioMath.h" +#include "widgets.h" + +using Comp = HulaComp; + +struct Hula : Module +{ + std::shared_ptr hula; + + Hula() + { + config (Comp::NUM_PARAMS, Comp::NUM_INPUTS, Comp::NUM_OUTPUTS, Comp::NUM_LIGHTS); + hula = std::make_shared (this); + std::shared_ptr icomp = Comp::getDescription(); + SqHelper::setupParams (icomp, this); + onSampleRateChange(); + hula->init(); + } + + void process (const ProcessArgs& args) override + { + hula->step(); + } + + void onSampleRateChange() override + { + float rate = SqHelper::engineGetSampleRate(); + hula->setSampleRate (rate); + } +}; + +struct HulaWidget : ModuleWidget +{ + HulaWidget (Hula* module) + { + setModule (module); + std::shared_ptr icomp = Comp::getDescription(); + SqHelper::setPanel (this, "res/Hula.svg"); + + addChild (createWidget (Vec (0, 0))); + addChild (createWidget (Vec (box.size.x - 1 * RACK_GRID_WIDTH, 0))); + addChild (createWidget (Vec (0, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild (createWidget (Vec (box.size.x - 1 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam (createParamCentered (mm2px (Vec (15.235, 26.803)), module, Comp::RATIO_PARAM)); + addParam (createParamCentered (mm2px (Vec (9.26, 50.302)), module, Comp::SEMITONE_PARAM)); + addParam (createParamCentered (mm2px (Vec (21.392, 50.542)), module, Comp::OCTAVE_PARAM)); + //addParam (createParamCentered (mm2px (Vec (25.135, 72.792)), module, Comp::DEPTH_CV_ATTENUVERTER_PARAM)); + addParam (createParamCentered (mm2px (Vec (21.392, 73.032)), module, Comp::DEPTH_PARAM)); + //addParam (createParamCentered (mm2px (Vec (25.135, 96.353)), module, Comp::FEEDBACK_CV_ATTENUVERTER_PARAM)); + addParam (createParamCentered (mm2px (Vec (21.392, 96.594)), module, Comp::FEEDBACK_PARAM)); + + addInput (createInputCentered (mm2px (Vec (9.26, 72.792)), module, Comp::DEPTH_CV_INPUT)); + addInput (createInputCentered (mm2px (Vec (9.26, 96.353)), module, Comp::FEEDBACK_CV_INPUT)); + addInput (createInputCentered (mm2px (Vec (6.169, 112.865)), module, Comp::VOCT_INPUT)); + addInput (createInputCentered (mm2px (Vec (15.679, 112.865)), module, Comp::FM_INPUT)); + + addOutput (createOutputCentered (mm2px (Vec (25.188, 112.865)), module, Comp::MAIN_OUTPUT)); + } +}; + +Model* modelHula = createModel ("Hula"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index bafdb70..c2b8e1d 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -18,4 +18,6 @@ void init (::rack::Plugin* p) p->addModel (modelIverson); p->addModel (modelIversonJr); p->addModel (modelZilah); + p->addModel (modelHula); + p->addModel (modelAmburgh); } diff --git a/src/plugin.hpp b/src/plugin.hpp index 91c1b8b..22f2302 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -16,3 +16,7 @@ extern Model* modelTe; extern Model* modelIverson; extern Model* modelIversonJr; extern Model* modelZilah; +extern Model* modelHula; +extern Model* modelAmburgh; + + diff --git a/test/main.cpp b/test/main.cpp index 84b358c..a257221 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -25,7 +25,16 @@ #include +#include "simd/functions.hpp" +#include "simd/sse_mathfun.h" +#include "simd/sse_mathfun_extension.h" +#include "simd/vector.hpp" + +using float_4 = rack::simd::float_4; +using namespace rack; + // external tests +extern void testSynthFilter(); extern void testEva(); extern void testEmpty(); extern void testAudioMath(); @@ -36,7 +45,6 @@ extern void testPolyShiftRegister(); extern void testTestSignal(); extern void testKSDelay(); extern void testCombFilter(); -extern void testSynthFilter(); extern void testMaccomo(); extern void testSaturator(); extern void testUtilityFilter(); @@ -86,6 +94,7 @@ int main (int argc, char** argv) // run external tests defined above + testSynthFilter(); testTriggerSequencer(); testIverson(); testLala(); @@ -102,7 +111,6 @@ int main (int argc, char** argv) testPolyShiftRegister(); testKSDelay(); testCombFilter(); - testSynthFilter(); testMaccomo(); testUtilityFilter(); diff --git a/test/perfTest.cpp b/test/perfTest.cpp index 09b2cd8..a55a30d 100644 --- a/test/perfTest.cpp +++ b/test/perfTest.cpp @@ -37,6 +37,11 @@ SOFTWARE. extern double overheadInOut; extern double overheadOutOnly; +#include "simd/functions.hpp" +#include "simd/sse_mathfun.h" +#include "simd/sse_mathfun_extension.h" +#include "simd/vector.hpp" + #include "MeasureTime.h" #include "TestComposite.h" @@ -52,6 +57,9 @@ extern double overheadOutOnly; #include "Eva.h" #include "Zazel.h" +using float_4 = rack::simd::float_4; +using namespace rack; + #ifdef _USE_WINDOWS_PERFTIME double SqTime::frequency = 0; #endif @@ -249,6 +257,23 @@ static void testFastApprox() static void testLookupTable() { + MeasureTime::run ( + overheadInOut, "lookup.hulaSin", []() { + float x = lookup.hulaSin (TestBuffers::get()); + return x; + }, + 1); + + float_4 f4; + MeasureTime::run ( + overheadInOut, "lookup.hulaSin4", [&f4]() { + f4[0] = TestBuffers::get(); + + float_4 x = lookup.hulaSin4 (f4); + return x[0]; + }, + 1); + MeasureTime::run ( overheadInOut, "std::sin", []() { float x = std::sin (TestBuffers::get()); @@ -591,6 +616,7 @@ void perfTest() assert (overheadInOut > 0); assert (overheadOutOnly > 0); + testLookupTable(); test1(); testUtilityFilters(); testZazel(); @@ -602,7 +628,7 @@ void perfTest() testHardLimiter(); testKSDelay(); testPolyShiftRegister(); - testLookupTable(); + testCombFilter(); testEva(); } \ No newline at end of file diff --git a/test/testAudioMath.cpp b/test/testAudioMath.cpp index c615411..86e8808 100644 --- a/test/testAudioMath.cpp +++ b/test/testAudioMath.cpp @@ -21,10 +21,17 @@ #include "AudioMath.h" #include "asserts.h" +#include "../src/composites/Eva.h" #include #include #include +#include "simd/functions.hpp" +#include "simd/sse_mathfun.h" +#include "simd/sse_mathfun_extension.h" +#include "simd/vector.hpp" + +using float_4 = rack::simd::float_4; using namespace sspo; static void testAreSame() @@ -90,11 +97,26 @@ static void testRand01() //printf ("rand01 %d \n", static_cast(bands.size())); } +static void testlinearInterpolateSimd() +{ + float_4 a{ -10.4, 11.7, 0.004, 3.2 }; + float_4 b{ 5.4, 2.7, 0.002, 5.8 }; + float_4 x{ 0.1, 0.2, 0.3, 0.7 }; + + float_4 r = AudioMath::linearInterpolate (a, b, x); + + for (auto i = 0; i < 4; ++i) + { + assert (AudioMath::linearInterpolate (a[i], b[i], x[i]) == r[i] && "linearInterpolate"); + } +} + void testAudioMath() { printf ("AudioMath\n"); testAreSame(); testFastTanh(); testlinearInterpolate(); + testlinearInterpolateSimd(); testRand01(); } diff --git a/test/testLookupTable.cpp b/test/testLookupTable.cpp index 7e48557..cc4c327 100644 --- a/test/testLookupTable.cpp +++ b/test/testLookupTable.cpp @@ -32,7 +32,7 @@ static void testCreate() sspo::AudioMath::LookupTable::Table sineTable = LookupTable::makeTable (0.0f, 5.0f, 0.001f, [] (const float x) -> float { return std::sin (x); }); assert (sineTable.minX == 0.0f); assert (sineTable.maxX == 5.0f); - assert (sineTable.interval = 0.001f); + assert (sineTable.interval == 0.001f); auto index = 0; for (float i = 0.000f; i < 5.0f; i += 0.001f) @@ -83,9 +83,22 @@ static void testConsume() //std::cout << sspo::AudioMath::LookupTable::makeHeader(sineTable, "SineTable") << "\n\n"; } +static void testConsumeSimd() +{ + /// pow2 and hulaSin have simd lookups for simultanious reading + float_4 a{ -3.0, -0, 1.4, 2.0 }; + float_4 r = lookup.hulaSin4 (a); + + for (auto i = 0; i < 4; ++i) + assertClose (lookup.hulaSin (a[i]), r[i], 0.001f); + + printf ("testConsumeSimd Test Lookup ok"); +} + void testLookupTable() { printf ("testLookupTable\n"); testCreate(); testConsume(); + testConsumeSimd(); } diff --git a/test/testSynthFilter.cpp b/test/testSynthFilter.cpp index e6318a0..e84b7ac 100644 --- a/test/testSynthFilter.cpp +++ b/test/testSynthFilter.cpp @@ -33,7 +33,7 @@ namespace ts = sspo::TestSignal; using namespace sspo; static std::string makeFilename (std::string filename, - SynthFilter::Type type, + SynthFilter::Type type, const float cutoff, const float sr, const int length) @@ -44,12 +44,12 @@ static std::string makeFilename (std::string filename, } static void makeOnePoleImpulse (std::string filename, - SynthFilter::Type type, + SynthFilter::Type type, const float cutoff, const float sr, const int length) { - OnePoleFilter filter; + OnePoleFilter filter; filter.setType (type); filter.setUseNonLinearProcessing (false); filter.setParameters (cutoff, 0.0f, 0.0f, sr); @@ -65,12 +65,12 @@ static void makeOnePoleImpulse (std::string filename, } static void testOnePoleImpulse (std::string filename, - SynthFilter::Type type, + SynthFilter::Type type, const float cutoff, const float sr, const int length) { - OnePoleFilter filter; + OnePoleFilter filter; filter.setType (type); filter.setUseNonLinearProcessing (false); filter.setParameters (cutoff, 0.0f, 0.0f, sr); @@ -85,16 +85,17 @@ static void testOnePoleImpulse (std::string filename, assert (ts::areSame (impulse, test)); } -static void testMoogLadderSlope (SynthFilter::Type type, +static void testMoogLadderSlope (SynthFilter::Type type, const float cutoff, const float sr, const float expected, const float tol = 1.5f) { - MoogLadderFilter filter; + MoogLadderFilter filter; filter.setType (type); filter.setUseNonLinearProcessing (true); filter.setParameters (cutoff, 0.0f, 1.1f, 0.0f, sr); + filter.nonLinearProcess = [] (float a, float b) { return a * b; }; constexpr int fftSize = 1024 * 32; @@ -107,7 +108,7 @@ static void testMoogLadderSlope (SynthFilter::Type type, auto response = ts::getResponse (signal); - auto slope = (type == SynthFilter::Type::LPF2 || type == SynthFilter::Type::LPF4) + auto slope = (type == SynthFilter::Type::LPF2 || type == SynthFilter::Type::LPF4) ? Analyzer::getSlopeLowpass (response, cutoff, sr) : Analyzer::getSlopeHighpass (response, cutoff, sr); @@ -118,55 +119,55 @@ static void testMoogLadderSlope (SynthFilter::Type type, static void testMoogLPHP() { - testMoogLadderSlope (SynthFilter::Type::LPF2, 10.0, 44100.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 100.0, 44100.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 1000.0, 44100.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 2000.0, 44100.0f, -9.0); - - testMoogLadderSlope (SynthFilter::Type::LPF2, 10.0, 22050.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 100.0, 22050.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 1000.0, 22050.0f, -9.0); - - testMoogLadderSlope (SynthFilter::Type::LPF2, 10.0, 48000.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 100.0, 48000.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 1000.0, 48000.0f, -9.0); - testMoogLadderSlope (SynthFilter::Type::LPF2, 2000.0, 48000.0f, -9.0); - - testMoogLadderSlope (SynthFilter::Type::LPF4, 10.0, 44100.0f, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 100.0, 44100, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 1000.0, 44100, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 2000.0, 44100, -19.0); - - testMoogLadderSlope (SynthFilter::Type::LPF4, 10.0, 22050, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 100.0, 22050, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 1000.0, 22050, -19.0); - - testMoogLadderSlope (SynthFilter::Type::LPF4, 10.0, 48000, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 100.0, 48000, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 1000.0, 48000, -19.0); - testMoogLadderSlope (SynthFilter::Type::LPF4, 2000.0, 48000, -19.0); - - testMoogLadderSlope (SynthFilter::Type::HPF2, 100.0, 44100, -9.0); - testMoogLadderSlope (SynthFilter::Type::HPF2, 1000.0, 44100, -9.0); - testMoogLadderSlope (SynthFilter::Type::HPF2, 2000.0, 44100, -9.0); - - testMoogLadderSlope (SynthFilter::Type::HPF2, 100.0, 22050, -9.0); - testMoogLadderSlope (SynthFilter::Type::HPF2, 1000.0, 22050, -9.0); - - testMoogLadderSlope (SynthFilter::Type::HPF2, 100.0, 48000, -9.0); - testMoogLadderSlope (SynthFilter::Type::HPF2, 1000.0, 48000, -9.0); - testMoogLadderSlope (SynthFilter::Type::HPF2, 2000.0, 48000, -9.0); - - testMoogLadderSlope (SynthFilter::Type::HPF4, 100.0, 44100, -17.0); - testMoogLadderSlope (SynthFilter::Type::HPF4, 1000.0, 44100, -17.0); - testMoogLadderSlope (SynthFilter::Type::HPF4, 2000.0, 44100, -17.0); - - testMoogLadderSlope (SynthFilter::Type::HPF4, 100.0, 22050, -17.0); - testMoogLadderSlope (SynthFilter::Type::HPF4, 1000.0, 22050, -17.0); - - testMoogLadderSlope (SynthFilter::Type::HPF4, 100.0, 48000, -17.0); - testMoogLadderSlope (SynthFilter::Type::HPF4, 1000.0, 48000, -17.0); - testMoogLadderSlope (SynthFilter::Type::HPF4, 2000.0, 48000, -17.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 10.0, 44100.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 100.0, 44100.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 1000.0, 44100.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 2000.0, 44100.0f, -9.0); + + testMoogLadderSlope (SynthFilter::Type::LPF2, 10.0, 22050.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 100.0, 22050.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 1000.0, 22050.0f, -9.0); + + testMoogLadderSlope (SynthFilter::Type::LPF2, 10.0, 48000.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 100.0, 48000.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 1000.0, 48000.0f, -9.0); + testMoogLadderSlope (SynthFilter::Type::LPF2, 2000.0, 48000.0f, -9.0); + + testMoogLadderSlope (SynthFilter::Type::LPF4, 10.0, 44100.0f, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 100.0, 44100, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 1000.0, 44100, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 2000.0, 44100, -19.0); + + testMoogLadderSlope (SynthFilter::Type::LPF4, 10.0, 22050, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 100.0, 22050, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 1000.0, 22050, -19.0); + + testMoogLadderSlope (SynthFilter::Type::LPF4, 10.0, 48000, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 100.0, 48000, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 1000.0, 48000, -19.0); + testMoogLadderSlope (SynthFilter::Type::LPF4, 2000.0, 48000, -19.0); + + testMoogLadderSlope (SynthFilter::Type::HPF2, 100.0, 44100, -9.0); + testMoogLadderSlope (SynthFilter::Type::HPF2, 1000.0, 44100, -9.0); + testMoogLadderSlope (SynthFilter::Type::HPF2, 2000.0, 44100, -9.0); + + testMoogLadderSlope (SynthFilter::Type::HPF2, 100.0, 22050, -9.0); + testMoogLadderSlope (SynthFilter::Type::HPF2, 1000.0, 22050, -9.0); + + testMoogLadderSlope (SynthFilter::Type::HPF2, 100.0, 48000, -9.0); + testMoogLadderSlope (SynthFilter::Type::HPF2, 1000.0, 48000, -9.0); + testMoogLadderSlope (SynthFilter::Type::HPF2, 2000.0, 48000, -9.0); + + testMoogLadderSlope (SynthFilter::Type::HPF4, 100.0, 44100, -17.0); + testMoogLadderSlope (SynthFilter::Type::HPF4, 1000.0, 44100, -17.0); + testMoogLadderSlope (SynthFilter::Type::HPF4, 2000.0, 44100, -17.0); + + testMoogLadderSlope (SynthFilter::Type::HPF4, 100.0, 22050, -17.0); + testMoogLadderSlope (SynthFilter::Type::HPF4, 1000.0, 22050, -17.0); + + testMoogLadderSlope (SynthFilter::Type::HPF4, 100.0, 48000, -17.0); + testMoogLadderSlope (SynthFilter::Type::HPF4, 1000.0, 48000, -17.0); + testMoogLadderSlope (SynthFilter::Type::HPF4, 2000.0, 48000, -17.0); } static void testMoogBpPeak() @@ -175,9 +176,10 @@ static void testMoogBpPeak() auto sr = 44100.0f; - MoogLadderFilter filter; - filter.setType (SynthFilter::Type::BPF2); + MoogLadderFilter filter; + filter.setType (SynthFilter::Type::BPF2); filter.setUseNonLinearProcessing (false); + filter.nonLinearProcess = [] (float a, float b) { return a * b; }; constexpr int fftSize = 1024 * 8; const float binWidth = sr / fftSize; @@ -213,52 +215,52 @@ void testSynthFilter() //set below to #if 1 to run impulse tests, #if 0 to generate impulses #if 1 testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::LPF1, + SynthFilter::Type::LPF1, 20.0f, 5000, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::LPF1, + SynthFilter::Type::LPF1, 20.0f, 22050, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::LPF1, + SynthFilter::Type::LPF1, 20.0f, 44100, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::LPF1, + SynthFilter::Type::LPF1, 200.0f, 96000, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::LPF1, + SynthFilter::Type::LPF1, 2000.0f, 96000, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::HPF1, + SynthFilter::Type::HPF1, 20.0f, 5000, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::LPF1, + SynthFilter::Type::LPF1, 20.0f, 22050, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::HPF1, + SynthFilter::Type::HPF1, 20.0f, 44100, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::HPF1, + SynthFilter::Type::HPF1, 200.0f, 96000, 3000); testOnePoleImpulse ("./test/signal/onePoleImpulse", - SynthFilter::Type::HPF1, + SynthFilter::Type::HPF1, 2000.0f, 44100, 3000); diff --git a/test/testUtilityFilter.cpp b/test/testUtilityFilter.cpp index 5a95a6c..a71478d 100644 --- a/test/testUtilityFilter.cpp +++ b/test/testUtilityFilter.cpp @@ -790,6 +790,38 @@ static void testSlopeButterworthHp (const float cutoff, #endif } +static void testUpsampleDecimator() +{ + constexpr int OVERSAMPLE = 4; + Upsampler up; + Decimator decimator; + + constexpr int fftSize = 1024 * 32; + auto s = ts::makeSine (fftSize, 1000, 44100); + ts::Signal result; + + float buffer[OVERSAMPLE]; + + for (auto x : s) + { + up.process (x, buffer); + result.push_back (decimator.process (buffer)); + } + + assertEQ (result.size(), fftSize); + + auto s_response = ts::getResponse (s); + auto r_response = ts::getResponse (result); + + auto s_magnitude = FftAnalyzer::getMagnitude (s_response); + auto r_magnitude = FftAnalyzer::getMagnitude (r_response); + + auto s_max_bin = std::distance (s_magnitude.begin(), std::max_element (s_magnitude.begin(), s_magnitude.end())); + auto r_max_bin = std::distance (r_magnitude.begin(), std::max_element (r_magnitude.begin(), r_magnitude.end())); + + assertEQ (s_max_bin, r_max_bin); +} + static void testSlopeButterworthHp (const float_4 cutoff, const float_4 sr, const float expectedCorner, @@ -881,4 +913,5 @@ void testUtilityFilter() testButterworthHp(); testButterworthLpSmid(); testButterworthHpSmid(); + testUpsampleDecimator(); } \ No newline at end of file