From 417dbc1944d10ef8816da1df0a78c45f9cbd1e34 Mon Sep 17 00:00:00 2001 From: Geraint Luff Date: Sat, 21 Jun 2025 10:12:07 +0100 Subject: [PATCH] Start CLAP front-end --- LICENSE.txt | 7 + README.md | 36 ++-- SUPPORT.txt | 3 + clap/CMakeLists.txt | 89 +++++++++ clap/Makefile | 51 +++++ clap/REAPER/basics/basics.RPP | 360 ++++++++++++++++++++++++++++++++++ clap/env.sh.html | 7 + clap/source/basics.cpp | 62 ++++++ clap/source/clap-stfx.h | 41 ++++ clap/source/clap_entry.cpp | 14 ++ crunch.h | 286 ++++++++++++++------------- stfx/stfx-library.h | 7 +- 12 files changed, 808 insertions(+), 155 deletions(-) create mode 100644 LICENSE.txt create mode 100644 SUPPORT.txt create mode 100644 clap/CMakeLists.txt create mode 100644 clap/Makefile create mode 100644 clap/REAPER/basics/basics.RPP create mode 100644 clap/env.sh.html create mode 100644 clap/source/basics.cpp create mode 100644 clap/source/clap-stfx.h create mode 100644 clap/source/clap_entry.cpp diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..a288c29 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright 2022-2025 Signalsmith Audio / Geraint Luff + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 5fc944b..23e2cb9 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,44 @@ # Signalsmith Basics -A collection of basic effects, available as plugins and re-usable open-source C++ classes. +A collection of basic effects, available as plugins and re-usable open-source (MIT) C++ classes. + +* **Limiter** +* **Crunch** +* **Reverb** ## How to use -The [main project page](https://signalsmith-audio.co.uk/code/basics/) has details about the specific effects (and audio examples), but they are all quite similar to use: +Each effect can be used just by including the corresponding header (e.g. `limiter.h`). This defines two classes `LimiterFloat` and `LimiterDouble`. Some of these have initialisation arguments (e.g. maximum lookahead) but these are always optional. ```cpp // Limiter with maximum attack/lookahead of 100ms signalsmith::basics::Limiter effect(100); - -effect.configure(sampleRate, maxBlockSize, channels); -effect.configure(sampleRate, maxBlockSize, inputChannels, outputChannels); ``` -Then when processing (all on the audio thread): +Before use, these classes must be configured. The `.configure()` method returns `true` if the config was accepted. They can be configured multiple times, but (obviously) not while actively processing audio. ```cpp -// clear buffers -effect.reset() +effect.configure(sampleRate, maxBlockSize, channels); +``` -// Change parameters with assignment -effect.attackMs = 20; +To process audio, pass in a classic array-of-buffers for input and output (which shouldn't be the same): -// process a block +```cpp float **inputBuffers, **outputBuffers; int blockSize; effect.process(inputBuffers, outputBuffers, blockSize); ``` -You can also inspect latency (`effect.latencySamples()`) and tail length (`effect.tailSamples()`). +When playback (re)starts, you can also call `.reset()` to clear any tails/state. You can also inspect latency (`effect.latencySamples()`) and tail length (`effect.tailSamples()`). + +The actual implementations are templates (e.g. `LimiterSTFX`) which you shouldn't every need to use directly, although they should be fairly readable. These are wrapped up into the `...Float` and `...Double` classes by the code in `stfx/stfx-library.h`, which also provides helpers for parameter-smoothing/etc., and more typical `.configure()` and `.process()` functions (from `.configureSTFX()`/`.processSTFX()`). + +### Parameters + +Floating-point parameters are declared as `ParamRange`s in the `...STFX` template. This is an opaque type which can be assigned from / converted into `double` from any thread. Parameter smoothing is handled internally. + +Integer parameters are declared as `ParamStepped`s. This is a similarly opaque type which converts to/from `int`. + +### Implementation + +The `.state()` method of these templates contain a lot of detail, almost all of which is ignored (and optimised away) when using the `...Float`/`...Double` classes. diff --git a/SUPPORT.txt b/SUPPORT.txt new file mode 100644 index 0000000..a45773d --- /dev/null +++ b/SUPPORT.txt @@ -0,0 +1,3 @@ +# https://geraintluff.github.io/SUPPORT.txt/ + +2026-01-01 Geraint Luff diff --git a/clap/CMakeLists.txt b/clap/CMakeLists.txt new file mode 100644 index 0000000..33d918e --- /dev/null +++ b/clap/CMakeLists.txt @@ -0,0 +1,89 @@ +cmake_minimum_required(VERSION 3.28) + +project(plugins VERSION 1.0.0) +set(NAME basics) + +################ boilerplate config + +set(CMAKE_CXX_STANDARD 17) # for string_view + +if (APPLE) + set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13) + set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") + enable_language(OBJCXX) +endif () + +include(FetchContent) +FetchContent_Declare( + clap + GIT_REPOSITORY https://github.com/geraintluff/clap.git # https://github.com/free-audio/clap + GIT_TAG 2df92fe17911f15436176b9c37faec370166d14f # draft/web + GIT_SHALLOW OFF +) +FetchContent_MakeAvailable(clap) + +################ CLAP wrapper stuff + +if(NOT DEFINED VST3_SDK_ROOT) + if(EMSCRIPTEN) + # don't download the VST3 SDK + set(VST3_SDK_ROOT "./dummy/vst3sdk/path") + else() + set(CLAP_WRAPPER_DOWNLOAD_DEPENDENCIES TRUE) + endif() +endif() + +set(CLAP_WRAPPER_OUTPUT_NAME clap-wrapper-target) +set(CLAP_WRAPPER_DONT_ADD_TARGETS TRUE) + +include(FetchContent) +FetchContent_Declare( + clap-wrapper + GIT_REPOSITORY https://github.com/free-audio/clap-wrapper + GIT_TAG 0.12.1 # first version with WCLAP stuff + GIT_SHALLOW ON +) +FetchContent_MakeAvailable(clap-wrapper) + +################ Helpers + +FetchContent_Declare( + clap-helpers + GIT_REPOSITORY https://github.com/free-audio/clap-helpers + GIT_TAG 58ab81b1dc8219e859529c1306f364bb3aedf7d5 + GIT_SHALLOW OFF +) +FetchContent_MakeAvailable(clap-helpers) + +################ The actual plugin(s) + +add_library(${NAME}_static STATIC) +target_link_libraries(${NAME}_static PUBLIC + clap + clap-helpers +) +target_sources(${NAME}_static PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/source/${name}.cpp +) + +make_clapfirst_plugins( + TARGET_NAME ${NAME} + IMPL_TARGET ${NAME}_static + + RESOURCE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/resources" + + OUTPUT_NAME "${NAME}" + ENTRY_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/source/clap_entry.cpp + + BUNDLE_IDENTIFIER "uk.co.signalsmith-audio.plugins.${NAME}" + BUNDLE_VERSION "1.0.0" + WINDOWS_FOLDER_VST3 TRUE + + PLUGIN_FORMATS CLAP VST3 WCLAP # AUV2 + COPY_AFTER_BUILD FALSE + + AUV2_MANUFACTURER_NAME "Signalsmith Audio" + AUV2_MANUFACTURER_CODE "SigA" + AUV2_SUBTYPE_CODE "BdDt" + AUV2_INSTRUMENT_TYPE "aufx" +) diff --git a/clap/Makefile b/clap/Makefile new file mode 100644 index 0000000..375301d --- /dev/null +++ b/clap/Makefile @@ -0,0 +1,51 @@ +.PHONY: build build-emscripten emsdk +PROJECT := plugins +PLUGIN := example + +CMAKE_PARAMS := -DCMAKE_LIBRARY_OUTPUT_DIRECTORY=.. -G Xcode # -DCMAKE_BUILD_TYPE=Release + +mac-installer: mac-installer-$(PLUGIN) + +clean: + rm -rf out + +build: + cmake . -B out/build $(CMAKE_PARAMS) + +release-%: build + cmake --build out/build --target $* --config Release + +mac-installer-%: release-%_vst3 release-%_clap emscripten-%_wclap + ../stfx/front-end/clap/mac/make-pkg-installer.sh out/$*.pkg $* uk.co.signalsmith.installer.stfx.$* out/Release/$* + +####### Emscripten ####### +# based on https://stunlock.gg/posts/emscripten_with_cmake/ + +CURRENT_DIR := $(shell pwd) +EMSDK ?= $(CURRENT_DIR)/emsdk +EMSDK_ENV = unset CMAKE_TOOLCHAIN_FILE; EMSDK_QUIET=1 . "$(EMSDK)/emsdk_env.sh"; + +emsdk: + @ if ! test -d "$(EMSDK)" ;\ + then \ + echo "SDK not found - cloning from Github" ;\ + git clone https://github.com/emscripten-core/emsdk.git "$(EMSDK)" ;\ + cd "$(EMSDK)" && git pull && ./emsdk install latest && ./emsdk activate latest ;\ + $(EMSDK_ENV) emcc --check && python3 --version && cmake --version ;\ + fi + +build-emscripten: emsdk + $(EMSDK_ENV) emcmake cmake . -B out/build-emscripten -DCMAKE_RUNTIME_OUTPUT_DIRECTORY=../Release -DCMAKE_BUILD_TYPE=Release + +emscripten-%: build-emscripten + $(EMSDK_ENV) cmake --build out/build-emscripten --target $* --config Release + +####### Dev stuff ####### + +format: + find source -iname \*.cpp -o -iname \*.h | xargs clang-format -i + +dev: release-$(PLUGIN)_clap + /Applications/REAPER.app/Contents/MacOS/REAPER REAPER/$(PLUGIN)/$(PLUGIN).RPP + +wclap: emscripten-$(PLUGIN)_wclap \ No newline at end of file diff --git a/clap/REAPER/basics/basics.RPP b/clap/REAPER/basics/basics.RPP new file mode 100644 index 0000000..4c68497 --- /dev/null +++ b/clap/REAPER/basics/basics.RPP @@ -0,0 +1,360 @@ + + RIPPLE 0 + GROUPOVERRIDE 0 0 0 + AUTOXFADE 129 + ENVATTACH 3 + POOLEDENVATTACH 0 + MIXERUIFLAGS 11 48 + ENVFADESZ10 40 + PEAKGAIN 1 + FEEDBACK 0 + PANLAW 1 + PROJOFFS 0 0 0 + MAXPROJLEN 0 0 + GRID 3199 8 1 8 1 0 0 0 + TIMEMODE 1 5 -1 30 0 0 -1 + VIDEO_CONFIG 0 0 256 + PANMODE 3 + PANLAWFLAGS 3 + CURSOR 0 + ZOOM 100 0 0 + VZOOMEX 6 0 + USE_REC_CFG 0 + RECMODE 1 + SMPTESYNC 0 30 100 40 1000 300 0 0 1 0 0 + LOOP 0 + LOOPGRAN 0 4 + RECORD_PATH "Media" "" + + + RENDER_FILE "" + RENDER_PATTERN "" + RENDER_FMT 0 2 0 + RENDER_1X 0 + RENDER_RANGE 1 0 0 18 1000 + RENDER_RESAMPLE 3 0 1 + RENDER_ADDTOPROJ 0 + RENDER_STEMS 0 + RENDER_DITHER 0 + TIMELOCKMODE 1 + TEMPOENVLOCKMODE 1 + ITEMMIX 1 + DEFPITCHMODE 589824 0 + TAKELANE 1 + SAMPLERATE 44100 0 0 + + LOCK 1 + + GLOBAL_AUTO -1 + TEMPO 120 4 4 + PLAYRATE 1 0 0.25 4 + SELECTION 0 0 + SELECTION2 0 0 + MASTERAUTOMODE 0 + MASTERTRACKHEIGHT 0 0 + MASTERPEAKCOL 16576 + MASTERMUTESOLO 0 + MASTERTRACKVIEW 1 0.6667 0.5 0.5 0 0 0 0 0 0 0 0 0 0 + MASTERHWOUT 0 0 1 0 0 0 0 -1 + MASTER_NCH 4 2 + MASTER_VOLUME 1 0 -1 -1 1 + MASTER_PANMODE 3 + MASTER_PANLAWFLAGS 3 + MASTER_FX 1 + MASTER_SEL 0 + + FLOATPOS 0 0 0 0 + FXID {81F27681-ACFE-1D40-9AAE-F8C9A0035B1F} + WAK 0 0 + BYPASS 0 0 0 + "" + cnRzcu5e7f4IAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAQAAAAAAAAACAAAAAAAAAAAgAAAABAAAAAAAAAAIAAAAAAAAA + BAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAABAAAAAAAAAAIAAAAAAAAAArAAAAAEAAAAAABAA + AQAAAAIAAAABAAAAZGVmYXVsdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMjcuMC4wLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA== + AAAQAAAA + > + FLOATPOS 0 0 0 0 + FXID {11A0D811-6420-CB40-95D6-F9D12E45C862} + WAK 0 0 + > + + + + + + > + FLOATPOS 0 0 0 0 + FXID {1F02CE63-5B2C-DD40-B8DA-6796E8E4CD70} + WAK 0 0 + > + > + + > + > + + + FLOATPOS 0 0 0 0 + FXID {6A5C4484-079E-F34D-8A18-CB456B094205} + WAK 0 0 + BYPASS 0 0 0 + "" + cWVlcu5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAagAAAAEAAAAAABAA + IQAAAAIAAAAEAAAAAQAAAMS7LCro8zZAAAAAAAAA8D9GtvP91Hj1PwEDAAAAAQAAAHj7zCB2ucJAdouc1Fqk9D8QWDm0yHbyPwEBAAAAAQAAAAAAAAAAAPA/AAAAAEAC + AABrAQAAAgAAAA== + AAAQAAAA + > + FLOATPOS 0 0 0 0 + FXID {5262179C-F519-424F-8C34-4765E9AE04E7} + WAK 0 0 + > + + > + > + "" + cWVlcu5e7f4CAAAAAQAAAAAAAAACAAAAAAAAAAIAAAABAAAAAAAAAAIAAAAAAAAAagAAAAEAAAAAABAA + IQAAAAIAAAADAAAAAQAAAJ0Q6aODFJFAAAAAAAAA8D+amZmZmZnpPwEEAAAAAQAAADNAfmGLZFZANFOMBg1H7z9/arx0kxjsPwEBAAAAAQAAAModDNkGk/o/AAAAAEAC + AABrAQAAAgAAAA== + AAAQAAAA + > + FLOATPOS 0 0 0 0 + FXID {4CCB9CFD-8185-AE45-8B5D-ADB8ED3C4344} + WAK 0 0 + > + + > + > +> diff --git a/clap/env.sh.html b/clap/env.sh.html new file mode 100644 index 0000000..373aff3 --- /dev/null +++ b/clap/env.sh.html @@ -0,0 +1,7 @@ + + +
\ No newline at end of file diff --git a/clap/source/basics.cpp b/clap/source/basics.cpp new file mode 100644 index 0000000..cd9ccfe --- /dev/null +++ b/clap/source/basics.cpp @@ -0,0 +1,62 @@ +#ifndef LOG_EXPR +# include +# define LOG_EXPR(expr) std::cout << #expr " = " << (expr) << std::endl; +#endif + +#include "../crunch.h" +#include "../limiter.h" +#include "../reverb.h" + +#include "./clap-stfx.h" + +static stfx::clap::Plugins plugins; +bool clap_init(const char *path) { + plugins.add({ + .clap_version = CLAP_VERSION, + .id = "uk.co.signalsmith.basics.crunch", + .name = "[Basics] Crunch", + .vendor = "Signalsmith Audio", + .url = "", + .manual_url = "", + .support_url = "", + .version = "1.0.0" + }, { + CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, + CLAP_PLUGIN_FEATURE_DISTORTION, + }); + + plugins.add({ + .clap_version = CLAP_VERSION, + .id = "uk.co.signalsmith.basics.limiter", + .name = "[Basics] Limiter", + .vendor = "Signalsmith Audio", + .url = "", + .manual_url = "", + .support_url = "", + .version = "1.0.0" + }, { + CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, + CLAP_PLUGIN_FEATURE_LIMITER, + }); + + plugins.add({ + .clap_version = CLAP_VERSION, + .id = "uk.co.signalsmith.basics.reverb", + .name = "[Basics] Reverb", + .vendor = "Signalsmith Audio", + .url = "", + .manual_url = "", + .support_url = "", + .version = "1.0.0" + }, { + CLAP_PLUGIN_FEATURE_AUDIO_EFFECT, + CLAP_PLUGIN_FEATURE_REVERB, + }); + return plugins.clap_init(path); +} +void clap_deinit() { + plugins.clap_deinit(path); +} +const void * clap_get_factory(const char *id) { + return plugins.clap_get_factory(id); +} diff --git a/clap/source/clap-stfx.h b/clap/source/clap-stfx.h new file mode 100644 index 0000000..8ef2da4 --- /dev/null +++ b/clap/source/clap-stfx.h @@ -0,0 +1,41 @@ +#include "clap/clap.h" + +#include +#include +#include + +namespace stfx { namespace clap { + +template class EffectSTFX> +struct Plugin : public clap_plugin { + template + Plugin(const clap_plugin_descriptor *desc, Args ...args) : effect(args...) { + this->desc = desc; + this->plugin_data = nullptr; + } +private: + stfx::LibraryEffect effect; +}; + +struct Plugins { + + template class EffectSTFX, class ...Args> + void add(clap_plugin_descriptor desc, std::initializer_list features, Args ...args) { + size_t index = featureLists.size(); + + featureLists.emplace_back(features); + featureLists[index].push_back(nullptr); + desc.features = featureLists[index].data(); + descriptors.push_back(desc); + + creates.push_back([=](){ + return new Plugin(&descriptors[index], args...); + }); + } +private: + std::vector> featureLists; + std::vector descriptors; + std::vector> creates; +}; + +}} // namespace diff --git a/clap/source/clap_entry.cpp b/clap/source/clap_entry.cpp new file mode 100644 index 0000000..4871811 --- /dev/null +++ b/clap/source/clap_entry.cpp @@ -0,0 +1,14 @@ +#include "clap/entry.h" + +extern bool clap_init(const char *); +extern void clap_deinit(); +extern const void * clap_get_factory(const char *); + +extern "C" { + const CLAP_EXPORT clap_plugin_entry clap_entry{ + .clap_version = CLAP_VERSION, + .init = clap_init, + .deinit = clap_deinit, + .get_factory = clap_get_factory + }; +} diff --git a/crunch.h b/crunch.h index d106c9b..660767a 100644 --- a/crunch.h +++ b/crunch.h @@ -1,5 +1,5 @@ /* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff -Released under the Boost Software License (see LICENSE.txt) */ +See LICENSE.txt and SUPPORT.txt */ #ifndef SIGNALSMITH_BASICS_CRUNCH_H #define SIGNALSMITH_BASICS_CRUNCH_H @@ -12,159 +12,163 @@ SIGNALSMITH_DSP_VERSION_CHECK(1, 4, 1) namespace signalsmith { namespace basics { - template - class CrunchSTFX : public BaseEffect { - using typename BaseEffect::Sample; - using typename BaseEffect::ParamRange; - using typename BaseEffect::ParamStepped; +template +class CrunchSTFX; - int channels = 0; - signalsmith::rates::Oversampler2xFIR oversampler; - struct GainshapeADAA { - Sample prevX = 0, prevIntegral = 0; - Sample fuzzPositive = 1, fuzzNegative = 1; +using CrunchFloat = stfx::LibraryEffect; +using CrunchDouble = stfx::LibraryEffect; - void setFuzzFactor(Sample k) { - fuzzPositive = 1 + k - k*k; - fuzzNegative = 1 - k - k*k; - prevIntegral = integralGain(prevX); - } +template +class CrunchSTFX : public BaseEffect { + static constexpr int oversampleHalfLatency = 16; + static constexpr Sample autoGainLevel = 0.1; - Sample gain(Sample x) const { - Sample fuzzGain = (x >= 0 ? fuzzPositive : fuzzNegative); - return fuzzGain/std::sqrt(1 + x*x); - } - Sample integralGain(Sample x) const { - if (x >= 0) { - return fuzzPositive*std::log(std::sqrt(1 + x*x) + x); - } else { // more accurate if we flip it - return -fuzzNegative*std::log(std::sqrt(1 + x*x) - x); - } - } - Sample averageGain(Sample range) const { - // Average gain from 0-range, ignoring fuzz - return std::log(std::sqrt(1 + range*range) + range)/range; - } - static constexpr Sample minDiffX = 1e-4; + using typename BaseEffect::Sample; + using typename BaseEffect::ParamRange; + using typename BaseEffect::ParamStepped; + + ParamRange drive{4}; + ParamRange fuzz{0}; + ParamRange toneHz{2000}; + ParamRange outGain{1}; + + const bool autoGain; + CrunchSTFX(bool autoGain=true) : autoGain(autoGain) {} + + template + void state(Storage &storage) { + storage.info("[Basics] Crunch", "A simple distortion/saturation"); + int version = storage.version(0); + if (version != 0) return; + + using namespace signalsmith::units; + storage.range("drive", drive) + .info("drive", "pre-distortion input gain") + .range(dbToGain(-12), 4, dbToGain(40)) + .unit("dB", 1, dbToGain, gainToDb) + .unit(""); + storage.range("fuzz", fuzz) + .info("fuzz", "amplitude-independent distortion") + .range(0, 0.5, 1) + .unit("%", 0, pcToRatio, ratioToPc); + storage.range("toneHz", toneHz) + .info("tone", "limits the brightness of the distortion") + .range(100, 4000, 20000) + .unit("Hz", 0, 0, 1000) + .unit("kHz", 1, kHzToHz, hzToKHz, 1000, 1e100); + + storage.range("outGain", outGain) + .info("out", "output gain") + .range(dbToGain(-12), 1, dbToGain(24)) + .unit("dB", 1, dbToGain, gainToDb) + .unit(""); + } + + template + void configureSTFX(Config &config) { + channels = config.outputChannels = config.inputChannels; + config.auxInputs.resize(0); + config.auxOutputs.resize(0); - void reset() { - prevX = 0; - prevIntegral = integralGain(prevX); - } + oversampler.resize(channels, config.maxBlockSize, oversampleHalfLatency, std::min(0.45, 21000/config.sampleRate)); + gainshapers.resize(channels); + toneFilters.resize(channels); + outputFilters.resize(channels); + } + + void reset() { + oversampler.reset(); + for (auto &g : gainshapers) g.reset(); + for (auto &f : toneFilters) f.reset(); + for (auto &f : outputFilters) f.reset(); + } + + int latencySamples() const { + return oversampleHalfLatency*2; + } + + template + void processSTFX(Io &io, Config &config, Block &block) { + auto inputGain = block.smooth(drive); - Sample operator()(Sample x) { - Sample diffX = x - prevX; - Sample integral = integralGain(x); - Sample diffIntegral = integral - prevIntegral; - prevX = x; - prevIntegral = integral; - if (std::abs(diffX) < minDiffX) return gain(x); - return diffIntegral/diffX; - } - }; - std::vector gainshapers; - using Filter = signalsmith::filters::BiquadStatic; - std::vector toneFilters, outputFilters; - - static constexpr int oversampleHalfLatency = 16; - static constexpr Sample autoGainLevel = 0.1; - public: - ParamRange drive{4}; - ParamRange fuzz{0}; - ParamRange toneHz{2000}; - ParamRange outGain{1}; - - bool autoGain; - CrunchSTFX(bool autoGain=true) : autoGain(autoGain) {} - - template - void state(Storage &storage) { - storage.info("[Basics] Crunch", "A simple distortion/saturation"); - int version = storage.version(0); - if (version != 0) return; - - using namespace signalsmith::units; - storage.range("drive", drive) - .info("drive", "pre-distortion input gain") - .range(dbToGain(-12), 4, dbToGain(40)) - .unit("dB", 1, dbToGain, gainToDb) - .unit(""); - storage.range("fuzz", fuzz) - .info("fuzz", "amplitude-independent distortion") - .range(0, 0.5, 1) - .unit("%", 0, pcToRatio, ratioToPc); - storage.range("toneHz", toneHz) - .info("tone", "limits the brightness of the distortion") - .range(100, 4000, 20000) - .unit("Hz", 0, 0, 1000) - .unit("kHz", 1, kHzToHz, hzToKHz, 1000, 1e100); - - storage.range("outGain", outGain) - .info("out", "output gain") - .range(dbToGain(-12), 1, dbToGain(24)) - .unit("dB", 1, dbToGain, gainToDb) - .unit(""); + double outputGainFrom = outGain.from(); + double outputGainTo = outGain.to(); + if (autoGain) { + Sample averageGain = gainshapers[0].averageGain(autoGainLevel*drive.from()); + outputGainFrom /= drive.from()*averageGain; + outputGainTo /= drive.to()*gainshapers[0].averageGain(autoGainLevel*drive.to()); } + auto outputGain = block.smooth(outputGainFrom, outputGainTo); - template - void configure(Config &config) { - channels = config.outputChannels = config.inputChannels; - config.auxInputs.resize(0); - config.auxOutputs.resize(0); - - oversampler.resize(channels, config.maxBlockSize, oversampleHalfLatency, std::min(0.45, 21000/config.sampleRate)); - gainshapers.resize(channels); - toneFilters.resize(channels); - outputFilters.resize(channels); + for (int c = 0; c < channels; ++c) { + auto &gainshaper = gainshapers[c]; + gainshaper.setFuzzFactor(fuzz); + auto &toneFilter = toneFilters[c]; + toneFilter.lowpass(toneHz/(config.sampleRate*2)); + auto &outputFilter = outputFilters[c]; + outputFilter.highpass((10 + 40*fuzz)/(config.sampleRate*2)); // more aggressive when fuzz is enabled, since it's very asymmetrical + + oversampler.upChannel(c, io.input[c], block.length); + Sample *samples = oversampler[c]; + for (int i = 0; i < block.length*2; ++i) { + double hi = i*0.5; + Sample x = samples[i]*inputGain.at(hi); + Sample gain = gainshaper(x)*outputGain.at(hi); + Sample y = x*toneFilter(gain); + samples[i] = outputFilter(y); + } + oversampler.downChannel(c, io.output[c], block.length); } - + } +private: + int channels = 0; + signalsmith::rates::Oversampler2xFIR oversampler; + struct GainshapeADAA { + Sample prevX = 0, prevIntegral = 0; + Sample fuzzPositive = 1, fuzzNegative = 1; + + void setFuzzFactor(Sample k) { + fuzzPositive = 1 + k - k*k; + fuzzNegative = 1 - k - k*k; + prevIntegral = integralGain(prevX); + } + + Sample gain(Sample x) const { + Sample fuzzGain = (x >= 0 ? fuzzPositive : fuzzNegative); + return fuzzGain/std::sqrt(1 + x*x); + } + Sample integralGain(Sample x) const { + if (x >= 0) { + return fuzzPositive*std::log(std::sqrt(1 + x*x) + x); + } else { // more accurate if we flip it + return -fuzzNegative*std::log(std::sqrt(1 + x*x) - x); + } + } + Sample averageGain(Sample range) const { + // Average gain from 0-range, ignoring fuzz + return std::log(std::sqrt(1 + range*range) + range)/range; + } + static constexpr Sample minDiffX = 1e-4; + void reset() { - oversampler.reset(); - for (auto &g : gainshapers) g.reset(); - for (auto &f : toneFilters) f.reset(); - for (auto &f : outputFilters) f.reset(); + prevX = 0; + prevIntegral = integralGain(prevX); } - - int latencySamples() const { - return oversampleHalfLatency*2; - } - - template - void processSTFX(Io &io, Config &config, Block &block) { - auto inputGain = block.smooth(drive); - double outputGainFrom = outGain.from(); - double outputGainTo = outGain.to(); - if (autoGain) { - Sample averageGain = gainshapers[0].averageGain(autoGainLevel*drive.from()); - outputGainFrom /= drive.from()*averageGain; - outputGainTo /= drive.to()*gainshapers[0].averageGain(autoGainLevel*drive.to()); - } - auto outputGain = block.smooth(outputGainFrom, outputGainTo); - - for (int c = 0; c < channels; ++c) { - auto &gainshaper = gainshapers[c]; - gainshaper.setFuzzFactor(fuzz); - auto &toneFilter = toneFilters[c]; - toneFilter.lowpass(toneHz/(config.sampleRate*2)); - auto &outputFilter = outputFilters[c]; - outputFilter.highpass((10 + 40*fuzz)/(config.sampleRate*2)); // more aggressive when fuzz is enabled, since it's very asymmetrical - - oversampler.upChannel(c, io.input[c], block.length); - Sample *samples = oversampler[c]; - for (int i = 0; i < block.length*2; ++i) { - double hi = i*0.5; - Sample x = samples[i]*inputGain.at(hi); - Sample gain = gainshaper(x)*outputGain.at(hi); - Sample y = x*toneFilter(gain); - samples[i] = outputFilter(y); - } - oversampler.downChannel(c, io.output[c], block.length); - } + Sample operator()(Sample x) { + Sample diffX = x - prevX; + Sample integral = integralGain(x); + Sample diffIntegral = integral - prevIntegral; + prevX = x; + prevIntegral = integral; + if (std::abs(diffX) < minDiffX) return gain(x); + return diffIntegral/diffX; } }; - - using Crunch = stfx::LibraryEffect; + std::vector gainshapers; + using Filter = signalsmith::filters::BiquadStatic; + std::vector toneFilters, outputFilters; +}; }} // namespace diff --git a/stfx/stfx-library.h b/stfx/stfx-library.h index e500841..cd1adca 100644 --- a/stfx/stfx-library.h +++ b/stfx/stfx-library.h @@ -1,6 +1,8 @@ -/* Copyright 2021-2022 Geraint Luff / Signalsmith Audio Ltd +/* Signalsmith's Templated FX + + Copyright 2021-2022 Geraint Luff / Signalsmith Audio Ltd Released under the Boost Software License (see LICENSE.txt) - + The main thing you need is `stfx::LibraryEffect` which produces a simple effect class from an STFX effect template. */ @@ -46,6 +48,7 @@ namespace stfx { return ms*0.001; } + // Templates to add commonly-used units to range parameters template RangeParam & rangeGain(RangeParam ¶m) { return param