From 9667c7b482a5e1f091a8bf84add0ca3e2cb68428 Mon Sep 17 00:00:00 2001 From: Sophia Poirier <2997196+sophiapoirier@users.noreply.github.com> Date: Sat, 19 Feb 2022 16:28:32 -0800 Subject: [PATCH] Transverb: various approaches to adapt to "distance" changes without crackly glitches --- transverb/transverb-base.h | 3 ++ transverb/transverb.h | 18 ++++++- transverb/transverbformalities.cpp | 81 +++++++++++++++++++++++++++--- transverb/transverbprocess.cpp | 66 ++++++++++++++++-------- 4 files changed, 137 insertions(+), 31 deletions(-) diff --git a/transverb/transverb-base.h b/transverb/transverb-base.h index 969ea82a..27a64631 100644 --- a/transverb/transverb-base.h +++ b/transverb/transverb-base.h @@ -47,6 +47,7 @@ enum kTomsound, kFreeze, kAttenuateFeedbackByMixLevel, + kDistChangeMode, kNumParameters }; @@ -65,6 +66,8 @@ enum { kQualityMode_DirtFi, kQualityMode_HiFi, kQualityMode_UltraHiFi, kQualityM enum { kSpeedMode_Fine, kSpeedMode_Semitone, kSpeedMode_Octave, kSpeedMode_NumModes }; static constexpr dfx::PropertyID kTransverbProperty_SpeedModeBase = dfx::kPluginProperty_EndOfList; +enum { kDistChangeMode_Reverse, kDistChangeMode_AdHocVarispeed, kDistChangeMode_DistanceVarispeed, kDistChangeMode_BufferVarispeed, kDistChangeMode_LoopingBufferVarispeed, kDistChangeMode_Count }; + dfx::PropertyID speedModeIndexToPropertyID(size_t inIndex) noexcept; size_t speedModePropertyIDToIndex(dfx::PropertyID inPropertyID) noexcept; diff --git a/transverb/transverb.h b/transverb/transverb.h index 7d8724a5..9340a5df 100644 --- a/transverb/transverb.h +++ b/transverb/transverb.h @@ -25,6 +25,7 @@ To contact the author, use the contact form at http://destroyfx.org/ #include #include #include +#include #include #include "dfxplugin.h" @@ -56,6 +57,8 @@ class TransverbDSP final : public DfxPluginCore { dfx::SmoothedValue mix, feed; double read = 0.; + std::optional targetdist; + double distspeedfactor = 1.; std::vector buf; dfx::IIRFilter filter; @@ -82,17 +85,23 @@ class TransverbDSP final : public DfxPluginCore { static constexpr int mod_bipolar(int value, int modulo); static inline double fmod_bipolar(double value, double modulo); + // distance in samples of a read position from the current write position + double getdist(double read) const; + void processdist(double distnormalized, Head& head); + // these store the parameter values int bsize = 0; dfx::SmoothedValue drymix; - long quality = 0; - bool tomsound = false; + int distchangemode {}; int writer = 0; std::array heads; int const MAXBUF; // the size of the audio buffer (dependent on sampling rate) + bool firstrendersincereset = false; + std::vector& buftemp; + std::vector const firCoefficientsWindow; }; @@ -115,6 +124,10 @@ class Transverb final : public DfxPlugin { long dfx_SetProperty(dfx::PropertyID inPropertyID, dfx::Scope inScope, unsigned long inItemIndex, void const* inData, size_t inDataSize) override; + auto& getscratchbuffer() noexcept { + return buftemp; + } + protected: size_t settings_sizeOfExtendedData() const noexcept override; void settings_saveExtendedData(void* outData, bool isPreset) override; @@ -131,6 +144,7 @@ class Transverb final : public DfxPlugin { } std::array speedModeStates {}; + std::vector buftemp; // shared between all DSP cores (memory optimization) }; diff --git a/transverb/transverbformalities.cpp b/transverb/transverbformalities.cpp index e87b1079..93947a8b 100644 --- a/transverb/transverbformalities.cpp +++ b/transverb/transverbformalities.cpp @@ -57,6 +57,7 @@ Transverb::Transverb(TARGET_API_BASE_INSTANCE_TYPE inInstance) initparameter_b(kTomsound, {"TOMSOUND", "TomSnd", "Tom7"}, false); initparameter_b(kFreeze, dfx::MakeParameterNames(dfx::kParameterNames_Freeze), false); initparameter_b(kAttenuateFeedbackByMixLevel, {"attenuate feedback by mix level", "AtnFdbk", "AtnFdb", "-fdb"}, false); + initparameter_list(kDistChangeMode, {"distance change mode", "DistMod", "DstMod", "DMod"}, kDistChangeMode_Reverse, kDistChangeMode_Reverse, kDistChangeMode_Count); setparameterenforcevaluelimits(kBsize, true); setparameterenforcevaluelimits(kDist1, true); @@ -66,10 +67,12 @@ Transverb::Transverb(TARGET_API_BASE_INSTANCE_TYPE inInstance) setparametervaluestring(kQuality, kQualityMode_HiFi, "hi-fi"); setparametervaluestring(kQuality, kQualityMode_UltraHiFi, "ultra hi-fi"); - // distance parameters only have meaningful effect at zero speed which probably will never occur randomly, - // and otherwise all they do is glitch a lot, so omit them - addparameterattributes(kDist1, DfxParam::kAttribute_OmitFromRandomizeAll); - addparameterattributes(kDist2, DfxParam::kAttribute_OmitFromRandomizeAll); + setparametervaluestring(kDistChangeMode, kDistChangeMode_Reverse, "reverse"); + setparametervaluestring(kDistChangeMode, kDistChangeMode_AdHocVarispeed, "ad hoc varispeed"); + setparametervaluestring(kDistChangeMode, kDistChangeMode_DistanceVarispeed, "distance varispeed"); + setparametervaluestring(kDistChangeMode, kDistChangeMode_BufferVarispeed, "buffer varispeed"); + setparametervaluestring(kDistChangeMode, kDistChangeMode_LoopingBufferVarispeed, "looping buffer varispeed"); + addparameterattributes(kFreeze, DfxParam::kAttribute_OmitFromRandomizeAll); addparameterattributes(kAttenuateFeedbackByMixLevel, DfxParam::kAttribute_OmitFromRandomizeAll); @@ -108,8 +111,11 @@ void Transverb::dfx_PostConstructor() { TransverbDSP::TransverbDSP(DfxPlugin* inDfxPlugin) : DfxPluginCore(inDfxPlugin), MAXBUF(static_cast(getparametermax_f(kBsize) * 0.001 * getsamplerate())), + buftemp(dynamic_cast(inDfxPlugin)->getscratchbuffer()), firCoefficientsWindow(dfx::FIRFilter::generateKaiserWindow(kNumFIRTaps, 60.0f)) { + buftemp.assign(MAXBUF, 0.f); + registerSmoothedAudioValue(&drymix); for (auto& head : heads) { @@ -125,12 +131,15 @@ TransverbDSP::TransverbDSP(DfxPlugin* inDfxPlugin) void TransverbDSP::reset() { std::for_each(heads.begin(), heads.end(), [](Head& head){ head.reset(); }); + + firstrendersincereset = true; } void TransverbDSP::Head::reset() { smoothcount = 0; lastdelayval = 0.f; + targetdist.reset(); filter.reset(); speedHasChanged = true; @@ -163,8 +172,6 @@ void TransverbDSP::processparameters() { heads[head].feed = *value; } } - quality = getparameter_i(kQuality); - tomsound = getparameter_b(kTomsound); if (auto const value = getparameterifchanged_f(kBsize)) { @@ -192,12 +199,12 @@ void TransverbDSP::processparameters() { std::for_each(heads.begin(), heads.end(), [bsize_f](Head& head){ head.read = fmod_bipolar(head.read, bsize_f); }); } + distchangemode = getparameter_i(kDistChangeMode); for (size_t head = 0; head < kNumDelays; head++) { if (auto const dist = getparameterifchanged_f(kDistParameters[head])) { - auto const bsize_f = static_cast(bsize); - heads[head].read = fmod_bipolar(static_cast(writer) - (*dist * bsize_f), bsize_f); + processdist(*dist, heads[head]); } } @@ -220,6 +227,64 @@ void TransverbDSP::processparameters() { heads[0].mix.setValueNow(getparametermin_f(kMix1)); } } + + firstrendersincereset = false; +} + +double TransverbDSP::getdist(double read) const { + + return fmod_bipolar(static_cast(writer) - read, static_cast(bsize)); +} + +void TransverbDSP::processdist(double distnormalized, Head& head) { + + auto const bsize_f = static_cast(bsize); + auto const distsamples = distnormalized * bsize_f; + auto const targetread = fmod_bipolar(static_cast(writer) - distsamples, bsize_f); + + if (firstrendersincereset) + { + head.read = targetread; + } + else + { + if (distchangemode == kDistChangeMode_Reverse) + { + head.targetdist = distsamples; + } + else if (auto const resamplerate = getdist(head.read) / std::max(distsamples, 1.); distchangemode == kDistChangeMode_AdHocVarispeed) + { + head.targetdist = distsamples; + head.distspeedfactor = resamplerate; + head.speedHasChanged = true; + } + else + { + constexpr double modulationthresholdsamples = 1.; + if (std::fabs(targetread - head.read) >= modulationthresholdsamples) + { + auto const bsizesteps = bsize_f / resamplerate; + bool const subslice = (distchangemode == kDistChangeMode_DistanceVarispeed); + bool const looping = (distchangemode == kDistChangeMode_LoopingBufferVarispeed); + auto const copylength_f = subslice ? distsamples : (looping ? bsize_f : std::min(bsize_f, bsizesteps)); + auto const copylength = static_cast(std::lround(copylength_f)); + assert(copylength <= buftemp.size()); + assert(static_cast(copylength) <= bsize); + auto const sourcestart = subslice ? head.read : fmod_bipolar(static_cast(writer) - (copylength_f * resamplerate), bsize_f); + for (size_t i = 0; i < copylength; i++) + { + auto const sourcepos = std::fmod(sourcestart + (resamplerate * static_cast(i)), bsize_f); + buftemp[i] = interpolateHermite(head.buf.data(), sourcepos, bsize, writer); + } + auto const destinationstart = subslice ? std::lround(targetread) : mod_bipolar(writer - static_cast(copylength), bsize); + auto const copylength1 = std::min(static_cast(bsize - destinationstart), copylength); + auto const copylength2 = copylength - copylength1; + std::copy_n(buftemp.cbegin(), copylength1, std::next(head.buf.begin(), destinationstart)); + std::copy_n(std::next(buftemp.cbegin(), copylength1), copylength2, head.buf.begin()); + } + head.read = targetread; + } + } } diff --git a/transverb/transverbprocess.cpp b/transverb/transverbprocess.cpp index 65fd9316..b41575b4 100644 --- a/transverb/transverbprocess.cpp +++ b/transverb/transverbprocess.cpp @@ -35,11 +35,13 @@ using namespace dfx::TV; void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long numSampleFrames) { - std::array delayvals {}; // delay buffer output values auto const bsize_float = static_cast(bsize); // cut down on casting + auto const quality = getparameter_i(kQuality); + auto const tomsound = getparameter_b(kTomsound); auto const freeze = getparameter_b(kFreeze); int const writerIncrement = freeze ? 0 : 1; auto const attenuateFeedbackByMixLevel = getparameter_b(kAttenuateFeedbackByMixLevel); + std::array delayvals {}; // delay buffer output values ///////////// S O P H I A S O U N D ////////////// @@ -68,6 +70,9 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long for (size_t h = 0; h < kNumDelays; h++) // delay heads loop { auto const read_int = static_cast(heads[h].read); + auto const reverseread = (distchangemode == kDistChangeMode_Reverse) && heads[h].targetdist.has_value(); + auto const distcatchup = (distchangemode == kDistChangeMode_AdHocVarispeed) && heads[h].targetdist.has_value(); + auto const speed = heads[h].speed.getValue() * (distcatchup ? heads[h].distspeedfactor : 1.); // filter setup if (quality == kQualityMode_UltraHiFi) @@ -76,20 +81,20 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long { lowpasspos[h] = read_int; // check to see if we need to lowpass the first delay head and init coefficients if so - if (heads[h].speed.getValue() > kUnitySpeed) + if (speed > kUnitySpeed) { filtermodes[h] = FilterMode::LowpassIIR; - speed_ints[h] = static_cast(heads[h].speed.getValue()); + speed_ints[h] = static_cast(speed); // it becomes too costly to try to IIR at higher speeds, so switch to FIR filtering - if (heads[h].speed.getValue() >= kFIRSpeedThreshold) + if (speed >= kFIRSpeedThreshold) { filtermodes[h] = FilterMode::LowpassFIR; // compensate for gain lost from filtering - mugs[h] = static_cast(std::pow(heads[h].speed.getValue() / kFIRSpeedThreshold, 0.78)); + mugs[h] = static_cast(std::pow(speed / kFIRSpeedThreshold, 0.78)); // update the coefficients only if necessary if (std::exchange(heads[h].speedHasChanged, false)) { - dfx::FIRFilter::calculateIdealLowpassCoefficients((samplerate / heads[h].speed.getValue()) * dfx::FIRFilter::kShelfStartLowpass, + dfx::FIRFilter::calculateIdealLowpassCoefficients((samplerate / speed) * dfx::FIRFilter::kShelfStartLowpass, samplerate, kNumFIRTaps, heads[h].firCoefficients.data(), firCoefficientsWindow.data()); heads[h].filter.reset(); @@ -97,7 +102,7 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long } else if (std::exchange(heads[h].speedHasChanged, false)) { - heads[h].filter.setLowpassCoefficients((samplerate / heads[h].speed.getValue()) * dfx::IIRFilter::kShelfStartLowpass); + heads[h].filter.setLowpassCoefficients((samplerate / speed) * dfx::IIRFilter::kShelfStartLowpass); } } // we need to highpass the delay head to remove mega sub bass @@ -106,7 +111,7 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long filtermodes[h] = FilterMode::Highpass; if (std::exchange(heads[h].speedHasChanged, false)) { - heads[h].filter.setHighpassCoefficients(kHighpassFilterCutoff / heads[h].speed.getValue()); + heads[h].filter.setHighpassCoefficients(kHighpassFilterCutoff / speed); } } } @@ -173,18 +178,18 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long // start smoothing if the writer has passed a reader or vice versa, // though not if reader and writer move at the same speed // (check the positions before wrapping around the heads) - auto const nextRead = static_cast(heads[h].read + heads[h].speed.getValue()); + auto const nextRead = static_cast(heads[h].read + speed); auto const nextWrite = writer + 1; bool const readCrossingAhead = (read_int < writer) && (nextRead >= nextWrite); bool const readCrossingBehind = (read_int >= writer) && (nextRead <= nextWrite); - bool const speedIsUnity = heads[h].speed.getValue() == kUnitySpeed; + bool const speedIsUnity = (speed == kUnitySpeed); if ((readCrossingAhead || readCrossingBehind) && !speedIsUnity) { // check because, at slow speeds, it's possible to go into this twice or more in a row if (heads[h].smoothcount <= 0) { // store the most recent output as the head's smoothing sample heads[h].lastdelayval = delayvals[h]; // truncate the smoothing duration if we're using too small of a buffer size - auto const bufferReadSteps = static_cast(bsize_float / heads[h].speed.getValue()); + auto const bufferReadSteps = static_cast(bsize_float / speed); auto const smoothdur = std::min(bufferReadSteps, kAudioSmoothingDur_samples); heads[h].smoothstep = 1.f / static_cast(smoothdur); // the scalar step value heads[h].smoothcount = smoothdur; // set the counter to the total duration @@ -192,9 +197,21 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long } // update read heads, wrapping around if they have gone past the end of the buffer - heads[h].read += heads[h].speed.getValue(); - if (heads[h].read >= bsize_float) { - heads[h].read = fmod_bipolar(heads[h].read, bsize_float); + if (reverseread) { + heads[h].read -= speed; + while (heads[h].read < 0.) { + heads[h].read += bsize_float; + } + } else { + heads[h].read += speed; + if (heads[h].read >= bsize_float) { + heads[h].read = fmod_bipolar(heads[h].read, bsize_float); + } + } + if (distcatchup || reverseread) { + if (std::fabs(getdist(heads[h].read) - *heads[h].targetdist) < speed) { + heads[h].targetdist.reset(); + } } // if we're doing IIR lowpass filtering, @@ -204,40 +221,47 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, unsigned long if (filtermodes[h] == FilterMode::LowpassIIR) { int lowpasscount = 0; + int const direction = reverseread ? -1 : 1; while (lowpasscount < speed_ints[h]) { switch (speed_ints[h] - lowpasscount) { case 1: heads[h].filter.processToCacheH1(heads[h].buf[lowpasspos[h]]); - lowpasspos[h] = (lowpasspos[h] + 1) % bsize; + lowpasspos[h] = mod_bipolar(lowpasspos[h] + (1 * direction), bsize); lowpasscount++; break; case 2: heads[h].filter.processToCacheH2(heads[h].buf.data(), lowpasspos[h], bsize); - lowpasspos[h] = (lowpasspos[h] + 2) % bsize; + lowpasspos[h] = mod_bipolar(lowpasspos[h] + (2 * direction), bsize); lowpasscount += 2; break; case 3: heads[h].filter.processToCacheH3(heads[h].buf.data(), lowpasspos[h], bsize); - lowpasspos[h] = (lowpasspos[h] + 3) % bsize; + lowpasspos[h] = mod_bipolar(lowpasspos[h] + (3 * direction), bsize); lowpasscount += 3; break; default: heads[h].filter.processToCacheH4(heads[h].buf.data(), lowpasspos[h], bsize); - lowpasspos[h] = (lowpasspos[h] + 4) % bsize; + lowpasspos[h] = mod_bipolar(lowpasspos[h] + (4 * direction), bsize); lowpasscount += 4; break; } } auto const nextread_int = static_cast(heads[h].read); // check whether we need to consume one more sample - bool const extrasample = ((lowpasspos[h] < nextread_int) && ((lowpasspos[h] + 1) == nextread_int)) || - ((lowpasspos[h] == (bsize - 1)) && (nextread_int == 0)); + bool const extrasample = [=] { + if (reverseread) { + return ((lowpasspos[h] > nextread_int) && ((lowpasspos[h] - 1) == nextread_int)) || + ((lowpasspos[h] == 0) && (nextread_int == (bsize - 1))); + } + return ((lowpasspos[h] < nextread_int) && ((lowpasspos[h] + 1) == nextread_int)) || + ((lowpasspos[h] == (bsize - 1)) && (nextread_int == 0)); + }(); if (extrasample) { heads[h].filter.processToCacheH1(heads[h].buf[lowpasspos[h]]); - lowpasspos[h] = (lowpasspos[h] + 1) % bsize; + lowpasspos[h] = mod_bipolar(lowpasspos[h] + (1 * direction), bsize); } } // it's simpler for highpassing;