Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transverb: adapt to "distance" changes without crackly glitches #63

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Transverb: various approaches to adapt to "distance" changes without …
…crackly glitches
  • Loading branch information
sophiapoirier committed Jul 15, 2023
commit 6421b556ddeef31f14e4531d09f6e9d0dcdaf6aa
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 <span>
#include <vector>

Expand Down Expand Up @@ -59,6 +60,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 @@ -85,17 +88,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 @@ -118,6 +127,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) const override;
Expand All @@ -134,6 +147,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::ranges::for_each(heads, [](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::ranges::for_each(heads, [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(std::span<float const> inAudio, std::span<float> outAudio) {

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(std::span<float const> inAudio, std::span<float> outA
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(std::span<float const> inAudio, std::span<float> outA
{
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(std::span<float const> inAudio, std::span<float> outA
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(std::span<float const> inAudio, std::span<float> outA
// 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 = std::fmod(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(std::span<float const> inAudio, std::span<float> outA
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(std::span(heads[h].buf).subspan(0, bsize), dfx::math::ToUnsigned(lowpasspos[h]));
lowpasspos[h] = (lowpasspos[h] + 2) % bsize;
lowpasspos[h] = mod_bipolar(lowpasspos[h] + (2 * direction), bsize);
lowpasscount += 2;
break;
case 3:
heads[h].filter.processToCacheH3(std::span(heads[h].buf).subspan(0, bsize), dfx::math::ToUnsigned(lowpasspos[h]));
lowpasspos[h] = (lowpasspos[h] + 3) % bsize;
lowpasspos[h] = mod_bipolar(lowpasspos[h] + (3 * direction), bsize);
lowpasscount += 3;
break;
default:
heads[h].filter.processToCacheH4(std::span(heads[h].buf).subspan(0, bsize), dfx::math::ToUnsigned(lowpasspos[h]));
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