-
Notifications
You must be signed in to change notification settings - Fork 7
/
transverbprocess.cpp
382 lines (337 loc) · 16.1 KB
/
transverbprocess.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
/*------------------------------------------------------------------------
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 <https://www.gnu.org/licenses/>.
To contact the author, use the contact form at https://destroyfx.org/
------------------------------------------------------------------------*/
#include "transverb.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <numeric>
#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<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 //////////////
// 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<int, kNumDelays> speed_ints {};
// position trackers for the lowpass filters
std::array<int, kNumDelays> lowpasspos {};
// the type of filtering to use in ultra hi-fi mode
std::array<FilterMode, kNumDelays> filtermodes {};
filtermodes.fill(FilterMode::None);
// make-up gain for lowpass filtering
std::array<float, kNumDelays> 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<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)
{
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<int>(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<float>(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<int>(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<int>(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<float>(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<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 = (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 / 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
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<int>(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<int>(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<double>(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<size_t>(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 */
}