/*------------------------------------------------------------------------ Copyright (C) 2001-2022 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 "dfxmath.h" #include "firfilter.h" using namespace dfx::TV; void TransverbDSP::process(float const* inAudio, float* outAudio, size_t numSampleFrames) { 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 ////////////// // do it proper if (!tomsound) { auto const samplerate = getsamplerate(); auto const speedSmoothingStride = dfx::math::GetFrequencyBasedSmoothingStride(samplerate); // int versions of these float values, for reducing casting operations std::array speed_ints {}; // position trackers for the lowpass filters std::array lowpasspos {}; // the type of filtering to use in ultra hi-fi mode std::array filtermodes {}; filtermodes.fill(FilterMode::None); // make-up gain for lowpass filtering std::array mugs {}; mugs.fill(1.f); for (size_t i = 0; i < numSampleFrames; i++) // samples loop { bool const firstSample = (i == 0); bool const speedSmoothingStrideHit = ((i % speedSmoothingStride) == 0); float delaysum = 0.f; 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) { if (firstSample || (heads[h].speed.isSmoothing() && speedSmoothingStrideHit)) { lowpasspos[h] = read_int; // check to see if we need to lowpass the first delay head and init coefficients if so if (speed > kUnitySpeed) { filtermodes[h] = FilterMode::LowpassIIR; speed_ints[h] = static_cast(speed); // it becomes too costly to try to IIR at higher speeds, so switch to FIR filtering if (speed >= kFIRSpeedThreshold) { filtermodes[h] = FilterMode::LowpassFIR; // compensate for gain lost from filtering 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 / 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 / speed) * dfx::IIRFilter::kShelfStartLowpass); } } // we need to highpass the delay head to remove mega sub bass else { filtermodes[h] = FilterMode::Highpass; if (std::exchange(heads[h].speedHasChanged, false)) { heads[h].filter.setHighpassCoefficients(kHighpassFilterCutoff / speed); } } } } // read from read heads switch (quality) { // no interpolation or filtering case kQualityMode_DirtFi: default: delayvals[h] = heads[h].buf[read_int]; break; // spline interpolation, but no filtering case kQualityMode_HiFi: //delayvals[h] = interpolateLinear(heads[h].buf.data(), heads[h].read, bsize, writer); delayvals[h] = interpolateHermite(heads[h].buf.data(), heads[h].read, bsize, writer); break; // spline interpolation plus anti-aliasing lowpass filtering for high speeds // or sub-bass-removing highpass filtering for low speeds case kQualityMode_UltraHiFi: switch (filtermodes[h]) { case FilterMode::Highpass: case FilterMode::LowpassIIR: // interpolate the values in the IIR output history delayvals[h] = heads[h].filter.interpolateHermitePostFilter(heads[h].read); break; case FilterMode::LowpassFIR: { // get two consecutive FIR output values for linear interpolation auto const lp1 = dfx::FIRFilter::process(std::span(heads[h].buf).subspan(0, bsize), heads[h].firCoefficients, mod_bipolar(read_int - static_cast(kNumFIRTaps), bsize)); auto const lp2 = dfx::FIRFilter::process(std::span(heads[h].buf).subspan(0, bsize), heads[h].firCoefficients, mod_bipolar(read_int - static_cast(kNumFIRTaps) + 1, bsize)); // interpolate output linearly (avoid shit sound) and compensate gain delayvals[h] = interpolateLinear(lp1, lp2, heads[h].read) * mugs[h]; break; } default: delayvals[h] = interpolateHermite(heads[h].buf.data(), heads[h].read, bsize, writer); break; } break; } // end of quality switch // crossfade the last stored smoothing sample with // the current sample if smoothing is in progress if (heads[h].smoothcount > 0) { auto const smoothpos = heads[h].smoothstep * static_cast(heads[h].smoothcount); delayvals[h] = std::lerp(delayvals[h], heads[h].lastdelayval, smoothpos); heads[h].smoothcount--; } // then write into buffer (w/ feedback) if (!freeze) { float const mixlevel = attenuateFeedbackByMixLevel ? heads[h].mix.getValue() : 1.f; heads[h].buf[writer] = inAudio[i] + (delayvals[h] * heads[h].feed.getValue() * mixlevel); } // make output delaysum += delayvals[h] * heads[h].mix.getValue(); // 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 + 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 = (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 / 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 } } // update read heads, wrapping around if they have gone past the end of the buffer 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, // then we probably need to process a few consecutive samples in order // to get the continuous impulse (or whatever you call that), // probably whatever the speed multiplier is, that's how many samples 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] = mod_bipolar(lowpasspos[h] + (1 * direction), bsize); lowpasscount++; break; case 2: heads[h].filter.processToCacheH2(heads[h].buf.data(), lowpasspos[h], 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] = mod_bipolar(lowpasspos[h] + (3 * direction), bsize); lowpasscount += 3; break; default: heads[h].filter.processToCacheH4(heads[h].buf.data(), lowpasspos[h], 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 = [=] { 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] = mod_bipolar(lowpasspos[h] + (1 * direction), bsize); } } // it's simpler for highpassing; // we may not even need to process anything for this sample else if (filtermodes[h] == FilterMode::Highpass) { // only if we've traversed to a new integer sample position if (static_cast(heads[h].read) != read_int) { heads[h].filter.processToCache(heads[h].buf[read_int]); } } heads[h].speedHasChanged |= heads[h].speed.isSmoothing(); } // end of delay heads loop // mix output outAudio[i] = (inAudio[i] * drymix.getValue()) + delaysum; // update write head writer += writerIncrement; // wrap around the write head if it has gone past the end of the buffer writer %= bsize; incrementSmoothedAudioValues(); } // end of samples loop } // end of !TOMSOUND ///////////// T O M S O U N D ////////////// else { // the essense of TOMSOUND comes from the error that Tom made // of putting the channels loop within the samples loop, // rather than the other way around; the result is that the // writer and readers get incremented for each channel on each // sample frame, i.e. doubly incremented, hence the double r/w // incrementing in this single-channel emulation of TOMSOUND constexpr int tomsoundMultiple = 2; constexpr auto tomsoundMultiple_float = static_cast(tomsoundMultiple); // If a speed value is very near but not quite a whole number, // and if the buffer size is an even number of samples, // it is possible for that small variance to accumulate in the // read position until the read head is consistently reading // odd-number buffer samples while the write head is (due to // the even-size buffer size) only writing into even-number // buffer sample position, resulting in reading only silence. // This workaround forces the writer to always wrap to an // odd value of buffer size (rounding down, for bounds safety) // regardless of the actual buffer size. auto const bsizeWriteWrap = bsize - ((bsize % tomsoundMultiple) ? 0 : 1); for(size_t i = 0; i < numSampleFrames; i++) { //for(size_t ch = 0; ch < getnumoutputs(); ch++) { /* read from read heads */ for(size_t h = 0; h < kNumDelays; h++) { /* another characteristic of TOMSOUND is sharing a single buffer across heads */ /* (however it is only viable with the legacy behavior of applying mix to feedback) */ auto& buf = attenuateFeedbackByMixLevel ? heads.front().buf : heads[h].buf; switch(quality) { case kQualityMode_DirtFi: default: delayvals[h] = buf[static_cast(heads[h].read)]; break; case kQualityMode_HiFi: delayvals[h] = interpolateLinear(buf.data(), heads[h].read, bsize); break; case kQualityMode_UltraHiFi: delayvals[h] = dfx::math::InterpolateHermite(buf.data(), heads[h].read, bsize); break; } } /* then write into buffer (w/ feedback) */ if (!freeze) { if(attenuateFeedbackByMixLevel) { auto& buf = heads.front().buf; buf[writer] = inAudio[i]; for(size_t h = 0; h < kNumDelays; h++) { buf[writer] += heads[h].feed.getValue() * heads[h].mix.getValue() * delayvals[h]; } } else { for(size_t h = 0; h < kNumDelays; h++) { heads[h].buf[writer] = inAudio[i] + (heads[h].feed.getValue() * delayvals[h]); } } } /* update rw heads */ writer += writerIncrement * tomsoundMultiple; if (writer >= bsize) writer %= bsizeWriteWrap; for (auto& head : heads) { head.read += head.speed.getValue() * tomsoundMultiple_float; if (head.read >= bsize_float) head.read = fmod_bipolar(head.read, bsize_float); } /* make output */ outAudio[i] = inAudio[i] * drymix.getValue(); for(size_t h = 0; h < kNumDelays; h++) { outAudio[i] += heads[h].mix.getValue() * delayvals[h]; } //} /* end of channels loop */ incrementSmoothedAudioValues(); } /* end of samples loop */ } /* end of TOMSOUND */ }