diff --git a/CMakeLists.txt b/CMakeLists.txt index c3f088c..d7a5686 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_CXX_STANDARD 17) add_library(${PROJECT_NAME} INTERFACE) target_include_directories(${PROJECT_NAME} INTERFACE include) +# there's a bit more below this if (${SST_EFFECTS_BUILD_TESTS}) include(cmake/CPM.cmake) @@ -37,11 +38,18 @@ if (${SST_EFFECTS_BUILD_TESTS}) ) endif () + if (NOT TARGET eurorack) + CPMAddPackage(NAME eurorack + GITHUB_REPOSITORY surge-synthesizer/eurorack + GIT_TAG surge + ) + endif () + if (NOT TARGET simde) CPMAddPackage(NAME simde GITHUB_REPOSITORY simd-everywhere/simde VERSION 0.7.2 - ) + ) add_library(simde INTERFACE) target_include_directories(simde INTERFACE ${simde_SOURCE_DIR}) endif () @@ -67,3 +75,11 @@ if (${SST_EFFECTS_BUILD_TESTS}) target_compile_definitions(${PROJECT_NAME}-test PRIVATE CATCH_CONFIG_DISABLE_EXCEPTIONS=1) endif () + + +if (TARGET eurorack) + target_link_libraries(${PROJECT_NAME} INTERFACE eurorack) + target_compile_definitions(${PROJECT_NAME} INTERFACE SST_EFFECTS_EURORACK=1) +else() + message(STATUS "sst-effects built without eurorack library; Nimbus effect is no-op") +endif() \ No newline at end of file diff --git a/include/sst/effects/EffectCore.h b/include/sst/effects/EffectCore.h index d052675..0649a38 100644 --- a/include/sst/effects/EffectCore.h +++ b/include/sst/effects/EffectCore.h @@ -171,10 +171,7 @@ template struct EffectTemplateBase : public FXConfig::BaseCl } } - inline float intValue(int idx) const - { - return FXConfig::intValueAt(asBase(), valueStorage, idx); - } + inline int intValue(int idx) const { return FXConfig::intValueAt(asBase(), valueStorage, idx); } inline float temposyncRatio(int idx) const { diff --git a/include/sst/effects/Nimbus.h b/include/sst/effects/Nimbus.h new file mode 100644 index 0000000..f17d240 --- /dev/null +++ b/include/sst/effects/Nimbus.h @@ -0,0 +1,210 @@ +/* + * sst-effects - an open source library of audio effects + * built by Surge Synth Team. + * + * Copyright 2018-2023, various authors, as described in the GitHub + * transaction log. + * + * sst-effects is released under the GNU General Public Licence v3 + * or later (GPL-3.0-or-later). The license is found in the "LICENSE" + * file in the root of this repository, or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * The majority of these effects at initiation were factored from + * Surge XT, and so git history prior to April 2023 is found in the + * surge repo, https://github.com/surge-synthesizer/surge + * + * All source in sst-effects available at + * https://github.com/surge-synthesizer/sst-effects + */ + +#ifndef INCLUDE_SST_EFFECTS_NIMBUS_H +#define INCLUDE_SST_EFFECTS_NIMBUS_H + +#include +#include "EffectCore.h" +#include "sst/basic-blocks/params/ParamMetadata.h" +#include "sst/basic-blocks/dsp/Lag.h" +#include "sst/basic-blocks/dsp/BlockInterpolators.h" +#include "sst/basic-blocks/dsp/LanczosResampler.h" +#include "sst/basic-blocks/mechanics/block-ops.h" +#include "sst/basic-blocks/mechanics/simd-ops.h" + +/* + * Unlike other effects, Numbus is split into Nimbus and NimbusImpl.h to allow + * inclusion of this in header files without pulling in the eurorack entire core + * into your header as oppposed to TU space + * + * For you, using NimbusImpl may be fine, or you may want to mix and match and + * do an explicit instantiation or so on. If you are reading this and you don't + * know what to do, "just include NimbusImpl.h". And if you do know what to do, + * then do that! + */ + +namespace clouds +{ +class GranularProcessor; +} + +namespace sst::effects::nimbus +{ +#if !SST_EFFECTS_EURORACK +template struct Nimbus : core::EffectTemplateBase +{ + Nimbus(typename FXConfig::GlobalStorage *s, typename FXConfig::EffectStorage *e, + typename FXConfig::ValueStorage *p) + : core::EffectTemplateBase(s, e, p) + { + std::cerr << "Warning: Using nimbus without eurorack module" << std::endl; + } + + static constexpr const char *effectName{"nimbus"}; + static constexpr int numParams{0}; + void initialize() {} + void processBlock(float *__restrict, float *__restrict) {} + void suspendProcessing() {} + int getRingoutDecay() const { return -1; } + sst::basic_blocks::params::ParamMetaData paramAt(int i) const { return {}; } + void onSampleRateChanged() {} +}; +#else +namespace sdsp = sst::basic_blocks::dsp; +namespace mech = sst::basic_blocks::mechanics; + +template struct Nimbus : core::EffectTemplateBase +{ + enum nmb_params + { + nmb_mode, + nmb_quality, + + nmb_position, + nmb_size, + nmb_pitch, + nmb_density, + nmb_texture, + nmb_spread, + + nmb_freeze, + nmb_feedback, + + nmb_reverb, + nmb_mix, + + nmb_num_params, + }; + static constexpr int numParams{nmb_num_params}; + static constexpr const char *effectName{"nimbus"}; + + Nimbus(typename FXConfig::GlobalStorage *s, typename FXConfig::EffectStorage *e, + typename FXConfig::ValueStorage *p); + ~Nimbus(); + + void initialize(); + void processBlock(float *__restrict L, float *__restrict R); + + void suspendProcessing() { initialize(); } + int getRingoutDecay() const { return -1; } + void onSampleRateChanged() { initialize(); } + + basic_blocks::params::ParamMetaData paramAt(int idx) const + { + auto np = (nmb_params)idx; + using pmd = sst::basic_blocks::params::ParamMetaData; + switch (np) + { + case nmb_mode: + return pmd() + .asInt() +#if EURORACK_CLOUDS_IS_SUPERPARASITES + .withRange(0, 7) +#else + .withRange(0, 3) +#endif + .withName("Mode") + .withDefault(0) + .withUnorderedMapFormatting({{0, "Granularizer"}, + {1, "Pitch Shifter"}, + {2, "Looping Delay"}, + {3, "Spectral Madness"}, + {4, "Oliverb"}, + {5, "Reonestor"}, + {6, "Kammerl"}, + {7, "Spectral Cloud"}}); + // TODO: Make this also marked as param-invalidating and use conditions for names + case nmb_quality: + return pmd() + .asInt() + .withRange(0, 3) + .withName("Quality") + .withDefault(0) + .withUnorderedMapFormatting({{0, "32k 16-bit Stereo"}, + {1, "32k 16-bit Mono"}, + {2, "16k 8-bit Stereo"}, + {3, "16k 8-bit Mono"}}); + case nmb_position: + return pmd().asPercent().withName("Position").withDefault(0.f); + case nmb_size: + return pmd().asPercentBipolar().withName("Size").withDefault(0.f); + case nmb_pitch: + return pmd().asSemitoneRange(-48, 48).withDefault(0.f).withName("Pitch"); + case nmb_density: + return pmd().asPercentBipolar().withName("Density").withDefault(0.f); + case nmb_texture: + return pmd().asPercentBipolar().withName("Texture").withDefault(0.f); + case nmb_spread: + return pmd().asPercent().withName("Spread").withDefault(0.f); + case nmb_freeze: + // TODO: On/Off FOrmatting around 0.5 here + return pmd() + .asFloat() + .withRange(0.f, 1.f) + .withDefault(0.f) + .withName("Freeze") + .withLinearScaleFormatting(""); + case nmb_feedback: + return pmd().asPercent().withName("Feedback").withDefault(0.f); + case nmb_reverb: + return pmd().asPercent().withName("Reverb").withDefault(0.f); + case nmb_mix: + return pmd().asPercent().withName("Mix").withDefault(0.f); + case nmb_num_params: + break; + } + return {}; + } + + // Only used by rack + void setNimbusTrigger(bool b) { nimbusTrigger = b; } + + protected: + float L alignas(16)[FXConfig::blockSize], R alignas(16)[FXConfig::blockSize]; + + sdsp::lipol_sse mix; + + uint8_t *block_mem, *block_ccm; + clouds::GranularProcessor *processor; + static constexpr int processor_sr = 32000; + static constexpr float processor_sr_inv = 1.f / 32000; + int old_nmb_mode = 0; + bool nimbusTrigger{false}; + + using resamp_t = sst::basic_blocks::dsp::LanczosResampler; + std::unique_ptr surgeSR_to_euroSR, euroSR_to_surgeSR; + + static constexpr int raw_out_sz = FXConfig::blockSize << 6; // power of 2 pls + float resampled_output[2][raw_out_sz]; // at sr + size_t resampReadPtr = 0, resampWritePtr = 1; // see comment in init + + static constexpr int nimbusprocess_blocksize = 8; + float stub_input[2][nimbusprocess_blocksize]; // This is the extra sample we have around + size_t numStubs{0}; + int consumed = 0, created = 0; + bool builtBuffer{false}; +}; + +#endif + +} // namespace sst::effects::nimbus + +#endif diff --git a/include/sst/effects/NimbusImpl.h b/include/sst/effects/NimbusImpl.h new file mode 100644 index 0000000..672eb1d --- /dev/null +++ b/include/sst/effects/NimbusImpl.h @@ -0,0 +1,247 @@ +/* + * sst-effects - an open source library of audio effects + * built by Surge Synth Team. + * + * Copyright 2018-2023, various authors, as described in the GitHub + * transaction log. + * + * sst-effects is released under the GNU General Public Licence v3 + * or later (GPL-3.0-or-later). The license is found in the "LICENSE" + * file in the root of this repository, or at + * https://www.gnu.org/licenses/gpl-3.0.en.html + * + * The majority of these effects at initiation were factored from + * Surge XT, and so git history prior to April 2023 is found in the + * surge repo, https://github.com/surge-synthesizer/surge + * + * All source in sst-effects available at + * https://github.com/surge-synthesizer/sst-effects + */ + +#ifndef INCLUDE_SST_EFFECTS_NIMBUSIMPL_H +#define INCLUDE_SST_EFFECTS_NIMBUSIMPL_H + +#include "Nimbus.h" + +#if SST_EFFECTS_EURORACK +#ifdef _MSC_VER +#define __attribute__(x) +#endif + +#define TEST // remember this is how you tell the eurorack code to use dsp not hardware +#if EURORACK_CLOUDS_IS_SUPERPARASITES +#include "supercell/dsp/granular_processor.h" +#else +#include "clouds/dsp/granular_processor.h" +#endif + +#undef TEST +#ifdef _MSC_VER +#undefine __attribute__ +#endif + +namespace sst::effects::nimbus +{ +template +Nimbus::Nimbus(typename FXConfig::GlobalStorage *s, typename FXConfig::EffectStorage *e, + typename FXConfig::ValueStorage *p) + : core::EffectTemplateBase(s, e, p) +{ + const int memLen = 118784; + const int ccmLen = 65536 - 128; + block_mem = new uint8_t[memLen](); + block_ccm = new uint8_t[ccmLen](); + processor = new clouds::GranularProcessor(); +#if EURORACK_CLOUDS_IS_SUPERPARASITES +#else + memset(processor, 0, sizeof(*processor)); +#endif + + processor->Init(block_mem, memLen, block_ccm, ccmLen); + mix.set_blocksize(FXConfig::blockSize); +} + +template Nimbus::~Nimbus() +{ + delete[] block_mem; + delete[] block_ccm; + delete processor; +} + +template void Nimbus::initialize() +{ + mix.set_target(1.f); + mix.instantize(); + + surgeSR_to_euroSR = std::make_unique(this->sampleRate(), processor_sr); + euroSR_to_surgeSR = std::make_unique(processor_sr, this->sampleRate()); + + memset(resampled_output, 0, raw_out_sz * 2 * sizeof(float)); + + consumed = 0; + created = 0; + builtBuffer = false; + resampReadPtr = 0; + resampWritePtr = 1; // why 1? well while we are stalling we want to output 0 so write 1 ahead +} + +template +void Nimbus::processBlock(float *__restrict dataL, float *__restrict dataR) +{ + if (!surgeSR_to_euroSR || !euroSR_to_surgeSR) + return; + + /* Resample Temp Buffers */ + float resample_this[2][FXConfig::blockSize << 3]; + float resample_into[2][FXConfig::blockSize << 3]; + + for (int i = 0; i < FXConfig::blockSize; ++i) + { + surgeSR_to_euroSR->push(dataL[i], dataR[i]); + } + + float srgToEur[2][FXConfig::blockSize << 3]; + auto outputFramesGen = surgeSR_to_euroSR->populateNext(resample_into[0], resample_into[1], + FXConfig::blockSize << 3); + + if (outputFramesGen) + { + clouds::ShortFrame input[FXConfig::blockSize << 3]; + clouds::ShortFrame output[FXConfig::blockSize << 3]; + + int frames_to_go = outputFramesGen; + int outpos = 0; + + auto modeInt = this->intValue(nmb_mode); + // Just make sure we are safe if we swap between superparasites and not +#if EURORACK_CLOUDS_IS_SUPERPARASITES + modeInt = std::clamp(modeInt, 0, 7); +#else + modeInt = std::clamp(modeInt, 0, 3); +#endif + processor->set_playback_mode( + (clouds::PlaybackMode)((int)clouds::PLAYBACK_MODE_GRANULAR + modeInt)); + processor->set_quality(this->intValue(nmb_quality)); + + int consume_ptr = 0; + + while (frames_to_go + numStubs >= nimbusprocess_blocksize) + { + int sp = 0; + while (numStubs > 0) + { + input[sp].l = (short)(std::clamp(stub_input[0][sp], -1.f, 1.f) * 32767.0f); + input[sp].r = (short)(std::clamp(stub_input[1][sp], -1.f, 1.f) * 32767.0f); + sp++; + numStubs--; + } + + for (int i = sp; i < nimbusprocess_blocksize; ++i) + { + input[i].l = + (short)(std::clamp(resample_into[0][consume_ptr], -1.f, 1.f) * 32767.0f); + input[i].r = + (short)(std::clamp(resample_into[1][consume_ptr], -1.f, 1.f) * 32767.0f); + consume_ptr++; + } + + int inputSz = nimbusprocess_blocksize; // sdata.output_frames_gen + sp; + + auto parm = processor->mutable_parameters(); + + float den_val, tex_val; + + den_val = (this->floatValue(nmb_density) + 1.f) * 0.5; + tex_val = (this->floatValue(nmb_texture) + 1.f) * 0.5; + + parm->position = std::clamp(this->floatValue(nmb_position), 0.f, 1.f); + parm->size = std::clamp(this->floatValue(nmb_size), 0.f, 1.f); + parm->density = std::clamp(den_val, 0.f, 1.f); + parm->texture = std::clamp(tex_val, 0.f, 1.f); + parm->pitch = std::clamp(this->floatValue(nmb_pitch), -48.f, 48.f); + parm->stereo_spread = std::clamp(this->floatValue(nmb_spread), 0.f, 1.f); + parm->feedback = std::clamp(this->floatValue(nmb_feedback), 0.f, 1.f); + parm->freeze = this->floatValue(nmb_freeze) > 0.5; + parm->reverb = std::clamp(this->floatValue(nmb_reverb), 0.f, 1.f); + parm->dry_wet = 1.f; + +#if EURORACK_CLOUDS_IS_SUPERPARASITES + parm->capture = nimbusTrigger; +#else + parm->trigger = nimbusTrigger; // this is an external granulating source. Skip it +#endif + parm->gate = parm->freeze; // This is the CV for the freeze button + + processor->Prepare(); + processor->Process(input, output, inputSz); + + for (int i = 0; i < inputSz; ++i) + { + resample_this[0][outpos + i] = output[i].l / 32767.0f; + resample_this[1][outpos + i] = output[i].r / 32767.0f; + } + outpos += inputSz; + frames_to_go -= (nimbusprocess_blocksize - sp); + } + + if (frames_to_go > 0) + { + int startSub = numStubs; + int addStub = frames_to_go; + numStubs += frames_to_go; + + for (int i = 0; i < addStub; ++i) + { + stub_input[0][i + startSub] = resample_into[0][consume_ptr]; + stub_input[1][i + startSub] = resample_into[1][consume_ptr]; + consume_ptr++; + } + } + + if (outpos > 0) + { + for (int i = 0; i < outpos; ++i) + { + euroSR_to_surgeSR->push(resample_this[0][i], resample_this[1][i]); + } + + auto dsoutputFramesGen = euroSR_to_surgeSR->populateNext( + resample_into[0], resample_into[1], FXConfig::blockSize << 3); + created += dsoutputFramesGen; + + size_t w = resampWritePtr; + for (int i = 0; i < dsoutputFramesGen; ++i) + { + resampled_output[0][w] = resample_into[0][i]; + resampled_output[1][w] = resample_into[1][i]; + + w = (w + 1U) & (raw_out_sz - 1U); + } + resampWritePtr = w; + } + } + + // If you hit this you need to adjust this gapping ratio probably. + static_assert(FXConfig::blockSize >= nimbusprocess_blocksize); + int ratio = std::max((int)std::ceil(processor_sr_inv * this->sampleRate()) - 2, 0); + bool rpi = (created) > (FXConfig::blockSize * (1 + ratio) + 8); // leave some buffer + if (rpi) + builtBuffer = true; + + size_t rp = resampReadPtr; + for (int i = 0; i < FXConfig::blockSize; ++i) + { + L[i] = resampled_output[0][rp]; + R[i] = resampled_output[1][rp]; + rp = (rp + rpi) & (raw_out_sz - 1); + } + resampReadPtr = rp; + + mix.set_target_smoothed(std::clamp(this->floatValue(nmb_mix), 0.f, 1.f)); + mix.fade_2_blocks_inplace(dataL, L, dataR, R); +} + +} // namespace sst::effects::nimbus +#endif + +#endif // SURGE_NIMBUSIMPL_H diff --git a/tests/concrete-runs.cpp b/tests/concrete-runs.cpp index 94e053f..91773e4 100644 --- a/tests/concrete-runs.cpp +++ b/tests/concrete-runs.cpp @@ -31,6 +31,9 @@ #include "sst/effects/Bonsai.h" #include "sst/effects/Phaser.h" #include "sst/effects/Reverb2.h" +#include "sst/effects/TreeMonster.h" +#include "sst/effects/Nimbus.h" +#include "sst/effects/NimbusImpl.h" namespace sfx = sst::effects; @@ -98,4 +101,9 @@ TEST_CASE("Can Run Types with Concrete Config") SECTION("Delay") { Tester>::TestFX(); } SECTION("Bonsai") { Tester>::TestFX(); } SECTION("Phaser") { Tester>::TestFX(); } + SECTION("TreeMonster") + { + Tester>::TestFX(); + } + SECTION("Nimbus") { Tester>::TestFX(); } } \ No newline at end of file diff --git a/tests/create-effect.cpp b/tests/create-effect.cpp index 2d6f42d..f9d9194 100644 --- a/tests/create-effect.cpp +++ b/tests/create-effect.cpp @@ -30,6 +30,8 @@ #include "sst/effects/Phaser.h" #include "sst/effects/Reverb2.h" #include "sst/effects/TreeMonster.h" +#include "sst/effects/Nimbus.h" +#include "sst/effects/NimbusImpl.h" struct TestConfig { @@ -120,4 +122,5 @@ TEST_CASE("Can Create Types") SECTION("Phaser") { Tester>::TestFX(); } SECTION("Reverb2") { Tester>::TestFX(); } SECTION("TreeMonster") { Tester>::TestFX(); } + SECTION("Nimbus") { Tester>::TestFX(); } } \ No newline at end of file