/*------------------------------------------------------------------------
Copyright (C) 2001-2023 Tom Murphy 7 and Sophia Poirier
This file is part of Transverb.
Transverb is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
Transverb is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Transverb. If not, see .
To contact the author, use the contact form at http://destroyfx.org
------------------------------------------------------------------------*/
#include "transverb.h"
#include
#include
#include
#include
#include
#include
#include "dfxmisc.h"
#include "firfilter.h"
// these are macros that do boring entry point stuff for us
DFX_EFFECT_ENTRY(Transverb)
DFX_CORE_ENTRY(TransverbDSP)
using namespace dfx::TV;
Transverb::Transverb(TARGET_API_BASE_INSTANCE_TYPE inInstance)
: DfxPlugin(inInstance, kNumParameters, kNumPresets) {
initparameter_f(kBsize, {"buffer size", "BufSize", "BufSiz", "BfSz"}, 2700.0, 333.0, 1.0, 3000.0, DfxParam::Unit::MS);
initparameter_f(kDrymix, {"dry mix", "DryMix", "Dry"}, 1.0, 1.0, 0.0, 1.0, DfxParam::Unit::LinearGain, DfxParam::Curve::Squared);
initparameter_f(kMix1, {"1:mix", "1mix"}, 1.0, 1.0, 0.0, 1.0, DfxParam::Unit::LinearGain, DfxParam::Curve::Squared);
initparameter_f(kDist1, {"1:distance", "1:dist", "1dst"}, 0.90009, 0.5, 0.0, 1.0, DfxParam::Unit::Scalar);
initparameter_f(kSpeed1, {"1:speed", "1speed", "1spd"}, 0.0, 0.0, -3.0, 6.0, DfxParam::Unit::Octaves);
initparameter_f(kFeed1, {"1:feedback", "1feedbk", "1fedbk", "1fdb"}, 0.0, 33.3, 0.0, 100.0, DfxParam::Unit::Percent);
initparameter_f(kMix2, {"2:mix", "2mix"}, 0.0, 1.0, 0.0, 1.0, DfxParam::Unit::LinearGain, DfxParam::Curve::Squared);
initparameter_f(kDist2, {"2:distance", "2:dist", "2dst"}, 0.1, 0.5, 0.0, 1.0, DfxParam::Unit::Scalar);
initparameter_f(kSpeed2, {"2:speed", "2speed", "2spd"}, 1.0, 0.0, -3.0, 6.0, DfxParam::Unit::Octaves);
initparameter_f(kFeed2, {"2:feedback", "2feedbk", "2fedbk", "2fdb"}, 0.0, 33.3, 0.0, 100.0, DfxParam::Unit::Percent);
initparameter_list(kQuality, {"quality", "Qualty", "Qlty"}, kQualityMode_UltraHiFi, kQualityMode_UltraHiFi, kQualityMode_NumModes);
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) {
setparameterenforcevaluelimits(parameterID, true);
}
setparametervaluestring(kQuality, kQualityMode_DirtFi, "dirt-fi");
setparametervaluestring(kQuality, kQualityMode_HiFi, "hi-fi");
setparametervaluestring(kQuality, kQualityMode_UltraHiFi, "ultra hi-fi");
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);
std::vector mixparameters(1, kDrymix);
for (size_t head = 0; head < kNumDelays; head++) {
addparametergroup("head #" + std::to_string(head + 1),
{kSpeedParameters[head], kFeedParameters[head], kDistParameters[head]});
mixparameters.push_back(kMixParameters[head]);
}
addparametergroup("mix", mixparameters);
settailsize_seconds(getparametermax_f(kBsize) * 0.001);
setpresetname(0, PLUGIN_NAME_STRING); // default preset name
initPresets();
addchannelconfig(kChannelConfig_AnyMatchedIO); // N-in/N-out
addchannelconfig(1, kNumDelays); // mono-input / per-head-output
speedModeStates.fill(kSpeedMode_Fine);
}
void Transverb::dfx_PostConstructor() {
#if TARGET_PLUGIN_USES_MIDI
// since we don't use notes for any specialized control of Transverb,
// allow them to be assigned to control parameters via MIDI learn
getsettings().setAllowPitchbendEvents(true);
getsettings().setAllowNoteEvents(true);
#endif
}
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) {
head.buf.assign(MAXBUF, 0.f);
head.filter.setSampleRate(getsamplerate());
registerSmoothedAudioValue(head.speed);
registerSmoothedAudioValue(head.mix);
registerSmoothedAudioValue(head.feed);
}
}
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;
std::ranges::fill(buf, 0.f);
}
void TransverbDSP::processparameters() {
if (auto const value = getparameterifchanged_f(kDrymix))
{
// balance the audio energy when mono input is fanned out to multiple output channels
auto const dryGainScalar = std::sqrt(static_cast(getplugin().getnuminputs()) /
static_cast(getplugin().getnumoutputs()));
drymix = *value * dryGainScalar;
}
for (size_t head = 0; head < kNumDelays; head++)
{
if (auto const value = getparameterifchanged_f(kMixParameters[head]))
{
heads[head].mix = *value;
}
if (auto const value = getparameterifchanged_f(kSpeedParameters[head]))
{
heads[head].speed = std::pow(2., *value);
heads[head].speedHasChanged = true;
}
if (auto const value = getparameterifchanged_scalar(kFeedParameters[head]))
{
heads[head].feed = *value;
}
}
if (auto const value = getparameterifchanged_f(kBsize))
{
auto const entryBsize = std::exchange(bsize, std::clamp(static_cast(*value * getsamplerate() * 0.001), 1, MAXBUF));
// the commented-out logic below is the beginning of an attempt to clean up the buffer states
// when the buffer resizes, however the tidiness maybe feels counter to the spirit of Transverb?
if (bsize > entryBsize)
{
//for (auto& head : heads)
{
//std::fill(std::next(head.buf.begin(), entryBsize), std::next(head.buf.begin(), bsize), 0.f);
}
}
else if (writer > bsize)
{
//auto const entryWriter = writer;
writer %= bsize;
//auto const copyCount = std::min(entryBsize - bsize, writer);
//for (auto& head : heads)
{
//std::copy_n(std::next(head.buf.cbegin(), entryWriter - copyCount), copyCount, std::next(head.buf.begin(), writer - copyCount));
}
}
auto const bsize_f = static_cast(bsize);
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]))
{
processdist(*dist, heads[head]);
}
}
if (getparameterchanged(kQuality) || getparameterchanged(kTomsound))
{
std::ranges::for_each(heads, [](Head& head){ head.speedHasChanged = true; });
}
// stereo-split-heads mode (head 1 goes to left output and 2 to right)
if (getplugin().asymmetricalchannels())
{
static_assert(kNumDelays >= 2);
assert(getplugin().getnumoutputs() == 2);
if (GetChannelNum() == 0)
{
heads[1].mix.setValueNow(getparametermin_f(kMix2));
}
else if (GetChannelNum() == 1)
{
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;
}
}
}
//--------- presets --------
void Transverb::initPresets() {
size_t i = 1;
setpresetname(i, "phaser up");
setpresetparameter_f(i, kBsize, 48.687074827);
setpresetparameter_f(i, kDrymix, 0.45);
setpresetparameter_f(i, kMix1, 0.5);
setpresetparameter_f(i, kDist1, 0.1);
setpresetparameter_f(i, kSpeed1, 0.048406605 / 12.);
setpresetparameter_f(i, kFeed1, 33.5);
setpresetparameter_f(i, kMix2, 0.0);
setpresetparameter_f(i, kDist2, 0.0);
setpresetparameter_f(i, kSpeed2, getparametermin_f(kSpeed2));
setpresetparameter_f(i, kFeed2, 0.0);
setpresetparameter_i(i, kQuality, kQualityMode_UltraHiFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "phaser down");
setpresetparameter_f(i, kBsize, 27.0);
setpresetparameter_f(i, kDrymix, 0.45);
setpresetparameter_f(i, kMix1, 0.5);
setpresetparameter_f(i, kDist1, 0.0);
setpresetparameter_f(i, kSpeed1, -0.12 / 12.);//-0.048542333 / 12.);
setpresetparameter_f(i, kFeed1, 38.);
setpresetparameter_f(i, kMix2, 0.0);
setpresetparameter_f(i, kDist2, 0.0);
setpresetparameter_f(i, kSpeed2, getparametermin_f(kSpeed2));
setpresetparameter_f(i, kFeed2, 0.0);
setpresetparameter_i(i, kQuality, kQualityMode_UltraHiFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "aquinas");
setpresetparameter_f(i, kBsize, 2605.1);
setpresetparameter_f(i, kDrymix, 0.6276);
setpresetparameter_f(i, kMix1, 1.0);
setpresetparameter_f(i, kDist1, 0.007);
setpresetparameter_f(i, kSpeed1, -4.665660556);
setpresetparameter_f(i, kFeed1, 54.0);
setpresetparameter_f(i, kMix2, 0.757);
setpresetparameter_f(i, kDist2, 0.557);
setpresetparameter_f(i, kSpeed2, 2.444534569);
setpresetparameter_f(i, kFeed2, 34.822);
setpresetparameter_i(i, kQuality, kQualityMode_UltraHiFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "glup drums");
setpresetparameter_f(i, kBsize, 184.27356);
setpresetparameter_f(i, kDrymix, 0.157);
setpresetparameter_f(i, kMix1, 1.0);
setpresetparameter_f(i, kDist1, 0.9055);
setpresetparameter_f(i, kSpeed1, -8. / 12.);
setpresetparameter_f(i, kFeed1, 97.6);
setpresetparameter_f(i, kMix2, 0.0);
setpresetparameter_f(i, kDist2, 0.803);
setpresetparameter_f(i, kSpeed2, 0.978195651);
setpresetparameter_f(i, kFeed2, 0.0);
setpresetparameter_i(i, kQuality, kQualityMode_UltraHiFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "space invaders");
setpresetparameter_f(i, kSpeed1, -0.23 / 12.);
setpresetparameter_f(i, kFeed1, 73.0);
setpresetparameter_f(i, kDist1, 0.8143);
setpresetparameter_f(i, kSpeed2, 4.3 / 12.);
setpresetparameter_f(i, kFeed2, 5.0225);
setpresetparameter_f(i, kDist2, 0.4006);
setpresetparameter_f(i, kBsize, 16.8);
setpresetparameter_f(i, kDrymix, 0.000576);
setpresetparameter_f(i, kMix1, 1.0);
setpresetparameter_f(i, kMix2, 0.1225);
setpresetparameter_i(i, kQuality, kQualityMode_UltraHiFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "mudslap (by Styrofoam)");
setpresetparameter_f(i, kSpeed1, -0.9748);
setpresetparameter_f(i, kFeed1, 45.6285);
setpresetparameter_f(i, kDist1, 0.97655);
setpresetparameter_f(i, kSpeed2, -1.0252);
setpresetparameter_f(i, kFeed2, 46.4819);
setpresetparameter_f(i, kDist2, 0.9808);
setpresetparameter_f(i, kBsize, 122.4947);
setpresetparameter_f(i, kDrymix, 0.);
setpresetparameter_f(i, kMix1, 1.);
setpresetparameter_f(i, kMix2, 1.);
setpresetparameter_i(i, kQuality, kQualityMode_DirtFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "subverb (by Styrofoam)");
setpresetparameter_f(i, kSpeed1, -2.9232);
setpresetparameter_f(i, kFeed1, 49.6802);
setpresetparameter_f(i, kDist1, 0.94244);
setpresetparameter_f(i, kSpeed2, -2.90405);
setpresetparameter_f(i, kFeed2, 47.5475);
setpresetparameter_f(i, kDist2, 0.968);
setpresetparameter_f(i, kBsize, 2341.3708);
setpresetparameter_f(i, kDrymix, 0.);
setpresetparameter_f(i, kMix1, 1.);
setpresetparameter_f(i, kMix2, 1.);
setpresetparameter_i(i, kQuality, kQualityMode_UltraHiFi);
setpresetparameter_b(i, kTomsound, true);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "vocoder beat (by Styrofoam)");
setpresetparameter_f(i, kSpeed1, 2.);
setpresetparameter_f(i, kFeed1, 57.3561);
setpresetparameter_f(i, kDist1, 0.9915);
setpresetparameter_f(i, kSpeed2, 1.);
setpresetparameter_f(i, kFeed2, 27.5053);
setpresetparameter_f(i, kDist2, 0.9979);
setpresetparameter_f(i, kBsize, 64.9446);
setpresetparameter_f(i, kDrymix, 0.);
setpresetparameter_f(i, kMix1, 1.);
setpresetparameter_f(i, kMix2, 1.);
setpresetparameter_i(i, kQuality, kQualityMode_DirtFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
setpresetname(i, "yo pitch! (by Styrofoam)");
setpresetparameter_f(i, kSpeed1, -2.);
setpresetparameter_f(i, kFeed1, 80.3769);
setpresetparameter_f(i, kDist1, 0.977638);
setpresetparameter_f(i, kSpeed2, 2.);
setpresetparameter_f(i, kFeed2, 18.937530198);
setpresetparameter_f(i, kDist2, 0.989335);
setpresetparameter_f(i, kBsize, 1938.5203);
setpresetparameter_f(i, kDrymix, 0.);
setpresetparameter_f(i, kMix1, 1.);
setpresetparameter_f(i, kMix2, 0.6483);
setpresetparameter_i(i, kQuality, kQualityMode_UltraHiFi);
setpresetparameter_b(i, kTomsound, false);
setpresetparameter_b(i, kFreeze, false);
i++;
/*
setpresetname(i, "");
setpresetparameter_f(i, kSpeed1, );
setpresetparameter_f(i, kFeed1, );
setpresetparameter_f(i, kDist1, 0.0);
setpresetparameter_f(i, kSpeed2, );
setpresetparameter_f(i, kFeed2, );
setpresetparameter_f(i, kDist2, 0.0);
setpresetparameter_f(i, kBsize, );
setpresetparameter_f(i, kDrymix, 0.);
setpresetparameter_f(i, kMix1, 0.);
setpresetparameter_f(i, kMix2, 0.);
setpresetparameter_i(i, kQuality, );
setpresetparameter_b(i, kTomsound, );
setpresetparameter_b(i, kFreeze, false);
i++;
*/
// special randomizing "preset"
setpresetname(getnumpresets() - 1, "random");
}
//-----------------------------------------------------------------------------
bool Transverb::loadpreset(size_t index)
{
if (!presetisvalid(index))
{
return false;
}
if (getpresetname(index) == "random")
{
randomizeparameters();
return true;
}
return DfxPlugin::loadpreset(index);
}
//--------- presets (end) --------
/* this randomizes the values of all of Transverb's parameters, sometimes in smart ways */
void Transverb::randomizeparameters()
{
// randomize the non-mix-level parameters
for (dfx::ParameterID i = 0; i < kDrymix; i++)
{
if (hasparameterattribute(i, DfxParam::kAttribute_OmitFromRandomizeAll) || hasparameterattribute(i, DfxParam::kAttribute_Unused))
{
continue;
}
// make slow speeds more probable (for fairer distribution)
// TODO C++23: std::ranges::contains
if (std::ranges::find(kSpeedParameters, i) != kSpeedParameters.cend())
{
auto const rand = generateParameterRandomValue(-1., 1.);
auto const range = (rand < 0.) ? getparametermin_f(i) : getparametermax_f(i);
setparameter_f(i, std::fabs(rand) * range);
}
// make smaller buffer sizes more probable (because they sound better), though prevent smallest
else if (i == kBsize)
{
setparameter_gen(kBsize, std::pow(generateParameterRandomValue(0.07, 1.), 1.38));
}
else
{
// TODO: this is not thread-safe (wrt randomizer state) given that Transverb can execute this method
// from the main thread (GUI) or MIDI thread (though likelihood of concurrency is quite low)
randomizeparameter(i);
}
}
// do fancy mix level randomization
// store the current total gain sum
auto const mixSum = getparameter_f(kDrymix) + getparameter_f(kMix1) + getparameter_f(kMix2);
// randomize the mix parameters
auto newDrymix = expandparametervalue(kDrymix, generateParameterRandomValue());
std::array newMix {};
for (size_t head = 0; head < kNumDelays; head++)
{
newMix[head] = expandparametervalue(kMixParameters[head], generateParameterRandomValue());
}
// calculate a scalar to make up for total gain changes
auto const mixDiffScalar = mixSum / (newDrymix + std::accumulate(newMix.cbegin(), newMix.cend(), 0.));
// apply the scalar to the new mix parameter values
newDrymix *= mixDiffScalar;
std::ranges::transform(newMix, newMix.begin(), [mixDiffScalar](double value){ return value * mixDiffScalar; });
// clip the the delay head mix values at unity gain so that we don't get mega-feedback blasts
newDrymix = std::clamp(newDrymix, 0., getparametermax_f(kDrymix));
std::ranges::transform(newMix, newMix.begin(), [](double value){ return std::clamp(value, 0., 1.); });
// set the new randomized mix parameter values as the new values
setparameter_f(kDrymix, newDrymix);
for (size_t head = 0; head < kNumDelays; head++)
{
setparameter_f(kMixParameters[head], newMix[head]);
}
// randomize the state parameters
// make TOMSOUND less probable (only 1/3 of the time)
auto const newTomsound = static_cast(generateParameterRandomValue(0, 2) % 2);
setparameter_b(kTomsound, newTomsound);
if (newTomsound)
{
randomizeparameter(kQuality);
}
else
{
// make higher qualities more probable (happen 4/5 of the time)
setparameter_i(kQuality, generateParameterRandomValue(1, (kQualityMode_NumModes * 2) - 1) % kQualityMode_NumModes);
}
for (dfx::ParameterID i = 0; i < kNumParameters; i++)
{
if (hasparameterattribute(i, DfxParam::kAttribute_OmitFromRandomizeAll) || hasparameterattribute(i, DfxParam::kAttribute_Unused))
{
continue;
}
postupdate_parameter(i); // inform any parameter listeners of the changes
}
}
dfx::StatusCode Transverb::dfx_GetPropertyInfo(dfx::PropertyID inPropertyID, dfx::Scope inScope, unsigned int inItemIndex,
size_t& outDataSize, dfx::PropertyFlags& outFlags)
{
if (isSpeedModePropertyID(inPropertyID))
{
outDataSize = sizeof(uint8_t); // single-byte representation allows for no concerns about transmission endianness
outFlags = dfx::kPropertyFlag_Readable | dfx::kPropertyFlag_Writable;
return dfx::kStatus_NoError;
}
return DfxPlugin::dfx_GetPropertyInfo(inPropertyID, inScope, inItemIndex, outDataSize, outFlags);
}
dfx::StatusCode Transverb::dfx_GetProperty(dfx::PropertyID inPropertyID, dfx::Scope inScope, unsigned int inItemIndex,
void* outData)
{
if (isSpeedModePropertyID(inPropertyID))
{
dfx::MemCpyObject(static_cast(speedModeStateFromPropertyID(inPropertyID)), outData);
return dfx::kStatus_NoError;
}
return DfxPlugin::dfx_GetProperty(inPropertyID, inScope, inItemIndex, outData);
}
dfx::StatusCode Transverb::dfx_SetProperty(dfx::PropertyID inPropertyID, dfx::Scope inScope, unsigned int inItemIndex,
void const* inData, size_t inDataSize)
{
if (isSpeedModePropertyID(inPropertyID))
{
speedModeStateFromPropertyID(inPropertyID) = dfx::Enliven(inData);
dfx_PropertyChanged(inPropertyID, inScope, inItemIndex);
return dfx::kStatus_NoError;
}
return DfxPlugin::dfx_SetProperty(inPropertyID, inScope, inItemIndex, inData, inDataSize);
}
size_t Transverb::settings_sizeOfExtendedData() const noexcept
{
return speedModeStates.size() * sizeof(speedModeStates.front());
}
void Transverb::settings_saveExtendedData(void* outData, bool /*isPreset*/) const
{
auto speedModeStatesSerialization = speedModeStates;
if constexpr (!DfxSettings::serializationIsNativeEndian())
{
dfx::ReverseBytes(speedModeStatesSerialization.data(), speedModeStatesSerialization.size());
}
std::memcpy(outData, speedModeStatesSerialization.data(), settings_sizeOfExtendedData());
}
void Transverb::settings_restoreExtendedData(void const* inData, size_t storedExtendedDataSize,
unsigned int /*dataVersion*/, bool /*isPreset*/)
{
if (storedExtendedDataSize >= settings_sizeOfExtendedData())
{
std::memcpy(speedModeStates.data(), inData, settings_sizeOfExtendedData());
if constexpr (!DfxSettings::serializationIsNativeEndian())
{
dfx::ReverseBytes(speedModeStates.data(), speedModeStates.size());
}
for (size_t i = 0; i < kNumDelays; i++)
{
dfx_PropertyChanged(speedModeIndexToPropertyID(i));
}
}
}
void Transverb::settings_doChunkRestoreSetParameterStuff(dfx::ParameterID parameterID, float value, unsigned int dataVersion, std::optional presetIndex)
{
// prevent old speed mode 1 settings from applying to freeze
if ((dataVersion < 0x00010501) && (parameterID == kFreeze))
{
auto const defaultValue = getparameterdefault_b(parameterID);
if (presetIndex)
{
setpresetparameter_b(*presetIndex, parameterID, defaultValue);
}
else
{
setparameter_b(parameterID, defaultValue);
}
}
if (dataVersion <= 0x00010502)
{
// invert old erroneously reversed dist parameter values
// TODO C++23: std::ranges::contains
if (std::ranges::find(kDistParameters, parameterID) != kDistParameters.cend())
{
auto const valueNormalized = 1. - contractparametervalue(parameterID, value);
if (presetIndex)
{
setpresetparameter_gen(*presetIndex, parameterID, valueNormalized);
}
else
{
setparameter_gen(parameterID, valueNormalized);
}
}
// hi-fi mode used to behave the same as and ultra hi-fi mode in TOMSOUND
if (parameterID == kTomsound) // assumes that TOMSOUND parameter is restored after quality
{
if (presetIndex)
{
if (getpresetparameter_b(*presetIndex, kTomsound) && (getpresetparameter_i(*presetIndex, kQuality) == kQualityMode_HiFi))
{
setpresetparameter_i(*presetIndex, kQuality, kQualityMode_UltraHiFi);
}
}
else
{
if (getparameter_b(kTomsound) && (getparameter_i(kQuality) == kQualityMode_HiFi))
{
setparameter_i(kQuality, kQualityMode_UltraHiFi);
}
}
}
// apply legacy feedback behavior
// https://github.com/sophiapoirier/destroyfx/issues/61
constexpr bool attenuate = true;
if (presetIndex)
{
setpresetparameter_b(*presetIndex, kAttenuateFeedbackByMixLevel, attenuate);
}
else
{
setparameter_b(kAttenuateFeedbackByMixLevel, attenuate);
}
}
}
#pragma mark -
dfx::PropertyID dfx::TV::speedModeIndexToPropertyID(size_t inIndex) noexcept
{
return kTransverbProperty_SpeedModeBase + static_cast(inIndex);
}
size_t dfx::TV::speedModePropertyIDToIndex(dfx::PropertyID inPropertyID) noexcept
{
assert(inPropertyID >= kTransverbProperty_SpeedModeBase);
return inPropertyID - kTransverbProperty_SpeedModeBase;
}
bool dfx::TV::isSpeedModePropertyID(dfx::PropertyID inPropertyID) noexcept
{
return (inPropertyID >= kTransverbProperty_SpeedModeBase) && (speedModePropertyIDToIndex(inPropertyID) < kNumDelays);
}