commit 94ec5fff54b4308769a87d487334248fd9c93261 Author: Geraint Date: Sun Jun 12 00:05:04 2022 +0100 Start delay diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79b5594 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/.DS_Store diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c81af07 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "dsp"] + path = dsp + url = https://signalsmith-audio.co.uk/code/dsp.git/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN 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/delay.h b/delay.h new file mode 100644 index 0000000..946c597 --- /dev/null +++ b/delay.h @@ -0,0 +1,179 @@ +/* Copyright 2022 Signalsmith Audio Ltd. / Geraint Luff +Released under the Boost Software License (see LICENSE.txt) */ +#ifndef SIGNALSMITH_BASICS_DELAY_H +#define SIGNALSMITH_BASICS_DELAY_H + +#include "./dsp/delay.h" +SIGNALSMITH_DSP_VERSION_CHECK(1, 1, 0) + +#include "./stfx-library.h" + +#include + +namespace signalsmith { namespace basics { + + template + class DelaySTFX : public BaseEffect { + using typename BaseEffect::ParamRange; + using typename BaseEffect::ParamSteps; + + // Unit conversions + static double kHz_hz(double kHz) { + return kHz*1000; + } + static double hz_kHz(double hz) { + return hz*0.001; + } + static double db_gain(double db) { + return std::pow(10, db*0.05); + } + static double gain_db(double gain) { + return std::log10(std::max(gain, 1e-10))*20; + } + static double pc_linear(double percent) { + return percent*0.01; + } + static double linear_pc(double linear) { + return linear*100; + } + + struct EchoSpec { + double maxDelayMs = 0; + + ParamRange ms{0}; + ParamRange gain{0.4}; + ParamRange highpass{200}; + ParamRange lowpass{12000}; + + ParamSteps steps{2}; + + template + void state(Storage &storage) { + storage.param("ms", ms) + .info("delay", "echo spacing (fixed time)") + .range(0, 150, maxDelayMs) + .unit("ms", 0); + storage.param("steps", steps) + .info("delay", "echo spacing (tempo-dependent)") + .range(0, 8) + .unit("steps", 0); + storage.param("gain", gain) + .info("gain", "echo decay") + .range(0, 0.2, 0.99) + .exact(0, "off") + .unit("dB", 1, db_gain, gain_db) + .unit("", 2); + storage.param("highpass", highpass) + .info("highpass", "highpass") + .range(10, 500, 20000) + .unit("kHz", 1, kHz_hz, hz_kHz, 1000, 1e10) + .unit("Hz", 0); + storage.param("lowpass", lowpass) + .info("lowpass", "lowpass") + .range(10, 8000, 20000) + .unit("kHz", 1, kHz_hz, hz_kHz, 1000, 1e10) + .unit("Hz", 0); + } + }; + EchoSpec initial; + EchoSpec feedback; + + ParamRange wobbleRate{0.4}; + ParamRange wobbleDepth{0}; + ParamRange wobbleVariation{0.4}; + + enum {steps4, steps4t, steps8, steps8t, steps16, steps16t, steps32, steps32t, stepEnumCount}; + static constexpr Sample stepFactors[stepEnumCount] = {1.0, 2.0/3, 0.5, 1.0/3, 0.25, 0.5/3, 0.125, 0.25/3}; + ParamSteps beatSteps = steps16; + + int channels = 0; + int maxDelaySamples = 0; + signalsmith::delay::MultiBuffer multiBuffer; + signalsmith::delay::Reader reader; + public: + + DelaySTFX(double maxDelayMs=2000) { + initial.maxDelayMs = feedback.maxDelayMs = maxDelayMs; + } + + template + void state(Storage &storage) { + storage.info("Basics: Delay", "A delay with feedback, filters and modulation"); + int version = storage.version(2); + if (version < 2) return; + + storage("initial", initial); + storage("feedback", feedback); + + storage.param("beatSteps", beatSteps) + .info("beat step", "How long a tempo-dependent \"step\" is") + .names("1/4", "1/4 T", "1/8", "1/8 T", "1/16", "1/16 T", "1/32", "1/32 T"); + storage.param("wobbleRate", wobbleRate) + .info("wobble", "LFO rate") + .range(0.1, 1, 10) + .unit("Hz", 1); + storage.param("wobbleDepth", wobbleDepth) + .info("depth", "LFO detuning") + .range(0, 10, 50) + .unit("cents", 0); + storage.param("wobbleVariation", wobbleVariation) + .info("variation", "LFO variation") + .range(0, 0.25, 1) + .unit("%", 0, pc_linear, linear_pc); + } + + template + void configure(Config &config) { + channels = config.outputChannels = config.inputChannels; + config.auxInputs.resize(0); + config.auxOutputs.resize(0); + + maxDelaySamples = std::ceil(initial.maxDelayMs*0.001*config.sampleRate); + multiBuffer.resize(channels, maxDelaySamples + reader.inputLength + 1); + } + + void reset() { + multiBuffer.reset(); + } + + template + void process(Io &io, const Config &config, const Block &block) { + Sample stepSamplesFrom = 60*config.sampleRate/this->bpm.from()*stepFactors[beatSteps.from()]; + Sample stepSamplesTo = 60*config.sampleRate/this->bpm.to()*stepFactors[beatSteps.to()]; + + auto samplesDelay = [&](double ms, double steps, double stepSamples) { + return std::max(1, std::min(maxDelaySamples, ms*0.001*config.sampleRate + steps*stepSamples)); + }; + Sample initialDelayFrom = samplesDelay(initial.ms.from(), initial.steps.from(), stepSamplesFrom); + Sample initialDelayTo = samplesDelay(initial.ms.to(), initial.steps.to(), stepSamplesTo); + Sample feedbackDelayFrom = samplesDelay(feedback.ms.from(), feedback.steps.from(), stepSamplesFrom); + Sample feedbackDelayTo = samplesDelay(feedback.ms.to(), feedback.steps.to(), stepSamplesTo); + bool delayChanging = (initialDelayFrom != initialDelayTo) || (feedbackDelayFrom != feedbackDelayTo); + + auto smoothInitialGain = block.smooth(initial.gain); + auto smoothFeedbackGain = block.smooth(feedback.gain); + for (int i = 0; i < block.length; ++i) { + for (int c = 0; c < channels; ++c) { + Sample value = io.input[c][i]; + multiBuffer[c][0] = value; + Sample initialDelayed = reader.read(multiBuffer[c], initialDelayTo); + Sample feedbackDelayed = reader.read(multiBuffer[c], feedbackDelayTo); + if (delayChanging) { + Sample initialDelayedFrom = reader.read(multiBuffer[c], initialDelayFrom); + initialDelayed = block.fade(i, initialDelayedFrom, initialDelayed); + Sample feedbackDelayedFrom = reader.read(multiBuffer[c], feedbackDelayFrom); + feedbackDelayed = block.fade(i, feedbackDelayedFrom, feedbackDelayed); + } + multiBuffer[c][0] = value + feedbackDelayed*smoothFeedbackGain.at(i); + io.output[c][i] = value + initialDelayed*smoothInitialGain.at(i); + } + ++multiBuffer; + } + } + }; + + using Delay = stfx::LibraryEffect; + +}} // namespace + +#endif // include guard diff --git a/dsp b/dsp new file mode 160000 index 0000000..763fd47 --- /dev/null +++ b/dsp @@ -0,0 +1 @@ +Subproject commit 763fd4751da69ce412878a31ffd383811e562f5f diff --git a/stfx-library.h b/stfx-library.h new file mode 100644 index 0000000..348d480 --- /dev/null +++ b/stfx-library.h @@ -0,0 +1,423 @@ +/* Copyright 2021 Geraint Luff / Signalsmith Audio Ltd + + This file is released under the MIT License - if you need anything else, let us know. 🙂 + + The main thing you need is `stfx::LibraryEffect` + which produces a simple effect class from an STFX effect template. +*/ + +#ifndef STFX_LIBRARY_H +#define STFX_LIBRARY_H + +#include +#include + +namespace stfx { + + namespace { + // A value which changes linearly within a given index range (e.g. a block) + class LinearSegment { + double offset, gradient; + public: + LinearSegment(double offset, double gradient) : offset(offset), gradient(gradient) {} + + double at(int i) { + return offset + i*gradient; + } + bool changing() { + return gradient != 0; + } + }; + + /** A processing block (not including IO). + + There are two fades happening within each block: + * automation: changes are smoothed linearly across the block + * A/B fade: a slower transition for parameters which need to change slowly or do cross-fades + */ + class Block { + // slow fade start/rate + double fadeStart, fadeStep; + // per-block automation gradient rate + double blockFade; + // we might need some additional setup on the fiand/or after a directly + bool firstBlockAfterReset = false; + + template + bool paramsChanging(Param ¶m) const { + return param.from() != param.to(); + } + template + bool paramsChanging(Param ¶m, Others &...others) const { + return paramsChanging(param) || paramsChanging(others...); + } + public: + // Block length in samples + int length; + + Block(int length, double fadeStart, double fadeStep, bool firstBlockAfterReset=false) : fadeStart(fadeStart), fadeStep(fadeStep), blockFade(1.0/length), firstBlockAfterReset(firstBlockAfterReset), length(length) {} + // Not copyable, because that's probably a mistake + Block(const Block &) = delete; + Block & operator =(const Block&) = delete; + + /// Fade ratio at a given sample index + double fade(int i) const { + return fadeStart + i*fadeStep; + } + /// Mix two values according to the fade ratio + template + Value fade(int i, Value from, Value to) const { + return from + (to - from)*fade(i); + } + + /// Is there a fade currently active? + bool fading() const { + return fadeStep != 0; + } + /// Is a fade happening, and are any of the parameters included in it? + template + bool fading(Param ¶m, Others &...others) const { + return fading() && paramsChanging(param, others...); + } + + /// Set up an A/B fade. Executes once at the beginning of a fade, and also once directly after a `.reset()`. + template + void setupFade(Fn fn) const { + setupFade(fading(), fn); + } + /// Same as above, but only run on fade-start if `fading` is true. Mostly useful when used with `.fading(params...)` + template + void setupFade(bool fading, Fn fn) const { + if (firstBlockAfterReset) fn(); + if (fading && fadeStart == 0) fn(); + } + + /// Produce a linear segment corresponding to A/B fading a parameter + template + LinearSegment smooth(Param ¶m) const { + return smooth(param.from(), param.to()); + } + /// Same as above, but the parameter's values can be mapped + template + LinearSegment smooth(Param ¶m, Map map) const { + return smooth(map(param.from()), map(param.to())); + } + /// Produce a linear segment corresponding to A/B fading two values. + /// These values should _not_ generally change every block, but only when a new fade starts. They should probably be derived from `param.from()` and `param.to()`, and this method is mostly useful for combining multiple parameters. + LinearSegment smooth(double from, double to) const { + double diff = to - from; + return {from + diff*fadeStart, diff*fadeStep}; + } + + /// Automation provides a curve for a parameter, and is also an event list where the events update the curve. + /// This pattern allows sample-accurate automation in environments where that's supported. + struct BlockAutomation : public LinearSegment { + BlockAutomation(const LinearSegment &smoothed) : LinearSegment(smoothed) {} + + // For this implementation, we just provide a linear segment and no update events. + static constexpr int size() { + return 0; + } + struct DoNothingEvent { + int offset = 0; + void operator ()() {} + }; + DoNothingEvent operator [](int) { + return DoNothingEvent(); + } + }; + /// Get an automation curve for a parameter + template + BlockAutomation automation(Param ¶m) const { + double start = param._libPrevBlock(); + auto diff = param._libCurrent() - start; + return LinearSegment{start, diff*blockFade}; + } + + /// Blocks can be processed in sub-blocks, which are split up by events. + /// This method can return a sub-block (with `.split()` and `.forEach()` methods). + template + const Block & split(EventList &&list, int count) const { + for (int i = 0; i < count; ++i) list[i](); + return *this; + } + template + const Block & split(EventList &&list, Others &&...others) const { + return split(list).split(std::forward(others)...); + } + /// Base-case for the templated recursion + const Block & split() const { + return *this; + } + /// Execute the callback once per sub-block, with sample-index arguments: `callback(int start, int end)` + template + void forEach(Callback callback) const { + callback(0, length); + } + }; + + // Parameters can be assigned using `=`, and store their own history for transitions + template + class LibraryParam { + Value current, prevBlock; + // Used for the 20ms parameter fade + Value _from, _to; + public: + LibraryParam(const Value &initial) : current(initial), prevBlock(initial), _from(initial), _to(initial) {} + operator Value () const { + return current; + } + LibraryParam & operator =(const Value &v) { + current = v; + return *this; + } + // Return the + Value from() const { + return _from; + } + Value to() const { + return _to; + } + + // Shuffle the internal values along to start a new fade, return whether it's actually changing + bool _libStartFade() { + _from = _to; + _to = current; + return (_to != _from); + } + // Store previous value for block-level automation + void _libEndBlock() { + prevBlock = current; + } + Value _libCurrent() { + return current; + } + Value _libPrevBlock() { + return prevBlock; + } + }; + + // When we want to ignore parameter info + struct PInfoPlaceholder { + PInfoPlaceholder & info(const char*, const char*) { + return *this; + } + PInfoPlaceholder & info(std::string, std::string) { + return *this; + } + template + PInfoPlaceholder & range(V1, V2) { + return *this; + } + template + PInfoPlaceholder & range(V1, V2, V3) { + return *this; + } + template + PInfoPlaceholder & unit(Args...) { + return *this; + } + PInfoPlaceholder & exact(double, const char *) { + return *this; + } + template + PInfoPlaceholder & names(Args ...) { + return *this; + } + }; + + } + + /// Base class for our effect to inherit from. Provides parameter classes and some default config. + class LibraryEffectBase { + protected: + using ParamRange = LibraryParam; + using ParamSteps = LibraryParam; + + template + class ParamEnum : public ParamSteps { + static Enum castToEnum(int v) {return (Enum)v;}; + public: + using Value = Enum; + ParamEnum(Enum value) : ParamSteps((int)value) {} + operator Enum() { + return (Enum)this->latest; + } + }; + public: + double paramFadeMs() { + return 20; + } + int tailSamples() { + return 0; + } + }; + + /// Creates an effect class from an effect template, with optional extra config. + /// The effect template takes `EffectTemplate` + template class EffectTemplate, class ...ExtraConfig> + class LibraryEffect : public EffectTemplate { + using EffectClass = EffectTemplate; + + // This is passed to the effect's `.state()` method during initialisation, and collects pointers to the effect's parameters + class CollectParams { + public: + std::vector *> rangeParams; + std::vector *> stepParams; + + // Add registered parameters to the list + PInfoPlaceholder param(const char *, LibraryParam ¶m, const char *codeExpr=nullptr) { + (void)codeExpr; + rangeParams.push_back(¶m); + return {}; + } + PInfoPlaceholder param(const char *, LibraryParam ¶m, const char *codeExpr=nullptr) { + (void)codeExpr; + stepParams.push_back(¶m); + return {}; + } + + // The effect might ask us to store/fetch the serialisation version, we just echo it back + static int version(int v) {return v;} + + // We ignore any basic type + void operator ()(bool) {} + void operator ()(int) {} + void operator ()(long) {} + void operator ()(double) {} + void operator ()(float) {} + // Assume all other arguments have a `.state()`, and recurse into it + template + void operator ()(OtherObject &obj) { + obj.state(*this); + } + // Drop all names/labels we're given + template + void operator ()(const char *, V &v) { + (*this)(v); + } + template + void group(const char *, Fn fn) { + fn(*this); + } + // Drop any name/description we're given + template + void info(Args...) {} + } params; + + bool justHadReset = true; + // Keep track of the A/B fade state + double fadeRatio = 0; + public: + template + LibraryEffect(Args &&...args) : EffectClass(std::forward(args)...) { + EffectClass::state(params); + } + + struct Config { + double sampleRate = 48000; + int inputChannels = 2, outputChannels = 2; + std::vector auxInputs, auxOutputs; + int maxBlockSize = 256; + + bool operator ==(const Config &other) { + return sampleRate == other.sampleRate + && inputChannels == other.inputChannels + && outputChannels == other.outputChannels + && auxInputs == other.auxInputs + && auxOutputs == other.auxOutputs + && maxBlockSize == other.maxBlockSize; + } + }; + /// The current (proposed) effect configuration + Config config; + /// Returns `true` if the current `.config` was accepted. Otherwise, you can check how `.config` was modified, make your own adjustments (if needed) and try again. + bool configure() { + Config prevConfig = config; + EffectClass::configure(config); + if (config == prevConfig) { + reset(); + return true; + } + return false; + } + /// Attempts to find a valid configuration by iteration + bool configurePersistent(int attempts=10) { + for (int i = 0; i < attempts; ++i) { + if (configure()) return true; + } + return false; + } + /// Returns true if the effect was successfully configured with _exactly_ these parameters + bool configure(double sampleRate, int maxBlockSize, int channels=2, int outputChannels=-1) { + if (outputChannels < 0) outputChannels = channels; + config.sampleRate = sampleRate; + config.inputChannels = channels; + config.outputChannels = outputChannels; + config.maxBlockSize = maxBlockSize; + + return configure(); + } + + /// Clears effect buffers and resets parameters + void reset() { + for (auto param : params.rangeParams) { + param->_libStartFade(); + param->_libEndBlock(); + } + for (auto param : params.stepParams) { + param->_libStartFade(); + param->_libEndBlock(); + } + EffectClass::reset(); + justHadReset = true; + } + + /// Wraps the common `process(float** inputs, float** outputs, int length)` call into the `.process(io, config, block)`. + /// It actually accepts any objects which support `inputs[channel][index]`, so you could write adapters for interleaved buffers etc. + template + void process(Inputs inputs, Outputs outputs, int blockLength) { + // How long should the parameter fade take? + double fadeSamples = EffectClass::paramFadeMs()*0.001*config.sampleRate; + // Fade position at the end of the block + double fadeRatioEnd = fadeRatio + blockLength/fadeSamples; + // If the fade will finish this block, get there exactly + double fadeRatioStep = (fadeRatioEnd >= 1) ? (1 - fadeRatio)/blockLength : 1/fadeSamples; + + // If we're just starting a new fade, move all the parameter values along + if (fadeRatio == 0) { + bool needsFade = false; + for (auto param : params.rangeParams) { + if (param->_libStartFade()) needsFade = true; + } + for (auto param : params.stepParams) { + if (param->_libStartFade()) needsFade = true; + } + // None of our parameters are actually fading, just skip it + if (!needsFade) fadeRatioStep = fadeRatioEnd = 0; + } + + struct Io { + Inputs input; + Outputs output; + }; + Io io{inputs, outputs}; + Block block(blockLength, fadeRatio, fadeRatioStep, justHadReset); + + ((EffectClass *)this)->process(io, (const Config &)config, (const Block &)block); + + if (fadeRatioEnd >= 1) { + // Fade just finished, so we reset + fadeRatio = 0; + } else { + fadeRatio = fadeRatioEnd; + } + justHadReset = false; + for (auto param : params.rangeParams) param->_libEndBlock(); + for (auto param : params.stepParams) param->_libEndBlock(); + } + }; + +} // stfx:: + +#endif // include guard