Skip to content

Commit

Permalink
Transverb: various approaches to adapt to "distance" changes without …
Browse files Browse the repository at this point in the history
…crackly glitches
  • Loading branch information
sophiapoirier committed Oct 3, 2022
1 parent 928e674 commit fe57c7f
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 31 deletions.
3 changes: 3 additions & 0 deletions transverb/transverb-base.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ enum : dfx::ParameterID
kTomsound,
kFreeze,
kAttenuateFeedbackByMixLevel,
kDistChangeMode,

kNumParameters
};
Expand All @@ -66,6 +67,8 @@ enum { kQualityMode_DirtFi, kQualityMode_HiFi, kQualityMode_UltraHiFi, kQualityM
enum : unsigned int { 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;
Expand Down
18 changes: 16 additions & 2 deletions transverb/transverb.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ To contact the author, use the contact form at http:https://destroyfx.org
#include <cmath>
#include <cstdint>
#include <numeric>
#include <optional>
#include <vector>

#include "dfxplugin.h"
Expand Down Expand Up @@ -57,6 +58,8 @@ class TransverbDSP final : public DfxPluginCore {
dfx::SmoothedValue<float> mix, feed;

double read = 0.;
std::optional<double> targetdist;
double distspeedfactor = 1.;
std::vector<float> buf;

dfx::IIRFilter filter;
Expand All @@ -83,17 +86,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<float> drymix;
long quality = 0;
bool tomsound = false;
int distchangemode {};

int writer = 0;
std::array<Head, dfx::TV::kNumDelays> heads;

int const MAXBUF; // the size of the audio buffer (dependent on sampling rate)

bool firstrendersincereset = false;
std::vector<float>& buftemp;

std::vector<float> const firCoefficientsWindow;
};

Expand All @@ -116,6 +125,10 @@ class Transverb final : public DfxPlugin {
dfx::StatusCode dfx_SetProperty(dfx::PropertyID inPropertyID, dfx::Scope inScope, unsigned int 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;
Expand All @@ -132,6 +145,7 @@ class Transverb final : public DfxPlugin {
}

std::array<uint32_t, dfx::TV::kNumDelays> speedModeStates {};
std::vector<float> buftemp; // shared between all DSP cores (memory optimization)
};


Expand Down
81 changes: 73 additions & 8 deletions transverb/transverbformalities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
for (auto const parameterID : kDistParameters) {
Expand All @@ -67,10 +68,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);

Expand Down Expand Up @@ -109,8 +112,11 @@ void Transverb::dfx_PostConstructor() {
TransverbDSP::TransverbDSP(DfxPlugin* inDfxPlugin)
: DfxPluginCore(inDfxPlugin),
MAXBUF(static_cast<int>(getparametermax_f(kBsize) * 0.001 * getsamplerate())),
buftemp(dynamic_cast<Transverb*>(inDfxPlugin)->getscratchbuffer()),
firCoefficientsWindow(dfx::FIRFilter::generateKaiserWindow(kNumFIRTaps, 60.0f)) {

buftemp.assign(MAXBUF, 0.f);

registerSmoothedAudioValue(&drymix);

for (auto& head : heads) {
Expand All @@ -126,12 +132,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;

Expand Down Expand Up @@ -164,8 +173,6 @@ void TransverbDSP::processparameters() {
heads[head].feed = *value;
}
}
quality = getparameter_i(kQuality);
tomsound = getparameter_b(kTomsound);

if (auto const value = getparameterifchanged_f(kBsize))
{
Expand Down Expand Up @@ -193,12 +200,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<double>(bsize);
heads[head].read = fmod_bipolar(static_cast<double>(writer) - (*dist * bsize_f), bsize_f);
processdist(*dist, heads[head]);
}
}

Expand All @@ -221,6 +228,64 @@ void TransverbDSP::processparameters() {
heads[0].mix.setValueNow(getparametermin_f(kMix1));
}
}

firstrendersincereset = false;
}

double TransverbDSP::getdist(double read) const {

return fmod_bipolar(static_cast<double>(writer) - read, static_cast<double>(bsize));
}

void TransverbDSP::processdist(double distnormalized, Head& head) {

auto const bsize_f = static_cast<double>(bsize);
auto const distsamples = distnormalized * bsize_f;
auto const targetread = fmod_bipolar(static_cast<double>(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<size_t>(std::lround(copylength_f));
assert(copylength <= buftemp.size());
assert(static_cast<int>(copylength) <= bsize);
auto const sourcestart = subslice ? head.read : fmod_bipolar(static_cast<double>(writer) - (copylength_f * resamplerate), bsize_f);
for (size_t i = 0; i < copylength; i++)
{
auto const sourcepos = std::fmod(sourcestart + (resamplerate * static_cast<double>(i)), bsize_f);
buftemp[i] = interpolateHermite(head.buf.data(), sourcepos, bsize, writer);
}
auto const destinationstart = subslice ? std::lround(targetread) : mod_bipolar(writer - static_cast<int>(copylength), bsize);
auto const copylength1 = std::min(static_cast<size_t>(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;
}
}
}


Expand Down
66 changes: 45 additions & 21 deletions transverb/transverbprocess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ using namespace dfx::TV;

void TransverbDSP::process(float const* inAudio, float* outAudio, size_t numSampleFrames) {

std::array<float, kNumDelays> delayvals {}; // delay buffer output values
auto const bsize_float = static_cast<double>(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<float, kNumDelays> delayvals {}; // delay buffer output values


///////////// S O P H I A S O U N D //////////////
Expand Down Expand Up @@ -69,6 +71,9 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, size_t numSamp
for (size_t h = 0; h < kNumDelays; h++) // delay heads loop
{
auto const read_int = static_cast<int>(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)
Expand All @@ -77,27 +82,27 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, size_t numSamp
{
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<int>(heads[h].speed.getValue());
speed_ints[h] = static_cast<int>(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<float>(std::pow(heads[h].speed.getValue() / kFIRSpeedThreshold, 0.78));
mugs[h] = static_cast<float>(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, heads[h].firCoefficients, firCoefficientsWindow);
heads[h].filter.reset();
}
}
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
Expand All @@ -106,7 +111,7 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, size_t numSamp
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);
}
}
}
Expand Down Expand Up @@ -173,28 +178,40 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, size_t numSamp
// 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<int>(heads[h].read + heads[h].speed.getValue());
auto const nextRead = static_cast<int>(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<int>(bsize_float / heads[h].speed.getValue());
auto const bufferReadSteps = static_cast<int>(bsize_float / speed);
auto const smoothdur = std::min(bufferReadSteps, kAudioSmoothingDur_samples);
heads[h].smoothstep = 1.f / static_cast<float>(smoothdur); // the scalar step value
heads[h].smoothcount = smoothdur; // set the counter to the total duration
}
}

// 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,
Expand All @@ -204,40 +221,47 @@ void TransverbDSP::process(float const* inAudio, float* outAudio, size_t numSamp
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(), dfx::math::ToUnsigned(lowpasspos[h]), dfx::math::ToUnsigned(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(), dfx::math::ToUnsigned(lowpasspos[h]), dfx::math::ToUnsigned(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(), dfx::math::ToUnsigned(lowpasspos[h]), dfx::math::ToUnsigned(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<int>(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;
Expand Down

0 comments on commit fe57c7f

Please sign in to comment.