├── .clang-format ├── FastMath.hpp ├── LICENSE ├── PlateReverb.hpp ├── README.md └── SvfInputMixing.hpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | 3 | IndentWidth: 4 4 | PointerAlignment: Left 5 | 6 | EmptyLineAfterAccessModifier: Always 7 | 8 | AlignAfterOpenBracket: AlwaysBreak 9 | PenaltyReturnTypeOnItsOwnLine: 9999 10 | -------------------------------------------------------------------------------- /FastMath.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Mike Jarmy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | 29 | //------------------------------------------------------------------------------ 30 | // FastMath contains some fast approximations for trigonometric functions. 31 | //------------------------------------------------------------------------------ 32 | 33 | template class FastMath { 34 | 35 | public: 36 | 37 | // Sine approximation. Range is [-pi, pi]. 38 | // 39 | // https://web.archive.org/web/20100613230051/http://www.devmaster.net/forums/showthread.php?t=5784 40 | // https://www.desmos.com/calculator/f0eryaepsl 41 | static inline F fastSin(F x) { 42 | static constexpr F B = 4 / M_PI; 43 | static constexpr F C = -4 / (M_PI * M_PI); 44 | static constexpr F P = 0.225; 45 | 46 | F y = B * x + C * x * std::abs(x); 47 | 48 | // Extra precision. 49 | y = P * (y * std::abs(y) - y) + y; 50 | 51 | return y; 52 | } 53 | 54 | // Hyperbolic tangent approximation. 55 | // 56 | // https://www.kvraudio.com/forum/viewtopic.php?p=5447225#p5447225 57 | // https://www.desmos.com/calculator/bjc7zsl4ek 58 | static inline F fastTanh(const F x) { 59 | const F ax = std::abs(x); 60 | const F x2 = x * x; 61 | const F z = 62 | x * (0.773062670268356 + ax + 63 | (0.757118539838817 + 0.0139332362248817 * x2 * x2) * x2 * ax); 64 | 65 | return z / (0.795956503022967 + std::abs(z)); 66 | } 67 | 68 | private: 69 | 70 | FastMath() {} 71 | ~FastMath() {} 72 | 73 | FastMath(const FastMath&) = delete; 74 | FastMath& operator=(const FastMath&) = delete; 75 | }; 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mike Jarmy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PlateReverb.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Mike Jarmy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | #include "FastMath.hpp" 32 | 33 | //------------------------------------------------------------------------------ 34 | // PlateReverb is an implementation of the classic plate reverb algorithm 35 | // described by Jon Dattorro. 36 | // 37 | // Dattorro, J. 1997. "Effect Design Part 1: Reverberators and Other Filters." 38 | // Journal of the Audio Engineering Society, Vol. 45, No. 9 39 | // 40 | // https://ccrma.stanford.edu/~dattorro/EffectDesignPart1.pdf 41 | // 42 | // Parameters: 43 | // 44 | // mix: Dry/wet mix. 45 | // predelay: Delay before reverb. 46 | // lowpass: Apply a lowpass filter before reverb. 47 | // decay: How quickly the reverb decays. 48 | // size: The size of our imaginary plate. 49 | // damping: How much high frequencies are filtered during reverb. 50 | // 51 | //------------------------------------------------------------------------------ 52 | 53 | template class PlateReverb { 54 | 55 | public: 56 | 57 | static constexpr F kMaxPredelay = 0.1; // seconds 58 | static constexpr F kMaxSize = 2.0; 59 | 60 | PlateReverb() {} 61 | ~PlateReverb() {} 62 | 63 | // Set the sample rate. Note that we are re-mallocing all of the various 64 | // delay lines here. 65 | void setSampleRate(F sampleRate_) { 66 | sampleRate = sampleRate_; 67 | 68 | // Ratio of our sample rate to the sample rate that is used in 69 | // Dattorro's paper. 70 | F r = sampleRate / 29761.0; 71 | 72 | // Predelay 73 | predelayLine.reset(new DelayLine(std::ceil(sampleRate * kMaxPredelay))); 74 | 75 | // Lowpass filters 76 | lowpass.setSampleRate(sampleRate); 77 | leftTank.damping.setSampleRate(sampleRate); 78 | rightTank.damping.setSampleRate(sampleRate); 79 | 80 | // Diffusers 81 | diffusers[0].reset(new DelayAllpass(std::ceil(142 * r), 0.75)); 82 | diffusers[1].reset(new DelayAllpass(std::ceil(107 * r), 0.75)); 83 | diffusers[2].reset(new DelayAllpass(std::ceil(379 * r), 0.625)); 84 | diffusers[3].reset(new DelayAllpass(std::ceil(277 * r), 0.625)); 85 | 86 | // Tanks 87 | F maxModDepth = 8.0 * kMaxSize * r; 88 | leftTank.resetDelayLines( 89 | std::ceil(kMaxSize * 672 * r), -0.7, // apf1 90 | maxModDepth, 91 | std::ceil(kMaxSize * 4453 * r), // del1 92 | std::ceil(kMaxSize * 1800 * r), 0.5, // apf2 93 | std::ceil(kMaxSize * 3720 * r) // del2 94 | ); 95 | rightTank.resetDelayLines( 96 | std::ceil(kMaxSize * 908 * r), -0.7, // apf1 97 | maxModDepth, 98 | std::ceil(kMaxSize * 4217 * r), // del1 99 | std::ceil(kMaxSize * 2656 * r), 0.5, // apf2 100 | std::ceil(kMaxSize * 3163 * r) // del2 101 | ); 102 | 103 | leftTank.lfo.setSampleRate(sampleRate); 104 | rightTank.lfo.setSampleRate(sampleRate); 105 | leftTank.lfo.setFrequency(1.0); 106 | rightTank.lfo.setFrequency(0.95); 107 | 108 | // Tap points 109 | baseLeftTaps = { 110 | 266 * r, // rightTank.del1 111 | 2974 * r, // rightTank.del1 112 | 1913 * r, // rightTank.apf2 113 | 1996 * r, // rightTank.del2 114 | 1990 * r, // leftTank.del1 115 | 187 * r, // leftTank.apf2 116 | 1066 * r, // leftTank.del2 117 | }; 118 | baseRightTaps = { 119 | 353 * r, // leftTank.del1 120 | 3627 * r, // leftTank.del1 121 | 1228 * r, // leftTank.apf2 122 | 2673 * r, // leftTank.del2 123 | 2111 * r, // rightTank.del1 124 | 335 * r, // rightTank.apf2 125 | 121 * r, // rightTank.del2 126 | }; 127 | } 128 | 129 | // Dry/wet mix. 130 | void setMix(F m /* [0, 1] */) { mix = clamp(m, 0.0, 1.0); } 131 | 132 | // Delay before reverb. 133 | void setPredelay(F pd /* in seconds, [0, 0.1] */) { 134 | predelay = clamp(pd, 0.0, kMaxPredelay) * sampleRate; 135 | } 136 | 137 | // Apply a lowpass filter before reverb. 138 | void setLowpass(F cutoff /* Hz */) { 139 | cutoff = clamp(cutoff, 16.0, 20000.0); 140 | lowpass.setCutoff(cutoff); 141 | } 142 | 143 | // How quickly the reverb decays. 144 | void setDecay(F dr /* [0, 1) */) { 145 | decayRate = clamp(dr, 0.0, 0.9999999); 146 | leftTank.setDecay(decayRate); 147 | rightTank.setDecay(decayRate); 148 | } 149 | 150 | // The size of our imaginary plate. 151 | // 152 | // The size parameter scales the delay time for all of the delay lines and 153 | // APFs in each tank, and for all of the tap points. 154 | // 155 | // Note that there is no size parameter in Dattorro's paper; it is an 156 | // extension to the original algorithm. 157 | void setSize(F sz /* [0, 2] */) { 158 | 159 | F sizeRatio = clamp(sz, 0.0, kMaxSize) / kMaxSize; 160 | 161 | // Scale the tank delays and APFs in each tank 162 | leftTank.setSizeRatio(sizeRatio); 163 | rightTank.setSizeRatio(sizeRatio); 164 | 165 | // Scale the taps 166 | for (I i = 0; i < kNumTaps; i++) { 167 | leftTaps[i] = baseLeftTaps[i] * sizeRatio; 168 | rightTaps[i] = baseRightTaps[i] * sizeRatio; 169 | } 170 | } 171 | 172 | // How much high frequencies are filtered during reverb. 173 | void setDamping(F cutoff /* Hz */) { 174 | cutoff = clamp(cutoff, 16.0, 20000.0); 175 | 176 | leftTank.damping.setCutoff(cutoff); 177 | rightTank.damping.setCutoff(cutoff); 178 | } 179 | 180 | // Process a stereo pair of samples. 181 | void process(F dryLeft, F dryRight, F* leftOut, F* rightOut) { 182 | 183 | // Note that this is "synthetic stereo". We produce a stereo pair 184 | // of output samples based on the summed input. 185 | F sum = dryLeft + dryRight; 186 | 187 | // Predelay 188 | sum = predelayLine->tapAndPush(predelay, sum); 189 | 190 | // Input lowpass 191 | sum = lowpass.process(sum); 192 | 193 | // Diffusers 194 | sum = diffusers[0]->process(sum, diffusers[0]->getSize()); 195 | sum = diffusers[1]->process(sum, diffusers[1]->getSize()); 196 | sum = diffusers[2]->process(sum, diffusers[2]->getSize()); 197 | sum = diffusers[3]->process(sum, diffusers[3]->getSize()); 198 | 199 | // Tanks 200 | F leftIn = sum + rightTank.out * decayRate; 201 | F rightIn = sum + leftTank.out * decayRate; 202 | leftTank.process(leftIn); 203 | rightTank.process(rightIn); 204 | 205 | // Tap for output 206 | F wetLeft = rightTank.del1->tap(leftTaps[0]) // 266 207 | + rightTank.del1->tap(leftTaps[1]) // 2974 208 | - rightTank.apf2->tap(leftTaps[2]) // 1913 209 | + rightTank.del2->tap(leftTaps[3]) // 1996 210 | - leftTank.del1->tap(leftTaps[4]) // 1990 211 | - leftTank.apf2->tap(leftTaps[5]) // 187 212 | - leftTank.del2->tap(leftTaps[6]); // 1066 213 | 214 | F wetRight = leftTank.del1->tap(rightTaps[0]) // 353 215 | + leftTank.del1->tap(rightTaps[1]) // 3627 216 | - leftTank.apf2->tap(rightTaps[2]) // 1228 217 | + leftTank.del2->tap(rightTaps[3]) // 2673 218 | - rightTank.del1->tap(rightTaps[4]) // 2111 219 | - rightTank.apf2->tap(rightTaps[5]) // 335 220 | - rightTank.del2->tap(rightTaps[6]); // 121 221 | 222 | // Mix 223 | *leftOut = dryLeft * (1 - mix) + wetLeft * mix; 224 | *rightOut = dryRight * (1 - mix) + wetRight * mix; 225 | } 226 | 227 | private: 228 | 229 | //-------------------------------------------------------------- 230 | // OnePoleFilter 231 | //-------------------------------------------------------------- 232 | 233 | class OnePoleFilter { 234 | 235 | public: 236 | 237 | OnePoleFilter() {} 238 | ~OnePoleFilter() {} 239 | 240 | void setSampleRate(F sampleRate_) { 241 | sampleRate = sampleRate_; 242 | recalc(); 243 | } 244 | 245 | void setCutoff(F cutoff_ /* Hz */) { 246 | cutoff = cutoff_; 247 | recalc(); 248 | } 249 | 250 | F process(F x) { 251 | z = x * a + z * b; 252 | return z; 253 | } 254 | 255 | private: 256 | 257 | F sampleRate = 1; 258 | F cutoff = 0; 259 | 260 | F a = 0; 261 | F b = 0; 262 | F z = 0; 263 | 264 | void recalc() { 265 | b = std::exp(-2 * M_PI * cutoff / sampleRate); 266 | a = 1 - b; 267 | } 268 | }; 269 | 270 | //-------------------------------------------------------------- 271 | // DelayLine 272 | //-------------------------------------------------------------- 273 | 274 | class DelayLine { 275 | 276 | public: 277 | 278 | DelayLine(I size_) : size(size_) { 279 | 280 | // For speed, create a bigger buffer than we really need. 281 | I bufferSize = ceilPowerOfTwo(size); 282 | buffer.reset(new F[bufferSize]); 283 | std::memset(&buffer[0], 0, bufferSize * sizeof(F)); 284 | 285 | mask = bufferSize - 1; 286 | 287 | writeIdx = 0; 288 | } 289 | 290 | ~DelayLine() {} 291 | 292 | inline void push(F val) { 293 | buffer[writeIdx++] = val; 294 | writeIdx &= mask; 295 | } 296 | 297 | inline F tap(F delay /* samples */) { 298 | // We always want to be able to properly handle any delay value that 299 | // gets passed in here, without going past the original size. 300 | assert(delay <= size); 301 | 302 | I d = delay; 303 | F frac = 1 - (delay - d); 304 | 305 | I readIdx = (writeIdx - 1) - d; 306 | F a = buffer[(readIdx - 1) & mask]; 307 | F b = buffer[readIdx & mask]; 308 | 309 | return a + (b - a) * frac; 310 | } 311 | 312 | // This does read-before-write. 313 | inline F tapAndPush(F delay, F val) { 314 | F out = tap(delay); 315 | push(val); 316 | return out; 317 | } 318 | 319 | inline I getSize() { return size; } 320 | 321 | private: 322 | 323 | const I size; 324 | 325 | std::unique_ptr buffer; 326 | I mask; 327 | 328 | I writeIdx; 329 | 330 | static I ceilPowerOfTwo(I n) { 331 | return (I)std::pow(2, std::ceil(std::log(n) / std::log(2))); 332 | } 333 | }; 334 | 335 | //------------------------------------------ 336 | // DelayAllpass 337 | //------------------------------------------ 338 | 339 | class DelayAllpass { 340 | 341 | public: 342 | 343 | DelayAllpass(I size_, F gain_) : delayLine(size_), gain(gain_) {} 344 | 345 | ~DelayAllpass() {} 346 | 347 | inline F process(F x, F delay) { 348 | F wd = delayLine.tap(delay); 349 | F w = x + gain * wd; 350 | F y = -gain * w + wd; 351 | delayLine.push(w); 352 | return y; 353 | } 354 | 355 | inline void setGain(F gain_) { gain = gain_; } 356 | 357 | inline F tap(F delay) { return delayLine.tap(delay); } 358 | 359 | inline I getSize() { return delayLine.getSize(); } 360 | 361 | private: 362 | 363 | DelayLine delayLine; 364 | F gain; 365 | }; 366 | 367 | //-------------------------------------------------------------- 368 | // Lfo 369 | //-------------------------------------------------------------- 370 | 371 | class Lfo { 372 | 373 | public: 374 | 375 | Lfo() {} 376 | ~Lfo() {} 377 | 378 | void setSampleRate(F sampleRate_) { 379 | sampleRate = sampleRate_; 380 | recalc(); 381 | } 382 | 383 | void setFrequency(F freq_) { 384 | freq = freq_; 385 | recalc(); 386 | } 387 | 388 | inline F process() { 389 | F out = -FastMath::fastSin(phase); 390 | 391 | phase += phaseInc; 392 | if (phase > M_PI) { 393 | phase = -M_PI; 394 | } 395 | 396 | return out; 397 | } 398 | 399 | private: 400 | 401 | F sampleRate = 1; 402 | F freq = 0; 403 | 404 | F phaseInc = 0; 405 | F phase = -M_PI; 406 | 407 | void recalc() { 408 | phaseInc = freq / sampleRate; 409 | phaseInc *= 2 * M_PI; 410 | } 411 | }; 412 | 413 | //------------------------------------------ 414 | // Tank 415 | //------------------------------------------ 416 | 417 | class Tank { 418 | 419 | public: 420 | 421 | Tank() {} 422 | ~Tank() {} 423 | 424 | void resetDelayLines( 425 | I apf1Size_, F apf1Gain_, // First APF 426 | F maxModDepth_, 427 | I delay1Size_, // First delay 428 | I apf2Size_, F apf2Gain_, // Second APF 429 | I delay2Size_ // Second delay 430 | ) { 431 | apf1Size = apf1Size_; 432 | maxModDepth = maxModDepth_; 433 | F maxApf1Size = apf1Size + maxModDepth + 1; 434 | apf1.reset(new DelayAllpass(maxApf1Size, apf1Gain_)); 435 | 436 | del1.reset(new DelayLine(delay1Size_)); 437 | apf2.reset(new DelayAllpass(apf2Size_, apf2Gain_)); 438 | del2.reset(new DelayLine(delay2Size_)); 439 | 440 | // We've changed the various delay line sizes and associated values, 441 | // so update the sizeRatio values too. 442 | recalcSizeRatio(); 443 | } 444 | 445 | void setDecay(F decayRate_) { 446 | decayRate = decayRate_; 447 | apf2->setGain(clamp(decayRate + 0.15, 0.25, 0.5)); 448 | } 449 | 450 | void setSizeRatio(F sizeRatio_) { 451 | sizeRatio = sizeRatio_; 452 | recalcSizeRatio(); 453 | } 454 | 455 | void process(F val) { 456 | 457 | // APF1: "Controls density of tail." 458 | val = apf1->process(val, apf1Delay + lfo.process() * modDepth); 459 | val = del1->tapAndPush(del1Delay, val); 460 | 461 | val = damping.process(val); 462 | val *= decayRate; 463 | 464 | // APF2: "Decorrelates tank signals." 465 | val = apf2->process(val, apf2Delay); 466 | val = del2->tapAndPush(del2Delay, val); 467 | 468 | out = val; 469 | } 470 | 471 | F out = 0.0; 472 | 473 | std::unique_ptr apf1 = nullptr; 474 | std::unique_ptr apf2 = nullptr; 475 | std::unique_ptr del1 = nullptr; 476 | std::unique_ptr del2 = nullptr; 477 | OnePoleFilter damping; 478 | Lfo lfo; 479 | 480 | private: 481 | 482 | I apf1Size = 0; 483 | F maxModDepth = 0; 484 | F modDepth = 0; 485 | 486 | F apf1Delay = 0; 487 | F apf2Delay = 0; 488 | F del1Delay = 0; 489 | F del2Delay = 0; 490 | 491 | F decayRate = 0; 492 | F sizeRatio = 0; 493 | 494 | void recalcSizeRatio() { 495 | 496 | apf1Delay = apf1Size * sizeRatio; 497 | modDepth = maxModDepth * sizeRatio; 498 | 499 | apf2Delay = apf2->getSize() * sizeRatio; 500 | del1Delay = del1->getSize() * sizeRatio; 501 | del2Delay = del2->getSize() * sizeRatio; 502 | } 503 | }; 504 | 505 | //-------------------------------------------------------------- 506 | //-------------------------------------------------------------- 507 | //-------------------------------------------------------------- 508 | 509 | F sampleRate = 1.0; 510 | 511 | F mix = 0.0; 512 | F predelay = 0.0; 513 | F decayRate = 0.0; 514 | 515 | std::unique_ptr predelayLine = nullptr; 516 | OnePoleFilter lowpass; 517 | std::array, 4> diffusers = { 518 | nullptr, nullptr, nullptr, nullptr}; 519 | 520 | Tank leftTank; 521 | Tank rightTank; 522 | 523 | static const I kNumTaps = 7; 524 | std::array baseLeftTaps = {}; 525 | std::array baseRightTaps = {}; 526 | std::array leftTaps = {}; 527 | std::array rightTaps = {}; 528 | 529 | static inline F clamp(F val, F low, F high) { 530 | return std::min(std::max(val, low), high); 531 | } 532 | }; 533 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dsp-lib 2 | 3 | `dsp-lib` is a collection of utilities related to audio DSP 4 | 5 | ### PlateReverb 6 | 7 | `PlateReverb` is an implementation of the classic plate reverb algorithm 8 | described by Jon Dattorro. 9 | 10 | Dattorro, J. 1997. ["Effect Design Part 1: Reverberators and Other Filters."](https://ccrma.stanford.edu/~dattorro/EffectDesignPart1.pdf) 11 | Journal of the Audio Engineering Society, Vol. 45, No. 9 12 | 13 | ### SvfInputMixing 14 | 15 | `SvfInputMixing` is a [State Variable Filter](https://en.wikipedia.org/wiki/State_variable_filter) 16 | that provides a configurable mix of lowpass, bandpass and highpass filtering. It 17 | can also be used as a peaking filter or notch filter. 18 | 19 | `SvfInputMixing` is a C++ port of the algorithm described in the [paper](https://cytomic.com/files/dsp/SvfInputMixing.pdf) 20 | "Input mixing linear trapezoidal State Variable Filter (SVF) in state increment 21 | form", by Andy Simper of Cytomic. 22 | 23 | ### FastMath 24 | 25 | `FastMath` contains some fast approximations for trigonometric functions. 26 | -------------------------------------------------------------------------------- /SvfInputMixing.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023 Mike Jarmy 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | #pragma once 26 | 27 | #include 28 | 29 | //------------------------------------------------------------------------------ 30 | // 31 | // SvfInputMixing is a State Variable Filter that provides a configurable mix of 32 | // lowpass, bandpass and highpass filtering. It can also be used as a peaking 33 | // filter or notch filter. 34 | // 35 | // SvfInputMixing is a C++ port of the algorithm described in the paper "Input 36 | // mixing linear trapezoidal State Variable Filter (SVF) in state increment 37 | // form", by Andy Simper of Cytomic. 38 | // 39 | // https://en.wikipedia.org/wiki/State_variable_filter 40 | // https://cytomic.com/files/dsp/SvfInputMixing.pdf 41 | // 42 | //------------------------------------------------------------------------------ 43 | 44 | template class SvfInputMixing { 45 | 46 | public: 47 | 48 | SvfInputMixing() {} 49 | 50 | ~SvfInputMixing() {} 51 | 52 | // Change the cutoff frequency, resonance, and/or sample rate. 53 | // 54 | // cutoff: cutoff frequency -- In Hz. The range is [16.0, sampleRate/2.0]. 55 | // res: resonance, aka Q -- The range is [0.0, 0.999]. 56 | // sampleRate -- samples per second, e.g. 44100. 57 | // 58 | void init(F cutoff, F res, F sampleRate) { 59 | 60 | F k = 2 - 2 * res; 61 | F w = M_PI * cutoff / sampleRate; 62 | F s1 = std::sin(w); 63 | F s2 = std::sin(2 * w); 64 | F nrm = 1 / (2 + (2 - k) * s2); 65 | F s1n = 2 * s1 * s1 * nrm; 66 | F s2n = s2 * nrm; 67 | 68 | g0m0 = k * s1n + s2n; 69 | g0m1 = -s1n; 70 | g0m2 = -s2n; 71 | g1 = -s1n; 72 | g2 = -s2n; 73 | g3m0 = s1n; 74 | g3m1 = s2n; 75 | g3m2 = -s1n - k * s2n; 76 | g4 = s2n; 77 | g5 = -s1n - k * s2n; 78 | } 79 | 80 | // Change the output mixture of lowpass, bandpass and highpass 81 | // filtering. 82 | // 83 | // For many use cases, the sum of the three parameters should be 1.0, e.g.: 84 | // 85 | // lowpass only: [1, 0, 0] 86 | // bandpass only: [0, 1, 0] 87 | // highpass only: [0, 0, 1] 88 | // even mix of lowpass and highpass: [0.5, 0.0, 0.5] 89 | // mostly lowpass plus a little bandpass: [0.9, 0.1, 0] 90 | // etc, etc. 91 | // 92 | // You can also create a notch or peaking filter: 93 | // 94 | // notch: [1, 0, 1] 95 | // peaking: [-1, 0, 1] 96 | // 97 | void setMix(F lowMix_, F bandMix_, F highMix_) { 98 | lowMix = lowMix_; 99 | bandMix = bandMix_; 100 | highMix = highMix_; 101 | } 102 | 103 | // Reset the filter. 104 | void clear() { 105 | ic1eq = 0.0; 106 | ic2eq = 0.0; 107 | } 108 | 109 | // Filter one sample based on the current settings. 110 | F tick(F input) { 111 | 112 | F vlow = lowMix * input; 113 | F vband = bandMix * input; 114 | F vhigh = highMix * input; 115 | 116 | F t1 = 117 | vlow * g0m0 + vband * g0m1 + vhigh * g0m2 + g1 * ic1eq + g2 * ic2eq; 118 | F t2 = 119 | vlow * g3m0 + vband * g3m1 + vhigh * g3m2 + g4 * ic1eq + g5 * ic2eq; 120 | F vc2 = t2 + ic2eq; 121 | ic1eq = ic1eq + 2.0 * t1; 122 | ic2eq = ic2eq + 2.0 * t2; 123 | 124 | return vhigh + vc2; 125 | } 126 | 127 | private: 128 | 129 | F g0m0 = 0.0; 130 | F g0m1 = 0.0; 131 | F g0m2 = 0.0; 132 | F g1 = 0.0; 133 | F g2 = 0.0; 134 | F g3m0 = 0.0; 135 | F g3m1 = 0.0; 136 | F g3m2 = 0.0; 137 | F g4 = 0.0; 138 | F g5 = 0.0; 139 | 140 | F lowMix = 0.0; 141 | F bandMix = 0.0; 142 | F highMix = 0.0; 143 | 144 | F ic1eq = 0.0; 145 | F ic2eq = 0.0; 146 | }; 147 | --------------------------------------------------------------------------------