├── LICENSE.txt ├── README.md ├── common.h ├── curves.h ├── delay.h ├── envelopes.h ├── fft.h ├── filters.h ├── mix.h ├── perf.h ├── rates.h ├── spectral.h └── windows.h /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Geraint Luff / Signalsmith Audio Ltd. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Signalsmith Audio's DSP Library 2 | 3 | A C++11 header-only library, providing classes/templates for (mostly audio) signal-processing tasks. 4 | 5 | More detail is in the [main project page](https://signalsmith-audio.co.uk/code/dsp/), and the [Doxygen docs](https://signalsmith-audio.co.uk/code/dsp/html/modules.html). 6 | 7 | ## Basic use 8 | 9 | ``` 10 | git clone https://signalsmith-audio.co.uk/code/dsp.git 11 | ``` 12 | 13 | Just include the header file(s) you need, and start using classes: 14 | 15 | ```cpp 16 | #include "dsp/delay.h" 17 | 18 | using Delay = signalsmith::delay::Delay; 19 | Delay delayLine(1024); 20 | ``` 21 | 22 | You can add a compile-time version-check to make sure you have a compatible version of the library: 23 | ```cpp 24 | #include "dsp/envelopes.h" 25 | SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 2) 26 | ``` 27 | 28 | ### Development / contributing 29 | 30 | Tests (and source-scripts for the above docs) are available in a separate repo: 31 | 32 | ``` 33 | git clone https://signalsmith-audio.co.uk/code/dsp-doc.git 34 | ``` 35 | 36 | The goal (where possible) is to measure/test the actual audio characteristics of the tools (e.g. frequency responses and aliasing levels). 37 | 38 | ### License 39 | 40 | This code is [MIT licensed](LICENSE.txt). If you'd prefer something else, get in touch. 41 | -------------------------------------------------------------------------------- /common.h: -------------------------------------------------------------------------------- 1 | #ifndef SIGNALSMITH_DSP_COMMON_H 2 | #define SIGNALSMITH_DSP_COMMON_H 3 | 4 | #if defined(__FAST_MATH__) && (__apple_build_version__ >= 16000000) && (__apple_build_version__ <= 16000099) 5 | # error Apple Clang 16.0.0 generates incorrect SIMD for ARM. If you HAVE to use this version of Clang, turn off -ffast-math. 6 | #endif 7 | 8 | #ifndef M_PI 9 | #define M_PI 3.14159265358979323846264338327950288 10 | #endif 11 | 12 | namespace signalsmith { 13 | /** @defgroup Common Common 14 | @brief Definitions and helper classes used by the rest of the library 15 | 16 | @{ 17 | @file 18 | */ 19 | 20 | #define SIGNALSMITH_DSP_VERSION_MAJOR 1 21 | #define SIGNALSMITH_DSP_VERSION_MINOR 6 22 | #define SIGNALSMITH_DSP_VERSION_PATCH 2 23 | #define SIGNALSMITH_DSP_VERSION_STRING "1.6.2" 24 | 25 | /** Version compatability check. 26 | \code{.cpp} 27 | static_assert(signalsmith::version(1, 4, 1), "version check"); 28 | \endcode 29 | ... or use the equivalent `SIGNALSMITH_DSP_VERSION_CHECK`. 30 | Major versions are not compatible with each other. Minor and patch versions are backwards-compatible. 31 | */ 32 | constexpr bool versionCheck(int major, int minor, int patch=0) { 33 | return major == SIGNALSMITH_DSP_VERSION_MAJOR 34 | && (SIGNALSMITH_DSP_VERSION_MINOR > minor 35 | || (SIGNALSMITH_DSP_VERSION_MINOR == minor && SIGNALSMITH_DSP_VERSION_PATCH >= patch)); 36 | } 37 | 38 | /// Check the library version is compatible (semver). 39 | #define SIGNALSMITH_DSP_VERSION_CHECK(major, minor, patch) \ 40 | static_assert(::signalsmith::versionCheck(major, minor, patch), "signalsmith library version is " SIGNALSMITH_DSP_VERSION_STRING); 41 | 42 | /** @} */ 43 | } // signalsmith:: 44 | #else 45 | // If we've already included it, check it's the same version 46 | static_assert(SIGNALSMITH_DSP_VERSION_MAJOR == 1 && SIGNALSMITH_DSP_VERSION_MINOR == 6 && SIGNALSMITH_DSP_VERSION_PATCH == 2, "multiple versions of the Signalsmith DSP library"); 47 | #endif // include guard 48 | -------------------------------------------------------------------------------- /curves.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_CURVES_H 4 | #define SIGNALSMITH_DSP_CURVES_H 5 | 6 | #include 7 | #include // std::stable_sort 8 | 9 | namespace signalsmith { 10 | namespace curves { 11 | /** @defgroup Curves Curves 12 | @brief User-defined mapping functions 13 | 14 | @{ 15 | @file 16 | */ 17 | 18 | /// Linear map for real values. 19 | template 20 | class Linear { 21 | Sample a1, a0; 22 | public: 23 | Linear() : Linear(0, 1) {} 24 | Linear(Sample a0, Sample a1) : a1(a1), a0(a0) {} 25 | /// Construct by from/to value pairs 26 | Linear(Sample x0, Sample x1, Sample y0, Sample y1) : a1((x0 == x1) ? 0 : (y1 - y0)/(x1 - x0)), a0(y0 - x0*a1) {} 27 | 28 | Sample operator ()(Sample x) const { 29 | return a0 + x*a1; 30 | } 31 | 32 | Sample dx() const { 33 | return a1; 34 | } 35 | 36 | /// Returns the inverse map (with some numerical error) 37 | Linear inverse() const { 38 | Sample invA1 = 1/a1; 39 | return Linear(-a0*invA1, invA1); 40 | } 41 | }; 42 | 43 | /// A real-valued cubic curve. It has a "start" point where accuracy is highest. 44 | template 45 | class Cubic { 46 | Sample xStart, a0, a1, a2, a3; 47 | 48 | // Only use with y0 != y1 49 | static inline Sample gradient(Sample x0, Sample x1, Sample y0, Sample y1) { 50 | return (y1 - y0)/(x1 - x0); 51 | } 52 | // Ensure a gradient produces monotonic segments 53 | static inline void ensureMonotonic(Sample &curveGrad, Sample gradA, Sample gradB) { 54 | if ((gradA <= 0 && gradB >= 0) || (gradA >= 0 && gradB <= 0)) { 55 | curveGrad = 0; // point is a local minimum/maximum 56 | } else { 57 | if (std::abs(curveGrad) > std::abs(gradA*3)) { 58 | curveGrad = gradA*3; 59 | } 60 | if (std::abs(curveGrad) > std::abs(gradB*3)) { 61 | curveGrad = gradB*3; 62 | } 63 | } 64 | } 65 | // When we have duplicate x-values (either side) make up a gradient 66 | static inline void chooseGradient(Sample &curveGrad, Sample grad1, Sample curveGradOther, Sample y0, Sample y1, bool monotonic) { 67 | curveGrad = 2*grad1 - curveGradOther; 68 | if (y0 != y1 && (y1 > y0) != (grad1 >= 0)) { // not duplicate y, but a local min/max 69 | curveGrad = 0; 70 | } else if (monotonic) { 71 | if (grad1 >= 0) { 72 | curveGrad = std::max(0, curveGrad); 73 | } else { 74 | curveGrad = std::min(0, curveGrad); 75 | } 76 | } 77 | } 78 | public: 79 | Cubic() : Cubic(0, 0, 0, 0, 0) {} 80 | Cubic(Sample xStart, Sample a0, Sample a1, Sample a2, Sample a3) : xStart(xStart), a0(a0), a1(a1), a2(a2), a3(a3) {} 81 | 82 | Sample operator ()(Sample x) const { 83 | x -= xStart; 84 | return a0 + x*(a1 + x*(a2 + x*a3)); 85 | } 86 | /// The reference x-value, used as the centre of the cubic expansion 87 | Sample start() const { 88 | return xStart; 89 | } 90 | /// Differentiate 91 | Cubic dx() const { 92 | return {xStart, a1, 2*a2, 3*a3, 0}; 93 | } 94 | Sample dx(Sample x) const { 95 | x -= xStart; 96 | return a1 + x*(2*a2 + x*(3*a3)); 97 | } 98 | 99 | /// Cubic segment based on start/end values and gradients 100 | static Cubic hermite(Sample x0, Sample x1, Sample y0, Sample y1, Sample g0, Sample g1) { 101 | Sample xScale = 1/(x1 - x0); 102 | return { 103 | x0, y0, g0, 104 | (3*(y1 - y0)*xScale - 2*g0 - g1)*xScale, 105 | (2*(y0 - y1)*xScale + g0 + g1)*(xScale*xScale) 106 | }; 107 | } 108 | 109 | /** Cubic segment (valid between `x1` and `x2`), which is smooth when applied to an adjacent set of points. 110 | If `x0 == x1` or `x2 == x3` it will choose a gradient which continues in a quadratic curve, or 0 if the point is a local minimum/maximum. 111 | */ 112 | static Cubic smooth(Sample x0, Sample x1, Sample x2, Sample x3, Sample y0, Sample y1, Sample y2, Sample y3, bool monotonic=false) { 113 | if (x1 == x2) return {0, y1, 0, 0, 0}; // zero-width segment, just return constant 114 | 115 | Sample grad1 = gradient(x1, x2, y1, y2); 116 | Sample curveGrad1 = grad1; 117 | bool chooseGrad1 = false; 118 | if (x0 != x1) { // we have a defined x0-x1 gradient 119 | Sample grad0 = gradient(x0, x1, y0, y1); 120 | curveGrad1 = (grad0 + grad1)*Sample(0.5); 121 | if (monotonic) ensureMonotonic(curveGrad1, grad0, grad1); 122 | } else if (y0 != y1 && (y1 > y0) != (grad1 >= 0)) { 123 | curveGrad1 = 0; // set to 0 if it's a min/max 124 | } else { 125 | curveGrad1 = 0; 126 | chooseGrad1 = true; 127 | } 128 | Sample curveGrad2; 129 | if (x2 != x3) { // we have a defined x1-x2 gradient 130 | Sample grad2 = gradient(x2, x3, y2, y3); 131 | curveGrad2 = (grad1 + grad2)*Sample(0.5); 132 | if (monotonic) ensureMonotonic(curveGrad2, grad1, grad2); 133 | } else { 134 | chooseGradient(curveGrad2, grad1, curveGrad1, y2, y3, monotonic); 135 | } 136 | if (chooseGrad1) { 137 | chooseGradient(curveGrad1, grad1, curveGrad2, y0, y1, monotonic); 138 | } 139 | return hermite(x1, x2, y1, y2, curveGrad1, curveGrad2); 140 | } 141 | }; 142 | 143 | /** Smooth interpolation (optionally monotonic) between points, using cubic segments. 144 | \diagram{cubic-segments-example.svg,Example curve including a repeated point and an instantaneous jump. The curve is flat beyond the first/last points.} 145 | To produce a sharp corner, use a repeated point. The gradient is flat at the edges, unless you use repeated points at the start/end.*/ 146 | template 147 | class CubicSegmentCurve { 148 | struct Point { 149 | Sample x, y; 150 | Sample lineGrad = 0, curveGrad = 0; 151 | bool hasCurveGrad = false; 152 | 153 | Point() : Point(0, 0) {} 154 | Point(Sample x, Sample y) : x(x), y(y) {} 155 | 156 | bool operator <(const Point &other) const { 157 | return x < other.x; 158 | } 159 | }; 160 | std::vector points; 161 | Point first{0, 0}, last{0, 0}; 162 | 163 | std::vector> _segments{1}; 164 | // Not public because it's only valid inside the bounds 165 | const Cubic & findSegment(Sample x) const { 166 | // Binary search 167 | size_t low = 0, high = _segments.size(); 168 | while (true) { 169 | size_t mid = (low + high)/2; 170 | if (low == mid) break; 171 | if (_segments[mid].start() <= x) { 172 | low = mid; 173 | } else { 174 | high = mid; 175 | } 176 | } 177 | return _segments[low]; 178 | } 179 | public: 180 | Sample lowGrad = 0; 181 | Sample highGrad = 0; 182 | 183 | /// Clear existing points and segments 184 | void clear() { 185 | points.resize(0); 186 | _segments.resize(0); 187 | first = last = {0, 0}; 188 | } 189 | 190 | /// Add a new point, but does not recalculate the segments. `corner` just writes the point twice, for convenience. 191 | CubicSegmentCurve & add(Sample x, Sample y, bool corner=false) { 192 | points.push_back({x, y}); 193 | if (corner) points.push_back({x, y}); 194 | return *this; 195 | } 196 | 197 | /// Recalculates the segments. 198 | void update(bool monotonic=false, bool extendGrad=true, Sample monotonicFactor=3) { 199 | if (points.empty()) add(0, 0); 200 | std::stable_sort(points.begin(), points.end()); // Ensure ascending order 201 | _segments.resize(0); 202 | 203 | // Calculate the point-to-point gradients 204 | for (size_t i = 1; i < points.size(); ++i) { 205 | auto &prev = points[i - 1]; 206 | auto &next = points[i]; 207 | if (prev.x != next.x) { 208 | prev.lineGrad = (next.y - prev.y)/(next.x - prev.x); 209 | } else { 210 | prev.lineGrad = 0; 211 | } 212 | } 213 | 214 | for (auto &p : points) p.hasCurveGrad = false; 215 | points[0].curveGrad = lowGrad; 216 | points[0].hasCurveGrad = true; 217 | points.back().curveGrad = highGrad; 218 | points.back().hasCurveGrad = true; 219 | 220 | // Calculate curve gradient where we know it 221 | for (size_t i = 1; i + 1 < points.size(); ++i) { 222 | auto &p0 = points[i - 1]; 223 | auto &p1 = points[i]; 224 | auto &p2 = points[i + 1]; 225 | if (p0.x != p1.x && p1.x != p2.x) { 226 | p1.curveGrad = (p0.lineGrad + p1.lineGrad)*Sample(0.5); 227 | p1.hasCurveGrad = true; 228 | } 229 | } 230 | 231 | for (size_t i = 1; i < points.size(); ++i) { 232 | Point &p1 = points[i - 1]; 233 | Point &p2 = points[i]; 234 | if (p1.x == p2.x) continue; 235 | if (p1.hasCurveGrad) { 236 | if (!p2.hasCurveGrad) { 237 | p2.curveGrad = 2*p1.lineGrad - p1.curveGrad; 238 | } 239 | } else if (p2.hasCurveGrad) { 240 | p1.curveGrad = 2*p1.lineGrad - p2.curveGrad; 241 | } else { 242 | p1.curveGrad = p2.curveGrad = p1.lineGrad; 243 | } 244 | } 245 | 246 | if (monotonic) { 247 | for (size_t i = 1; i < points.size(); ++i) { 248 | Point &p1 = points[i - 1]; 249 | Point &p2 = points[i]; 250 | if (p1.x != p2.x) { 251 | if (p1.lineGrad >= 0) { 252 | p1.curveGrad = std::max(0, std::min(p1.curveGrad, p1.lineGrad*monotonicFactor)); 253 | p2.curveGrad = std::max(0, std::min(p2.curveGrad, p1.lineGrad*monotonicFactor)); 254 | } else { 255 | p1.curveGrad = std::min(0, std::max(p1.curveGrad, p1.lineGrad*monotonicFactor)); 256 | p2.curveGrad = std::min(0, std::max(p2.curveGrad, p1.lineGrad*monotonicFactor)); 257 | } 258 | } 259 | } 260 | } 261 | 262 | for (size_t i = 1; i < points.size(); ++i) { 263 | Point &p1 = points[i - 1]; 264 | Point &p2 = points[i]; 265 | if (p1.x != p2.x) { 266 | _segments.push_back(Segment::hermite(p1.x, p2.x, p1.y, p2.y, p1.curveGrad, p2.curveGrad)); 267 | } 268 | } 269 | 270 | first = points[0]; 271 | last = points.back(); 272 | if (extendGrad && _segments.size()) { 273 | if (points[0].x != points[1].x || points[0].y == points[1].y) { 274 | lowGrad = _segments[0].dx(first.x); 275 | } 276 | auto &last = points.back(), &last2 = points[points.size() - 2]; 277 | if (last.x != last2.x || last.y == last2.y) { 278 | highGrad = _segments.back().dx(last.x); 279 | } 280 | } 281 | } 282 | 283 | /// Reads a value out from the curve. 284 | Sample operator()(Sample x) const { 285 | if (x <= first.x) return first.y + (x - first.x)*lowGrad; 286 | if (x >= last.x) return last.y + (x - last.x)*highGrad; 287 | return findSegment(x)(x); 288 | } 289 | 290 | CubicSegmentCurve dx() const { 291 | CubicSegmentCurve result{*this}; 292 | result.first.y = lowGrad; 293 | result.last.y = highGrad; 294 | result.lowGrad = result.highGrad = 0; 295 | for (auto &s : result._segments) { 296 | s = s.dx(); 297 | } 298 | return result; 299 | } 300 | Sample dx(Sample x) const { 301 | if (x < first.x) return lowGrad; 302 | if (x >= last.x) return highGrad; 303 | return findSegment(x).dx(x); 304 | } 305 | 306 | using Segment = Cubic; 307 | std::vector & segments() { 308 | return _segments; 309 | } 310 | const std::vector & segments() const { 311 | return _segments; 312 | } 313 | }; 314 | 315 | /** A warped-range map, based on 1/x 316 | \diagram{curves-reciprocal-example.svg}*/ 317 | template 318 | class Reciprocal { 319 | Sample a, b, c, d; // (a + bx)/(c + dx) 320 | Reciprocal(Sample a, Sample b, Sample c, Sample d) : a(a), b(b), c(c), d(d) {} 321 | public: 322 | /** Decent approximation to the Bark scale 323 | 324 | The Bark index goes from 1-24, but this map is valid from approximately 0.25 - 27.5. 325 | You can get the bandwidth by `barkScale.dx(barkIndex)`. 326 | \diagram{curves-reciprocal-approx-bark.svg}*/ 327 | static Reciprocal barkScale() { 328 | return {1, 10, 24, 60, 1170, 13500}; 329 | } 330 | /// Returns a map from 0-1 to the given (non-negative) Hz range. 331 | static Reciprocal barkRange(Sample lowHz, Sample highHz) { 332 | Reciprocal bark = barkScale(); 333 | Sample lowBark = bark.inverse(lowHz), highBark = bark.inverse(highHz); 334 | return Reciprocal(lowBark, (lowBark + highBark)/2, highBark).then(bark); 335 | } 336 | 337 | Reciprocal() : Reciprocal(0, 0.5, 1) {} 338 | /// If no x-range given, default to the unit range 339 | Reciprocal(Sample y0, Sample y1, Sample y2) : Reciprocal(0, 0.5, 1, y0, y1, y2) {} 340 | Reciprocal(Sample x0, Sample x1, Sample x2, Sample y0, Sample y1, Sample y2) { 341 | Sample kx = (x1 - x0)/(x2 - x1); 342 | Sample ky = (y1 - y0)/(y2 - y1); 343 | a = (kx*x2)*y0 - (ky*x0)*y2; 344 | b = ky*y2 - kx*y0; 345 | c = kx*x2 - ky*x0; 346 | d = ky - kx; 347 | } 348 | 349 | Sample operator ()(double x) const { 350 | return (a + b*x)/(c + d*x); 351 | } 352 | Reciprocal inverse() const { 353 | return Reciprocal(-a, c, b, -d); 354 | } 355 | Sample inverse(Sample y) const { 356 | return (c*y - a)/(b - d*y); 357 | } 358 | Sample dx(Sample x) const { 359 | Sample l = (c + d*x); 360 | return (b*c - a*d)/(l*l); 361 | } 362 | 363 | /// Combine two `Reciprocal`s together in sequence 364 | Reciprocal then(const Reciprocal &other) const { 365 | return Reciprocal(other.a*c + other.b*a, other.a*d + other.b*b, other.c*c + other.d*a, other.c*d + other.d*b); 366 | } 367 | }; 368 | 369 | /** @} */ 370 | }} // namespace 371 | #endif // include guard 372 | -------------------------------------------------------------------------------- /delay.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_DELAY_H 4 | #define SIGNALSMITH_DSP_DELAY_H 5 | 6 | #include 7 | #include 8 | #include // for std::ceil() 9 | #include 10 | 11 | #include 12 | #include "./fft.h" 13 | #include "./windows.h" 14 | 15 | namespace signalsmith { 16 | namespace delay { 17 | /** @defgroup Delay Delay utilities 18 | @brief Standalone templated classes for delays 19 | 20 | You can set up a `Buffer` or `MultiBuffer`, and get interpolated samples using a `Reader` (separately on each channel in the multi-channel case) - or you can use `Delay`/`MultiDelay` which include their own buffers. 21 | 22 | Interpolation quality is chosen using a template class, from @ref Interpolators. 23 | 24 | @{ 25 | @file 26 | */ 27 | 28 | /** @brief Single-channel delay buffer 29 | 30 | Access is used with `buffer[]`, relative to the internal read/write position ("head"). This head is moved using `++buffer` (or `buffer += n`), such that `buffer[1] == (buffer + 1)[0]` in a similar way iterators/pointers. 31 | 32 | Operations like `buffer - 10` or `buffer++` return a View, which holds a fixed position in the buffer (based on the read/write position at the time). 33 | 34 | The capacity includes both positive and negative indices. For example, a capacity of 100 would support using any of the ranges: 35 | 36 | * `buffer[-99]` to buffer[0]` 37 | * `buffer[-50]` to buffer[49]` 38 | * `buffer[0]` to buffer[99]` 39 | 40 | Although buffers are usually used with historical samples accessed using negative indices e.g. `buffer[-10]`, you could equally use it flipped around (moving the head backwards through the buffer using `--buffer`). 41 | */ 42 | template 43 | class Buffer { 44 | unsigned bufferIndex; 45 | unsigned bufferMask; 46 | std::vector buffer; 47 | public: 48 | Buffer(int minCapacity=0) { 49 | resize(minCapacity); 50 | } 51 | // We shouldn't accidentally copy a delay buffer 52 | Buffer(const Buffer &other) = delete; 53 | Buffer & operator =(const Buffer &other) = delete; 54 | // But moving one is fine 55 | Buffer(Buffer &&other) = default; 56 | Buffer & operator =(Buffer &&other) = default; 57 | 58 | void resize(int minCapacity, Sample value=Sample()) { 59 | int bufferLength = 1; 60 | while (bufferLength < minCapacity) bufferLength *= 2; 61 | buffer.assign(bufferLength, value); 62 | bufferMask = unsigned(bufferLength - 1); 63 | bufferIndex = 0; 64 | } 65 | void reset(Sample value=Sample()) { 66 | buffer.assign(buffer.size(), value); 67 | } 68 | 69 | /// Holds a view for a particular position in the buffer 70 | template 71 | class View { 72 | using CBuffer = typename std::conditional::type; 73 | using CSample = typename std::conditional::type; 74 | CBuffer *buffer = nullptr; 75 | unsigned bufferIndex = 0; 76 | public: 77 | View(CBuffer &buffer, int offset=0) : buffer(&buffer), bufferIndex(buffer.bufferIndex + (unsigned)offset) {} 78 | View(const View &other, int offset=0) : buffer(other.buffer), bufferIndex(other.bufferIndex + (unsigned)offset) {} 79 | View & operator =(const View &other) { 80 | buffer = other.buffer; 81 | bufferIndex = other.bufferIndex; 82 | return *this; 83 | } 84 | 85 | CSample & operator[](int offset) { 86 | return buffer->buffer[(bufferIndex + (unsigned)offset)&buffer->bufferMask]; 87 | } 88 | const Sample & operator[](int offset) const { 89 | return buffer->buffer[(bufferIndex + (unsigned)offset)&buffer->bufferMask]; 90 | } 91 | 92 | /// Write data into the buffer 93 | template 94 | void write(Data &&data, int length) { 95 | for (int i = 0; i < length; ++i) { 96 | (*this)[i] = data[i]; 97 | } 98 | } 99 | /// Read data out from the buffer 100 | template 101 | void read(int length, Data &&data) const { 102 | for (int i = 0; i < length; ++i) { 103 | data[i] = (*this)[i]; 104 | } 105 | } 106 | 107 | View operator +(int offset) const { 108 | return View(*this, offset); 109 | } 110 | View operator -(int offset) const { 111 | return View(*this, -offset); 112 | } 113 | }; 114 | using MutableView = View; 115 | using ConstView = View; 116 | 117 | MutableView view(int offset=0) { 118 | return MutableView(*this, offset); 119 | } 120 | ConstView view(int offset=0) const { 121 | return ConstView(*this, offset); 122 | } 123 | ConstView constView(int offset=0) const { 124 | return ConstView(*this, offset); 125 | } 126 | 127 | Sample & operator[](int offset) { 128 | return buffer[(bufferIndex + (unsigned)offset)&bufferMask]; 129 | } 130 | const Sample & operator[](int offset) const { 131 | return buffer[(bufferIndex + (unsigned)offset)&bufferMask]; 132 | } 133 | 134 | /// Write data into the buffer 135 | template 136 | void write(Data &&data, int length) { 137 | for (int i = 0; i < length; ++i) { 138 | (*this)[i] = data[i]; 139 | } 140 | } 141 | /// Read data out from the buffer 142 | template 143 | void read(int length, Data &&data) const { 144 | for (int i = 0; i < length; ++i) { 145 | data[i] = (*this)[i]; 146 | } 147 | } 148 | 149 | Buffer & operator ++() { 150 | ++bufferIndex; 151 | return *this; 152 | } 153 | Buffer & operator +=(int i) { 154 | bufferIndex += (unsigned)i; 155 | return *this; 156 | } 157 | Buffer & operator --() { 158 | --bufferIndex; 159 | return *this; 160 | } 161 | Buffer & operator -=(int i) { 162 | bufferIndex -= (unsigned)i; 163 | return *this; 164 | } 165 | 166 | MutableView operator ++(int) { 167 | MutableView view(*this); 168 | ++bufferIndex; 169 | return view; 170 | } 171 | MutableView operator +(int i) { 172 | return MutableView(*this, i); 173 | } 174 | ConstView operator +(int i) const { 175 | return ConstView(*this, i); 176 | } 177 | MutableView operator --(int) { 178 | MutableView view(*this); 179 | --bufferIndex; 180 | return view; 181 | } 182 | MutableView operator -(int i) { 183 | return MutableView(*this, -i); 184 | } 185 | ConstView operator -(int i) const { 186 | return ConstView(*this, -i); 187 | } 188 | }; 189 | 190 | /** @brief Multi-channel delay buffer 191 | 192 | This behaves similarly to the single-channel `Buffer`, with the following differences: 193 | 194 | * `buffer[c]` returns a view for a single channel, which behaves like the single-channel `Buffer::View`. 195 | * The constructor and `.resize()` take an additional first `channel` argument. 196 | */ 197 | template 198 | class MultiBuffer { 199 | int channels, stride; 200 | Buffer buffer; 201 | public: 202 | using ConstChannel = typename Buffer::ConstView; 203 | using MutableChannel = typename Buffer::MutableView; 204 | 205 | MultiBuffer(int channels=0, int capacity=0) : channels(channels), stride(capacity), buffer(channels*capacity) {} 206 | 207 | void resize(int nChannels, int capacity, Sample value=Sample()) { 208 | channels = nChannels; 209 | stride = capacity; 210 | buffer.resize(channels*capacity, value); 211 | } 212 | void reset(Sample value=Sample()) { 213 | buffer.reset(value); 214 | } 215 | 216 | /// A reference-like multi-channel result for a particular sample index 217 | template 218 | class Stride { 219 | using CChannel = typename std::conditional::type; 220 | using CSample = typename std::conditional::type; 221 | CChannel view; 222 | int channels, stride; 223 | public: 224 | Stride(CChannel view, int channels, int stride) : view(view), channels(channels), stride(stride) {} 225 | Stride(const Stride &other) : view(other.view), channels(other.channels), stride(other.stride) {} 226 | 227 | CSample & operator[](int channel) { 228 | return view[channel*stride]; 229 | } 230 | const Sample & operator[](int channel) const { 231 | return view[channel*stride]; 232 | } 233 | 234 | /// Reads from the buffer into a multi-channel result 235 | template 236 | void get(Data &&result) const { 237 | for (int c = 0; c < channels; ++c) { 238 | result[c] = view[c*stride]; 239 | } 240 | } 241 | /// Writes from multi-channel data into the buffer 242 | template 243 | void set(Data &&data) { 244 | for (int c = 0; c < channels; ++c) { 245 | view[c*stride] = data[c]; 246 | } 247 | } 248 | template 249 | Stride & operator =(const Data &data) { 250 | set(data); 251 | return *this; 252 | } 253 | Stride & operator =(const Stride &data) { 254 | set(data); 255 | return *this; 256 | } 257 | }; 258 | 259 | Stride at(int offset) { 260 | return {buffer.view(offset), channels, stride}; 261 | } 262 | Stride at(int offset) const { 263 | return {buffer.view(offset), channels, stride}; 264 | } 265 | 266 | /// Holds a particular position in the buffer 267 | template 268 | class View { 269 | using CChannel = typename std::conditional::type; 270 | CChannel view; 271 | int channels, stride; 272 | public: 273 | View(CChannel view, int channels, int stride) : view(view), channels(channels), stride(stride) {} 274 | 275 | CChannel operator[](int channel) { 276 | return view + channel*stride; 277 | } 278 | ConstChannel operator[](int channel) const { 279 | return view + channel*stride; 280 | } 281 | 282 | Stride at(int offset) { 283 | return {view + offset, channels, stride}; 284 | } 285 | Stride at(int offset) const { 286 | return {view + offset, channels, stride}; 287 | } 288 | }; 289 | using MutableView = View; 290 | using ConstView = View; 291 | 292 | MutableView view(int offset=0) { 293 | return MutableView(buffer.view(offset), channels, stride); 294 | } 295 | ConstView view(int offset=0) const { 296 | return ConstView(buffer.view(offset), channels, stride); 297 | } 298 | ConstView constView(int offset=0) const { 299 | return ConstView(buffer.view(offset), channels, stride); 300 | } 301 | 302 | MutableChannel operator[](int channel) { 303 | return buffer + channel*stride; 304 | } 305 | ConstChannel operator[](int channel) const { 306 | return buffer + channel*stride; 307 | } 308 | 309 | MultiBuffer & operator ++() { 310 | ++buffer; 311 | return *this; 312 | } 313 | MultiBuffer & operator +=(int i) { 314 | buffer += i; 315 | return *this; 316 | } 317 | MutableView operator ++(int) { 318 | return MutableView(buffer++, channels, stride); 319 | } 320 | MutableView operator +(int i) { 321 | return MutableView(buffer + i, channels, stride); 322 | } 323 | ConstView operator +(int i) const { 324 | return ConstView(buffer + i, channels, stride); 325 | } 326 | MultiBuffer & operator --() { 327 | --buffer; 328 | return *this; 329 | } 330 | MultiBuffer & operator -=(int i) { 331 | buffer -= i; 332 | return *this; 333 | } 334 | MutableView operator --(int) { 335 | return MutableView(buffer--, channels, stride); 336 | } 337 | MutableView operator -(int i) { 338 | return MutableView(buffer - i, channels, stride); 339 | } 340 | ConstView operator -(int i) const { 341 | return ConstView(buffer - i, channels, stride); 342 | } 343 | }; 344 | 345 | /** \defgroup Interpolators Interpolators 346 | \ingroup Delay 347 | @{ */ 348 | /// Nearest-neighbour interpolator 349 | /// \diagram{delay-random-access-nearest.svg,aliasing and maximum amplitude/delay errors for different input frequencies} 350 | template 351 | struct InterpolatorNearest { 352 | static constexpr int inputLength = 1; 353 | static constexpr Sample latency = -0.5; // Because we're truncating, which rounds down too often 354 | 355 | template 356 | static Sample fractional(const Data &data, Sample) { 357 | return data[0]; 358 | } 359 | }; 360 | /// Linear interpolator 361 | /// \diagram{delay-random-access-linear.svg,aliasing and maximum amplitude/delay errors for different input frequencies} 362 | template 363 | struct InterpolatorLinear { 364 | static constexpr int inputLength = 2; 365 | static constexpr int latency = 0; 366 | 367 | template 368 | static Sample fractional(const Data &data, Sample fractional) { 369 | Sample a = data[0], b = data[1]; 370 | return a + fractional*(b - a); 371 | } 372 | }; 373 | /// Spline cubic interpolator 374 | /// \diagram{delay-random-access-cubic.svg,aliasing and maximum amplitude/delay errors for different input frequencies} 375 | template 376 | struct InterpolatorCubic { 377 | static constexpr int inputLength = 4; 378 | static constexpr int latency = 1; 379 | 380 | template 381 | static Sample fractional(const Data &data, Sample fractional) { 382 | // Cubic interpolation 383 | Sample a = data[0], b = data[1], c = data[2], d = data[3]; 384 | Sample cbDiff = c - b; 385 | Sample k1 = (c - a)*0.5; 386 | Sample k3 = k1 + (d - b)*0.5 - cbDiff*2; 387 | Sample k2 = cbDiff - k3 - k1; 388 | return b + fractional*(k1 + fractional*(k2 + fractional*k3)); // 16 ops total, not including the indexing 389 | } 390 | }; 391 | 392 | // Efficient Algorithms and Structures for Fractional Delay Filtering Based on Lagrange Interpolation 393 | // Franck 2009 https://www.aes.org/e-lib/browse.cfm?elib=14647 394 | namespace _franck_impl { 395 | template 396 | struct ProductRange { 397 | using Array = std::array; 398 | static constexpr int mid = (low + high)/2; 399 | using Left = ProductRange; 400 | using Right = ProductRange; 401 | 402 | Left left; 403 | Right right; 404 | 405 | const Sample total; 406 | ProductRange(Sample x) : left(x), right(x), total(left.total*right.total) {} 407 | 408 | template 409 | Sample calculateResult(Sample extraFactor, const Data &data, const Array &invFactors) { 410 | return left.calculateResult(extraFactor*right.total, data, invFactors) 411 | + right.calculateResult(extraFactor*left.total, data, invFactors); 412 | } 413 | }; 414 | template 415 | struct ProductRange { 416 | using Array = std::array; 417 | 418 | const Sample total; 419 | ProductRange(Sample x) : total(x - index) {} 420 | 421 | template 422 | Sample calculateResult(Sample extraFactor, const Data &data, const Array &invFactors) { 423 | return extraFactor*data[index]*invFactors[index]; 424 | } 425 | }; 426 | } 427 | /** Fixed-order Lagrange interpolation. 428 | \diagram{interpolator-LagrangeN.svg,aliasing and amplitude/delay errors for different sizes} 429 | */ 430 | template 431 | struct InterpolatorLagrangeN { 432 | static constexpr int inputLength = n + 1; 433 | static constexpr int latency = (n - 1)/2; 434 | 435 | using Array = std::array; 436 | Array invDivisors; 437 | 438 | InterpolatorLagrangeN() { 439 | for (int j = 0; j <= n; ++j) { 440 | double divisor = 1; 441 | for (int k = 0; k < j; ++k) divisor *= (j - k); 442 | for (int k = j + 1; k <= n; ++k) divisor *= (j - k); 443 | invDivisors[j] = 1/divisor; 444 | } 445 | } 446 | 447 | template 448 | Sample fractional(const Data &data, Sample fractional) const { 449 | constexpr int mid = n/2; 450 | using Left = _franck_impl::ProductRange; 451 | using Right = _franck_impl::ProductRange; 452 | 453 | Sample x = fractional + latency; 454 | 455 | Left left(x); 456 | Right right(x); 457 | 458 | return left.calculateResult(right.total, data, invDivisors) + right.calculateResult(left.total, data, invDivisors); 459 | } 460 | }; 461 | template 462 | using InterpolatorLagrange3 = InterpolatorLagrangeN; 463 | template 464 | using InterpolatorLagrange7 = InterpolatorLagrangeN; 465 | template 466 | using InterpolatorLagrange19 = InterpolatorLagrangeN; 467 | 468 | /** Fixed-size Kaiser-windowed sinc interpolation. 469 | \diagram{interpolator-KaiserSincN.svg,aliasing and amplitude/delay errors for different sizes} 470 | If `minimumPhase` is enabled, a minimum-phase version of the kernel is used: 471 | \diagram{interpolator-KaiserSincN-min.svg,aliasing and amplitude/delay errors for minimum-phase mode} 472 | */ 473 | template 474 | struct InterpolatorKaiserSincN { 475 | static constexpr int inputLength = n; 476 | static constexpr Sample latency = minimumPhase ? 0 : (n*Sample(0.5) - 1); 477 | 478 | int subSampleSteps; 479 | std::vector coefficients; 480 | 481 | InterpolatorKaiserSincN() : InterpolatorKaiserSincN(0.5 - 0.45/std::sqrt(n)) {} 482 | InterpolatorKaiserSincN(double passFreq) : InterpolatorKaiserSincN(passFreq, 1 - passFreq) {} 483 | InterpolatorKaiserSincN(double passFreq, double stopFreq) { 484 | subSampleSteps = 2*n; // Heuristic again. Really it depends on the bandwidth as well. 485 | double kaiserBandwidth = (stopFreq - passFreq)*(n + 1.0/subSampleSteps); 486 | kaiserBandwidth += 1.25/kaiserBandwidth; // We want to place the first zero, but (because using this to window a sinc essentially integrates it in the freq-domain), our ripples (and therefore zeroes) are out of phase. This is a heuristic fix. 487 | double sincScale = M_PI*(passFreq + stopFreq); 488 | 489 | double centreIndex = n*subSampleSteps*0.5, scaleFactor = 1.0/subSampleSteps; 490 | std::vector windowedSinc(subSampleSteps*n + 1); 491 | 492 | ::signalsmith::windows::Kaiser::withBandwidth(kaiserBandwidth, false).fill(windowedSinc, windowedSinc.size()); 493 | 494 | for (size_t i = 0; i < windowedSinc.size(); ++i) { 495 | double x = (i - centreIndex)*scaleFactor; 496 | int intX = std::round(x); 497 | if (intX != 0 && std::abs(x - intX) < 1e-6) { 498 | // Exact 0s 499 | windowedSinc[i] = 0; 500 | } else if (std::abs(x) > 1e-6) { 501 | double p = x*sincScale; 502 | windowedSinc[i] *= std::sin(p)/p; 503 | } 504 | } 505 | 506 | if (minimumPhase) { 507 | signalsmith::fft::FFT fft(windowedSinc.size()*2, 1); 508 | windowedSinc.resize(fft.size(), 0); 509 | std::vector> spectrum(fft.size()); 510 | std::vector> cepstrum(fft.size()); 511 | fft.fft(windowedSinc, spectrum); 512 | for (size_t i = 0; i < fft.size(); ++i) { 513 | spectrum[i] = std::log(std::abs(spectrum[i]) + 1e-30); 514 | } 515 | fft.fft(spectrum, cepstrum); 516 | for (size_t i = 1; i < fft.size()/2; ++i) { 517 | cepstrum[i] *= 0; 518 | } 519 | for (size_t i = fft.size()/2 + 1; i < fft.size(); ++i) { 520 | cepstrum[i] *= 2; 521 | } 522 | Sample scaling = Sample(1)/fft.size(); 523 | fft.ifft(cepstrum, spectrum); 524 | 525 | for (size_t i = 0; i < fft.size(); ++i) { 526 | Sample phase = spectrum[i].imag()*scaling; 527 | Sample mag = std::exp(spectrum[i].real()*scaling); 528 | spectrum[i] = {mag*std::cos(phase), mag*std::sin(phase)}; 529 | } 530 | fft.ifft(spectrum, cepstrum); 531 | windowedSinc.resize(subSampleSteps*n + 1); 532 | windowedSinc.shrink_to_fit(); 533 | for (size_t i = 0; i < windowedSinc.size(); ++i) { 534 | windowedSinc[i] = cepstrum[i].real()*scaling; 535 | } 536 | } 537 | 538 | // Re-order into FIR fractional-delay blocks 539 | coefficients.resize(n*(subSampleSteps + 1)); 540 | for (int k = 0; k <= subSampleSteps; ++k) { 541 | for (int i = 0; i < n; ++i) { 542 | coefficients[k*n + i] = windowedSinc[(subSampleSteps - k) + i*subSampleSteps]; 543 | } 544 | } 545 | } 546 | 547 | template 548 | Sample fractional(const Data &data, Sample fractional) const { 549 | Sample subSampleDelay = fractional*subSampleSteps; 550 | int lowIndex = subSampleDelay; 551 | if (lowIndex >= subSampleSteps) lowIndex = subSampleSteps - 1; 552 | Sample subSampleFractional = subSampleDelay - lowIndex; 553 | int highIndex = lowIndex + 1; 554 | 555 | Sample sumLow = 0, sumHigh = 0; 556 | const Sample *coeffLow = coefficients.data() + lowIndex*n; 557 | const Sample *coeffHigh = coefficients.data() + highIndex*n; 558 | for (int i = 0; i < n; ++i) { 559 | sumLow += data[i]*coeffLow[i]; 560 | sumHigh += data[i]*coeffHigh[i]; 561 | } 562 | return sumLow + (sumHigh - sumLow)*subSampleFractional; 563 | } 564 | }; 565 | 566 | template 567 | using InterpolatorKaiserSinc20 = InterpolatorKaiserSincN; 568 | template 569 | using InterpolatorKaiserSinc8 = InterpolatorKaiserSincN; 570 | template 571 | using InterpolatorKaiserSinc4 = InterpolatorKaiserSincN; 572 | 573 | template 574 | using InterpolatorKaiserSinc20Min = InterpolatorKaiserSincN; 575 | template 576 | using InterpolatorKaiserSinc8Min = InterpolatorKaiserSincN; 577 | template 578 | using InterpolatorKaiserSinc4Min = InterpolatorKaiserSincN; 579 | /// @} 580 | 581 | /** @brief A delay-line reader which uses an external buffer 582 | 583 | This is useful if you have multiple delay-lines reading from the same buffer. 584 | */ 585 | template class Interpolator=InterpolatorLinear> 586 | class Reader : public Interpolator /* so we can get the empty-base-class optimisation */ { 587 | using Super = Interpolator; 588 | public: 589 | Reader () {} 590 | /// Pass in a configured interpolator 591 | Reader (const Interpolator &interpolator) : Super(interpolator) {} 592 | 593 | template 594 | Sample read(const Buffer &buffer, Sample delaySamples) const { 595 | int startIndex = delaySamples; 596 | Sample remainder = delaySamples - startIndex; 597 | 598 | // Delay buffers use negative indices, but interpolators use positive ones 599 | using View = decltype(buffer - startIndex); 600 | struct Flipped { 601 | View view; 602 | Sample operator [](int i) const { 603 | return view[-i]; 604 | } 605 | }; 606 | return Super::fractional(Flipped{buffer - startIndex}, remainder); 607 | } 608 | }; 609 | 610 | /** @brief A single-channel delay-line containing its own buffer.*/ 611 | template class Interpolator=InterpolatorLinear> 612 | class Delay : private Reader { 613 | using Super = Reader; 614 | Buffer buffer; 615 | public: 616 | static constexpr Sample latency = Super::latency; 617 | 618 | Delay(int capacity=0) : buffer(1 + capacity + Super::inputLength) {} 619 | /// Pass in a configured interpolator 620 | Delay(const Interpolator &interp, int capacity=0) : Super(interp), buffer(1 + capacity + Super::inputLength) {} 621 | 622 | void reset(Sample value=Sample()) { 623 | buffer.reset(value); 624 | } 625 | void resize(int minCapacity, Sample value=Sample()) { 626 | buffer.resize(minCapacity + Super::inputLength, value); 627 | } 628 | 629 | /** Read a sample from `delaySamples` >= 0 in the past. 630 | The interpolator may add its own latency on top of this (see `Delay::latency`). The default interpolation (linear) has 0 latency. 631 | */ 632 | Sample read(Sample delaySamples) const { 633 | return Super::read(buffer, delaySamples); 634 | } 635 | /// Writes a sample. Returns the same object, so that you can say `delay.write(v).read(delay)`. 636 | Delay & write(Sample value) { 637 | ++buffer; 638 | buffer[0] = value; 639 | return *this; 640 | } 641 | }; 642 | 643 | /** @brief A multi-channel delay-line with its own buffer. */ 644 | template class Interpolator=InterpolatorLinear> 645 | class MultiDelay : private Reader { 646 | using Super = Reader; 647 | int channels; 648 | MultiBuffer multiBuffer; 649 | public: 650 | static constexpr Sample latency = Super::latency; 651 | 652 | MultiDelay(int channels=0, int capacity=0) : channels(channels), multiBuffer(channels, 1 + capacity + Super::inputLength) {} 653 | 654 | void reset(Sample value=Sample()) { 655 | multiBuffer.reset(value); 656 | } 657 | void resize(int nChannels, int capacity, Sample value=Sample()) { 658 | channels = nChannels; 659 | multiBuffer.resize(channels, capacity + Super::inputLength, value); 660 | } 661 | 662 | /// A single-channel delay-line view, similar to a `const Delay` 663 | struct ChannelView { 664 | static constexpr Sample latency = Super::latency; 665 | 666 | const Super &reader; 667 | typename MultiBuffer::ConstChannel channel; 668 | 669 | Sample read(Sample delaySamples) const { 670 | return reader.read(channel, delaySamples); 671 | } 672 | }; 673 | ChannelView operator [](int channel) const { 674 | return ChannelView{*this, multiBuffer[channel]}; 675 | } 676 | 677 | /// A multi-channel result, lazily calculating samples 678 | struct DelayView { 679 | Super &reader; 680 | typename MultiBuffer::ConstView view; 681 | Sample delaySamples; 682 | 683 | // Calculate samples on-the-fly 684 | Sample operator [](int c) const { 685 | return reader.read(view[c], delaySamples); 686 | } 687 | }; 688 | DelayView read(Sample delaySamples) { 689 | return DelayView{*this, multiBuffer.constView(), delaySamples}; 690 | } 691 | /// Reads into the provided output structure 692 | template 693 | void read(Sample delaySamples, Output &output) { 694 | for (int c = 0; c < channels; ++c) { 695 | output[c] = Super::read(multiBuffer[c], delaySamples); 696 | } 697 | } 698 | /// Reads separate delays for each channel 699 | template 700 | void readMulti(const Delays &delays, Output &output) { 701 | for (int c = 0; c < channels; ++c) { 702 | output[c] = Super::read(multiBuffer[c], delays[c]); 703 | } 704 | } 705 | template 706 | MultiDelay & write(const Data &data) { 707 | ++multiBuffer; 708 | for (int c = 0; c < channels; ++c) { 709 | multiBuffer[c][0] = data[c]; 710 | } 711 | return *this; 712 | } 713 | }; 714 | 715 | /** @} */ 716 | }} // signalsmith::delay:: 717 | #endif // include guard 718 | -------------------------------------------------------------------------------- /envelopes.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_ENVELOPES_H 4 | #define SIGNALSMITH_DSP_ENVELOPES_H 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace signalsmith { 12 | namespace envelopes { 13 | /** @defgroup Envelopes Envelopes and LFOs 14 | @brief LFOs, envelopes and filters for manipulating them 15 | 16 | @{ 17 | @file 18 | */ 19 | 20 | /** An LFO based on cubic segments. 21 | You can randomise the rate and/or the depth. Randomising the depth past `0.5` means it no longer neatly alternates sides: 22 | \diagram{cubic-lfo-example.svg,Some example LFO curves.} 23 | Without randomisation, it is approximately sine-like: 24 | \diagram{cubic-lfo-spectrum-pure.svg} 25 | */ 26 | class CubicLfo { 27 | float ratio = 0; 28 | float ratioStep = 0; 29 | 30 | float valueFrom = 0, valueTo = 1, valueRange = 1; 31 | float targetLow = 0, targetHigh = 1; 32 | float targetRate = 0; 33 | float rateRandom = 0.5, depthRandom = 0; 34 | bool freshReset = true; 35 | 36 | std::default_random_engine randomEngine; 37 | std::uniform_real_distribution randomUnit; 38 | float random() { 39 | return randomUnit(randomEngine); 40 | } 41 | float randomRate() { 42 | return targetRate*exp(rateRandom*(random() - 0.5)); 43 | } 44 | float randomTarget(float previous) { 45 | float randomOffset = depthRandom*random()*(targetLow - targetHigh); 46 | if (previous < (targetLow + targetHigh)*0.5f) { 47 | return targetHigh + randomOffset; 48 | } else { 49 | return targetLow - randomOffset; 50 | } 51 | } 52 | public: 53 | CubicLfo() : randomEngine(std::random_device()()), randomUnit(0, 1) { 54 | reset(); 55 | } 56 | CubicLfo(long seed) : randomUnit(0, 1) { 57 | randomEngine.seed(seed); 58 | reset(); 59 | } 60 | 61 | /// Resets the LFO state, starting with random phase. 62 | void reset() { 63 | ratio = random(); 64 | ratioStep = randomRate(); 65 | if (random() < 0.5) { 66 | valueFrom = targetLow; 67 | valueTo = targetHigh; 68 | } else { 69 | valueFrom = targetHigh; 70 | valueTo = targetLow; 71 | } 72 | valueRange = valueTo - valueFrom; 73 | freshReset = true; 74 | } 75 | /** Smoothly updates the LFO parameters. 76 | 77 | If called directly after `.reset()`, oscillation will immediately start within the specified range. Otherwise, it will remain smooth and fit within the new range after at most one cycle: 78 | \diagram{cubic-lfo-changes.svg} 79 | 80 | The LFO will complete a full oscillation in (approximately) `1/rate` samples. `rateVariation` can be any number, but 0-1 is a good range. 81 | 82 | `depthVariation` must be in the range [0, 1], where ≤ 0.5 produces random amplitude but still alternates up/down. 83 | \diagram{cubic-lfo-spectrum.svg,Spectra for the two types of randomisation - note the jump as depth variation goes past 50%} 84 | */ 85 | void set(float low, float high, float rate, float rateVariation=0, float depthVariation=0) { 86 | rate *= 2; // We want to go up and down during this period 87 | targetRate = rate; 88 | targetLow = std::min(low, high); 89 | targetHigh = std::max(low, high); 90 | rateRandom = rateVariation; 91 | depthRandom = std::min(1, std::max(0, depthVariation)); 92 | 93 | // If we haven't called .next() yet, don't bother being smooth. 94 | if (freshReset) return reset(); 95 | 96 | // Only update the current rate if it's outside our new random-variation range 97 | float maxRandomRatio = exp((float)0.5*rateRandom); 98 | if (ratioStep > rate*maxRandomRatio || ratioStep < rate/maxRandomRatio) { 99 | ratioStep = randomRate(); 100 | } 101 | } 102 | 103 | /// Returns the next output sample 104 | float next() { 105 | freshReset = false; 106 | float result = ratio*ratio*(3 - 2*ratio)*valueRange + valueFrom; 107 | 108 | ratio += ratioStep; 109 | while (ratio >= 1) { 110 | ratio -= 1; 111 | ratioStep = randomRate(); 112 | valueFrom = valueTo; 113 | valueTo = randomTarget(valueFrom); 114 | valueRange = valueTo - valueFrom; 115 | } 116 | return result; 117 | } 118 | }; 119 | 120 | /** Variable-width rectangular sum */ 121 | template 122 | class BoxSum { 123 | int bufferLength, index; 124 | std::vector buffer; 125 | Sample sum = 0, wrapJump = 0; 126 | public: 127 | BoxSum(int maxLength) { 128 | resize(maxLength); 129 | } 130 | 131 | /// Sets the maximum size (and reset contents) 132 | void resize(int maxLength) { 133 | bufferLength = maxLength + 1; 134 | buffer.resize(bufferLength); 135 | if (maxLength != 0) buffer.shrink_to_fit(); 136 | reset(); 137 | } 138 | 139 | /// Resets (with an optional "fill" value) 140 | void reset(Sample value=Sample()) { 141 | index = 0; 142 | sum = 0; 143 | for (size_t i = 0; i < buffer.size(); ++i) { 144 | buffer[i] = sum; 145 | sum += value; 146 | } 147 | wrapJump = sum; 148 | sum = 0; 149 | } 150 | 151 | Sample read(int width) { 152 | int readIndex = index - width; 153 | double result = sum; 154 | if (readIndex < 0) { 155 | result += wrapJump; 156 | readIndex += bufferLength; 157 | } 158 | return result - buffer[readIndex]; 159 | } 160 | 161 | void write(Sample value) { 162 | ++index; 163 | if (index == bufferLength) { 164 | index = 0; 165 | wrapJump = sum; 166 | sum = 0; 167 | } 168 | sum += value; 169 | buffer[index] = sum; 170 | } 171 | 172 | Sample readWrite(Sample value, int width) { 173 | write(value); 174 | return read(width); 175 | } 176 | }; 177 | 178 | /** Rectangular moving average filter (FIR). 179 | \diagram{box-filter-example.svg} 180 | A filter of length 1 has order 0 (i.e. does nothing). */ 181 | template 182 | class BoxFilter { 183 | BoxSum boxSum; 184 | int _length, _maxLength; 185 | Sample multiplier; 186 | public: 187 | BoxFilter(int maxLength) : boxSum(maxLength) { 188 | resize(maxLength); 189 | } 190 | /// Sets the maximum size (and current size, and resets) 191 | void resize(int maxLength) { 192 | _maxLength = maxLength; 193 | boxSum.resize(maxLength); 194 | set(maxLength); 195 | } 196 | /// Sets the current size (expanding/allocating only if needed) 197 | void set(int length) { 198 | _length = length; 199 | multiplier = Sample(1)/length; 200 | if (length > _maxLength) resize(length); 201 | } 202 | 203 | /// Resets (with an optional "fill" value) 204 | void reset(Sample fill=Sample()) { 205 | boxSum.reset(fill); 206 | } 207 | 208 | Sample operator()(Sample v) { 209 | return boxSum.readWrite(v, _length)*multiplier; 210 | } 211 | }; 212 | 213 | /** FIR filter made from a stack of `BoxFilter`s. 214 | This filter has a non-negative impulse (monotonic step response), making it useful for smoothing positive-only values. It provides an optimal set of box-lengths, chosen to minimise peaks in the stop-band: 215 | \diagram{box-stack-long.svg,Impulse responses for various stack sizes at length N=1000} 216 | Since the underlying box-averages must have integer width, the peaks are slightly higher for shorter lengths with higher numbers of layers: 217 | \diagram{box-stack-short-freq.svg,Frequency responses for various stack sizes at length N=30} 218 | */ 219 | template 220 | class BoxStackFilter { 221 | struct Layer { 222 | double ratio = 0, lengthError = 0; 223 | int length = 0; 224 | BoxFilter filter{0}; 225 | Layer() {} 226 | }; 227 | int _size; 228 | std::vector layers; 229 | 230 | template 231 | void setupLayers(const Iterable &ratios) { 232 | layers.resize(0); 233 | double sum = 0; 234 | for (auto ratio : ratios) { 235 | Layer layer; 236 | layer.ratio = ratio; 237 | layers.push_back(layer); 238 | sum += ratio; 239 | } 240 | double factor = 1/sum; 241 | for (auto &l : layers) { 242 | l.ratio *= factor; 243 | } 244 | } 245 | public: 246 | BoxStackFilter(int maxSize, int layers=4) { 247 | resize(maxSize, layers); 248 | } 249 | 250 | /// Returns an optimal set of length ratios (heuristic for larger depths) 251 | static std::vector optimalRatios(int layerCount) { 252 | // Coefficients up to 6, found through numerical search 253 | static double hardcoded[] = {1, 0.58224186169, 0.41775813831, 0.404078562416, 0.334851475794, 0.261069961789, 0.307944914938, 0.27369945234, 0.22913263601, 0.189222996712, 0.248329349789, 0.229253789144, 0.201191468123, 0.173033035122, 0.148192357821, 0.205275202874, 0.198413552119, 0.178256637764, 0.157821404506, 0.138663023387, 0.121570179349 /*, 0.178479592135, 0.171760666359, 0.158434068954, 0.143107825806, 0.125907148711, 0.11853946895, 0.103771229086, 0.155427880834, 0.153063152848, 0.142803459422, 0.131358358458, 0.104157805178, 0.119338029601, 0.0901675284678, 0.103683785192, 0.143949349747, 0.139813248378, 0.132051305252, 0.122216776152, 0.112888320989, 0.102534988632, 0.0928386714364, 0.0719750997699, 0.0817322396428, 0.130587011572, 0.127244563184, 0.121228748787, 0.113509941974, 0.105000272288, 0.0961938290157, 0.0880639725438, 0.0738389766046, 0.0746781936619, 0.0696544903682 */}; 254 | if (layerCount <= 0) { 255 | return {}; 256 | } else if (layerCount <= 6) { 257 | double *start = &hardcoded[layerCount*(layerCount - 1)/2]; 258 | return std::vector(start, start + layerCount); 259 | } 260 | std::vector result(layerCount); 261 | 262 | double invN = 1.0/layerCount, sqrtN = std::sqrt(layerCount); 263 | double p = 1 - invN; 264 | double k = 1 + 4.5/sqrtN + 0.08*sqrtN; 265 | 266 | double sum = 0; 267 | for (int i = 0; i < layerCount; ++i) { 268 | double x = i*invN; 269 | double power = -x*(1 - p*std::exp(-x*k)); 270 | double length = std::pow(2, power); 271 | result[i] = length; 272 | sum += length; 273 | } 274 | double factor = 1/sum; 275 | for (auto &r : result) r *= factor; 276 | return result; 277 | } 278 | /** Approximate (optimal) bandwidth for a given number of layers 279 | \diagram{box-stack-bandwidth.svg,Approximate main lobe width (bandwidth)} 280 | */ 281 | static constexpr double layersToBandwidth(int layers) { 282 | return 1.58*(layers + 0.1); 283 | } 284 | /** Approximate (optimal) peak in the stop-band 285 | \diagram{box-stack-peak.svg,Heuristic stop-band peak} 286 | */ 287 | static constexpr double layersToPeakDb(int layers) { 288 | return 5 - layers*18; 289 | } 290 | 291 | /// Sets size using an optimal (heuristic at larger sizes) set of length ratios 292 | void resize(int maxSize, int layerCount) { 293 | resize(maxSize, optimalRatios(layerCount)); 294 | } 295 | /// Sets the maximum (and current) impulse response length and explicit length ratios 296 | template 297 | auto resize(int maxSize, List ratios) -> decltype(void(std::begin(ratios)), void(std::end(ratios))) { 298 | setupLayers(ratios); 299 | for (auto &layer : layers) layer.filter.resize(0); // .set() will expand it later 300 | _size = -1; 301 | set(maxSize); 302 | reset(); 303 | } 304 | void resize(int maxSize, std::initializer_list ratios) { 305 | resize &>(maxSize, ratios); 306 | } 307 | 308 | /// Sets the impulse response length (does not reset if `size` ≤ `maxSize`) 309 | void set(int size) { 310 | if (layers.size() == 0) return; // meaningless 311 | 312 | if (_size == size) return; 313 | _size = size; 314 | int order = size - 1; 315 | int totalOrder = 0; 316 | 317 | for (auto &layer : layers) { 318 | double layerOrderFractional = layer.ratio*order; 319 | int layerOrder = int(layerOrderFractional); 320 | layer.length = layerOrder + 1; 321 | layer.lengthError = layerOrder - layerOrderFractional; 322 | totalOrder += layerOrder; 323 | } 324 | // Round some of them up, so the total is correct - this is O(N²), but `layers.size()` is small 325 | while (totalOrder < order) { 326 | int minIndex = 0; 327 | double minError = layers[0].lengthError; 328 | for (size_t i = 1; i < layers.size(); ++i) { 329 | if (layers[i].lengthError < minError) { 330 | minError = layers[i].lengthError; 331 | minIndex = i; 332 | } 333 | } 334 | layers[minIndex].length++; 335 | layers[minIndex].lengthError += 1; 336 | totalOrder++; 337 | } 338 | for (auto &layer : layers) layer.filter.set(layer.length); 339 | } 340 | 341 | /// Resets the filter 342 | void reset(Sample fill=Sample()) { 343 | for (auto &layer : layers) layer.filter.reset(fill); 344 | } 345 | 346 | Sample operator()(Sample v) { 347 | for (auto &layer : layers) { 348 | v = layer.filter(v); 349 | } 350 | return v; 351 | } 352 | }; 353 | 354 | /** Peak-hold filter. 355 | \diagram{peak-hold.svg} 356 | 357 | The size is variable, and can be changed instantly with `.set()`, or by using `.push()`/`.pop()` in an unbalanced way. 358 | 359 | This has complexity O(1) every sample when the length remains constant (balanced `.push()`/`.pop()`, or using `filter(v)`), and amortised O(1) complexity otherwise. To avoid allocations while running, it pre-allocates a vector (not a `std::deque`) which determines the maximum length. 360 | */ 361 | template 362 | class PeakHold { 363 | static constexpr Sample lowest = std::numeric_limits::lowest(); 364 | int bufferMask; 365 | std::vector buffer; 366 | int backIndex = 0, middleStart = 0, workingIndex = 0, middleEnd = 0, frontIndex = 0; 367 | Sample frontMax = lowest, workingMax = lowest, middleMax = lowest; 368 | 369 | public: 370 | PeakHold(int maxLength) { 371 | resize(maxLength); 372 | } 373 | int size() { 374 | return frontIndex - backIndex; 375 | } 376 | void resize(int maxLength) { 377 | int bufferLength = 1; 378 | while (bufferLength < maxLength) bufferLength *= 2; 379 | buffer.resize(bufferLength); 380 | bufferMask = bufferLength - 1; 381 | 382 | frontIndex = backIndex + maxLength; 383 | reset(); 384 | } 385 | void reset(Sample fill=lowest) { 386 | int prevSize = size(); 387 | buffer.assign(buffer.size(), fill); 388 | frontMax = workingMax = middleMax = lowest; 389 | middleEnd = workingIndex = frontIndex = 0; 390 | middleStart = middleEnd - (prevSize/2); 391 | backIndex = frontIndex - prevSize; 392 | } 393 | /** Sets the size immediately. 394 | Must be `0 <= newSize <= maxLength` (see constructor and `.resize()`). 395 | 396 | Shrinking doesn't destroy information, and if you expand again (with `preserveCurrentPeak=false`), you will get the same output as before shrinking. Expanding when `preserveCurrentPeak` is enabled is destructive, re-writing its history such that the current output value is unchanged.*/ 397 | void set(int newSize, bool preserveCurrentPeak=false) { 398 | while (size() < newSize) { 399 | Sample &backPrev = buffer[backIndex&bufferMask]; 400 | --backIndex; 401 | Sample &back = buffer[backIndex&bufferMask]; 402 | back = preserveCurrentPeak ? backPrev : std::max(back, backPrev); 403 | } 404 | while (size() > newSize) { 405 | pop(); 406 | } 407 | } 408 | 409 | void push(Sample v) { 410 | buffer[frontIndex&bufferMask] = v; 411 | ++frontIndex; 412 | frontMax = std::max(frontMax, v); 413 | } 414 | void pop() { 415 | if (backIndex == middleStart) { 416 | // Move along the maximums 417 | workingMax = lowest; 418 | middleMax = frontMax; 419 | frontMax = lowest; 420 | 421 | int prevFrontLength = frontIndex - middleEnd; 422 | int prevMiddleLength = middleEnd - middleStart; 423 | if (prevFrontLength <= prevMiddleLength + 1) { 424 | // Swap over simply 425 | middleStart = middleEnd; 426 | middleEnd = frontIndex; 427 | workingIndex = middleEnd; 428 | } else { 429 | // The front is longer than the middle - only happens if unbalanced 430 | // We don't move *all* of the front over, keeping half the surplus in the front 431 | int middleLength = (frontIndex - middleStart)/2; 432 | middleStart = middleEnd; 433 | middleEnd += middleLength; 434 | 435 | // Working index is close enough that it will be finished by the time the back is empty 436 | int backLength = middleStart - backIndex; 437 | int workingLength = std::min(backLength, middleEnd - middleStart); 438 | workingIndex = middleStart + workingLength; 439 | 440 | // Since the front was not completely consumed, we re-calculate the front's maximum 441 | for (int i = middleEnd; i != frontIndex; ++i) { 442 | frontMax = std::max(frontMax, buffer[i&bufferMask]); 443 | } 444 | // The index might not start at the end of the working block - compute the last bit immediately 445 | for (int i = middleEnd - 1; i != workingIndex - 1; --i) { 446 | buffer[i&bufferMask] = workingMax = std::max(workingMax, buffer[i&bufferMask]); 447 | } 448 | } 449 | 450 | // Is the new back (previous middle) empty? Only happens if unbalanced 451 | if (backIndex == middleStart) { 452 | // swap over again (front's empty, no change) 453 | workingMax = lowest; 454 | middleMax = frontMax; 455 | frontMax = lowest; 456 | middleStart = workingIndex = middleEnd; 457 | 458 | if (backIndex == middleStart) { 459 | --backIndex; // Only happens if you pop from an empty list - fail nicely 460 | } 461 | } 462 | 463 | buffer[frontIndex&bufferMask] = lowest; // In case of length 0, when everything points at this value 464 | } 465 | 466 | ++backIndex; 467 | if (workingIndex != middleStart) { 468 | --workingIndex; 469 | buffer[workingIndex&bufferMask] = workingMax = std::max(workingMax, buffer[workingIndex&bufferMask]); 470 | } 471 | } 472 | Sample read() { 473 | Sample backMax = buffer[backIndex&bufferMask]; 474 | return std::max(backMax, std::max(middleMax, frontMax)); 475 | } 476 | 477 | // For simple use as a constant-length filter 478 | Sample operator ()(Sample v) { 479 | push(v); 480 | pop(); 481 | return read(); 482 | } 483 | }; 484 | 485 | /** Peak-decay filter with a linear shape and fixed-time return to constant value. 486 | \diagram{peak-decay-linear.svg} 487 | This is equivalent to a `BoxFilter` which resets itself whenever the output would be less than the input. 488 | */ 489 | template 490 | class PeakDecayLinear { 491 | static constexpr Sample lowest = std::numeric_limits::lowest(); 492 | PeakHold peakHold; 493 | Sample value = lowest; 494 | Sample stepMultiplier = 1; 495 | public: 496 | PeakDecayLinear(int maxLength) : peakHold(maxLength) { 497 | set(maxLength); 498 | } 499 | void resize(int maxLength) { 500 | peakHold.resize(maxLength); 501 | reset(); 502 | } 503 | void set(double length) { 504 | peakHold.set(std::ceil(length)); 505 | // Overshoot slightly but don't exceed 1 506 | stepMultiplier = Sample(1.0001)/std::max(1.0001, length); 507 | } 508 | void reset(Sample start=lowest) { 509 | peakHold.reset(start); 510 | set(peakHold.size()); 511 | value = start; 512 | } 513 | 514 | Sample operator ()(Sample v) { 515 | Sample peak = peakHold.read(); 516 | peakHold(v); 517 | return value = std::max(v, value + (v - peak)*stepMultiplier); 518 | } 519 | }; 520 | 521 | /** @} */ 522 | }} // signalsmith::envelopes:: 523 | #endif // include guard 524 | -------------------------------------------------------------------------------- /fft.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_FFT_V5 4 | #define SIGNALSMITH_FFT_V5 5 | 6 | #include "./perf.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace signalsmith { namespace fft { 13 | /** @defgroup FFT FFT (complex and real) 14 | @brief Fourier transforms (complex and real) 15 | 16 | @{ 17 | @file 18 | */ 19 | 20 | namespace _fft_impl { 21 | 22 | template 23 | SIGNALSMITH_INLINE V complexReal(const std::complex &c) { 24 | return ((V*)(&c))[0]; 25 | } 26 | template 27 | SIGNALSMITH_INLINE V complexImag(const std::complex &c) { 28 | return ((V*)(&c))[1]; 29 | } 30 | 31 | // Complex multiplication has edge-cases around Inf/NaN - handling those properly makes std::complex non-inlineable, so we use our own 32 | template 33 | SIGNALSMITH_INLINE std::complex complexMul(const std::complex &a, const std::complex &b) { 34 | V aReal = complexReal(a), aImag = complexImag(a); 35 | V bReal = complexReal(b), bImag = complexImag(b); 36 | return conjugateSecond ? std::complex{ 37 | bReal*aReal + bImag*aImag, 38 | bReal*aImag - bImag*aReal 39 | } : std::complex{ 40 | aReal*bReal - aImag*bImag, 41 | aReal*bImag + aImag*bReal 42 | }; 43 | } 44 | 45 | template 46 | SIGNALSMITH_INLINE std::complex complexAddI(const std::complex &a, const std::complex &b) { 47 | V aReal = complexReal(a), aImag = complexImag(a); 48 | V bReal = complexReal(b), bImag = complexImag(b); 49 | return flipped ? std::complex{ 50 | aReal + bImag, 51 | aImag - bReal 52 | } : std::complex{ 53 | aReal - bImag, 54 | aImag + bReal 55 | }; 56 | } 57 | 58 | // Use SFINAE to get an iterator from std::begin(), if supported - otherwise assume the value itself is an iterator 59 | template 60 | struct GetIterator { 61 | static T get(const T &t) { 62 | return t; 63 | } 64 | }; 65 | template 66 | struct GetIterator()))> { 67 | static auto get(const T &t) -> decltype(std::begin(t)) { 68 | return std::begin(t); 69 | } 70 | }; 71 | } 72 | 73 | /** Floating-point FFT implementation. 74 | It is fast for 2^a * 3^b. 75 | Here are the peak and RMS errors for `float`/`double` computation: 76 | \diagram{fft-errors.svg Simulated errors for pure-tone harmonic inputs\, compared to a theoretical upper bound from "Roundoff error analysis of the fast Fourier transform" (G. Ramos, 1971)} 77 | */ 78 | template 79 | class FFT { 80 | using complex = std::complex; 81 | size_t _size; 82 | std::vector workingVector; 83 | 84 | enum class StepType { 85 | generic, step2, step3, step4 86 | }; 87 | struct Step { 88 | StepType type; 89 | size_t factor; 90 | size_t startIndex; 91 | size_t innerRepeats; 92 | size_t outerRepeats; 93 | size_t twiddleIndex; 94 | }; 95 | std::vector factors; 96 | std::vector plan; 97 | std::vector twiddleVector; 98 | 99 | struct PermutationPair {size_t from, to;}; 100 | std::vector permutation; 101 | 102 | void addPlanSteps(size_t factorIndex, size_t start, size_t length, size_t repeats) { 103 | if (factorIndex >= factors.size()) return; 104 | 105 | size_t factor = factors[factorIndex]; 106 | if (factorIndex + 1 < factors.size()) { 107 | if (factors[factorIndex] == 2 && factors[factorIndex + 1] == 2) { 108 | ++factorIndex; 109 | factor = 4; 110 | } 111 | } 112 | 113 | size_t subLength = length/factor; 114 | Step mainStep{StepType::generic, factor, start, subLength, repeats, twiddleVector.size()}; 115 | 116 | if (factor == 2) mainStep.type = StepType::step2; 117 | if (factor == 3) mainStep.type = StepType::step3; 118 | if (factor == 4) mainStep.type = StepType::step4; 119 | 120 | // Twiddles 121 | bool foundStep = false; 122 | for (const Step &existingStep : plan) { 123 | if (existingStep.factor == mainStep.factor && existingStep.innerRepeats == mainStep.innerRepeats) { 124 | foundStep = true; 125 | mainStep.twiddleIndex = existingStep.twiddleIndex; 126 | break; 127 | } 128 | } 129 | if (!foundStep) { 130 | for (size_t i = 0; i < subLength; ++i) { 131 | for (size_t f = 0; f < factor; ++f) { 132 | double phase = 2*M_PI*i*f/length; 133 | complex twiddle = {V(std::cos(phase)), V(-std::sin(phase))}; 134 | twiddleVector.push_back(twiddle); 135 | } 136 | } 137 | } 138 | 139 | if (repeats == 1 && sizeof(complex)*subLength > 65536) { 140 | for (size_t i = 0; i < factor; ++i) { 141 | addPlanSteps(factorIndex + 1, start + i*subLength, subLength, 1); 142 | } 143 | } else { 144 | addPlanSteps(factorIndex + 1, start, subLength, repeats*factor); 145 | } 146 | plan.push_back(mainStep); 147 | } 148 | void setPlan() { 149 | factors.resize(0); 150 | size_t size = _size, factor = 2; 151 | while (size > 1) { 152 | if (size%factor == 0) { 153 | factors.push_back(factor); 154 | size /= factor; 155 | } else if (factor > sqrt(size)) { 156 | factor = size; 157 | } else { 158 | ++factor; 159 | } 160 | } 161 | 162 | plan.resize(0); 163 | twiddleVector.resize(0); 164 | addPlanSteps(0, 0, _size, 1); 165 | twiddleVector.shrink_to_fit(); 166 | 167 | permutation.resize(0); 168 | permutation.reserve(_size); 169 | permutation.push_back(PermutationPair{0, 0}); 170 | size_t indexLow = 0, indexHigh = factors.size(); 171 | size_t inputStepLow = _size, outputStepLow = 1; 172 | size_t inputStepHigh = 1, outputStepHigh = _size; 173 | while (outputStepLow*inputStepHigh < _size) { 174 | size_t f, inputStep, outputStep; 175 | if (outputStepLow <= inputStepHigh) { 176 | f = factors[indexLow++]; 177 | inputStep = (inputStepLow /= f); 178 | outputStep = outputStepLow; 179 | outputStepLow *= f; 180 | } else { 181 | f = factors[--indexHigh]; 182 | inputStep = inputStepHigh; 183 | inputStepHigh *= f; 184 | outputStep = (outputStepHigh /= f); 185 | } 186 | size_t oldSize = permutation.size(); 187 | for (size_t i = 1; i < f; ++i) { 188 | for (size_t j = 0; j < oldSize; ++j) { 189 | PermutationPair pair = permutation[j]; 190 | pair.from += i*inputStep; 191 | pair.to += i*outputStep; 192 | permutation.push_back(pair); 193 | } 194 | } 195 | } 196 | } 197 | 198 | template 199 | void fftStepGeneric(RandomAccessIterator &&origData, const Step &step) { 200 | complex *working = workingVector.data(); 201 | const size_t stride = step.innerRepeats; 202 | 203 | for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) { 204 | RandomAccessIterator data = origData; 205 | 206 | const complex *twiddles = twiddleVector.data() + step.twiddleIndex; 207 | const size_t factor = step.factor; 208 | for (size_t repeat = 0; repeat < step.innerRepeats; ++repeat) { 209 | for (size_t i = 0; i < step.factor; ++i) { 210 | working[i] = _fft_impl::complexMul(data[i*stride], twiddles[i]); 211 | } 212 | for (size_t f = 0; f < factor; ++f) { 213 | complex sum = working[0]; 214 | for (size_t i = 1; i < factor; ++i) { 215 | double phase = 2*M_PI*f*i/factor; 216 | complex twiddle = {V(std::cos(phase)), V(-std::sin(phase))}; 217 | sum += _fft_impl::complexMul(working[i], twiddle); 218 | } 219 | data[f*stride] = sum; 220 | } 221 | ++data; 222 | twiddles += factor; 223 | } 224 | origData += step.factor*step.innerRepeats; 225 | } 226 | } 227 | 228 | template 229 | SIGNALSMITH_INLINE void fftStep2(RandomAccessIterator &&origData, const Step &step) { 230 | const size_t stride = step.innerRepeats; 231 | const complex *origTwiddles = twiddleVector.data() + step.twiddleIndex; 232 | for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) { 233 | const complex* twiddles = origTwiddles; 234 | for (RandomAccessIterator data = origData; data < origData + stride; ++data) { 235 | complex A = data[0]; 236 | complex B = _fft_impl::complexMul(data[stride], twiddles[1]); 237 | 238 | data[0] = A + B; 239 | data[stride] = A - B; 240 | twiddles += 2; 241 | } 242 | origData += 2*stride; 243 | } 244 | } 245 | 246 | template 247 | SIGNALSMITH_INLINE void fftStep3(RandomAccessIterator &&origData, const Step &step) { 248 | constexpr complex factor3 = {-0.5, inverse ? 0.8660254037844386 : -0.8660254037844386}; 249 | const size_t stride = step.innerRepeats; 250 | const complex *origTwiddles = twiddleVector.data() + step.twiddleIndex; 251 | 252 | for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) { 253 | const complex* twiddles = origTwiddles; 254 | for (RandomAccessIterator data = origData; data < origData + stride; ++data) { 255 | complex A = data[0]; 256 | complex B = _fft_impl::complexMul(data[stride], twiddles[1]); 257 | complex C = _fft_impl::complexMul(data[stride*2], twiddles[2]); 258 | 259 | complex realSum = A + (B + C)*factor3.real(); 260 | complex imagSum = (B - C)*factor3.imag(); 261 | 262 | data[0] = A + B + C; 263 | data[stride] = _fft_impl::complexAddI(realSum, imagSum); 264 | data[stride*2] = _fft_impl::complexAddI(realSum, imagSum); 265 | 266 | twiddles += 3; 267 | } 268 | origData += 3*stride; 269 | } 270 | } 271 | 272 | template 273 | SIGNALSMITH_INLINE void fftStep4(RandomAccessIterator &&origData, const Step &step) { 274 | const size_t stride = step.innerRepeats; 275 | const complex *origTwiddles = twiddleVector.data() + step.twiddleIndex; 276 | 277 | for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) { 278 | const complex* twiddles = origTwiddles; 279 | for (RandomAccessIterator data = origData; data < origData + stride; ++data) { 280 | complex A = data[0]; 281 | complex C = _fft_impl::complexMul(data[stride], twiddles[2]); 282 | complex B = _fft_impl::complexMul(data[stride*2], twiddles[1]); 283 | complex D = _fft_impl::complexMul(data[stride*3], twiddles[3]); 284 | 285 | complex sumAC = A + C, sumBD = B + D; 286 | complex diffAC = A - C, diffBD = B - D; 287 | 288 | data[0] = sumAC + sumBD; 289 | data[stride] = _fft_impl::complexAddI(diffAC, diffBD); 290 | data[stride*2] = sumAC - sumBD; 291 | data[stride*3] = _fft_impl::complexAddI(diffAC, diffBD); 292 | 293 | twiddles += 4; 294 | } 295 | origData += 4*stride; 296 | } 297 | } 298 | 299 | template 300 | void permute(InputIterator input, OutputIterator data) { 301 | for (auto pair : permutation) { 302 | data[pair.from] = input[pair.to]; 303 | } 304 | } 305 | 306 | template 307 | void run(InputIterator &&input, OutputIterator &&data) { 308 | permute(input, data); 309 | 310 | for (const Step &step : plan) { 311 | switch (step.type) { 312 | case StepType::generic: 313 | fftStepGeneric(data + step.startIndex, step); 314 | break; 315 | case StepType::step2: 316 | fftStep2(data + step.startIndex, step); 317 | break; 318 | case StepType::step3: 319 | fftStep3(data + step.startIndex, step); 320 | break; 321 | case StepType::step4: 322 | fftStep4(data + step.startIndex, step); 323 | break; 324 | } 325 | } 326 | } 327 | 328 | static bool validSize(size_t size) { 329 | constexpr static bool filter[32] = { 330 | 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, // 0-9 331 | 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, // 10-19 332 | 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, // 20-29 333 | 0, 0 334 | }; 335 | return filter[size]; 336 | } 337 | public: 338 | static size_t fastSizeAbove(size_t size) { 339 | size_t power2 = 1; 340 | while (size >= 32) { 341 | size = (size - 1)/2 + 1; 342 | power2 *= 2; 343 | } 344 | while (size < 32 && !validSize(size)) { 345 | ++size; 346 | } 347 | return power2*size; 348 | } 349 | static size_t fastSizeBelow(size_t size) { 350 | size_t power2 = 1; 351 | while (size >= 32) { 352 | size /= 2; 353 | power2 *= 2; 354 | } 355 | while (size > 1 && !validSize(size)) { 356 | --size; 357 | } 358 | return power2*size; 359 | } 360 | 361 | FFT(size_t size, int fastDirection=0) : _size(0) { 362 | if (fastDirection > 0) size = fastSizeAbove(size); 363 | if (fastDirection < 0) size = fastSizeBelow(size); 364 | this->setSize(size); 365 | } 366 | 367 | size_t setSize(size_t size) { 368 | if (size != _size) { 369 | _size = size; 370 | workingVector.resize(size); 371 | setPlan(); 372 | } 373 | return _size; 374 | } 375 | size_t setFastSizeAbove(size_t size) { 376 | return setSize(fastSizeAbove(size)); 377 | } 378 | size_t setFastSizeBelow(size_t size) { 379 | return setSize(fastSizeBelow(size)); 380 | } 381 | const size_t & size() const { 382 | return _size; 383 | } 384 | 385 | template 386 | void fft(InputIterator &&input, OutputIterator &&output) { 387 | auto inputIter = _fft_impl::GetIterator::get(input); 388 | auto outputIter = _fft_impl::GetIterator::get(output); 389 | return run(inputIter, outputIter); 390 | } 391 | 392 | template 393 | void ifft(InputIterator &&input, OutputIterator &&output) { 394 | auto inputIter = _fft_impl::GetIterator::get(input); 395 | auto outputIter = _fft_impl::GetIterator::get(output); 396 | return run(inputIter, outputIter); 397 | } 398 | }; 399 | 400 | struct FFTOptions { 401 | static constexpr int halfFreqShift = 1; 402 | }; 403 | 404 | template 405 | class RealFFT { 406 | static constexpr bool modified = (optionFlags&FFTOptions::halfFreqShift); 407 | 408 | using complex = std::complex; 409 | std::vector complexBuffer1, complexBuffer2; 410 | std::vector twiddlesMinusI; 411 | std::vector modifiedRotations; 412 | FFT complexFft; 413 | public: 414 | static size_t fastSizeAbove(size_t size) { 415 | return FFT::fastSizeAbove((size + 1)/2)*2; 416 | } 417 | static size_t fastSizeBelow(size_t size) { 418 | return FFT::fastSizeBelow(size/2)*2; 419 | } 420 | 421 | RealFFT(size_t size=0, int fastDirection=0) : complexFft(0) { 422 | if (fastDirection > 0) size = fastSizeAbove(size); 423 | if (fastDirection < 0) size = fastSizeBelow(size); 424 | this->setSize(std::max(size, 2)); 425 | } 426 | 427 | size_t setSize(size_t size) { 428 | complexBuffer1.resize(size/2); 429 | complexBuffer2.resize(size/2); 430 | 431 | size_t hhSize = size/4 + 1; 432 | twiddlesMinusI.resize(hhSize); 433 | for (size_t i = 0; i < hhSize; ++i) { 434 | V rotPhase = -2*M_PI*(modified ? i + 0.5 : i)/size; 435 | twiddlesMinusI[i] = {std::sin(rotPhase), -std::cos(rotPhase)}; 436 | } 437 | if (modified) { 438 | modifiedRotations.resize(size/2); 439 | for (size_t i = 0; i < size/2; ++i) { 440 | V rotPhase = -2*M_PI*i/size; 441 | modifiedRotations[i] = {std::cos(rotPhase), std::sin(rotPhase)}; 442 | } 443 | } 444 | 445 | return complexFft.setSize(size/2); 446 | } 447 | size_t setFastSizeAbove(size_t size) { 448 | return setSize(fastSizeAbove(size)); 449 | } 450 | size_t setFastSizeBelow(size_t size) { 451 | return setSize(fastSizeBelow(size)); 452 | } 453 | size_t size() const { 454 | return complexFft.size()*2; 455 | } 456 | 457 | template 458 | void fft(InputIterator &&input, OutputIterator &&output) { 459 | size_t hSize = complexFft.size(); 460 | for (size_t i = 0; i < hSize; ++i) { 461 | if (modified) { 462 | complexBuffer1[i] = _fft_impl::complexMul({input[2*i], input[2*i + 1]}, modifiedRotations[i]); 463 | } else { 464 | complexBuffer1[i] = {input[2*i], input[2*i + 1]}; 465 | } 466 | } 467 | 468 | complexFft.fft(complexBuffer1.data(), complexBuffer2.data()); 469 | 470 | if (!modified) output[0] = { 471 | complexBuffer2[0].real() + complexBuffer2[0].imag(), 472 | complexBuffer2[0].real() - complexBuffer2[0].imag() 473 | }; 474 | for (size_t i = modified ? 0 : 1; i <= hSize/2; ++i) { 475 | size_t conjI = modified ? (hSize - 1 - i) : (hSize - i); 476 | 477 | complex odd = (complexBuffer2[i] + conj(complexBuffer2[conjI]))*(V)0.5; 478 | complex evenI = (complexBuffer2[i] - conj(complexBuffer2[conjI]))*(V)0.5; 479 | complex evenRotMinusI = _fft_impl::complexMul(evenI, twiddlesMinusI[i]); 480 | 481 | output[i] = odd + evenRotMinusI; 482 | output[conjI] = conj(odd - evenRotMinusI); 483 | } 484 | } 485 | 486 | template 487 | void ifft(InputIterator &&input, OutputIterator &&output) { 488 | size_t hSize = complexFft.size(); 489 | if (!modified) complexBuffer1[0] = { 490 | input[0].real() + input[0].imag(), 491 | input[0].real() - input[0].imag() 492 | }; 493 | for (size_t i = modified ? 0 : 1; i <= hSize/2; ++i) { 494 | size_t conjI = modified ? (hSize - 1 - i) : (hSize - i); 495 | complex v = input[i], v2 = input[conjI]; 496 | 497 | complex odd = v + conj(v2); 498 | complex evenRotMinusI = v - conj(v2); 499 | complex evenI = _fft_impl::complexMul(evenRotMinusI, twiddlesMinusI[i]); 500 | 501 | complexBuffer1[i] = odd + evenI; 502 | complexBuffer1[conjI] = conj(odd - evenI); 503 | } 504 | 505 | complexFft.ifft(complexBuffer1.data(), complexBuffer2.data()); 506 | 507 | for (size_t i = 0; i < hSize; ++i) { 508 | complex v = complexBuffer2[i]; 509 | if (modified) v = _fft_impl::complexMul(v, modifiedRotations[i]); 510 | output[2*i] = v.real(); 511 | output[2*i + 1] = v.imag(); 512 | } 513 | } 514 | }; 515 | 516 | template 517 | struct ModifiedRealFFT : public RealFFT { 518 | using RealFFT::RealFFT; 519 | }; 520 | 521 | /// @} 522 | }} // namespace 523 | #endif // include guard 524 | -------------------------------------------------------------------------------- /filters.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_FILTERS_H 4 | #define SIGNALSMITH_DSP_FILTERS_H 5 | 6 | #include "./perf.h" 7 | 8 | #include 9 | #include 10 | 11 | namespace signalsmith { 12 | namespace filters { 13 | /** @defgroup Filters Basic filters 14 | @brief Classes for some common filter types 15 | 16 | @{ 17 | @file 18 | */ 19 | 20 | /** Filter design methods. 21 | These differ mostly in how they handle frequency-warping near Nyquist: 22 | \diagram{filters-lowpass.svg} 23 | \diagram{filters-highpass.svg} 24 | \diagram{filters-peak.svg} 25 | \diagram{filters-bandpass.svg} 26 | \diagram{filters-notch.svg} 27 | \diagram{filters-high-shelf.svg} 28 | \diagram{filters-low-shelf.svg} 29 | \diagram{filters-allpass.svg} 30 | */ 31 | enum class BiquadDesign { 32 | bilinear, ///< Bilinear transform, adjusting for centre frequency but not bandwidth 33 | cookbook, ///< RBJ's "Audio EQ Cookbook". Based on `bilinear`, adjusting bandwidth (for peak/notch/bandpass) to preserve the ratio between upper/lower boundaries. This performs oddly near Nyquist. 34 | oneSided, ///< Based on `bilinear`, adjusting bandwidth to preserve the lower boundary (leaving the upper one loose). 35 | vicanek ///< From Martin Vicanek's [Matched Second Order Digital Filters](https://vicanek.de/articles/BiquadFits.pdf). Falls back to `oneSided` for shelf and allpass filters. This takes the poles from the impulse-invariant approach, and then picks the zeros to create a better match. This means that Nyquist is not 0dB for peak/notch (or -Inf for lowpass), but it is a decent match to the analogue prototype. 36 | }; 37 | 38 | /** A standard biquad. 39 | 40 | This is not guaranteed to be stable if modulated at audio rate. 41 | 42 | The default highpass/lowpass bandwidth (`defaultBandwidth`) produces a Butterworth filter when bandwidth-compensation is disabled. 43 | 44 | Bandwidth compensation defaults to `BiquadDesign::oneSided` (or `BiquadDesign::cookbook` if `cookbookBandwidth` is enabled) for all filter types aside from highpass/lowpass (which use `BiquadDesign::bilinear`).*/ 45 | template 46 | class BiquadStatic { 47 | static constexpr BiquadDesign bwDesign = cookbookBandwidth ? BiquadDesign::cookbook : BiquadDesign::oneSided; 48 | Sample a1 = 0, a2 = 0, b0 = 1, b1 = 0, b2 = 0; 49 | Sample x1 = 0, x2 = 0, y1 = 0, y2 = 0; 50 | 51 | enum class Type {highpass, lowpass, highShelf, lowShelf, bandpass, notch, peak, allpass}; 52 | 53 | struct FreqSpec { 54 | double scaledFreq; 55 | double w0, sinW0, cosW0; 56 | double inv2Q; 57 | 58 | FreqSpec(double freq, BiquadDesign design) { 59 | scaledFreq = std::max(1e-6, std::min(0.4999, freq)); 60 | if (design == BiquadDesign::cookbook) { 61 | scaledFreq = std::min(0.45, scaledFreq); 62 | } 63 | w0 = 2*M_PI*scaledFreq; 64 | cosW0 = std::cos(w0); 65 | sinW0 = std::sin(w0); 66 | } 67 | 68 | void oneSidedCompQ() { 69 | // Ratio between our (digital) lower boundary f1 and centre f0 70 | double f1Factor = std::sqrt(inv2Q*inv2Q + 1) - inv2Q; 71 | // Bilinear means discrete-time freq f = continuous-time freq tan(pi*xf/pi) 72 | double ctF1 = std::tan(M_PI*scaledFreq*f1Factor), invCtF0 = (1 + cosW0)/sinW0; 73 | double ctF1Factor = ctF1*invCtF0; 74 | inv2Q = 0.5/ctF1Factor - 0.5*ctF1Factor; 75 | } 76 | }; 77 | SIGNALSMITH_INLINE static FreqSpec octaveSpec(double scaledFreq, double octaves, BiquadDesign design) { 78 | FreqSpec spec(scaledFreq, design); 79 | 80 | if (design == BiquadDesign::cookbook) { 81 | // Approximately preserves bandwidth between halfway points 82 | octaves *= spec.w0/spec.sinW0; 83 | } 84 | spec.inv2Q = std::sinh(std::log(2)*0.5*octaves); // 1/(2Q) 85 | if (design == BiquadDesign::oneSided) spec.oneSidedCompQ(); 86 | return spec; 87 | } 88 | SIGNALSMITH_INLINE static FreqSpec qSpec(double scaledFreq, double q, BiquadDesign design) { 89 | FreqSpec spec(scaledFreq, design); 90 | 91 | spec.inv2Q = 0.5/q; 92 | if (design == BiquadDesign::oneSided) spec.oneSidedCompQ(); 93 | return spec; 94 | } 95 | 96 | SIGNALSMITH_INLINE double dbToSqrtGain(double db) { 97 | return std::pow(10, db*0.025); 98 | } 99 | 100 | SIGNALSMITH_INLINE BiquadStatic & configure(Type type, FreqSpec calc, double sqrtGain, BiquadDesign design) { 101 | double w0 = calc.w0; 102 | 103 | if (design == BiquadDesign::vicanek) { 104 | if (type == Type::notch) { // Heuristic for notches near Nyquist 105 | calc.inv2Q *= (1 - calc.scaledFreq*0.5); 106 | } 107 | double Q = (type == Type::peak ? 0.5*sqrtGain : 0.5)/calc.inv2Q; 108 | double q = (type == Type::peak ? 1/sqrtGain : 1)*calc.inv2Q; 109 | double expmqw = std::exp(-q*w0); 110 | double da1, da2; 111 | if (q <= 1) { 112 | a1 = da1 = -2*expmqw*std::cos(std::sqrt(1 - q*q)*w0); 113 | } else { 114 | a1 = da1 = -2*expmqw*std::cosh(std::sqrt(q*q - 1)*w0); 115 | } 116 | a2 = da2 = expmqw*expmqw; 117 | double sinpd2 = std::sin(w0/2); 118 | double p0 = 1 - sinpd2*sinpd2, p1 = sinpd2*sinpd2, p2 = 4*p0*p1; 119 | double A0 = 1 + da1 + da2, A1 = 1 - da1 + da2, A2 = -4*da2; 120 | A0 *= A0; 121 | A1 *= A1; 122 | if (type == Type::lowpass) { 123 | double R1 = (A0*p0 + A1*p1 + A2*p2)*Q*Q; 124 | double B0 = A0, B1 = (R1 - B0*p0)/p1; 125 | b0 = 0.5*(std::sqrt(B0) + std::sqrt(std::max(0.0, B1))); 126 | b1 = std::sqrt(B0) - b0; 127 | b2 = 0; 128 | return *this; 129 | } else if (type == Type::highpass) { 130 | b2 = b0 = std::sqrt(A0*p0 + A1*p1 + A2*p2)*Q/(4*p1); 131 | b1 = -2*b0; 132 | return *this; 133 | } else if (type == Type::bandpass) { 134 | double R1 = A0*p0 + A1*p1 + A2*p2; 135 | double R2 = -A0 + A1 + 4*(p0 - p1)*A2; 136 | double B2 = (R1 - R2*p1)/(4*p1*p1); 137 | double B1 = R2 + 4*(p1 - p0)*B2; 138 | b1 = -0.5*std::sqrt(std::max(0.0, B1)); 139 | b0 = 0.5*(std::sqrt(std::max(0.0, B2 + 0.25*B1)) - b1); 140 | b2 = -b0 - b1; 141 | return *this; 142 | } else if (type == Type::notch) { 143 | // The Vicanek paper doesn't cover notches (band-stop), but we know where the zeros should be: 144 | b0 = 1; 145 | double db1 = -2*std::cos(w0); // might be higher precision 146 | b1 = db1; 147 | b2 = 1; 148 | // Scale so that B0 == A0 to get 0dB at f=0 149 | double scale = std::sqrt(A0)/(b0 + db1 + b2); 150 | b0 *= scale; 151 | b1 *= scale; 152 | b2 *= scale; 153 | return *this; 154 | } else if (type == Type::peak) { 155 | double G2 = (sqrtGain*sqrtGain)*(sqrtGain*sqrtGain); 156 | double R1 = (A0*p0 + A1*p1 + A2*p2)*G2; 157 | double R2 = (-A0 + A1 + 4*(p0 - p1)*A2)*G2; 158 | double B0 = A0; 159 | double B2 = (R1 - R2*p1 - B0)/(4*p1*p1); 160 | double B1 = R2 + B0 + 4*(p1 - p0)*B2; 161 | double W = 0.5*(std::sqrt(B0) + std::sqrt(std::max(0.0, B1))); 162 | b0 = 0.5*(W + std::sqrt(std::max(0.0, W*W + B2))); 163 | b1 = 0.5*(std::sqrt(B0) - std::sqrt(std::max(0.0, B1))); 164 | b2 = -B2/(4*b0); 165 | return *this; 166 | } 167 | // All others fall back to `oneSided` 168 | design = BiquadDesign::oneSided; 169 | calc.oneSidedCompQ(); 170 | } 171 | 172 | double alpha = calc.sinW0*calc.inv2Q; 173 | double A = sqrtGain, sqrtA2alpha = 2*std::sqrt(A)*alpha; 174 | 175 | double a0; 176 | if (type == Type::highpass) { 177 | b1 = -1 - calc.cosW0; 178 | b0 = b2 = (1 + calc.cosW0)*0.5; 179 | a0 = 1 + alpha; 180 | a1 = -2*calc.cosW0; 181 | a2 = 1 - alpha; 182 | } else if (type == Type::lowpass) { 183 | b1 = 1 - calc.cosW0; 184 | b0 = b2 = b1*0.5; 185 | a0 = 1 + alpha; 186 | a1 = -2*calc.cosW0; 187 | a2 = 1 - alpha; 188 | } else if (type == Type::highShelf) { 189 | b0 = A*((A+1)+(A-1)*calc.cosW0+sqrtA2alpha); 190 | b2 = A*((A+1)+(A-1)*calc.cosW0-sqrtA2alpha); 191 | b1 = -2*A*((A-1)+(A+1)*calc.cosW0); 192 | a0 = (A+1)-(A-1)*calc.cosW0+sqrtA2alpha; 193 | a2 = (A+1)-(A-1)*calc.cosW0-sqrtA2alpha; 194 | a1 = 2*((A-1)-(A+1)*calc.cosW0); 195 | } else if (type == Type::lowShelf) { 196 | b0 = A*((A+1)-(A-1)*calc.cosW0+sqrtA2alpha); 197 | b2 = A*((A+1)-(A-1)*calc.cosW0-sqrtA2alpha); 198 | b1 = 2*A*((A-1)-(A+1)*calc.cosW0); 199 | a0 = (A+1)+(A-1)*calc.cosW0+sqrtA2alpha; 200 | a2 = (A+1)+(A-1)*calc.cosW0-sqrtA2alpha; 201 | a1 = -2*((A-1)+(A+1)*calc.cosW0); 202 | } else if (type == Type::bandpass) { 203 | b0 = alpha; 204 | b1 = 0; 205 | b2 = -alpha; 206 | a0 = 1 + alpha; 207 | a1 = -2*calc.cosW0; 208 | a2 = 1 - alpha; 209 | } else if (type == Type::notch) { 210 | b0 = 1; 211 | b1 = -2*calc.cosW0; 212 | b2 = 1; 213 | a0 = 1 + alpha; 214 | a1 = b1; 215 | a2 = 1 - alpha; 216 | } else if (type == Type::peak) { 217 | b0 = 1 + alpha*A; 218 | b1 = -2*calc.cosW0; 219 | b2 = 1 - alpha*A; 220 | a0 = 1 + alpha/A; 221 | a1 = b1; 222 | a2 = 1 - alpha/A; 223 | } else if (type == Type::allpass) { 224 | a0 = b2 = 1 + alpha; 225 | a1 = b1 = -2*calc.cosW0; 226 | a2 = b0 = 1 - alpha; 227 | } else { 228 | // reset to neutral 229 | a1 = a2 = b1 = b2 = 0; 230 | a0 = b0 = 1; 231 | } 232 | double invA0 = 1/a0; 233 | b0 *= invA0; 234 | b1 *= invA0; 235 | b2 *= invA0; 236 | a1 *= invA0; 237 | a2 *= invA0; 238 | return *this; 239 | } 240 | public: 241 | static constexpr double defaultQ = 0.7071067811865476; // sqrt(0.5) 242 | static constexpr double defaultBandwidth = 1.8999686269529916; // equivalent to above Q 243 | 244 | Sample operator ()(Sample x0) { 245 | Sample y0 = x0*b0 + x1*b1 + x2*b2 - y1*a1 - y2*a2; 246 | y2 = y1; 247 | y1 = y0; 248 | x2 = x1; 249 | x1 = x0; 250 | return y0; 251 | } 252 | 253 | void reset() { 254 | x1 = x2 = y1 = y2 = 0; 255 | } 256 | 257 | std::complex response(Sample scaledFreq) const { 258 | Sample w = scaledFreq*Sample(2*M_PI); 259 | std::complex invZ = {std::cos(w), -std::sin(w)}, invZ2 = invZ*invZ; 260 | return (b0 + invZ*b1 + invZ2*b2)/(Sample(1) + invZ*a1 + invZ2*a2); 261 | } 262 | Sample responseDb(Sample scaledFreq) const { 263 | Sample w = scaledFreq*Sample(2*M_PI); 264 | std::complex invZ = {std::cos(w), -std::sin(w)}, invZ2 = invZ*invZ; 265 | Sample energy = std::norm(b0 + invZ*b1 + invZ2*b2)/std::norm(Sample(1) + invZ*a1 + invZ2*a2); 266 | return 10*std::log10(energy); 267 | } 268 | 269 | /// @name Lowpass 270 | /// @{ 271 | BiquadStatic & lowpass(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=BiquadDesign::bilinear) { 272 | return configure(Type::lowpass, octaveSpec(scaledFreq, octaves, design), 0, design); 273 | } 274 | BiquadStatic & lowpassQ(double scaledFreq, double q, BiquadDesign design=BiquadDesign::bilinear) { 275 | return configure(Type::lowpass, qSpec(scaledFreq, q, design), 0, design); 276 | } 277 | /// @deprecated use `BiquadDesign` instead 278 | void lowpass(double scaledFreq, double octaves, bool correctBandwidth) { 279 | lowpass(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 280 | } 281 | /// @deprecated By the time you care about `design`, you should care about the bandwidth 282 | BiquadStatic & lowpass(double scaledFreq, BiquadDesign design) { 283 | return lowpass(scaledFreq, defaultBandwidth, design); 284 | } 285 | /// @} 286 | 287 | /// @name Highpass 288 | /// @{ 289 | BiquadStatic & highpass(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=BiquadDesign::bilinear) { 290 | return configure(Type::highpass, octaveSpec(scaledFreq, octaves, design), 0, design); 291 | } 292 | BiquadStatic & highpassQ(double scaledFreq, double q, BiquadDesign design=BiquadDesign::bilinear) { 293 | return configure(Type::highpass, qSpec(scaledFreq, q, design), 0, design); 294 | } 295 | /// @deprecated use `BiquadDesign` instead 296 | void highpass(double scaledFreq, double octaves, bool correctBandwidth) { 297 | highpass(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 298 | } 299 | /// @deprecated By the time you care about `design`, you should care about the bandwidth 300 | BiquadStatic & highpass(double scaledFreq, BiquadDesign design) { 301 | return highpass(scaledFreq, defaultBandwidth, design); 302 | } 303 | /// @} 304 | 305 | /// @name Bandpass 306 | /// @{ 307 | BiquadStatic & bandpass(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) { 308 | return configure(Type::bandpass, octaveSpec(scaledFreq, octaves, design), 0, design); 309 | } 310 | BiquadStatic & bandpassQ(double scaledFreq, double q, BiquadDesign design=bwDesign) { 311 | return configure(Type::bandpass, qSpec(scaledFreq, q, design), 0, design); 312 | } 313 | /// @deprecated use `BiquadDesign` instead 314 | void bandpass(double scaledFreq, double octaves, bool correctBandwidth) { 315 | bandpass(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 316 | } 317 | /// @deprecated By the time you care about `design`, you should care about the bandwidth 318 | BiquadStatic & bandpass(double scaledFreq, BiquadDesign design) { 319 | return bandpass(scaledFreq, defaultBandwidth, design); 320 | } 321 | /// @} 322 | 323 | /// @name Notch 324 | /// @{ 325 | BiquadStatic & notch(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) { 326 | return configure(Type::notch, octaveSpec(scaledFreq, octaves, design), 0, design); 327 | } 328 | BiquadStatic & notchQ(double scaledFreq, double q, BiquadDesign design=bwDesign) { 329 | return configure(Type::notch, qSpec(scaledFreq, q, design), 0, design); 330 | } 331 | /// @deprecated use `BiquadDesign` instead 332 | void notch(double scaledFreq, double octaves, bool correctBandwidth) { 333 | notch(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 334 | } 335 | /// @deprecated By the time you care about `design`, you should care about the bandwidth 336 | BiquadStatic & notch(double scaledFreq, BiquadDesign design) { 337 | return notch(scaledFreq, defaultBandwidth, design); 338 | } 339 | /// @deprecated alias for `.notch()` 340 | void bandStop(double scaledFreq, double octaves=1, bool correctBandwidth=true) { 341 | notch(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 342 | } 343 | /// @} 344 | 345 | /// @name Peak 346 | /// @{ 347 | BiquadStatic & peak(double scaledFreq, double gain, double octaves=1, BiquadDesign design=bwDesign) { 348 | return configure(Type::peak, octaveSpec(scaledFreq, octaves, design), std::sqrt(gain), design); 349 | } 350 | BiquadStatic & peakDb(double scaledFreq, double db, double octaves=1, BiquadDesign design=bwDesign) { 351 | return configure(Type::peak, octaveSpec(scaledFreq, octaves, design), dbToSqrtGain(db), design); 352 | } 353 | BiquadStatic & peakQ(double scaledFreq, double gain, double q, BiquadDesign design=bwDesign) { 354 | return configure(Type::peak, qSpec(scaledFreq, q, design), std::sqrt(gain), design); 355 | } 356 | BiquadStatic & peakDbQ(double scaledFreq, double db, double q, BiquadDesign design=bwDesign) { 357 | return configure(Type::peak, qSpec(scaledFreq, q, design), dbToSqrtGain(db), design); 358 | } 359 | /// @deprecated By the time you care about `design`, you should care about the bandwidth 360 | BiquadStatic & peak(double scaledFreq, double gain, BiquadDesign design) { 361 | return peak(scaledFreq, gain, 1, design); 362 | } 363 | /// @} 364 | 365 | /// @name High shelf 366 | /// @{ 367 | BiquadStatic & highShelf(double scaledFreq, double gain, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) { 368 | return configure(Type::highShelf, octaveSpec(scaledFreq, octaves, design), std::sqrt(gain), design); 369 | } 370 | BiquadStatic & highShelfDb(double scaledFreq, double db, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) { 371 | return configure(Type::highShelf, octaveSpec(scaledFreq, octaves, design), dbToSqrtGain(db), design); 372 | } 373 | BiquadStatic & highShelfQ(double scaledFreq, double gain, double q, BiquadDesign design=bwDesign) { 374 | return configure(Type::highShelf, qSpec(scaledFreq, q, design), std::sqrt(gain), design); 375 | } 376 | BiquadStatic & highShelfDbQ(double scaledFreq, double db, double q, BiquadDesign design=bwDesign) { 377 | return configure(Type::highShelf, qSpec(scaledFreq, q, design), dbToSqrtGain(db), design); 378 | } 379 | /// @deprecated use `BiquadDesign` instead 380 | BiquadStatic & highShelf(double scaledFreq, double gain, double octaves, bool correctBandwidth) { 381 | return highShelf(scaledFreq, gain, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 382 | } 383 | /// @deprecated use `BiquadDesign` instead 384 | BiquadStatic & highShelfDb(double scaledFreq, double db, double octaves, bool correctBandwidth) { 385 | return highShelfDb(scaledFreq, db, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 386 | } 387 | /// @} 388 | 389 | /// @name Low shelf 390 | /// @{ 391 | BiquadStatic & lowShelf(double scaledFreq, double gain, double octaves=2, BiquadDesign design=bwDesign) { 392 | return configure(Type::lowShelf, octaveSpec(scaledFreq, octaves, design), std::sqrt(gain), design); 393 | } 394 | BiquadStatic & lowShelfDb(double scaledFreq, double db, double octaves=2, BiquadDesign design=bwDesign) { 395 | return configure(Type::lowShelf, octaveSpec(scaledFreq, octaves, design), dbToSqrtGain(db), design); 396 | } 397 | BiquadStatic & lowShelfQ(double scaledFreq, double gain, double q, BiquadDesign design=bwDesign) { 398 | return configure(Type::lowShelf, qSpec(scaledFreq, q, design), std::sqrt(gain), design); 399 | } 400 | BiquadStatic & lowShelfDbQ(double scaledFreq, double db, double q, BiquadDesign design=bwDesign) { 401 | return configure(Type::lowShelf, qSpec(scaledFreq, q, design), dbToSqrtGain(db), design); 402 | } 403 | /// @deprecated use `BiquadDesign` instead 404 | BiquadStatic & lowShelf(double scaledFreq, double gain, double octaves, bool correctBandwidth) { 405 | return lowShelf(scaledFreq, gain, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 406 | } 407 | /// @deprecated use `BiquadDesign` instead 408 | BiquadStatic & lowShelfDb(double scaledFreq, double db, double octaves, bool correctBandwidth) { 409 | return lowShelfDb(scaledFreq, db, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear); 410 | } 411 | /// @} 412 | 413 | /// @name Allpass 414 | /// @{ 415 | BiquadStatic & allpass(double scaledFreq, double octaves=1, BiquadDesign design=bwDesign) { 416 | return configure(Type::allpass, octaveSpec(scaledFreq, octaves, design), 0, design); 417 | } 418 | BiquadStatic & allpassQ(double scaledFreq, double q, BiquadDesign design=bwDesign) { 419 | return configure(Type::allpass, qSpec(scaledFreq, q, design), 0, design); 420 | } 421 | /// @} 422 | 423 | BiquadStatic & addGain(double factor) { 424 | b0 *= factor; 425 | b1 *= factor; 426 | b2 *= factor; 427 | return *this; 428 | } 429 | BiquadStatic & addGainDb(double db) { 430 | return addGain(std::pow(10, db*0.05)); 431 | } 432 | }; 433 | 434 | /** @} */ 435 | }} // signalsmith::filters:: 436 | #endif // include guard 437 | -------------------------------------------------------------------------------- /mix.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_MULTI_CHANNEL_H 4 | #define SIGNALSMITH_DSP_MULTI_CHANNEL_H 5 | 6 | #include 7 | 8 | namespace signalsmith { 9 | namespace mix { 10 | /** @defgroup Mix Multichannel mixing 11 | @brief Utilities for stereo/multichannel mixing operations 12 | 13 | @{ 14 | @file 15 | */ 16 | 17 | /** @defgroup Matrices Orthogonal matrices 18 | @brief Some common matrices used for audio 19 | @ingroup Mix 20 | @{ */ 21 | 22 | /// @brief Hadamard: high mixing levels, N log(N) operations 23 | template 24 | class Hadamard { 25 | public: 26 | static_assert(size >= 0, "Size must be positive (or -1 for dynamic)"); 27 | /// Applies the matrix, scaled so it's orthogonal 28 | template 29 | static void inPlace(Data &&data) { 30 | unscaledInPlace(data); 31 | 32 | Sample factor = scalingFactor(); 33 | for (int c = 0; c < size; ++c) { 34 | data[c] *= factor; 35 | } 36 | } 37 | 38 | /// Scaling factor applied to make it orthogonal 39 | static Sample scalingFactor() { 40 | /// TODO: test for C++20, or whatever makes this constexpr. Maybe a `#define` in `common.h`? 41 | return std::sqrt(Sample(1)/(size ? size : 1)); 42 | } 43 | 44 | /// Skips the scaling, so it's a matrix full of `1`s 45 | template 46 | static void unscaledInPlace(Data &&data) { 47 | if (size <= 1) return; 48 | constexpr int hSize = size/2; 49 | 50 | Hadamard::template unscaledInPlace(data); 51 | Hadamard::template unscaledInPlace(data); 52 | 53 | for (int i = 0; i < hSize; ++i) { 54 | Sample a = data[i + startIndex], b = data[i + startIndex + hSize]; 55 | data[i + startIndex] = (a + b); 56 | data[i + startIndex + hSize] = (a - b); 57 | } 58 | } 59 | }; 60 | /// @brief Hadamard with dynamic size 61 | template 62 | class Hadamard { 63 | int size; 64 | public: 65 | Hadamard(int size) : size(size) {} 66 | 67 | /// Applies the matrix, scaled so it's orthogonal 68 | template 69 | void inPlace(Data &&data) const { 70 | unscaledInPlace(data); 71 | 72 | Sample factor = scalingFactor(); 73 | for (int c = 0; c < size; ++c) { 74 | data[c] *= factor; 75 | } 76 | } 77 | 78 | /// Scaling factor applied to make it orthogonal 79 | Sample scalingFactor() const { 80 | return std::sqrt(Sample(1)/(size ? size : 1)); 81 | } 82 | 83 | /// Skips the scaling, so it's a matrix full of `1`s 84 | template 85 | void unscaledInPlace(Data &&data) const { 86 | int hSize = size/2; 87 | while (hSize > 0) { 88 | for (int startIndex = 0; startIndex < size; startIndex += hSize*2) { 89 | for (int i = startIndex; i < startIndex + hSize; ++i) { 90 | Sample a = data[i], b = data[i + hSize]; 91 | data[i] = (a + b); 92 | data[i + hSize] = (a - b); 93 | } 94 | } 95 | hSize /= 2; 96 | } 97 | } 98 | }; 99 | /// @brief Householder: moderate mixing, 2N operations 100 | template 101 | class Householder { 102 | public: 103 | static_assert(size >= 0, "Size must be positive (or -1 for dynamic)"); 104 | template 105 | static void inPlace(Data &&data) { 106 | if (size < 1) return; 107 | /// TODO: test for C++20, which makes `std::complex::operator/` constexpr 108 | const Sample factor = Sample(-2)/Sample(size ? size : 1); 109 | 110 | Sample sum = data[0]; 111 | for (int i = 1; i < size; ++i) { 112 | sum += data[i]; 113 | } 114 | sum *= factor; 115 | for (int i = 0; i < size; ++i) { 116 | data[i] += sum; 117 | } 118 | } 119 | /// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard 120 | constexpr static Sample scalingFactor() { 121 | return 1; 122 | } 123 | }; 124 | /// @brief Householder with dynamic size 125 | template 126 | class Householder { 127 | int size; 128 | public: 129 | Householder(int size) : size(size) {} 130 | 131 | template 132 | void inPlace(Data &&data) const { 133 | if (size < 1) return; 134 | const Sample factor = Sample(-2)/Sample(size ? size : 1); 135 | 136 | Sample sum = data[0]; 137 | for (int i = 1; i < size; ++i) { 138 | sum += data[i]; 139 | } 140 | sum *= factor; 141 | for (int i = 0; i < size; ++i) { 142 | data[i] += sum; 143 | } 144 | } 145 | /// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard 146 | constexpr static Sample scalingFactor() { 147 | return 1; 148 | } 149 | }; 150 | /// @} 151 | 152 | /** @brief Upmix/downmix a stereo signal to an (even) multi-channel signal 153 | 154 | When spreading out, it rotates the input by various amounts (e.g. a four-channel signal would produce `(left, right, mid side)`), such that energy is preserved for each pair. 155 | 156 | When mixing together, it uses the opposite rotations, such that upmix → downmix produces the same stereo signal (when scaled by `.scalingFactor1()`. 157 | */ 158 | template 159 | class StereoMultiMixer { 160 | static_assert((channels/2)*2 == channels, "StereoMultiMixer must have an even number of channels"); 161 | static_assert(channels > 0, "StereoMultiMixer must have a positive number of channels"); 162 | static constexpr int hChannels = channels/2; 163 | std::array coeffs; 164 | public: 165 | StereoMultiMixer() { 166 | coeffs[0] = 1; 167 | coeffs[1] = 0; 168 | for (int i = 1; i < hChannels; ++i) { 169 | double phase = M_PI*i/channels; 170 | coeffs[2*i] = std::cos(phase); 171 | coeffs[2*i + 1] = std::sin(phase); 172 | } 173 | } 174 | 175 | template 176 | void stereoToMulti(In &input, Out &output) const { 177 | output[0] = input[0]; 178 | output[1] = input[1]; 179 | for (int i = 2; i < channels; i += 2) { 180 | output[i] = input[0]*coeffs[i] + input[1]*coeffs[i + 1]; 181 | output[i + 1] = input[1]*coeffs[i] - input[0]*coeffs[i + 1]; 182 | } 183 | } 184 | template 185 | void multiToStereo(In &input, Out &output) const { 186 | output[0] = input[0]; 187 | output[1] = input[1]; 188 | for (int i = 2; i < channels; i += 2) { 189 | output[0] += input[i]*coeffs[i] - input[i + 1]*coeffs[i + 1]; 190 | output[1] += input[i + 1]*coeffs[i] + input[i]*coeffs[i + 1]; 191 | } 192 | } 193 | /// Scaling factor for the downmix, if channels are phase-aligned 194 | static constexpr Sample scalingFactor1() { 195 | return 2/Sample(channels); 196 | } 197 | /// Scaling factor for the downmix, if channels are independent 198 | static Sample scalingFactor2() { 199 | return std::sqrt(scalingFactor1()); 200 | } 201 | }; 202 | 203 | /// A cheap (polynomial) almost-energy-preserving crossfade 204 | /// Maximum energy error: 1.06%, average 0.64%, curves overshoot by 0.3% 205 | /// See: http://signalsmith-audio.co.uk/writing/2021/cheap-energy-crossfade/ 206 | template 207 | void cheapEnergyCrossfade(Sample x, Result &toCoeff, Result &fromCoeff) { 208 | Sample x2 = 1 - x; 209 | // Other powers p can be approximated by: k = -6.0026608 + p*(6.8773512 - 1.5838104*p) 210 | Sample A = x*x2, B = A*(1 + (Sample)1.4186*A); 211 | Sample C = (B + x), D = (B + x2); 212 | toCoeff = C*C; 213 | fromCoeff = D*D; 214 | } 215 | 216 | /** @} */ 217 | }} // signalsmith::delay:: 218 | #endif // include guard 219 | -------------------------------------------------------------------------------- /perf.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_PERF_H 4 | #define SIGNALSMITH_DSP_PERF_H 5 | 6 | #include 7 | 8 | #if defined(__SSE__) || defined(_M_X64) 9 | # include 10 | #else 11 | # include // for uintptr_t 12 | #endif 13 | 14 | namespace signalsmith { 15 | namespace perf { 16 | /** @defgroup Performance Performance helpers 17 | @brief Nothing serious, just some `#defines` and helpers 18 | 19 | @{ 20 | @file 21 | */ 22 | 23 | /// *Really* insist that a function/method is inlined (mostly for performance in DEBUG builds) 24 | #ifndef SIGNALSMITH_INLINE 25 | #ifdef __GNUC__ 26 | #define SIGNALSMITH_INLINE __attribute__((always_inline)) inline 27 | #elif defined(__MSVC__) 28 | #define SIGNALSMITH_INLINE __forceinline inline 29 | #else 30 | #define SIGNALSMITH_INLINE inline 31 | #endif 32 | #endif 33 | 34 | /** @brief Complex-multiplication (with optional conjugate second-arg), without handling NaN/Infinity 35 | The `std::complex` multiplication has edge-cases around NaNs which slow things down and prevent auto-vectorisation. Flags like `-ffast-math` sort this out anyway, but this helps with Debug builds. 36 | */ 37 | template 38 | SIGNALSMITH_INLINE static std::complex mul(const std::complex &a, const std::complex &b) { 39 | return conjugateSecond ? std::complex{ 40 | b.real()*a.real() + b.imag()*a.imag(), 41 | b.real()*a.imag() - b.imag()*a.real() 42 | } : std::complex{ 43 | a.real()*b.real() - a.imag()*b.imag(), 44 | a.real()*b.imag() + a.imag()*b.real() 45 | }; 46 | } 47 | 48 | #if defined(__SSE__) || defined(_M_X64) 49 | class StopDenormals { 50 | unsigned int controlStatusRegister; 51 | public: 52 | StopDenormals() : controlStatusRegister(_mm_getcsr()) { 53 | _mm_setcsr(controlStatusRegister|0x8040); // Flush-to-Zero and Denormals-Are-Zero 54 | } 55 | ~StopDenormals() { 56 | _mm_setcsr(controlStatusRegister); 57 | } 58 | }; 59 | #elif (defined (__ARM_NEON) || defined (__ARM_NEON__)) 60 | class StopDenormals { 61 | uintptr_t status; 62 | public: 63 | StopDenormals() { 64 | uintptr_t asmStatus; 65 | asm volatile("mrs %0, fpcr" : "=r"(asmStatus)); 66 | status = asmStatus = asmStatus|0x01000000U; // Flush to Zero 67 | asm volatile("msr fpcr, %0" : : "ri"(asmStatus)); 68 | } 69 | ~StopDenormals() { 70 | uintptr_t asmStatus = status; 71 | asm volatile("msr fpcr, %0" : : "ri"(asmStatus)); 72 | } 73 | }; 74 | #else 75 | # if __cplusplus >= 202302L 76 | # warning "The `StopDenormals` class doesn't do anything for this architecture" 77 | # endif 78 | class StopDenormals {}; // FIXME: add for other architectures 79 | #endif 80 | 81 | /** @} */ 82 | }} // signalsmith::perf:: 83 | 84 | #endif // include guard 85 | -------------------------------------------------------------------------------- /rates.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_RATES_H 4 | #define SIGNALSMITH_DSP_RATES_H 5 | 6 | #include "./windows.h" 7 | #include "./delay.h" 8 | 9 | namespace signalsmith { 10 | namespace rates { 11 | /** @defgroup Rates Multi-rate processing 12 | @brief Classes for oversampling/upsampling/downsampling etc. 13 | 14 | @{ 15 | @file 16 | */ 17 | 18 | /// @brief Fills a container with a Kaiser-windowed sinc for an FIR lowpass. 19 | /// \diagram{rates-kaiser-sinc.svg,33-point results for various pass/stop frequencies} 20 | template 21 | void fillKaiserSinc(Data &&data, int length, double passFreq, double stopFreq) { 22 | if (length <= 0) return; 23 | double kaiserBandwidth = (stopFreq - passFreq)*length; 24 | kaiserBandwidth += 1.25/kaiserBandwidth; // heuristic for transition band, see `InterpolatorKaiserSincN` 25 | auto kaiser = signalsmith::windows::Kaiser::withBandwidth(kaiserBandwidth); 26 | kaiser.fill(data, length); 27 | 28 | double centreIndex = (length - 1)*0.5; 29 | double sincScale = M_PI*(passFreq + stopFreq); 30 | double ampScale = (passFreq + stopFreq); 31 | for (int i = 0; i < length; ++i) { 32 | double x = (i - centreIndex), px = x*sincScale; 33 | double sinc = (std::abs(px) > 1e-6) ? std::sin(px)*ampScale/px : ampScale; 34 | data[i] *= sinc; 35 | } 36 | }; 37 | /// @brief If only the centre frequency is specified, a heuristic is used to balance ripples and transition width 38 | /// \diagram{rates-kaiser-sinc-heuristic.svg,The transition width is set to: 0.9/sqrt(length)} 39 | template 40 | void fillKaiserSinc(Data &&data, int length, double centreFreq) { 41 | double halfWidth = 0.45/std::sqrt(length); 42 | if (halfWidth > centreFreq) halfWidth = (halfWidth + centreFreq)*0.5; 43 | fillKaiserSinc(data, length, centreFreq - halfWidth, centreFreq + halfWidth); 44 | } 45 | 46 | /** 2x FIR oversampling for block-based processing. 47 | 48 | \diagram{rates-oversampler2xfir-responses-45.svg,Upsample response for various lengths} 49 | 50 | The oversampled signal is stored inside this object, with channels accessed via `oversampler[c]`. For example, you might do: 51 | \code{.cpp} 52 | // Upsample from multi-channel input (inputBuffers[c][i] is a sample) 53 | oversampler.up(inputBuffers, bufferLength) 54 | 55 | // Modify the contents at the higher rate 56 | for (int c = 0; c < 2; ++c) { 57 | float *channel = oversampler[c]; 58 | for (int i = 0; i < bufferLength*2; ++i) { 59 | channel[i] = std::abs(channel[i]); 60 | } 61 | } 62 | 63 | // Downsample into the multi-channel output 64 | oversampler.down(outputBuffers, bufferLength); 65 | \endcode 66 | 67 | The performance depends not just on the length, but also on where you end the passband, allowing a wider/narrower transition band. Frequencies above this limit (relative to the lower sample-rate) may alias when upsampling and downsampling. 68 | 69 | \diagram{rates-oversampler2xfir-lengths.svg,Resample error rates for different passband thresholds} 70 | 71 | Since both upsample and downsample are stateful, channels are meaningful. If your input channel-count doesn't match your output, you can size it to the larger of the two, and use `.upChannel()` and `.downChannel()` to only process the channels which exist.*/ 72 | template 73 | struct Oversampler2xFIR { 74 | Oversampler2xFIR() : Oversampler2xFIR(0, 0) {} 75 | Oversampler2xFIR(int channels, int maxBlock, int halfLatency=16, double passFreq=0.43) { 76 | resize(channels, maxBlock, halfLatency, passFreq); 77 | } 78 | 79 | void resize(int nChannels, int maxBlockLength) { 80 | resize(nChannels, maxBlockLength, oneWayLatency); 81 | } 82 | void resize(int nChannels, int maxBlockLength, int halfLatency, double passFreq=0.43) { 83 | oneWayLatency = halfLatency; 84 | kernelLength = oneWayLatency*2; 85 | channels = nChannels; 86 | halfSampleKernel.resize(kernelLength); 87 | fillKaiserSinc(halfSampleKernel, kernelLength, passFreq, 1 - passFreq); 88 | inputStride = kernelLength + maxBlockLength; 89 | inputBuffer.resize(channels*inputStride); 90 | stride = (maxBlockLength + kernelLength)*2; 91 | buffer.resize(stride*channels); 92 | } 93 | 94 | void reset() { 95 | inputBuffer.assign(inputBuffer.size(), 0); 96 | buffer.assign(buffer.size(), 0); 97 | } 98 | 99 | /// @brief Round-trip latency (or equivalently: upsample latency at the higher rate). 100 | /// This will be twice the value passed into the constructor or `.resize()`. 101 | int latency() const { 102 | return kernelLength; 103 | } 104 | 105 | /// Upsamples from a multi-channel input into the internal buffer 106 | template 107 | void up(Data &&data, int lowSamples) { 108 | for (int c = 0; c < channels; ++c) { 109 | upChannel(c, data[c], lowSamples); 110 | } 111 | } 112 | 113 | /// Upsamples a single-channel input into the internal buffer 114 | template 115 | void upChannel(int c, Data &&data, int lowSamples) { 116 | Sample *inputChannel = inputBuffer.data() + c*inputStride; 117 | for (int i = 0; i < lowSamples; ++i) { 118 | inputChannel[kernelLength + i] = data[i]; 119 | } 120 | Sample *output = (*this)[c]; 121 | for (int i = 0; i < lowSamples; ++i) { 122 | output[2*i] = inputChannel[i + oneWayLatency]; 123 | Sample *offsetInput = inputChannel + (i + 1); 124 | Sample sum = 0; 125 | for (int o = 0; o < kernelLength; ++o) { 126 | sum += offsetInput[o]*halfSampleKernel[o]; 127 | } 128 | output[2*i + 1] = sum; 129 | } 130 | // Copy the end of the buffer back to the beginning 131 | for (int i = 0; i < kernelLength; ++i) { 132 | inputChannel[i] = inputChannel[lowSamples + i]; 133 | } 134 | } 135 | 136 | /// Downsamples from the internal buffer to a multi-channel output 137 | template 138 | void down(Data &&data, int lowSamples) { 139 | for (int c = 0; c < channels; ++c) { 140 | downChannel(c, data[c], lowSamples); 141 | } 142 | } 143 | 144 | /// Downsamples a single channel from the internal buffer to a single-channel output 145 | template 146 | void downChannel(int c, Data &&data, int lowSamples) { 147 | Sample *input = buffer.data() + c*stride; // no offset for latency 148 | for (int i = 0; i < lowSamples; ++i) { 149 | Sample v1 = input[2*i + kernelLength]; 150 | Sample sum = 0; 151 | for (int o = 0; o < kernelLength; ++o) { 152 | Sample v2 = input[2*(i + o) + 1]; 153 | sum += v2*halfSampleKernel[o]; 154 | } 155 | Sample v2 = sum; 156 | Sample v = (v1 + v2)*Sample(0.5); 157 | data[i] = v; 158 | } 159 | // Copy the end of the buffer back to the beginning 160 | for (int i = 0; i < kernelLength*2; ++i) { 161 | input[i] = input[lowSamples*2 + i]; 162 | } 163 | } 164 | 165 | /// Gets the samples for a single (higher-rate) channel. The valid length depends how many input samples were passed into `.up()`/`.upChannel()`. 166 | Sample * operator[](int c) { 167 | return buffer.data() + kernelLength*2 + stride*c; 168 | } 169 | const Sample * operator[](int c) const { 170 | return buffer.data() + kernelLength*2 + stride*c; 171 | } 172 | 173 | private: 174 | int oneWayLatency, kernelLength; 175 | int channels; 176 | int stride, inputStride; 177 | std::vector inputBuffer; 178 | std::vector halfSampleKernel; 179 | std::vector buffer; 180 | }; 181 | 182 | /** @} */ 183 | }} // namespace 184 | #endif // include guard 185 | -------------------------------------------------------------------------------- /spectral.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_SPECTRAL_H 4 | #define SIGNALSMITH_DSP_SPECTRAL_H 5 | 6 | #include "./perf.h" 7 | #include "./fft.h" 8 | #include "./windows.h" 9 | #include "./delay.h" 10 | 11 | #include 12 | 13 | namespace signalsmith { 14 | namespace spectral { 15 | /** @defgroup Spectral Spectral Processing 16 | @brief Tools for frequency-domain manipulation of audio signals 17 | 18 | @{ 19 | @file 20 | */ 21 | 22 | /** @brief An FFT with built-in windowing and round-trip scaling 23 | 24 | This uses a Modified Real FFT, which applies half-bin shift before the transform. The result therefore has `N/2` bins, centred at the frequencies: `(i + 0.5)/N`. 25 | 26 | This avoids the awkward (real-valued) bands for DC-offset and Nyquist. 27 | */ 28 | template 29 | class WindowedFFT { 30 | using MRFFT = signalsmith::fft::ModifiedRealFFT; 31 | using Complex = std::complex; 32 | MRFFT mrfft{2}; 33 | 34 | std::vector fftWindow; 35 | std::vector timeBuffer; 36 | int offsetSamples = 0; 37 | public: 38 | /// Returns a fast FFT size <= `size` 39 | static int fastSizeAbove(int size, int divisor=1) { 40 | return MRFFT::fastSizeAbove(size/divisor)*divisor; 41 | } 42 | /// Returns a fast FFT size >= `size` 43 | static int fastSizeBelow(int size, int divisor=1) { 44 | return MRFFT::fastSizeBelow(1 + (size - 1)/divisor)*divisor; 45 | } 46 | 47 | WindowedFFT() {} 48 | WindowedFFT(int size, int rotateSamples=0) { 49 | setSize(size, rotateSamples); 50 | } 51 | template 52 | WindowedFFT(int size, WindowFn fn, Sample windowOffset=0.5, int rotateSamples=0) { 53 | setSize(size, fn, windowOffset, rotateSamples); 54 | } 55 | 56 | /// Sets the size, returning the window for modification (initially all 1s) 57 | std::vector & setSizeWindow(int size, int rotateSamples=0) { 58 | mrfft.setSize(size); 59 | fftWindow.assign(size, 1); 60 | timeBuffer.resize(size); 61 | offsetSamples = rotateSamples; 62 | if (offsetSamples < 0) offsetSamples += size; // TODO: for a negative rotation, the other half of the result is inverted 63 | return fftWindow; 64 | } 65 | /// Sets the FFT size, with a user-defined functor for the window 66 | template 67 | void setSize(int size, WindowFn fn, Sample windowOffset=0.5, int rotateSamples=0) { 68 | setSizeWindow(size, rotateSamples); 69 | 70 | Sample invSize = 1/(Sample)size; 71 | for (int i = 0; i < size; ++i) { 72 | Sample r = (i + windowOffset)*invSize; 73 | fftWindow[i] = fn(r); 74 | } 75 | } 76 | /// Sets the size (using the default Blackman-Harris window) 77 | void setSize(int size, int rotateSamples=0) { 78 | setSize(size, [](double x) { 79 | double phase = 2*M_PI*x; 80 | // Blackman-Harris 81 | return 0.35875 - 0.48829*std::cos(phase) + 0.14128*std::cos(phase*2) - 0.01168*std::cos(phase*3); 82 | }, Sample(0.5), rotateSamples); 83 | } 84 | 85 | const std::vector & window() const { 86 | return this->fftWindow; 87 | } 88 | int size() const { 89 | return mrfft.size(); 90 | } 91 | 92 | /// Performs an FFT, with windowing and rotation (if enabled) 93 | template 94 | void fft(Input &&input, Output &&output) { 95 | int fftSize = size(); 96 | const Sample norm = (withScaling ? 1/(Sample)fftSize : 1); 97 | for (int i = 0; i < offsetSamples; ++i) { 98 | // Inverted polarity since we're using the MRFFT 99 | timeBuffer[i + fftSize - offsetSamples] = -input[i]*norm*(withWindow ? fftWindow[i] : Sample(1)); 100 | } 101 | for (int i = offsetSamples; i < fftSize; ++i) { 102 | timeBuffer[i - offsetSamples] = input[i]*norm*(withWindow ? fftWindow[i] : Sample(1)); 103 | } 104 | mrfft.fft(timeBuffer, output); 105 | } 106 | /// Performs an FFT (no windowing or rotation) 107 | template 108 | void fftRaw(Input &&input, Output &&output) { 109 | mrfft.fft(input, output); 110 | } 111 | 112 | /// Inverse FFT, with windowing, 1/N scaling and rotation (if enabled) 113 | template 114 | void ifft(Input &&input, Output &&output) { 115 | mrfft.ifft(input, timeBuffer); 116 | int fftSize = mrfft.size(); 117 | const Sample norm = (withScaling ? 1/(Sample)fftSize : 1); 118 | 119 | for (int i = 0; i < offsetSamples; ++i) { 120 | // Inverted polarity since we're using the MRFFT 121 | output[i] = -timeBuffer[i + fftSize - offsetSamples]*norm*(withWindow ? fftWindow[i] : Sample(1)); 122 | } 123 | for (int i = offsetSamples; i < fftSize; ++i) { 124 | output[i] = timeBuffer[i - offsetSamples]*norm*(withWindow ? fftWindow[i] : Sample(1)); 125 | } 126 | } 127 | /// Performs an IFFT (no windowing, scaling or rotation) 128 | template 129 | void ifftRaw(Input &&input, Output &&output) { 130 | mrfft.ifft(input, output); 131 | } 132 | }; 133 | 134 | /** STFT synthesis, built on a `MultiBuffer`. 135 | 136 | Any window length and block interval is supported, but the FFT size may be rounded up to a faster size (by zero-padding). It uses a heuristically-optimal Kaiser window modified for perfect-reconstruction. 137 | 138 | \diagram{stft-aliasing-simulated.svg,Simulated bad-case aliasing (random phase-shift for each band) for overlapping ratios} 139 | 140 | There is a "latest valid index", and you can read the output up to one `historyLength` behind this (see `.resize()`). You can read up to one window-length _ahead_ to get partially-summed future output. 141 | 142 | \diagram{stft-buffer-validity.svg} 143 | 144 | You move the valid index along using `.ensureValid()`, passing in a functor which provides spectra (using `.analyse()` and/or direct modification through `.spectrum[c]`): 145 | 146 | \code 147 | void processSample(...) { 148 | stft.ensureValid([&](int) { 149 | // Here, we introduce (1 - windowSize) of latency 150 | stft.analyse(inputBuffer.view(1 - windowSize)) 151 | }); 152 | // read as a MultiBuffer 153 | auto result = stft.at(0); 154 | ++stft; // also moves the latest valid index 155 | } 156 | 157 | void processBlock(...) { 158 | // assuming `historyLength` == blockSize 159 | stft.ensureValid(blockSize, [&](int blockStartIndex) { 160 | int inputStart = blockStartIndex + (1 - windowSize); 161 | stft.analyse(inputBuffer.view(inputStart)); 162 | }); 163 | auto earliestValid = stft.at(0); 164 | auto latestValid = stft.at(blockSize); 165 | stft += blockSize; 166 | } 167 | \endcode 168 | 169 | The index passed to this functor will be greater than the previous valid index, and `<=` the index you pass in. Therefore, if you call `.ensureValid()` every sample, it can only ever be `0`. 170 | */ 171 | template 172 | class STFT : public signalsmith::delay::MultiBuffer { 173 | 174 | using Super = signalsmith::delay::MultiBuffer; 175 | using Complex = std::complex; 176 | 177 | int channels = 0, _windowSize = 0, _fftSize = 0, _interval = 1; 178 | int validUntilIndex = 0; 179 | 180 | class MultiSpectrum { 181 | int channels, stride; 182 | std::vector buffer; 183 | public: 184 | MultiSpectrum() : MultiSpectrum(0, 0) {} 185 | MultiSpectrum(int channels, int bands) : channels(channels), stride(bands), buffer(channels*bands, 0) {} 186 | 187 | void resize(int nChannels, int nBands) { 188 | channels = nChannels; 189 | stride = nBands; 190 | buffer.assign(channels*stride, 0); 191 | } 192 | 193 | void reset() { 194 | buffer.assign(buffer.size(), 0); 195 | } 196 | 197 | void swap(MultiSpectrum &other) { 198 | using std::swap; 199 | swap(buffer, other.buffer); 200 | } 201 | 202 | Complex * operator [](int channel) { 203 | return buffer.data() + channel*stride; 204 | } 205 | const Complex * operator [](int channel) const { 206 | return buffer.data() + channel*stride; 207 | } 208 | }; 209 | std::vector timeBuffer; 210 | 211 | bool rotate = false; 212 | void resizeInternal(int newChannels, int windowSize, int newInterval, int historyLength, int zeroPadding) { 213 | Super::resize(newChannels, 214 | windowSize /* for output summing */ 215 | + newInterval /* so we can read `windowSize` ahead (we'll be at most `interval-1` from the most recent block */ 216 | + historyLength); 217 | 218 | int fftSize = fft.fastSizeAbove(windowSize + zeroPadding); 219 | 220 | this->channels = newChannels; 221 | _windowSize = windowSize; 222 | this->_fftSize = fftSize; 223 | this->_interval = newInterval; 224 | validUntilIndex = -1; 225 | 226 | setWindow(windowShape, rotate); 227 | 228 | spectrum.resize(channels, fftSize/2); 229 | timeBuffer.resize(fftSize); 230 | } 231 | public: 232 | enum class Window {kaiser, acg}; 233 | /// \deprecated use `.setWindow()` which actually updates the window when you change it 234 | Window windowShape = Window::kaiser; 235 | // for convenience 236 | static constexpr Window kaiser = Window::kaiser; 237 | static constexpr Window acg = Window::acg; 238 | 239 | /** Swaps between the default (Kaiser) shape and Approximate Confined Gaussian (ACG). 240 | \diagram{stft-windows.svg,Default (Kaiser) windows and partial cumulative sum} 241 | The ACG has better rolloff since its edges go to 0: 242 | \diagram{stft-windows-acg.svg,ACG windows and partial cumulative sum} 243 | However, it generally has worse performance in terms of total sidelobe energy, affecting worst-case aliasing levels for (most) higher overlap ratios: 244 | \diagram{stft-aliasing-simulated-acg.svg,Simulated bad-case aliasing for ACG windows - compare with above}*/ 245 | // TODO: these should both be set before resize() 246 | void setWindow(Window shape, bool rotateToZero=false) { 247 | windowShape = shape; 248 | rotate = rotateToZero; 249 | 250 | auto &window = fft.setSizeWindow(_fftSize, rotateToZero ? _windowSize/2 : 0); 251 | if (windowShape == Window::kaiser) { 252 | using Kaiser = ::signalsmith::windows::Kaiser; 253 | /// Roughly optimal Kaiser for STFT analysis (forced to perfect reconstruction) 254 | auto kaiser = Kaiser::withBandwidth(_windowSize/double(_interval), true); 255 | kaiser.fill(window, _windowSize); 256 | } else { 257 | using Confined = ::signalsmith::windows::ApproximateConfinedGaussian; 258 | auto confined = Confined::withBandwidth(_windowSize/double(_interval)); 259 | confined.fill(window, _windowSize); 260 | } 261 | ::signalsmith::windows::forcePerfectReconstruction(window, _windowSize, _interval); 262 | 263 | // TODO: fill extra bits of an input buffer with NaN/Infinity, to break this, and then fix by adding zero-padding to WindowedFFT (as opposed to zero-valued window sections) 264 | for (int i = _windowSize; i < _fftSize; ++i) { 265 | window[i] = 0; 266 | } 267 | } 268 | 269 | using Spectrum = MultiSpectrum; 270 | Spectrum spectrum; 271 | WindowedFFT fft; 272 | 273 | STFT() {} 274 | /// Parameters passed straight to `.resize()` 275 | STFT(int channels, int windowSize, int interval, int historyLength=0, int zeroPadding=0) { 276 | resize(channels, windowSize, interval, historyLength, zeroPadding); 277 | } 278 | 279 | /// Sets the channel-count, FFT size and interval. 280 | void resize(int nChannels, int windowSize, int interval, int historyLength=0, int zeroPadding=0) { 281 | resizeInternal(nChannels, windowSize, interval, historyLength, zeroPadding); 282 | } 283 | 284 | int windowSize() const { 285 | return _windowSize; 286 | } 287 | int fftSize() const { 288 | return _fftSize; 289 | } 290 | int interval() const { 291 | return _interval; 292 | } 293 | /// Returns the (analysis and synthesis) window 294 | decltype(fft.window()) window() const { 295 | return fft.window(); 296 | } 297 | /// Calculates the effective window for the partially-summed future output (relative to the most recent block) 298 | std::vector partialSumWindow(bool includeLatestBlock=true) const { 299 | const auto &w = window(); 300 | std::vector result(_windowSize, 0); 301 | int firstOffset = (includeLatestBlock ? 0 : _interval); 302 | for (int offset = firstOffset; offset < _windowSize; offset += _interval) { 303 | for (int i = 0; i < _windowSize - offset; ++i) { 304 | Sample value = w[i + offset]; 305 | result[i] += value*value; 306 | } 307 | } 308 | return result; 309 | } 310 | 311 | /// Resets everything - since we clear the output sum, it will take `windowSize` samples to get proper output. 312 | void reset() { 313 | Super::reset(); 314 | spectrum.reset(); 315 | validUntilIndex = -1; 316 | } 317 | 318 | /** Generates valid output up to the specified index (or 0), using the callback as many times as needed. 319 | 320 | The callback should be a functor accepting a single integer argument, which is the index for which a spectrum is required. 321 | 322 | The block created from these spectra will start at this index in the output, plus `.latency()`. 323 | */ 324 | template 325 | void ensureValid(int i, AnalysisFn fn) { 326 | while (validUntilIndex < i) { 327 | int blockIndex = validUntilIndex + 1; 328 | fn(blockIndex); 329 | 330 | auto output = this->view(blockIndex); 331 | for (int c = 0; c < channels; ++c) { 332 | auto channel = output[c]; 333 | 334 | // Clear out the future sum, a window-length and an interval ahead 335 | for (int wi = _windowSize; wi < _windowSize + _interval; ++wi) { 336 | channel[wi] = 0; 337 | } 338 | 339 | // Add in the IFFT'd result 340 | fft.ifft(spectrum[c], timeBuffer); 341 | for (int wi = 0; wi < _windowSize; ++wi) { 342 | channel[wi] += timeBuffer[wi]; 343 | } 344 | } 345 | validUntilIndex += _interval; 346 | } 347 | } 348 | /// The same as above, assuming index 0 349 | template 350 | void ensureValid(AnalysisFn fn) { 351 | return ensureValid(0, fn); 352 | } 353 | /// Returns the next invalid index (a.k.a. the index of the next block) 354 | int nextInvalid() const { 355 | return validUntilIndex + 1; 356 | } 357 | 358 | /** Analyse a multi-channel input, for any type where `data[channel][index]` returns samples 359 | 360 | Results can be read/edited using `.spectrum`. */ 361 | template 362 | void analyse(Data &&data) { 363 | for (int c = 0; c < channels; ++c) { 364 | fft.fft(data[c], spectrum[c]); 365 | } 366 | } 367 | template 368 | void analyse(int c, Data &&data) { 369 | fft.fft(data, spectrum[c]); 370 | } 371 | /// Analyse without windowing or zero-rotation 372 | template 373 | void analyseRaw(Data &&data) { 374 | for (int c = 0; c < channels; ++c) { 375 | fft.fftRaw(data[c], spectrum[c]); 376 | } 377 | } 378 | template 379 | void analyseRaw(int c, Data &&data) { 380 | fft.fftRaw(data, spectrum[c]); 381 | } 382 | 383 | int bands() const { 384 | return _fftSize/2; 385 | } 386 | 387 | /** Internal latency (between the block-index requested in `.ensureValid()` and its position in the output) 388 | 389 | Currently unused, but it's in here to allow for a future implementation which spreads the FFT calculations out across each interval.*/ 390 | int latency() { 391 | return 0; 392 | } 393 | 394 | // @name Shift the underlying buffer (moving the "valid" index accordingly) 395 | // @{ 396 | STFT & operator ++() { 397 | Super::operator ++(); 398 | validUntilIndex--; 399 | return *this; 400 | } 401 | STFT & operator +=(int i) { 402 | Super::operator +=(i); 403 | validUntilIndex -= i; 404 | return *this; 405 | } 406 | STFT & operator --() { 407 | Super::operator --(); 408 | validUntilIndex++; 409 | return *this; 410 | } 411 | STFT & operator -=(int i) { 412 | Super::operator -=(i); 413 | validUntilIndex += i; 414 | return *this; 415 | } 416 | // @} 417 | 418 | typename Super::MutableView operator ++(int postIncrement) { 419 | auto result = Super::operator ++(postIncrement); 420 | validUntilIndex--; 421 | return result; 422 | } 423 | typename Super::MutableView operator --(int postIncrement) { 424 | auto result = Super::operator --(postIncrement); 425 | validUntilIndex++; 426 | return result; 427 | } 428 | }; 429 | 430 | /** STFT processing, with input/output. 431 | Before calling `.ensureValid(index)`, you should make sure the input is filled up to `index`. 432 | */ 433 | template 434 | class ProcessSTFT : public STFT { 435 | using Super = STFT; 436 | public: 437 | signalsmith::delay::MultiBuffer input; 438 | 439 | ProcessSTFT(int inChannels, int outChannels, int windowSize, int interval, int historyLength=0) { 440 | resize(inChannels, outChannels, windowSize, interval, historyLength); 441 | } 442 | 443 | /** Alter the spectrum, using input up to this point, for the output block starting from this point. 444 | Sub-classes should replace this with whatever processing is desired. */ 445 | virtual void processSpectrum(int /*blockIndex*/) {} 446 | 447 | /// Sets the input/output channels, FFT size and interval. 448 | void resize(int inChannels, int outChannels, int windowSize, int interval, int historyLength=0) { 449 | Super::resize(outChannels, windowSize, interval, historyLength); 450 | input.resize(inChannels, windowSize + interval + historyLength); 451 | } 452 | void reset(Sample value=Sample()) { 453 | Super::reset(value); 454 | input.reset(value); 455 | } 456 | 457 | /// Internal latency, including buffering samples for analysis. 458 | int latency() { 459 | return Super::latency() + (this->windowSize() - 1); 460 | } 461 | 462 | void ensureValid(int i=0) { 463 | Super::ensureValid(i, [&](int blockIndex) { 464 | this->analyse(input.view(blockIndex - this->windowSize() + 1)); 465 | this->processSpectrum(blockIndex); 466 | }); 467 | } 468 | 469 | // @name Shift the output, input, and valid index. 470 | // @{ 471 | ProcessSTFT & operator ++() { 472 | Super::operator ++(); 473 | ++input; 474 | return *this; 475 | } 476 | ProcessSTFT & operator +=(int i) { 477 | Super::operator +=(i); 478 | input += i; 479 | return *this; 480 | } 481 | ProcessSTFT & operator --() { 482 | Super::operator --(); 483 | --input; 484 | return *this; 485 | } 486 | ProcessSTFT & operator -=(int i) { 487 | Super::operator -=(i); 488 | input -= i; 489 | return *this; 490 | } 491 | // @} 492 | }; 493 | 494 | /** @} */ 495 | }} // signalsmith::spectral:: 496 | #endif // include guard 497 | -------------------------------------------------------------------------------- /windows.h: -------------------------------------------------------------------------------- 1 | #include "./common.h" 2 | 3 | #ifndef SIGNALSMITH_DSP_WINDOWS_H 4 | #define SIGNALSMITH_DSP_WINDOWS_H 5 | 6 | #include 7 | #include 8 | 9 | namespace signalsmith { 10 | namespace windows { 11 | /** @defgroup Windows Window functions 12 | @brief Windows for spectral analysis 13 | 14 | These are generally double-precision, because they are mostly calculated during setup/reconfiguring, not real-time code. 15 | 16 | @{ 17 | @file 18 | */ 19 | 20 | /** @brief The Kaiser window (almost) maximises the energy in the main-lobe compared to the side-lobes. 21 | 22 | Kaiser windows can be constructing using the shape-parameter (beta) or using the static `with???()` methods.*/ 23 | class Kaiser { 24 | // I_0(x)=\sum_{k=0}^{N}\frac{x^{2k}}{(k!)^2\cdot4^k} 25 | inline static double bessel0(double x) { 26 | const double significanceLimit = 1e-4; 27 | double result = 0; 28 | double term = 1; 29 | double m = 0; 30 | while (term > significanceLimit) { 31 | result += term; 32 | ++m; 33 | term *= (x*x)/(4*m*m); 34 | } 35 | 36 | return result; 37 | } 38 | double beta; 39 | double invB0; 40 | 41 | static double heuristicBandwidth(double bandwidth) { 42 | // Good peaks 43 | //return bandwidth + 8/((bandwidth + 3)*(bandwidth + 3)); 44 | // Good average 45 | //return bandwidth + 14/((bandwidth + 2.5)*(bandwidth + 2.5)); 46 | // Compromise 47 | return bandwidth + 8/((bandwidth + 3)*(bandwidth + 3)) + 0.25*std::max(3 - bandwidth, 0.0); 48 | } 49 | public: 50 | /// Set up a Kaiser window with a given shape. `beta` is `pi*alpha` (since there is ambiguity about shape parameters) 51 | Kaiser(double beta) : beta(beta), invB0(1/bessel0(beta)) {} 52 | 53 | /// @name Bandwidth methods 54 | /// @{ 55 | static Kaiser withBandwidth(double bandwidth, bool heuristicOptimal=false) { 56 | return Kaiser(bandwidthToBeta(bandwidth, heuristicOptimal)); 57 | } 58 | 59 | /** Returns the Kaiser shape where the main lobe has the specified bandwidth (as a factor of 1/window-length). 60 | \diagram{kaiser-windows.svg,You can see that the main lobe matches the specified bandwidth.} 61 | If `heuristicOptimal` is enabled, the main lobe width is _slightly_ wider, improving both the peak and total energy - see `bandwidthToEnergyDb()` and `bandwidthToPeakDb()`. 62 | \diagram{kaiser-windows-heuristic.svg, The main lobe extends to ±bandwidth/2.} */ 63 | static double bandwidthToBeta(double bandwidth, bool heuristicOptimal=false) { 64 | if (heuristicOptimal) { // Heuristic based on numerical search 65 | bandwidth = heuristicBandwidth(bandwidth); 66 | } 67 | bandwidth = std::max(bandwidth, 2.0); 68 | double alpha = std::sqrt(bandwidth*bandwidth*0.25 - 1); 69 | return alpha*M_PI; 70 | } 71 | 72 | static double betaToBandwidth(double beta) { 73 | double alpha = beta*(1.0/M_PI); 74 | return 2*std::sqrt(alpha*alpha + 1); 75 | } 76 | /// @} 77 | 78 | /// @name Performance methods 79 | /// @{ 80 | /** @brief Total energy ratio (in dB) between side-lobes and the main lobe. 81 | \diagram{windows-kaiser-sidelobe-energy.svg,Measured main/side lobe energy ratio. You can see that the heuristic improves performance for all bandwidth values.} 82 | This function uses an approximation which is accurate to ±0.5dB for 2 ⩽ bandwidth ≤ 10, or 1 ⩽ bandwidth ≤ 10 when `heuristicOptimal`is enabled. 83 | */ 84 | static double bandwidthToEnergyDb(double bandwidth, bool heuristicOptimal=false) { 85 | // Horrible heuristic fits 86 | if (heuristicOptimal) { 87 | if (bandwidth < 3) bandwidth += (3 - bandwidth)*0.5; 88 | return 12.9 + -3/(bandwidth + 0.4) - 13.4*bandwidth + (bandwidth < 3)*-9.6*(bandwidth - 3); 89 | } 90 | return 10.5 + 15/(bandwidth + 0.4) - 13.25*bandwidth + (bandwidth < 2)*13*(bandwidth - 2); 91 | } 92 | static double energyDbToBandwidth(double energyDb, bool heuristicOptimal=false) { 93 | double bw = 1; 94 | while (bw < 20 && bandwidthToEnergyDb(bw, heuristicOptimal) > energyDb) { 95 | bw *= 2; 96 | } 97 | double step = bw/2; 98 | while (step > 0.0001) { 99 | if (bandwidthToEnergyDb(bw, heuristicOptimal) > energyDb) { 100 | bw += step; 101 | } else { 102 | bw -= step; 103 | } 104 | step *= 0.5; 105 | } 106 | return bw; 107 | } 108 | /** @brief Peak ratio (in dB) between side-lobes and the main lobe. 109 | \diagram{windows-kaiser-sidelobe-peaks.svg,Measured main/side lobe peak ratio. You can see that the heuristic improves performance, except in the bandwidth range 1-2 where peak ratio was sacrificed to improve total energy ratio.} 110 | This function uses an approximation which is accurate to ±0.5dB for 2 ⩽ bandwidth ≤ 9, or 0.5 ⩽ bandwidth ≤ 9 when `heuristicOptimal`is enabled. 111 | */ 112 | static double bandwidthToPeakDb(double bandwidth, bool heuristicOptimal=false) { 113 | // Horrible heuristic fits 114 | if (heuristicOptimal) { 115 | return 14.2 - 20/(bandwidth + 1) - 13*bandwidth + (bandwidth < 3)*-6*(bandwidth - 3) + (bandwidth < 2.25)*5.8*(bandwidth - 2.25); 116 | } 117 | return 10 + 8/(bandwidth + 2) - 12.75*bandwidth + (bandwidth < 2)*4*(bandwidth - 2); 118 | } 119 | static double peakDbToBandwidth(double peakDb, bool heuristicOptimal=false) { 120 | double bw = 1; 121 | while (bw < 20 && bandwidthToPeakDb(bw, heuristicOptimal) > peakDb) { 122 | bw *= 2; 123 | } 124 | double step = bw/2; 125 | while (step > 0.0001) { 126 | if (bandwidthToPeakDb(bw, heuristicOptimal) > peakDb) { 127 | bw += step; 128 | } else { 129 | bw -= step; 130 | } 131 | step *= 0.5; 132 | } 133 | return bw; 134 | } 135 | /** @} */ 136 | 137 | /** Equivalent noise bandwidth (ENBW), a measure of frequency resolution. 138 | \diagram{windows-kaiser-enbw.svg,Measured ENBW\, with and without the heuristic bandwidth adjustment.} 139 | This approximation is accurate to ±0.05 up to a bandwidth of 22. 140 | */ 141 | static double bandwidthToEnbw(double bandwidth, bool heuristicOptimal=false) { 142 | if (heuristicOptimal) bandwidth = heuristicBandwidth(bandwidth); 143 | double b2 = std::max(bandwidth - 2, 0); 144 | return 1 + b2*(0.2 + b2*(-0.005 + b2*(-0.000005 + b2*0.0000022))); 145 | } 146 | 147 | /// Return the window's value for position in the range [0, 1] 148 | double operator ()(double unit) { 149 | double r = 2*unit - 1; 150 | double arg = std::sqrt(1 - r*r); 151 | return bessel0(beta*arg)*invB0; 152 | } 153 | 154 | /// Fills an arbitrary container with a Kaiser window 155 | template 156 | void fill(Data &&data, int size) const { 157 | double invSize = 1.0/size; 158 | for (int i = 0; i < size; ++i) { 159 | double r = (2*i + 1)*invSize - 1; 160 | double arg = std::sqrt(1 - r*r); 161 | data[i] = bessel0(beta*arg)*invB0; 162 | } 163 | } 164 | }; 165 | 166 | /** @brief The Approximate Confined Gaussian window is (almost) optimal 167 | 168 | ACG windows can be constructing using the shape-parameter (sigma) or using the static `with???()` methods.*/ 169 | class ApproximateConfinedGaussian { 170 | double gaussianFactor; 171 | 172 | double gaussian(double x) const { 173 | return std::exp(-x*x*gaussianFactor); 174 | } 175 | public: 176 | /// Heuristic map from bandwidth to the appropriately-optimal sigma 177 | static double bandwidthToSigma(double bandwidth) { 178 | return 0.3/std::sqrt(bandwidth); 179 | } 180 | static ApproximateConfinedGaussian withBandwidth(double bandwidth) { 181 | return ApproximateConfinedGaussian(bandwidthToSigma(bandwidth)); 182 | } 183 | 184 | ApproximateConfinedGaussian(double sigma) : gaussianFactor(0.0625/(sigma*sigma)) {} 185 | 186 | /// Fills an arbitrary container 187 | template 188 | void fill(Data &&data, int size) const { 189 | double invSize = 1.0/size; 190 | double offsetScale = gaussian(1)/(gaussian(3) + gaussian(-1)); 191 | double norm = 1/(gaussian(0) - 2*offsetScale*(gaussian(2))); 192 | for (int i = 0; i < size; ++i) { 193 | double r = (2*i + 1)*invSize - 1; 194 | data[i] = norm*(gaussian(r) - offsetScale*(gaussian(r - 2) + gaussian(r + 2))); 195 | } 196 | } 197 | }; 198 | 199 | /** Forces STFT perfect-reconstruction (WOLA) on an existing window, for a given STFT interval. 200 | For example, here are perfect-reconstruction versions of the approximately-optimal @ref Kaiser windows: 201 | \diagram{kaiser-windows-heuristic-pr.svg,Note the lower overall energy\, and the pointy top for 2x bandwidth. Spectral performance is about the same\, though.} 202 | */ 203 | template 204 | void forcePerfectReconstruction(Data &&data, int windowLength, int interval) { 205 | for (int i = 0; i < interval; ++i) { 206 | double sum2 = 0; 207 | for (int index = i; index < windowLength; index += interval) { 208 | sum2 += data[index]*data[index]; 209 | } 210 | double factor = 1/std::sqrt(sum2); 211 | for (int index = i; index < windowLength; index += interval) { 212 | data[index] *= factor; 213 | } 214 | } 215 | } 216 | 217 | /** @} */ 218 | }} // signalsmith::windows 219 | #endif // include guard 220 | --------------------------------------------------------------------------------