1
0

Start delay

This commit is contained in:
Geraint 2022-06-12 00:05:04 +01:00
commit 94ec5fff54
6 changed files with 630 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**/.DS_Store

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "dsp"]
path = dsp
url = https://signalsmith-audio.co.uk/code/dsp.git/

23
LICENSE.txt Normal file
View File

@ -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.

179
delay.h Normal file
View File

@ -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 <cmath>
namespace signalsmith { namespace basics {
template<typename Sample, class BaseEffect>
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<double>(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<class Storage>
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<Sample> multiBuffer;
signalsmith::delay::Reader<Sample, signalsmith::delay::InterpolatorLagrange7> reader;
public:
DelaySTFX(double maxDelayMs=2000) {
initial.maxDelayMs = feedback.maxDelayMs = maxDelayMs;
}
template<class Storage>
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<class Config>
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 <class Io, class Config, class Block>
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<Sample>(1, std::min<Sample>(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<float, DelaySTFX>;
}} // namespace
#endif // include guard

1
dsp Submodule

@ -0,0 +1 @@
Subproject commit 763fd4751da69ce412878a31ffd383811e562f5f

423
stfx-library.h Normal file
View File

@ -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<Sample, EffectTemplate>`
which produces a simple effect class from an STFX effect template.
*/
#ifndef STFX_LIBRARY_H
#define STFX_LIBRARY_H
#include <vector>
#include <string>
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<class Param>
bool paramsChanging(Param &param) const {
return param.from() != param.to();
}
template<class Param, class ...Others>
bool paramsChanging(Param &param, 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<class Value=double>
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<class Param, class ...Others>
bool fading(Param &param, 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<class Fn>
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<class Fn>
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<class Param>
LinearSegment smooth(Param &param) const {
return smooth(param.from(), param.to());
}
/// Same as above, but the parameter's values can be mapped
template<class Param, class Map>
LinearSegment smooth(Param &param, 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<class Param>
BlockAutomation automation(Param &param) 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<class EventList>
const Block & split(EventList &&list, int count) const {
for (int i = 0; i < count; ++i) list[i]();
return *this;
}
template<class EventList, class ...Others>
const Block & split(EventList &&list, Others &&...others) const {
return split(list).split(std::forward<Others>(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<class Callback>
void forEach(Callback callback) const {
callback(0, length);
}
};
// Parameters can be assigned using `=`, and store their own history for transitions
template<typename Value>
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<typename V1, typename V2>
PInfoPlaceholder & range(V1, V2) {
return *this;
}
template<typename V1, typename V2, typename V3>
PInfoPlaceholder & range(V1, V2, V3) {
return *this;
}
template<class ...Args>
PInfoPlaceholder & unit(Args...) {
return *this;
}
PInfoPlaceholder & exact(double, const char *) {
return *this;
}
template<typename ...Args>
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<double>;
using ParamSteps = LibraryParam<int>;
template<typename Enum, int size>
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<Sample, BaseClass, ...ExtraConfig>`
template<typename Sample, template <class, class, class...> class EffectTemplate, class ...ExtraConfig>
class LibraryEffect : public EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...> {
using EffectClass = EffectTemplate<Sample, stfx::LibraryEffectBase, ExtraConfig...>;
// This is passed to the effect's `.state()` method during initialisation, and collects pointers to the effect's parameters
class CollectParams {
public:
std::vector<LibraryParam<double> *> rangeParams;
std::vector<LibraryParam<int> *> stepParams;
// Add registered parameters to the list
PInfoPlaceholder param(const char *, LibraryParam<double> &param, const char *codeExpr=nullptr) {
(void)codeExpr;
rangeParams.push_back(&param);
return {};
}
PInfoPlaceholder param(const char *, LibraryParam<int> &param, const char *codeExpr=nullptr) {
(void)codeExpr;
stepParams.push_back(&param);
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<class OtherObject>
void operator ()(OtherObject &obj) {
obj.state(*this);
}
// Drop all names/labels we're given
template<class V>
void operator ()(const char *, V &v) {
(*this)(v);
}
template<class Fn>
void group(const char *, Fn fn) {
fn(*this);
}
// Drop any name/description we're given
template<class ...Args>
void info(Args...) {}
} params;
bool justHadReset = true;
// Keep track of the A/B fade state
double fadeRatio = 0;
public:
template<class ...Args>
LibraryEffect(Args &&...args) : EffectClass(std::forward<Args>(args)...) {
EffectClass::state(params);
}
struct Config {
double sampleRate = 48000;
int inputChannels = 2, outputChannels = 2;
std::vector<int> 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<class Inputs, class Outputs>
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