Add CMakeLists.txt, remove DSP library dependency/copy

This commit is contained in:
Geraint 2025-02-07 14:36:17 +00:00
parent 370c69a298
commit 6d52f2e861
19 changed files with 62 additions and 3914 deletions

15
CMakeLists.txt Normal file
View File

@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.24)
add_library(signalsmith-stretch INTERFACE)
set_target_properties(signalsmith-stretch PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR}/include)
include(FetchContent)
FetchContent_Declare(
signalsmith-linear
GIT_REPOSITORY https://github.com/Signalsmith-Audio/linear.git
GIT_TAG 355e014e330fde0c441004ba33e3964f04d4d28c
GIT_SHALLOW ON
)
FetchContent_MakeAvailable(signalsmith-linear)
target_link_libraries(signalsmith-stretch INTERFACE signalsmith-linear)

View File

@ -1,8 +1,12 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.24)
project(example-cmd VERSION 1.0.0) project(example-cmd VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD 11)
add_executable(stretch ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp) add_executable(stretch ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
add_subdirectory(".." "build-stretch") # Needs a (relative) build directory since it's not a subdir
target_link_libraries(stretch signalsmith-stretch)
include(FetchContent) include(FetchContent)
set(FETCHCONTENT_QUIET False) set(FETCHCONTENT_QUIET False)
FetchContent_Declare( FetchContent_Declare(
@ -11,3 +15,4 @@ FetchContent_Declare(
SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/inputs" SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/inputs"
) )
FetchContent_MakeAvailable(inputs) FetchContent_MakeAvailable(inputs)

View File

@ -1,15 +1,11 @@
all: out/stretch all: out/stretch
dev: clean out/stretch out/stretch: main.cpp ../signalsmith-stretch.h util/*.h util/*.hxx ../dsp/*.h
mkdir -p out/examples
inputs/run-all.sh out/examples/u2- out/stretch --semitones=2
cd ../linear/tests; make
out/stretch: ../signalsmith-stretch.h main.cpp util/*.h util/*.hxx ../dsp/*.h
mkdir -p out mkdir -p out
g++ -std=c++11 -O3 -g \ g++ -std=c++11 -O3 -g \
-Wall -Wextra -Wfatal-errors -Wpedantic -pedantic-errors \ -Wall -Wextra -Wfatal-errors -Wpedantic -pedantic-errors \
-framework Accelerate -DSIGNALSMITH_USE_ACCELERATE \ -framework Accelerate -DSIGNALSMITH_USE_ACCELERATE \
-I ../include \
main.cpp -o out/stretch main.cpp -o out/stretch
# Uses input files from: https://signalsmith-audio.co.uk/code/stretch/inputs.zip # Uses input files from: https://signalsmith-audio.co.uk/code/stretch/inputs.zip
@ -38,7 +34,3 @@ cmake:
# CMAKE_BUILD_TYPE is needed for single-config generators (e.g. Makefiles) # CMAKE_BUILD_TYPE is needed for single-config generators (e.g. Makefiles)
cmake -B build -DCMAKE_BUILD_TYPE=Release cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release cmake --build build --config Release
# Copy to out/ so that `make clean cmake examples` works
mkdir -p out
cp build/Release/* out/

View File

@ -1,12 +1,13 @@
#include "util/stopwatch.h" // helper for debugging
#include "util/memory-tracker.hxx"
#include <iostream> #include <iostream>
#define LOG_EXPR(expr) std::cout << #expr << " = " << (expr) << "\n"; #define LOG_EXPR(expr) std::cout << #expr << " = " << (expr) << "\n";
#include "../signalsmith-stretch.h" #include "signalsmith-stretch/signalsmith-stretch.h"
#include "util/simple-args.h"
#include "util/wav.h" #include "./util/stopwatch.h"
#include "./util/memory-tracker.hxx"
#include "./util/simple-args.h"
#include "./util/wav.h"
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
signalsmith::stretch::SignalsmithStretch<float/*, std::mt19937*/> stretch; // optional cheaper RNG for performance comparison signalsmith::stretch::SignalsmithStretch<float/*, std::mt19937*/> stretch; // optional cheaper RNG for performance comparison

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Geraint Luff / Signalsmith Audio Ltd.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,40 +0,0 @@
# Signalsmith Audio's DSP Library
A C++11 header-only library, providing classes/templates for (mostly audio) signal-processing tasks.
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).
## Basic use
```
git clone https://signalsmith-audio.co.uk/code/dsp.git
```
Just include the header file(s) you need, and start using classes:
```cpp
#include "dsp/delay.h"
using Delay = signalsmith::delay::Delay<float>;
Delay delayLine(1024);
```
You can add a compile-time version-check to make sure you have a compatible version of the library:
```cpp
#include "dsp/envelopes.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 1)
```
### Development / contributing
Tests (and source-scripts for the above docs) are available in a separate repo:
```
git clone https://signalsmith-audio.co.uk/code/dsp-doc.git
```
The goal (where possible) is to measure/test the actual audio characteristics of the tools (e.g. frequency responses and aliasing levels).
### License
This code is [MIT licensed](LICENSE.txt). If you'd prefer something else, get in touch.

View File

@ -1,47 +0,0 @@
#ifndef SIGNALSMITH_DSP_COMMON_H
#define SIGNALSMITH_DSP_COMMON_H
#if defined(__FAST_MATH__) && (__apple_build_version__ >= 16000000) && (__apple_build_version__ <= 16000099)
# error Apple Clang 16.0.0 generates incorrect SIMD for ARM. If you HAVE to use this version of Clang, turn off -ffast-math.
#endif
#ifndef M_PI
#define M_PI 3.14159265358979323846264338327950288
#endif
namespace signalsmith {
/** @defgroup Common Common
@brief Definitions and helper classes used by the rest of the library
@{
@file
*/
#define SIGNALSMITH_DSP_VERSION_MAJOR 1
#define SIGNALSMITH_DSP_VERSION_MINOR 6
#define SIGNALSMITH_DSP_VERSION_PATCH 1
#define SIGNALSMITH_DSP_VERSION_STRING "1.6.1"
/** Version compatability check.
\code{.cpp}
static_assert(signalsmith::version(1, 4, 1), "version check");
\endcode
... or use the equivalent `SIGNALSMITH_DSP_VERSION_CHECK`.
Major versions are not compatible with each other. Minor and patch versions are backwards-compatible.
*/
constexpr bool versionCheck(int major, int minor, int patch=0) {
return major == SIGNALSMITH_DSP_VERSION_MAJOR
&& (SIGNALSMITH_DSP_VERSION_MINOR > minor
|| (SIGNALSMITH_DSP_VERSION_MINOR == minor && SIGNALSMITH_DSP_VERSION_PATCH >= patch));
}
/// Check the library version is compatible (semver).
#define SIGNALSMITH_DSP_VERSION_CHECK(major, minor, patch) \
static_assert(::signalsmith::versionCheck(major, minor, patch), "signalsmith library version is " SIGNALSMITH_DSP_VERSION_STRING);
/** @} */
} // signalsmith::
#else
// If we've already included it, check it's the same version
static_assert(SIGNALSMITH_DSP_VERSION_MAJOR == 1 && SIGNALSMITH_DSP_VERSION_MINOR == 6 && SIGNALSMITH_DSP_VERSION_PATCH == 1, "multiple versions of the Signalsmith DSP library");
#endif // include guard

View File

@ -1,371 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_CURVES_H
#define SIGNALSMITH_DSP_CURVES_H
#include <vector>
#include <algorithm> // std::stable_sort
namespace signalsmith {
namespace curves {
/** @defgroup Curves Curves
@brief User-defined mapping functions
@{
@file
*/
/// Linear map for real values.
template<typename Sample=double>
class Linear {
Sample a1, a0;
public:
Linear() : Linear(0, 1) {}
Linear(Sample a0, Sample a1) : a1(a1), a0(a0) {}
/// Construct by from/to value pairs
Linear(Sample x0, Sample x1, Sample y0, Sample y1) : a1((x0 == x1) ? 0 : (y1 - y0)/(x1 - x0)), a0(y0 - x0*a1) {}
Sample operator ()(Sample x) const {
return a0 + x*a1;
}
Sample dx() const {
return a1;
}
/// Returns the inverse map (with some numerical error)
Linear inverse() const {
Sample invA1 = 1/a1;
return Linear(-a0*invA1, invA1);
}
};
/// A real-valued cubic curve. It has a "start" point where accuracy is highest.
template<typename Sample=double>
class Cubic {
Sample xStart, a0, a1, a2, a3;
// Only use with y0 != y1
static inline Sample gradient(Sample x0, Sample x1, Sample y0, Sample y1) {
return (y1 - y0)/(x1 - x0);
}
// Ensure a gradient produces monotonic segments
static inline void ensureMonotonic(Sample &curveGrad, Sample gradA, Sample gradB) {
if ((gradA <= 0 && gradB >= 0) || (gradA >= 0 && gradB <= 0)) {
curveGrad = 0; // point is a local minimum/maximum
} else {
if (std::abs(curveGrad) > std::abs(gradA*3)) {
curveGrad = gradA*3;
}
if (std::abs(curveGrad) > std::abs(gradB*3)) {
curveGrad = gradB*3;
}
}
}
// When we have duplicate x-values (either side) make up a gradient
static inline void chooseGradient(Sample &curveGrad, Sample grad1, Sample curveGradOther, Sample y0, Sample y1, bool monotonic) {
curveGrad = 2*grad1 - curveGradOther;
if (y0 != y1 && (y1 > y0) != (grad1 >= 0)) { // not duplicate y, but a local min/max
curveGrad = 0;
} else if (monotonic) {
if (grad1 >= 0) {
curveGrad = std::max<Sample>(0, curveGrad);
} else {
curveGrad = std::min<Sample>(0, curveGrad);
}
}
}
public:
Cubic() : Cubic(0, 0, 0, 0, 0) {}
Cubic(Sample xStart, Sample a0, Sample a1, Sample a2, Sample a3) : xStart(xStart), a0(a0), a1(a1), a2(a2), a3(a3) {}
Sample operator ()(Sample x) const {
x -= xStart;
return a0 + x*(a1 + x*(a2 + x*a3));
}
/// The reference x-value, used as the centre of the cubic expansion
Sample start() const {
return xStart;
}
/// Differentiate
Cubic dx() const {
return {xStart, a1, 2*a2, 3*a3, 0};
}
Sample dx(Sample x) const {
x -= xStart;
return a1 + x*(2*a2 + x*(3*a3));
}
/// Cubic segment based on start/end values and gradients
static Cubic hermite(Sample x0, Sample x1, Sample y0, Sample y1, Sample g0, Sample g1) {
Sample xScale = 1/(x1 - x0);
return {
x0, y0, g0,
(3*(y1 - y0)*xScale - 2*g0 - g1)*xScale,
(2*(y0 - y1)*xScale + g0 + g1)*(xScale*xScale)
};
}
/** Cubic segment (valid between `x1` and `x2`), which is smooth when applied to an adjacent set of points.
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.
*/
static Cubic smooth(Sample x0, Sample x1, Sample x2, Sample x3, Sample y0, Sample y1, Sample y2, Sample y3, bool monotonic=false) {
if (x1 == x2) return {0, y1, 0, 0, 0}; // zero-width segment, just return constant
Sample grad1 = gradient(x1, x2, y1, y2);
Sample curveGrad1 = grad1;
bool chooseGrad1 = false;
if (x0 != x1) { // we have a defined x0-x1 gradient
Sample grad0 = gradient(x0, x1, y0, y1);
curveGrad1 = (grad0 + grad1)*Sample(0.5);
if (monotonic) ensureMonotonic(curveGrad1, grad0, grad1);
} else if (y0 != y1 && (y1 > y0) != (grad1 >= 0)) {
curveGrad1 = 0; // set to 0 if it's a min/max
} else {
curveGrad1 = 0;
chooseGrad1 = true;
}
Sample curveGrad2;
if (x2 != x3) { // we have a defined x1-x2 gradient
Sample grad2 = gradient(x2, x3, y2, y3);
curveGrad2 = (grad1 + grad2)*Sample(0.5);
if (monotonic) ensureMonotonic(curveGrad2, grad1, grad2);
} else {
chooseGradient(curveGrad2, grad1, curveGrad1, y2, y3, monotonic);
}
if (chooseGrad1) {
chooseGradient(curveGrad1, grad1, curveGrad2, y0, y1, monotonic);
}
return hermite(x1, x2, y1, y2, curveGrad1, curveGrad2);
}
};
/** Smooth interpolation (optionally monotonic) between points, using cubic segments.
\diagram{cubic-segments-example.svg,Example curve including a repeated point and an instantaneous jump. The curve is flat beyond the first/last points.}
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.*/
template<typename Sample=double>
class CubicSegmentCurve {
struct Point {
Sample x, y;
Sample lineGrad = 0, curveGrad = 0;
bool hasCurveGrad = false;
Point() : Point(0, 0) {}
Point(Sample x, Sample y) : x(x), y(y) {}
bool operator <(const Point &other) const {
return x < other.x;
}
};
std::vector<Point> points;
Point first{0, 0}, last{0, 0};
std::vector<Cubic<Sample>> _segments{1};
// Not public because it's only valid inside the bounds
const Cubic<Sample> & findSegment(Sample x) const {
// Binary search
size_t low = 0, high = _segments.size();
while (true) {
size_t mid = (low + high)/2;
if (low == mid) break;
if (_segments[mid].start() <= x) {
low = mid;
} else {
high = mid;
}
}
return _segments[low];
}
public:
Sample lowGrad = 0;
Sample highGrad = 0;
/// Clear existing points and segments
void clear() {
points.resize(0);
_segments.resize(0);
first = last = {0, 0};
}
/// Add a new point, but does not recalculate the segments. `corner` just writes the point twice, for convenience.
CubicSegmentCurve & add(Sample x, Sample y, bool corner=false) {
points.push_back({x, y});
if (corner) points.push_back({x, y});
return *this;
}
/// Recalculates the segments.
void update(bool monotonic=false, bool extendGrad=true, Sample monotonicFactor=3) {
if (points.empty()) add(0, 0);
std::stable_sort(points.begin(), points.end()); // Ensure ascending order
_segments.resize(0);
// Calculate the point-to-point gradients
for (size_t i = 1; i < points.size(); ++i) {
auto &prev = points[i - 1];
auto &next = points[i];
if (prev.x != next.x) {
prev.lineGrad = (next.y - prev.y)/(next.x - prev.x);
} else {
prev.lineGrad = 0;
}
}
for (auto &p : points) p.hasCurveGrad = false;
points[0].curveGrad = lowGrad;
points[0].hasCurveGrad = true;
points.back().curveGrad = highGrad;
points.back().hasCurveGrad = true;
// Calculate curve gradient where we know it
for (size_t i = 1; i + 1 < points.size(); ++i) {
auto &p0 = points[i - 1];
auto &p1 = points[i];
auto &p2 = points[i + 1];
if (p0.x != p1.x && p1.x != p2.x) {
p1.curveGrad = (p0.lineGrad + p1.lineGrad)*Sample(0.5);
p1.hasCurveGrad = true;
}
}
for (size_t i = 1; i < points.size(); ++i) {
Point &p1 = points[i - 1];
Point &p2 = points[i];
if (p1.x == p2.x) continue;
if (p1.hasCurveGrad) {
if (!p2.hasCurveGrad) {
p2.curveGrad = 2*p1.lineGrad - p1.curveGrad;
}
} else if (p2.hasCurveGrad) {
p1.curveGrad = 2*p1.lineGrad - p2.curveGrad;
} else {
p1.curveGrad = p2.curveGrad = p1.lineGrad;
}
}
if (monotonic) {
for (size_t i = 1; i < points.size(); ++i) {
Point &p1 = points[i - 1];
Point &p2 = points[i];
if (p1.x != p2.x) {
if (p1.lineGrad >= 0) {
p1.curveGrad = std::max<Sample>(0, std::min(p1.curveGrad, p1.lineGrad*monotonicFactor));
p2.curveGrad = std::max<Sample>(0, std::min(p2.curveGrad, p1.lineGrad*monotonicFactor));
} else {
p1.curveGrad = std::min<Sample>(0, std::max(p1.curveGrad, p1.lineGrad*monotonicFactor));
p2.curveGrad = std::min<Sample>(0, std::max(p2.curveGrad, p1.lineGrad*monotonicFactor));
}
}
}
}
for (size_t i = 1; i < points.size(); ++i) {
Point &p1 = points[i - 1];
Point &p2 = points[i];
if (p1.x != p2.x) {
_segments.push_back(Segment::hermite(p1.x, p2.x, p1.y, p2.y, p1.curveGrad, p2.curveGrad));
}
}
first = points[0];
last = points.back();
if (extendGrad && _segments.size()) {
if (points[0].x != points[1].x || points[0].y == points[1].y) {
lowGrad = _segments[0].dx(first.x);
}
auto &last = points.back(), &last2 = points[points.size() - 1];
if (last.x != last2.x || last.y == last2.y) {
highGrad = _segments.back().dx(last.x);
}
}
}
/// Reads a value out from the curve.
Sample operator()(Sample x) const {
if (x <= first.x) return first.y + (x - first.x)*lowGrad;
if (x >= last.x) return last.y + (x - last.x)*highGrad;
return findSegment(x)(x);
}
CubicSegmentCurve dx() const {
CubicSegmentCurve result{*this};
result.first.y = lowGrad;
result.last.y = highGrad;
result.lowGrad = result.highGrad = 0;
for (auto &s : result._segments) {
s = s.dx();
}
return result;
}
Sample dx(Sample x) const {
if (x < first.x) return lowGrad;
if (x >= last.x) return highGrad;
return findSegment(x).dx(x);
}
using Segment = Cubic<Sample>;
std::vector<Segment> & segments() {
return _segments;
}
const std::vector<Segment> & segments() const {
return _segments;
}
};
/** A warped-range map, based on 1/x
\diagram{curves-reciprocal-example.svg}*/
template<typename Sample=double>
class Reciprocal {
Sample a, b, c, d; // (a + bx)/(c + dx)
Reciprocal(Sample a, Sample b, Sample c, Sample d) : a(a), b(b), c(c), d(d) {}
public:
/** Decent approximation to the Bark scale
The Bark index goes from 1-24, but this map is valid from approximately 0.25 - 27.5.
You can get the bandwidth by `barkScale.dx(barkIndex)`.
\diagram{curves-reciprocal-approx-bark.svg}*/
static Reciprocal<Sample> barkScale() {
return {1, 10, 24, 60, 1170, 13500};
}
/// Returns a map from 0-1 to the given (non-negative) Hz range.
static Reciprocal<Sample> barkRange(Sample lowHz, Sample highHz) {
Reciprocal bark = barkScale();
Sample lowBark = bark.inverse(lowHz), highBark = bark.inverse(highHz);
return Reciprocal(lowBark, (lowBark + highBark)/2, highBark).then(bark);
}
Reciprocal() : Reciprocal(0, 0.5, 1) {}
/// If no x-range given, default to the unit range
Reciprocal(Sample y0, Sample y1, Sample y2) : Reciprocal(0, 0.5, 1, y0, y1, y2) {}
Reciprocal(Sample x0, Sample x1, Sample x2, Sample y0, Sample y1, Sample y2) {
Sample kx = (x1 - x0)/(x2 - x1);
Sample ky = (y1 - y0)/(y2 - y1);
a = (kx*x2)*y0 - (ky*x0)*y2;
b = ky*y2 - kx*y0;
c = kx*x2 - ky*x0;
d = ky - kx;
}
Sample operator ()(double x) const {
return (a + b*x)/(c + d*x);
}
Reciprocal inverse() const {
return Reciprocal(-a, c, b, -d);
}
Sample inverse(Sample y) const {
return (c*y - a)/(b - d*y);
}
Sample dx(Sample x) const {
Sample l = (c + d*x);
return (b*c - a*d)/(l*l);
}
/// Combine two `Reciprocal`s together in sequence
Reciprocal then(const Reciprocal &other) const {
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);
}
};
/** @} */
}} // namespace
#endif // include guard

View File

@ -1,717 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_DELAY_H
#define SIGNALSMITH_DSP_DELAY_H
#include <vector>
#include <array>
#include <cmath> // for std::ceil()
#include <type_traits>
#include <complex>
#include "./fft.h"
#include "./windows.h"
namespace signalsmith {
namespace delay {
/** @defgroup Delay Delay utilities
@brief Standalone templated classes for delays
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.
Interpolation quality is chosen using a template class, from @ref Interpolators.
@{
@file
*/
/** @brief Single-channel delay buffer
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.
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).
The capacity includes both positive and negative indices. For example, a capacity of 100 would support using any of the ranges:
* `buffer[-99]` to buffer[0]`
* `buffer[-50]` to buffer[49]`
* `buffer[0]` to buffer[99]`
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`).
*/
template<typename Sample>
class Buffer {
unsigned bufferIndex;
unsigned bufferMask;
std::vector<Sample> buffer;
public:
Buffer(int minCapacity=0) {
resize(minCapacity);
}
// We shouldn't accidentally copy a delay buffer
Buffer(const Buffer &other) = delete;
Buffer & operator =(const Buffer &other) = delete;
// But moving one is fine
Buffer(Buffer &&other) = default;
Buffer & operator =(Buffer &&other) = default;
void resize(int minCapacity, Sample value=Sample()) {
int bufferLength = 1;
while (bufferLength < minCapacity) bufferLength *= 2;
buffer.assign(bufferLength, value);
bufferMask = unsigned(bufferLength - 1);
bufferIndex = 0;
}
void reset(Sample value=Sample()) {
buffer.assign(buffer.size(), value);
}
/// Holds a view for a particular position in the buffer
template<bool isConst>
class View {
using CBuffer = typename std::conditional<isConst, const Buffer, Buffer>::type;
using CSample = typename std::conditional<isConst, const Sample, Sample>::type;
CBuffer *buffer = nullptr;
unsigned bufferIndex = 0;
public:
View(CBuffer &buffer, int offset=0) : buffer(&buffer), bufferIndex(buffer.bufferIndex + (unsigned)offset) {}
View(const View &other, int offset=0) : buffer(other.buffer), bufferIndex(other.bufferIndex + (unsigned)offset) {}
View & operator =(const View &other) {
buffer = other.buffer;
bufferIndex = other.bufferIndex;
return *this;
}
CSample & operator[](int offset) {
return buffer->buffer[(bufferIndex + (unsigned)offset)&buffer->bufferMask];
}
const Sample & operator[](int offset) const {
return buffer->buffer[(bufferIndex + (unsigned)offset)&buffer->bufferMask];
}
/// Write data into the buffer
template<typename Data>
void write(Data &&data, int length) {
for (int i = 0; i < length; ++i) {
(*this)[i] = data[i];
}
}
/// Read data out from the buffer
template<typename Data>
void read(int length, Data &&data) const {
for (int i = 0; i < length; ++i) {
data[i] = (*this)[i];
}
}
View operator +(int offset) const {
return View(*this, offset);
}
View operator -(int offset) const {
return View(*this, -offset);
}
};
using MutableView = View<false>;
using ConstView = View<true>;
MutableView view(int offset=0) {
return MutableView(*this, offset);
}
ConstView view(int offset=0) const {
return ConstView(*this, offset);
}
ConstView constView(int offset=0) const {
return ConstView(*this, offset);
}
Sample & operator[](int offset) {
return buffer[(bufferIndex + (unsigned)offset)&bufferMask];
}
const Sample & operator[](int offset) const {
return buffer[(bufferIndex + (unsigned)offset)&bufferMask];
}
/// Write data into the buffer
template<typename Data>
void write(Data &&data, int length) {
for (int i = 0; i < length; ++i) {
(*this)[i] = data[i];
}
}
/// Read data out from the buffer
template<typename Data>
void read(int length, Data &&data) const {
for (int i = 0; i < length; ++i) {
data[i] = (*this)[i];
}
}
Buffer & operator ++() {
++bufferIndex;
return *this;
}
Buffer & operator +=(int i) {
bufferIndex += (unsigned)i;
return *this;
}
Buffer & operator --() {
--bufferIndex;
return *this;
}
Buffer & operator -=(int i) {
bufferIndex -= (unsigned)i;
return *this;
}
MutableView operator ++(int) {
MutableView view(*this);
++bufferIndex;
return view;
}
MutableView operator +(int i) {
return MutableView(*this, i);
}
ConstView operator +(int i) const {
return ConstView(*this, i);
}
MutableView operator --(int) {
MutableView view(*this);
--bufferIndex;
return view;
}
MutableView operator -(int i) {
return MutableView(*this, -i);
}
ConstView operator -(int i) const {
return ConstView(*this, -i);
}
};
/** @brief Multi-channel delay buffer
This behaves similarly to the single-channel `Buffer`, with the following differences:
* `buffer[c]` returns a view for a single channel, which behaves like the single-channel `Buffer::View`.
* The constructor and `.resize()` take an additional first `channel` argument.
*/
template<typename Sample>
class MultiBuffer {
int channels, stride;
Buffer<Sample> buffer;
public:
using ConstChannel = typename Buffer<Sample>::ConstView;
using MutableChannel = typename Buffer<Sample>::MutableView;
MultiBuffer(int channels=0, int capacity=0) : channels(channels), stride(capacity), buffer(channels*capacity) {}
void resize(int nChannels, int capacity, Sample value=Sample()) {
channels = nChannels;
stride = capacity;
buffer.resize(channels*capacity, value);
}
void reset(Sample value=Sample()) {
buffer.reset(value);
}
/// A reference-like multi-channel result for a particular sample index
template<bool isConst>
class Stride {
using CChannel = typename std::conditional<isConst, ConstChannel, MutableChannel>::type;
using CSample = typename std::conditional<isConst, const Sample, Sample>::type;
CChannel view;
int channels, stride;
public:
Stride(CChannel view, int channels, int stride) : view(view), channels(channels), stride(stride) {}
Stride(const Stride &other) : view(other.view), channels(other.channels), stride(other.stride) {}
CSample & operator[](int channel) {
return view[channel*stride];
}
const Sample & operator[](int channel) const {
return view[channel*stride];
}
/// Reads from the buffer into a multi-channel result
template<class Data>
void get(Data &&result) const {
for (int c = 0; c < channels; ++c) {
result[c] = view[c*stride];
}
}
/// Writes from multi-channel data into the buffer
template<class Data>
void set(Data &&data) {
for (int c = 0; c < channels; ++c) {
view[c*stride] = data[c];
}
}
template<class Data>
Stride & operator =(const Data &data) {
set(data);
return *this;
}
Stride & operator =(const Stride &data) {
set(data);
return *this;
}
};
Stride<false> at(int offset) {
return {buffer.view(offset), channels, stride};
}
Stride<true> at(int offset) const {
return {buffer.view(offset), channels, stride};
}
/// Holds a particular position in the buffer
template<bool isConst>
class View {
using CChannel = typename std::conditional<isConst, ConstChannel, MutableChannel>::type;
CChannel view;
int channels, stride;
public:
View(CChannel view, int channels, int stride) : view(view), channels(channels), stride(stride) {}
CChannel operator[](int channel) {
return view + channel*stride;
}
ConstChannel operator[](int channel) const {
return view + channel*stride;
}
Stride<isConst> at(int offset) {
return {view + offset, channels, stride};
}
Stride<true> at(int offset) const {
return {view + offset, channels, stride};
}
};
using MutableView = View<false>;
using ConstView = View<true>;
MutableView view(int offset=0) {
return MutableView(buffer.view(offset), channels, stride);
}
ConstView view(int offset=0) const {
return ConstView(buffer.view(offset), channels, stride);
}
ConstView constView(int offset=0) const {
return ConstView(buffer.view(offset), channels, stride);
}
MutableChannel operator[](int channel) {
return buffer + channel*stride;
}
ConstChannel operator[](int channel) const {
return buffer + channel*stride;
}
MultiBuffer & operator ++() {
++buffer;
return *this;
}
MultiBuffer & operator +=(int i) {
buffer += i;
return *this;
}
MutableView operator ++(int) {
return MutableView(buffer++, channels, stride);
}
MutableView operator +(int i) {
return MutableView(buffer + i, channels, stride);
}
ConstView operator +(int i) const {
return ConstView(buffer + i, channels, stride);
}
MultiBuffer & operator --() {
--buffer;
return *this;
}
MultiBuffer & operator -=(int i) {
buffer -= i;
return *this;
}
MutableView operator --(int) {
return MutableView(buffer--, channels, stride);
}
MutableView operator -(int i) {
return MutableView(buffer - i, channels, stride);
}
ConstView operator -(int i) const {
return ConstView(buffer - i, channels, stride);
}
};
/** \defgroup Interpolators Interpolators
\ingroup Delay
@{ */
/// Nearest-neighbour interpolator
/// \diagram{delay-random-access-nearest.svg,aliasing and maximum amplitude/delay errors for different input frequencies}
template<typename Sample>
struct InterpolatorNearest {
static constexpr int inputLength = 1;
static constexpr Sample latency = -0.5; // Because we're truncating, which rounds down too often
template<class Data>
static Sample fractional(const Data &data, Sample) {
return data[0];
}
};
/// Linear interpolator
/// \diagram{delay-random-access-linear.svg,aliasing and maximum amplitude/delay errors for different input frequencies}
template<typename Sample>
struct InterpolatorLinear {
static constexpr int inputLength = 2;
static constexpr int latency = 0;
template<class Data>
static Sample fractional(const Data &data, Sample fractional) {
Sample a = data[0], b = data[1];
return a + fractional*(b - a);
}
};
/// Spline cubic interpolator
/// \diagram{delay-random-access-cubic.svg,aliasing and maximum amplitude/delay errors for different input frequencies}
template<typename Sample>
struct InterpolatorCubic {
static constexpr int inputLength = 4;
static constexpr int latency = 1;
template<class Data>
static Sample fractional(const Data &data, Sample fractional) {
// Cubic interpolation
Sample a = data[0], b = data[1], c = data[2], d = data[3];
Sample cbDiff = c - b;
Sample k1 = (c - a)*0.5;
Sample k3 = k1 + (d - b)*0.5 - cbDiff*2;
Sample k2 = cbDiff - k3 - k1;
return b + fractional*(k1 + fractional*(k2 + fractional*k3)); // 16 ops total, not including the indexing
}
};
// Efficient Algorithms and Structures for Fractional Delay Filtering Based on Lagrange Interpolation
// Franck 2009 https://www.aes.org/e-lib/browse.cfm?elib=14647
namespace _franck_impl {
template<typename Sample, int n, int low, int high>
struct ProductRange {
using Array = std::array<Sample, (n + 1)>;
static constexpr int mid = (low + high)/2;
using Left = ProductRange<Sample, n, low, mid>;
using Right = ProductRange<Sample, n, mid + 1, high>;
Left left;
Right right;
const Sample total;
ProductRange(Sample x) : left(x), right(x), total(left.total*right.total) {}
template<class Data>
Sample calculateResult(Sample extraFactor, const Data &data, const Array &invFactors) {
return left.calculateResult(extraFactor*right.total, data, invFactors)
+ right.calculateResult(extraFactor*left.total, data, invFactors);
}
};
template<typename Sample, int n, int index>
struct ProductRange<Sample, n, index, index> {
using Array = std::array<Sample, (n + 1)>;
const Sample total;
ProductRange(Sample x) : total(x - index) {}
template<class Data>
Sample calculateResult(Sample extraFactor, const Data &data, const Array &invFactors) {
return extraFactor*data[index]*invFactors[index];
}
};
}
/** Fixed-order Lagrange interpolation.
\diagram{interpolator-LagrangeN.svg,aliasing and amplitude/delay errors for different sizes}
*/
template<typename Sample, int n>
struct InterpolatorLagrangeN {
static constexpr int inputLength = n + 1;
static constexpr int latency = (n - 1)/2;
using Array = std::array<Sample, (n + 1)>;
Array invDivisors;
InterpolatorLagrangeN() {
for (int j = 0; j <= n; ++j) {
double divisor = 1;
for (int k = 0; k < j; ++k) divisor *= (j - k);
for (int k = j + 1; k <= n; ++k) divisor *= (j - k);
invDivisors[j] = 1/divisor;
}
}
template<class Data>
Sample fractional(const Data &data, Sample fractional) const {
constexpr int mid = n/2;
using Left = _franck_impl::ProductRange<Sample, n, 0, mid>;
using Right = _franck_impl::ProductRange<Sample, n, mid + 1, n>;
Sample x = fractional + latency;
Left left(x);
Right right(x);
return left.calculateResult(right.total, data, invDivisors) + right.calculateResult(left.total, data, invDivisors);
}
};
template<typename Sample>
using InterpolatorLagrange3 = InterpolatorLagrangeN<Sample, 3>;
template<typename Sample>
using InterpolatorLagrange7 = InterpolatorLagrangeN<Sample, 7>;
template<typename Sample>
using InterpolatorLagrange19 = InterpolatorLagrangeN<Sample, 19>;
/** Fixed-size Kaiser-windowed sinc interpolation.
\diagram{interpolator-KaiserSincN.svg,aliasing and amplitude/delay errors for different sizes}
If `minimumPhase` is enabled, a minimum-phase version of the kernel is used:
\diagram{interpolator-KaiserSincN-min.svg,aliasing and amplitude/delay errors for minimum-phase mode}
*/
template<typename Sample, int n, bool minimumPhase=false>
struct InterpolatorKaiserSincN {
static constexpr int inputLength = n;
static constexpr Sample latency = minimumPhase ? 0 : (n*Sample(0.5) - 1);
int subSampleSteps;
std::vector<Sample> coefficients;
InterpolatorKaiserSincN() : InterpolatorKaiserSincN(0.5 - 0.45/std::sqrt(n)) {}
InterpolatorKaiserSincN(double passFreq) : InterpolatorKaiserSincN(passFreq, 1 - passFreq) {}
InterpolatorKaiserSincN(double passFreq, double stopFreq) {
subSampleSteps = 2*n; // Heuristic again. Really it depends on the bandwidth as well.
double kaiserBandwidth = (stopFreq - passFreq)*(n + 1.0/subSampleSteps);
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.
double sincScale = M_PI*(passFreq + stopFreq);
double centreIndex = n*subSampleSteps*0.5, scaleFactor = 1.0/subSampleSteps;
std::vector<Sample> windowedSinc(subSampleSteps*n + 1);
::signalsmith::windows::Kaiser::withBandwidth(kaiserBandwidth, false).fill(windowedSinc, windowedSinc.size());
for (size_t i = 0; i < windowedSinc.size(); ++i) {
double x = (i - centreIndex)*scaleFactor;
int intX = std::round(x);
if (intX != 0 && std::abs(x - intX) < 1e-6) {
// Exact 0s
windowedSinc[i] = 0;
} else if (std::abs(x) > 1e-6) {
double p = x*sincScale;
windowedSinc[i] *= std::sin(p)/p;
}
}
if (minimumPhase) {
signalsmith::fft::FFT<Sample> fft(windowedSinc.size()*2, 1);
windowedSinc.resize(fft.size(), 0);
std::vector<std::complex<Sample>> spectrum(fft.size());
std::vector<std::complex<Sample>> cepstrum(fft.size());
fft.fft(windowedSinc, spectrum);
for (size_t i = 0; i < fft.size(); ++i) {
spectrum[i] = std::log(std::abs(spectrum[i]) + 1e-30);
}
fft.fft(spectrum, cepstrum);
for (size_t i = 1; i < fft.size()/2; ++i) {
cepstrum[i] *= 0;
}
for (size_t i = fft.size()/2 + 1; i < fft.size(); ++i) {
cepstrum[i] *= 2;
}
Sample scaling = Sample(1)/fft.size();
fft.ifft(cepstrum, spectrum);
for (size_t i = 0; i < fft.size(); ++i) {
Sample phase = spectrum[i].imag()*scaling;
Sample mag = std::exp(spectrum[i].real()*scaling);
spectrum[i] = {mag*std::cos(phase), mag*std::sin(phase)};
}
fft.ifft(spectrum, cepstrum);
windowedSinc.resize(subSampleSteps*n + 1);
windowedSinc.shrink_to_fit();
for (size_t i = 0; i < windowedSinc.size(); ++i) {
windowedSinc[i] = cepstrum[i].real()*scaling;
}
}
// Re-order into FIR fractional-delay blocks
coefficients.resize(n*(subSampleSteps + 1));
for (int k = 0; k <= subSampleSteps; ++k) {
for (int i = 0; i < n; ++i) {
coefficients[k*n + i] = windowedSinc[(subSampleSteps - k) + i*subSampleSteps];
}
}
}
template<class Data>
Sample fractional(const Data &data, Sample fractional) const {
Sample subSampleDelay = fractional*subSampleSteps;
int lowIndex = subSampleDelay;
if (lowIndex >= subSampleSteps) lowIndex = subSampleSteps - 1;
Sample subSampleFractional = subSampleDelay - lowIndex;
int highIndex = lowIndex + 1;
Sample sumLow = 0, sumHigh = 0;
const Sample *coeffLow = coefficients.data() + lowIndex*n;
const Sample *coeffHigh = coefficients.data() + highIndex*n;
for (int i = 0; i < n; ++i) {
sumLow += data[i]*coeffLow[i];
sumHigh += data[i]*coeffHigh[i];
}
return sumLow + (sumHigh - sumLow)*subSampleFractional;
}
};
template<typename Sample>
using InterpolatorKaiserSinc20 = InterpolatorKaiserSincN<Sample, 20>;
template<typename Sample>
using InterpolatorKaiserSinc8 = InterpolatorKaiserSincN<Sample, 8>;
template<typename Sample>
using InterpolatorKaiserSinc4 = InterpolatorKaiserSincN<Sample, 4>;
template<typename Sample>
using InterpolatorKaiserSinc20Min = InterpolatorKaiserSincN<Sample, 20, true>;
template<typename Sample>
using InterpolatorKaiserSinc8Min = InterpolatorKaiserSincN<Sample, 8, true>;
template<typename Sample>
using InterpolatorKaiserSinc4Min = InterpolatorKaiserSincN<Sample, 4, true>;
/// @}
/** @brief A delay-line reader which uses an external buffer
This is useful if you have multiple delay-lines reading from the same buffer.
*/
template<class Sample, template<typename> class Interpolator=InterpolatorLinear>
class Reader : public Interpolator<Sample> /* so we can get the empty-base-class optimisation */ {
using Super = Interpolator<Sample>;
public:
Reader () {}
/// Pass in a configured interpolator
Reader (const Interpolator<Sample> &interpolator) : Super(interpolator) {}
template<typename Buffer>
Sample read(const Buffer &buffer, Sample delaySamples) const {
int startIndex = delaySamples;
Sample remainder = delaySamples - startIndex;
// Delay buffers use negative indices, but interpolators use positive ones
using View = decltype(buffer - startIndex);
struct Flipped {
View view;
Sample operator [](int i) const {
return view[-i];
}
};
return Super::fractional(Flipped{buffer - startIndex}, remainder);
}
};
/** @brief A single-channel delay-line containing its own buffer.*/
template<class Sample, template<typename> class Interpolator=InterpolatorLinear>
class Delay : private Reader<Sample, Interpolator> {
using Super = Reader<Sample, Interpolator>;
Buffer<Sample> buffer;
public:
static constexpr Sample latency = Super::latency;
Delay(int capacity=0) : buffer(1 + capacity + Super::inputLength) {}
/// Pass in a configured interpolator
Delay(const Interpolator<Sample> &interp, int capacity=0) : Super(interp), buffer(1 + capacity + Super::inputLength) {}
void reset(Sample value=Sample()) {
buffer.reset(value);
}
void resize(int minCapacity, Sample value=Sample()) {
buffer.resize(minCapacity + Super::inputLength, value);
}
/** Read a sample from `delaySamples` >= 0 in the past.
The interpolator may add its own latency on top of this (see `Delay::latency`). The default interpolation (linear) has 0 latency.
*/
Sample read(Sample delaySamples) const {
return Super::read(buffer, delaySamples);
}
/// Writes a sample. Returns the same object, so that you can say `delay.write(v).read(delay)`.
Delay & write(Sample value) {
++buffer;
buffer[0] = value;
return *this;
}
};
/** @brief A multi-channel delay-line with its own buffer. */
template<class Sample, template<typename> class Interpolator=InterpolatorLinear>
class MultiDelay : private Reader<Sample, Interpolator> {
using Super = Reader<Sample, Interpolator>;
int channels;
MultiBuffer<Sample> multiBuffer;
public:
static constexpr Sample latency = Super::latency;
MultiDelay(int channels=0, int capacity=0) : channels(channels), multiBuffer(channels, 1 + capacity + Super::inputLength) {}
void reset(Sample value=Sample()) {
multiBuffer.reset(value);
}
void resize(int nChannels, int capacity, Sample value=Sample()) {
channels = nChannels;
multiBuffer.resize(channels, capacity + Super::inputLength, value);
}
/// A single-channel delay-line view, similar to a `const Delay`
struct ChannelView {
static constexpr Sample latency = Super::latency;
const Super &reader;
typename MultiBuffer<Sample>::ConstChannel channel;
Sample read(Sample delaySamples) const {
return reader.read(channel, delaySamples);
}
};
ChannelView operator [](int channel) const {
return ChannelView{*this, multiBuffer[channel]};
}
/// A multi-channel result, lazily calculating samples
struct DelayView {
Super &reader;
typename MultiBuffer<Sample>::ConstView view;
Sample delaySamples;
// Calculate samples on-the-fly
Sample operator [](int c) const {
return reader.read(view[c], delaySamples);
}
};
DelayView read(Sample delaySamples) {
return DelayView{*this, multiBuffer.constView(), delaySamples};
}
/// Reads into the provided output structure
template<class Output>
void read(Sample delaySamples, Output &output) {
for (int c = 0; c < channels; ++c) {
output[c] = Super::read(multiBuffer[c], delaySamples);
}
}
/// Reads separate delays for each channel
template<class Delays, class Output>
void readMulti(const Delays &delays, Output &output) {
for (int c = 0; c < channels; ++c) {
output[c] = Super::read(multiBuffer[c], delays[c]);
}
}
template<class Data>
MultiDelay & write(const Data &data) {
++multiBuffer;
for (int c = 0; c < channels; ++c) {
multiBuffer[c][0] = data[c];
}
return *this;
}
};
/** @} */
}} // signalsmith::delay::
#endif // include guard

View File

@ -1,523 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_ENVELOPES_H
#define SIGNALSMITH_DSP_ENVELOPES_H
#include <cmath>
#include <random>
#include <vector>
#include <iterator>
namespace signalsmith {
namespace envelopes {
/** @defgroup Envelopes Envelopes and LFOs
@brief LFOs, envelopes and filters for manipulating them
@{
@file
*/
/** An LFO based on cubic segments.
You can randomise the rate and/or the depth. Randomising the depth past `0.5` means it no longer neatly alternates sides:
\diagram{cubic-lfo-example.svg,Some example LFO curves.}
Without randomisation, it is approximately sine-like:
\diagram{cubic-lfo-spectrum-pure.svg}
*/
class CubicLfo {
float ratio = 0;
float ratioStep = 0;
float valueFrom = 0, valueTo = 1, valueRange = 1;
float targetLow = 0, targetHigh = 1;
float targetRate = 0;
float rateRandom = 0.5, depthRandom = 0;
bool freshReset = true;
std::default_random_engine randomEngine;
std::uniform_real_distribution<float> randomUnit;
float random() {
return randomUnit(randomEngine);
}
float randomRate() {
return targetRate*exp(rateRandom*(random() - 0.5));
}
float randomTarget(float previous) {
float randomOffset = depthRandom*random()*(targetLow - targetHigh);
if (previous < (targetLow + targetHigh)*0.5f) {
return targetHigh + randomOffset;
} else {
return targetLow - randomOffset;
}
}
public:
CubicLfo() : randomEngine(std::random_device()()), randomUnit(0, 1) {
reset();
}
CubicLfo(long seed) : randomUnit(0, 1) {
randomEngine.seed(seed);
reset();
}
/// Resets the LFO state, starting with random phase.
void reset() {
ratio = random();
ratioStep = randomRate();
if (random() < 0.5) {
valueFrom = targetLow;
valueTo = targetHigh;
} else {
valueFrom = targetHigh;
valueTo = targetLow;
}
valueRange = valueTo - valueFrom;
freshReset = true;
}
/** Smoothly updates the LFO parameters.
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:
\diagram{cubic-lfo-changes.svg}
The LFO will complete a full oscillation in (approximately) `1/rate` samples. `rateVariation` can be any number, but 0-1 is a good range.
`depthVariation` must be in the range [0, 1], where 0.5 produces random amplitude but still alternates up/down.
\diagram{cubic-lfo-spectrum.svg,Spectra for the two types of randomisation - note the jump as depth variation goes past 50%}
*/
void set(float low, float high, float rate, float rateVariation=0, float depthVariation=0) {
rate *= 2; // We want to go up and down during this period
targetRate = rate;
targetLow = std::min(low, high);
targetHigh = std::max(low, high);
rateRandom = rateVariation;
depthRandom = std::min<float>(1, std::max<float>(0, depthVariation));
// If we haven't called .next() yet, don't bother being smooth.
if (freshReset) return reset();
// Only update the current rate if it's outside our new random-variation range
float maxRandomRatio = exp((float)0.5*rateRandom);
if (ratioStep > rate*maxRandomRatio || ratioStep < rate/maxRandomRatio) {
ratioStep = randomRate();
}
}
/// Returns the next output sample
float next() {
freshReset = false;
float result = ratio*ratio*(3 - 2*ratio)*valueRange + valueFrom;
ratio += ratioStep;
while (ratio >= 1) {
ratio -= 1;
ratioStep = randomRate();
valueFrom = valueTo;
valueTo = randomTarget(valueFrom);
valueRange = valueTo - valueFrom;
}
return result;
}
};
/** Variable-width rectangular sum */
template<typename Sample=double>
class BoxSum {
int bufferLength, index;
std::vector<Sample> buffer;
Sample sum = 0, wrapJump = 0;
public:
BoxSum(int maxLength) {
resize(maxLength);
}
/// Sets the maximum size (and reset contents)
void resize(int maxLength) {
bufferLength = maxLength + 1;
buffer.resize(bufferLength);
if (maxLength != 0) buffer.shrink_to_fit();
reset();
}
/// Resets (with an optional "fill" value)
void reset(Sample value=Sample()) {
index = 0;
sum = 0;
for (size_t i = 0; i < buffer.size(); ++i) {
buffer[i] = sum;
sum += value;
}
wrapJump = sum;
sum = 0;
}
Sample read(int width) {
int readIndex = index - width;
double result = sum;
if (readIndex < 0) {
result += wrapJump;
readIndex += bufferLength;
}
return result - buffer[readIndex];
}
void write(Sample value) {
++index;
if (index == bufferLength) {
index = 0;
wrapJump = sum;
sum = 0;
}
sum += value;
buffer[index] = sum;
}
Sample readWrite(Sample value, int width) {
write(value);
return read(width);
}
};
/** Rectangular moving average filter (FIR).
\diagram{box-filter-example.svg}
A filter of length 1 has order 0 (i.e. does nothing). */
template<typename Sample=double>
class BoxFilter {
BoxSum<Sample> boxSum;
int _length, _maxLength;
Sample multiplier;
public:
BoxFilter(int maxLength) : boxSum(maxLength) {
resize(maxLength);
}
/// Sets the maximum size (and current size, and resets)
void resize(int maxLength) {
_maxLength = maxLength;
boxSum.resize(maxLength);
set(maxLength);
}
/// Sets the current size (expanding/allocating only if needed)
void set(int length) {
_length = length;
multiplier = Sample(1)/length;
if (length > _maxLength) resize(length);
}
/// Resets (with an optional "fill" value)
void reset(Sample fill=Sample()) {
boxSum.reset(fill);
}
Sample operator()(Sample v) {
return boxSum.readWrite(v, _length)*multiplier;
}
};
/** FIR filter made from a stack of `BoxFilter`s.
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:
\diagram{box-stack-long.svg,Impulse responses for various stack sizes at length N=1000}
Since the underlying box-averages must have integer width, the peaks are slightly higher for shorter lengths with higher numbers of layers:
\diagram{box-stack-short-freq.svg,Frequency responses for various stack sizes at length N=30}
*/
template<typename Sample=double>
class BoxStackFilter {
struct Layer {
double ratio = 0, lengthError = 0;
int length = 0;
BoxFilter<Sample> filter{0};
Layer() {}
};
int _size;
std::vector<Layer> layers;
template<class Iterable>
void setupLayers(const Iterable &ratios) {
layers.resize(0);
double sum = 0;
for (auto ratio : ratios) {
Layer layer;
layer.ratio = ratio;
layers.push_back(layer);
sum += ratio;
}
double factor = 1/sum;
for (auto &l : layers) {
l.ratio *= factor;
}
}
public:
BoxStackFilter(int maxSize, int layers=4) {
resize(maxSize, layers);
}
/// Returns an optimal set of length ratios (heuristic for larger depths)
static std::vector<double> optimalRatios(int layerCount) {
// Coefficients up to 6, found through numerical search
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 */};
if (layerCount <= 0) {
return {};
} else if (layerCount <= 6) {
double *start = &hardcoded[layerCount*(layerCount - 1)/2];
return std::vector<double>(start, start + layerCount);
}
std::vector<double> result(layerCount);
double invN = 1.0/layerCount, sqrtN = std::sqrt(layerCount);
double p = 1 - invN;
double k = 1 + 4.5/sqrtN + 0.08*sqrtN;
double sum = 0;
for (int i = 0; i < layerCount; ++i) {
double x = i*invN;
double power = -x*(1 - p*std::exp(-x*k));
double length = std::pow(2, power);
result[i] = length;
sum += length;
}
double factor = 1/sum;
for (auto &r : result) r *= factor;
return result;
}
/** Approximate (optimal) bandwidth for a given number of layers
\diagram{box-stack-bandwidth.svg,Approximate main lobe width (bandwidth)}
*/
static constexpr double layersToBandwidth(int layers) {
return 1.58*(layers + 0.1);
}
/** Approximate (optimal) peak in the stop-band
\diagram{box-stack-peak.svg,Heuristic stop-band peak}
*/
static constexpr double layersToPeakDb(int layers) {
return 5 - layers*18;
}
/// Sets size using an optimal (heuristic at larger sizes) set of length ratios
void resize(int maxSize, int layerCount) {
resize(maxSize, optimalRatios(layerCount));
}
/// Sets the maximum (and current) impulse response length and explicit length ratios
template<class List>
auto resize(int maxSize, List ratios) -> decltype(void(std::begin(ratios)), void(std::end(ratios))) {
setupLayers(ratios);
for (auto &layer : layers) layer.filter.resize(0); // .set() will expand it later
_size = -1;
set(maxSize);
reset();
}
void resize(int maxSize, std::initializer_list<double> ratios) {
resize<const std::initializer_list<double> &>(maxSize, ratios);
}
/// Sets the impulse response length (does not reset if `size` ≤ `maxSize`)
void set(int size) {
if (layers.size() == 0) return; // meaningless
if (_size == size) return;
_size = size;
int order = size - 1;
int totalOrder = 0;
for (auto &layer : layers) {
double layerOrderFractional = layer.ratio*order;
int layerOrder = int(layerOrderFractional);
layer.length = layerOrder + 1;
layer.lengthError = layerOrder - layerOrderFractional;
totalOrder += layerOrder;
}
// Round some of them up, so the total is correct - this is O(N²), but `layers.size()` is small
while (totalOrder < order) {
int minIndex = 0;
double minError = layers[0].lengthError;
for (size_t i = 1; i < layers.size(); ++i) {
if (layers[i].lengthError < minError) {
minError = layers[i].lengthError;
minIndex = i;
}
}
layers[minIndex].length++;
layers[minIndex].lengthError += 1;
totalOrder++;
}
for (auto &layer : layers) layer.filter.set(layer.length);
}
/// Resets the filter
void reset(Sample fill=Sample()) {
for (auto &layer : layers) layer.filter.reset(fill);
}
Sample operator()(Sample v) {
for (auto &layer : layers) {
v = layer.filter(v);
}
return v;
}
};
/** Peak-hold filter.
\diagram{peak-hold.svg}
The size is variable, and can be changed instantly with `.set()`, or by using `.push()`/`.pop()` in an unbalanced way.
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.
*/
template<typename Sample>
class PeakHold {
static constexpr Sample lowest = std::numeric_limits<Sample>::lowest();
int bufferMask;
std::vector<Sample> buffer;
int backIndex = 0, middleStart = 0, workingIndex = 0, middleEnd = 0, frontIndex = 0;
Sample frontMax = lowest, workingMax = lowest, middleMax = lowest;
public:
PeakHold(int maxLength) {
resize(maxLength);
}
int size() {
return frontIndex - backIndex;
}
void resize(int maxLength) {
int bufferLength = 1;
while (bufferLength < maxLength) bufferLength *= 2;
buffer.resize(bufferLength);
bufferMask = bufferLength - 1;
frontIndex = backIndex + maxLength;
reset();
}
void reset(Sample fill=lowest) {
int prevSize = size();
buffer.assign(buffer.size(), fill);
frontMax = workingMax = middleMax = lowest;
middleEnd = workingIndex = frontIndex = 0;
middleStart = middleEnd - (prevSize/2);
backIndex = frontIndex - prevSize;
}
/** Sets the size immediately.
Must be `0 <= newSize <= maxLength` (see constructor and `.resize()`).
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.*/
void set(int newSize, bool preserveCurrentPeak=false) {
while (size() < newSize) {
Sample &backPrev = buffer[backIndex&bufferMask];
--backIndex;
Sample &back = buffer[backIndex&bufferMask];
back = preserveCurrentPeak ? backPrev : std::max(back, backPrev);
}
while (size() > newSize) {
pop();
}
}
void push(Sample v) {
buffer[frontIndex&bufferMask] = v;
++frontIndex;
frontMax = std::max(frontMax, v);
}
void pop() {
if (backIndex == middleStart) {
// Move along the maximums
workingMax = lowest;
middleMax = frontMax;
frontMax = lowest;
int prevFrontLength = frontIndex - middleEnd;
int prevMiddleLength = middleEnd - middleStart;
if (prevFrontLength <= prevMiddleLength + 1) {
// Swap over simply
middleStart = middleEnd;
middleEnd = frontIndex;
workingIndex = middleEnd;
} else {
// The front is longer than the middle - only happens if unbalanced
// We don't move *all* of the front over, keeping half the surplus in the front
int middleLength = (frontIndex - middleStart)/2;
middleStart = middleEnd;
middleEnd += middleLength;
// Working index is close enough that it will be finished by the time the back is empty
int backLength = middleStart - backIndex;
int workingLength = std::min(backLength, middleEnd - middleStart);
workingIndex = middleStart + workingLength;
// Since the front was not completely consumed, we re-calculate the front's maximum
for (int i = middleEnd; i != frontIndex; ++i) {
frontMax = std::max(frontMax, buffer[i&bufferMask]);
}
// The index might not start at the end of the working block - compute the last bit immediately
for (int i = middleEnd - 1; i != workingIndex - 1; --i) {
buffer[i&bufferMask] = workingMax = std::max(workingMax, buffer[i&bufferMask]);
}
}
// Is the new back (previous middle) empty? Only happens if unbalanced
if (backIndex == middleStart) {
// swap over again (front's empty, no change)
workingMax = lowest;
middleMax = frontMax;
frontMax = lowest;
middleStart = workingIndex = middleEnd;
if (backIndex == middleStart) {
--backIndex; // Only happens if you pop from an empty list - fail nicely
}
}
buffer[frontIndex&bufferMask] = lowest; // In case of length 0, when everything points at this value
}
++backIndex;
if (workingIndex != middleStart) {
--workingIndex;
buffer[workingIndex&bufferMask] = workingMax = std::max(workingMax, buffer[workingIndex&bufferMask]);
}
}
Sample read() {
Sample backMax = buffer[backIndex&bufferMask];
return std::max(backMax, std::max(middleMax, frontMax));
}
// For simple use as a constant-length filter
Sample operator ()(Sample v) {
push(v);
pop();
return read();
}
};
/** Peak-decay filter with a linear shape and fixed-time return to constant value.
\diagram{peak-decay-linear.svg}
This is equivalent to a `BoxFilter` which resets itself whenever the output would be less than the input.
*/
template<typename Sample=double>
class PeakDecayLinear {
static constexpr Sample lowest = std::numeric_limits<Sample>::lowest();
PeakHold<Sample> peakHold;
Sample value = lowest;
Sample stepMultiplier = 1;
public:
PeakDecayLinear(int maxLength) : peakHold(maxLength) {
set(maxLength);
}
void resize(int maxLength) {
peakHold.resize(maxLength);
reset();
}
void set(double length) {
peakHold.set(std::ceil(length));
// Overshoot slightly but don't exceed 1
stepMultiplier = Sample(1.0001)/std::max(1.0001, length);
}
void reset(Sample start=lowest) {
peakHold.reset(start);
set(peakHold.size());
value = start;
}
Sample operator ()(Sample v) {
Sample peak = peakHold.read();
peakHold(v);
return value = std::max<Sample>(v, value + (v - peak)*stepMultiplier);
}
};
/** @} */
}} // signalsmith::envelopes::
#endif // include guard

523
dsp/fft.h
View File

@ -1,523 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_FFT_V5
#define SIGNALSMITH_FFT_V5
#include "./perf.h"
#include <vector>
#include <complex>
#include <cmath>
namespace signalsmith { namespace fft {
/** @defgroup FFT FFT (complex and real)
@brief Fourier transforms (complex and real)
@{
@file
*/
namespace _fft_impl {
template <typename V>
SIGNALSMITH_INLINE V complexReal(const std::complex<V> &c) {
return ((V*)(&c))[0];
}
template <typename V>
SIGNALSMITH_INLINE V complexImag(const std::complex<V> &c) {
return ((V*)(&c))[1];
}
// Complex multiplication has edge-cases around Inf/NaN - handling those properly makes std::complex non-inlineable, so we use our own
template <bool conjugateSecond, typename V>
SIGNALSMITH_INLINE std::complex<V> complexMul(const std::complex<V> &a, const std::complex<V> &b) {
V aReal = complexReal(a), aImag = complexImag(a);
V bReal = complexReal(b), bImag = complexImag(b);
return conjugateSecond ? std::complex<V>{
bReal*aReal + bImag*aImag,
bReal*aImag - bImag*aReal
} : std::complex<V>{
aReal*bReal - aImag*bImag,
aReal*bImag + aImag*bReal
};
}
template<bool flipped, typename V>
SIGNALSMITH_INLINE std::complex<V> complexAddI(const std::complex<V> &a, const std::complex<V> &b) {
V aReal = complexReal(a), aImag = complexImag(a);
V bReal = complexReal(b), bImag = complexImag(b);
return flipped ? std::complex<V>{
aReal + bImag,
aImag - bReal
} : std::complex<V>{
aReal - bImag,
aImag + bReal
};
}
// Use SFINAE to get an iterator from std::begin(), if supported - otherwise assume the value itself is an iterator
template<typename T, typename=void>
struct GetIterator {
static T get(const T &t) {
return t;
}
};
template<typename T>
struct GetIterator<T, decltype((void)std::begin(std::declval<T>()))> {
static auto get(const T &t) -> decltype(std::begin(t)) {
return std::begin(t);
}
};
}
/** Floating-point FFT implementation.
It is fast for 2^a * 3^b.
Here are the peak and RMS errors for `float`/`double` computation:
\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)}
*/
template<typename V=double>
class FFT {
using complex = std::complex<V>;
size_t _size;
std::vector<complex> workingVector;
enum class StepType {
generic, step2, step3, step4
};
struct Step {
StepType type;
size_t factor;
size_t startIndex;
size_t innerRepeats;
size_t outerRepeats;
size_t twiddleIndex;
};
std::vector<size_t> factors;
std::vector<Step> plan;
std::vector<complex> twiddleVector;
struct PermutationPair {size_t from, to;};
std::vector<PermutationPair> permutation;
void addPlanSteps(size_t factorIndex, size_t start, size_t length, size_t repeats) {
if (factorIndex >= factors.size()) return;
size_t factor = factors[factorIndex];
if (factorIndex + 1 < factors.size()) {
if (factors[factorIndex] == 2 && factors[factorIndex + 1] == 2) {
++factorIndex;
factor = 4;
}
}
size_t subLength = length/factor;
Step mainStep{StepType::generic, factor, start, subLength, repeats, twiddleVector.size()};
if (factor == 2) mainStep.type = StepType::step2;
if (factor == 3) mainStep.type = StepType::step3;
if (factor == 4) mainStep.type = StepType::step4;
// Twiddles
bool foundStep = false;
for (const Step &existingStep : plan) {
if (existingStep.factor == mainStep.factor && existingStep.innerRepeats == mainStep.innerRepeats) {
foundStep = true;
mainStep.twiddleIndex = existingStep.twiddleIndex;
break;
}
}
if (!foundStep) {
for (size_t i = 0; i < subLength; ++i) {
for (size_t f = 0; f < factor; ++f) {
double phase = 2*M_PI*i*f/length;
complex twiddle = {V(std::cos(phase)), V(-std::sin(phase))};
twiddleVector.push_back(twiddle);
}
}
}
if (repeats == 1 && sizeof(complex)*subLength > 65536) {
for (size_t i = 0; i < factor; ++i) {
addPlanSteps(factorIndex + 1, start + i*subLength, subLength, 1);
}
} else {
addPlanSteps(factorIndex + 1, start, subLength, repeats*factor);
}
plan.push_back(mainStep);
}
void setPlan() {
factors.resize(0);
size_t size = _size, factor = 2;
while (size > 1) {
if (size%factor == 0) {
factors.push_back(factor);
size /= factor;
} else if (factor > sqrt(size)) {
factor = size;
} else {
++factor;
}
}
plan.resize(0);
twiddleVector.resize(0);
addPlanSteps(0, 0, _size, 1);
twiddleVector.shrink_to_fit();
permutation.resize(0);
permutation.reserve(_size);
permutation.push_back(PermutationPair{0, 0});
size_t indexLow = 0, indexHigh = factors.size();
size_t inputStepLow = _size, outputStepLow = 1;
size_t inputStepHigh = 1, outputStepHigh = _size;
while (outputStepLow*inputStepHigh < _size) {
size_t f, inputStep, outputStep;
if (outputStepLow <= inputStepHigh) {
f = factors[indexLow++];
inputStep = (inputStepLow /= f);
outputStep = outputStepLow;
outputStepLow *= f;
} else {
f = factors[--indexHigh];
inputStep = inputStepHigh;
inputStepHigh *= f;
outputStep = (outputStepHigh /= f);
}
size_t oldSize = permutation.size();
for (size_t i = 1; i < f; ++i) {
for (size_t j = 0; j < oldSize; ++j) {
PermutationPair pair = permutation[j];
pair.from += i*inputStep;
pair.to += i*outputStep;
permutation.push_back(pair);
}
}
}
}
template<bool inverse, typename RandomAccessIterator>
void fftStepGeneric(RandomAccessIterator &&origData, const Step &step) {
complex *working = workingVector.data();
const size_t stride = step.innerRepeats;
for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) {
RandomAccessIterator data = origData;
const complex *twiddles = twiddleVector.data() + step.twiddleIndex;
const size_t factor = step.factor;
for (size_t repeat = 0; repeat < step.innerRepeats; ++repeat) {
for (size_t i = 0; i < step.factor; ++i) {
working[i] = _fft_impl::complexMul<inverse>(data[i*stride], twiddles[i]);
}
for (size_t f = 0; f < factor; ++f) {
complex sum = working[0];
for (size_t i = 1; i < factor; ++i) {
double phase = 2*M_PI*f*i/factor;
complex twiddle = {V(std::cos(phase)), V(-std::sin(phase))};
sum += _fft_impl::complexMul<inverse>(working[i], twiddle);
}
data[f*stride] = sum;
}
++data;
twiddles += factor;
}
origData += step.factor*step.innerRepeats;
}
}
template<bool inverse, typename RandomAccessIterator>
SIGNALSMITH_INLINE void fftStep2(RandomAccessIterator &&origData, const Step &step) {
const size_t stride = step.innerRepeats;
const complex *origTwiddles = twiddleVector.data() + step.twiddleIndex;
for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) {
const complex* twiddles = origTwiddles;
for (RandomAccessIterator data = origData; data < origData + stride; ++data) {
complex A = data[0];
complex B = _fft_impl::complexMul<inverse>(data[stride], twiddles[1]);
data[0] = A + B;
data[stride] = A - B;
twiddles += 2;
}
origData += 2*stride;
}
}
template<bool inverse, typename RandomAccessIterator>
SIGNALSMITH_INLINE void fftStep3(RandomAccessIterator &&origData, const Step &step) {
constexpr complex factor3 = {-0.5, inverse ? 0.8660254037844386 : -0.8660254037844386};
const size_t stride = step.innerRepeats;
const complex *origTwiddles = twiddleVector.data() + step.twiddleIndex;
for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) {
const complex* twiddles = origTwiddles;
for (RandomAccessIterator data = origData; data < origData + stride; ++data) {
complex A = data[0];
complex B = _fft_impl::complexMul<inverse>(data[stride], twiddles[1]);
complex C = _fft_impl::complexMul<inverse>(data[stride*2], twiddles[2]);
complex realSum = A + (B + C)*factor3.real();
complex imagSum = (B - C)*factor3.imag();
data[0] = A + B + C;
data[stride] = _fft_impl::complexAddI<false>(realSum, imagSum);
data[stride*2] = _fft_impl::complexAddI<true>(realSum, imagSum);
twiddles += 3;
}
origData += 3*stride;
}
}
template<bool inverse, typename RandomAccessIterator>
SIGNALSMITH_INLINE void fftStep4(RandomAccessIterator &&origData, const Step &step) {
const size_t stride = step.innerRepeats;
const complex *origTwiddles = twiddleVector.data() + step.twiddleIndex;
for (size_t outerRepeat = 0; outerRepeat < step.outerRepeats; ++outerRepeat) {
const complex* twiddles = origTwiddles;
for (RandomAccessIterator data = origData; data < origData + stride; ++data) {
complex A = data[0];
complex C = _fft_impl::complexMul<inverse>(data[stride], twiddles[2]);
complex B = _fft_impl::complexMul<inverse>(data[stride*2], twiddles[1]);
complex D = _fft_impl::complexMul<inverse>(data[stride*3], twiddles[3]);
complex sumAC = A + C, sumBD = B + D;
complex diffAC = A - C, diffBD = B - D;
data[0] = sumAC + sumBD;
data[stride] = _fft_impl::complexAddI<!inverse>(diffAC, diffBD);
data[stride*2] = sumAC - sumBD;
data[stride*3] = _fft_impl::complexAddI<inverse>(diffAC, diffBD);
twiddles += 4;
}
origData += 4*stride;
}
}
template<typename InputIterator, typename OutputIterator>
void permute(InputIterator input, OutputIterator data) {
for (auto pair : permutation) {
data[pair.from] = input[pair.to];
}
}
template<bool inverse, typename InputIterator, typename OutputIterator>
void run(InputIterator &&input, OutputIterator &&data) {
permute(input, data);
for (const Step &step : plan) {
switch (step.type) {
case StepType::generic:
fftStepGeneric<inverse>(data + step.startIndex, step);
break;
case StepType::step2:
fftStep2<inverse>(data + step.startIndex, step);
break;
case StepType::step3:
fftStep3<inverse>(data + step.startIndex, step);
break;
case StepType::step4:
fftStep4<inverse>(data + step.startIndex, step);
break;
}
}
}
static bool validSize(size_t size) {
constexpr static bool filter[32] = {
1, 1, 1, 1, 1, 0, 1, 0, 1, 1, // 0-9
0, 0, 1, 0, 0, 0, 1, 0, 1, 0, // 10-19
0, 0, 0, 0, 1, 0, 0, 0, 0, 0, // 20-29
0, 0
};
return filter[size];
}
public:
static size_t fastSizeAbove(size_t size) {
size_t power2 = 1;
while (size >= 32) {
size = (size - 1)/2 + 1;
power2 *= 2;
}
while (size < 32 && !validSize(size)) {
++size;
}
return power2*size;
}
static size_t fastSizeBelow(size_t size) {
size_t power2 = 1;
while (size >= 32) {
size /= 2;
power2 *= 2;
}
while (size > 1 && !validSize(size)) {
--size;
}
return power2*size;
}
FFT(size_t size, int fastDirection=0) : _size(0) {
if (fastDirection > 0) size = fastSizeAbove(size);
if (fastDirection < 0) size = fastSizeBelow(size);
this->setSize(size);
}
size_t setSize(size_t size) {
if (size != _size) {
_size = size;
workingVector.resize(size);
setPlan();
}
return _size;
}
size_t setFastSizeAbove(size_t size) {
return setSize(fastSizeAbove(size));
}
size_t setFastSizeBelow(size_t size) {
return setSize(fastSizeBelow(size));
}
const size_t & size() const {
return _size;
}
template<typename InputIterator, typename OutputIterator>
void fft(InputIterator &&input, OutputIterator &&output) {
auto inputIter = _fft_impl::GetIterator<InputIterator>::get(input);
auto outputIter = _fft_impl::GetIterator<OutputIterator>::get(output);
return run<false>(inputIter, outputIter);
}
template<typename InputIterator, typename OutputIterator>
void ifft(InputIterator &&input, OutputIterator &&output) {
auto inputIter = _fft_impl::GetIterator<InputIterator>::get(input);
auto outputIter = _fft_impl::GetIterator<OutputIterator>::get(output);
return run<true>(inputIter, outputIter);
}
};
struct FFTOptions {
static constexpr int halfFreqShift = 1;
};
template<typename V, int optionFlags=0>
class RealFFT {
static constexpr bool modified = (optionFlags&FFTOptions::halfFreqShift);
using complex = std::complex<V>;
std::vector<complex> complexBuffer1, complexBuffer2;
std::vector<complex> twiddlesMinusI;
std::vector<complex> modifiedRotations;
FFT<V> complexFft;
public:
static size_t fastSizeAbove(size_t size) {
return FFT<V>::fastSizeAbove((size + 1)/2)*2;
}
static size_t fastSizeBelow(size_t size) {
return FFT<V>::fastSizeBelow(size/2)*2;
}
RealFFT(size_t size=0, int fastDirection=0) : complexFft(0) {
if (fastDirection > 0) size = fastSizeAbove(size);
if (fastDirection < 0) size = fastSizeBelow(size);
this->setSize(std::max<size_t>(size, 2));
}
size_t setSize(size_t size) {
complexBuffer1.resize(size/2);
complexBuffer2.resize(size/2);
size_t hhSize = size/4 + 1;
twiddlesMinusI.resize(hhSize);
for (size_t i = 0; i < hhSize; ++i) {
V rotPhase = -2*M_PI*(modified ? i + 0.5 : i)/size;
twiddlesMinusI[i] = {std::sin(rotPhase), -std::cos(rotPhase)};
}
if (modified) {
modifiedRotations.resize(size/2);
for (size_t i = 0; i < size/2; ++i) {
V rotPhase = -2*M_PI*i/size;
modifiedRotations[i] = {std::cos(rotPhase), std::sin(rotPhase)};
}
}
return complexFft.setSize(size/2);
}
size_t setFastSizeAbove(size_t size) {
return setSize(fastSizeAbove(size));
}
size_t setFastSizeBelow(size_t size) {
return setSize(fastSizeBelow(size));
}
size_t size() const {
return complexFft.size()*2;
}
template<typename InputIterator, typename OutputIterator>
void fft(InputIterator &&input, OutputIterator &&output) {
size_t hSize = complexFft.size();
for (size_t i = 0; i < hSize; ++i) {
if (modified) {
complexBuffer1[i] = _fft_impl::complexMul<false>({input[2*i], input[2*i + 1]}, modifiedRotations[i]);
} else {
complexBuffer1[i] = {input[2*i], input[2*i + 1]};
}
}
complexFft.fft(complexBuffer1.data(), complexBuffer2.data());
if (!modified) output[0] = {
complexBuffer2[0].real() + complexBuffer2[0].imag(),
complexBuffer2[0].real() - complexBuffer2[0].imag()
};
for (size_t i = modified ? 0 : 1; i <= hSize/2; ++i) {
size_t conjI = modified ? (hSize - 1 - i) : (hSize - i);
complex odd = (complexBuffer2[i] + conj(complexBuffer2[conjI]))*(V)0.5;
complex evenI = (complexBuffer2[i] - conj(complexBuffer2[conjI]))*(V)0.5;
complex evenRotMinusI = _fft_impl::complexMul<false>(evenI, twiddlesMinusI[i]);
output[i] = odd + evenRotMinusI;
output[conjI] = conj(odd - evenRotMinusI);
}
}
template<typename InputIterator, typename OutputIterator>
void ifft(InputIterator &&input, OutputIterator &&output) {
size_t hSize = complexFft.size();
if (!modified) complexBuffer1[0] = {
input[0].real() + input[0].imag(),
input[0].real() - input[0].imag()
};
for (size_t i = modified ? 0 : 1; i <= hSize/2; ++i) {
size_t conjI = modified ? (hSize - 1 - i) : (hSize - i);
complex v = input[i], v2 = input[conjI];
complex odd = v + conj(v2);
complex evenRotMinusI = v - conj(v2);
complex evenI = _fft_impl::complexMul<true>(evenRotMinusI, twiddlesMinusI[i]);
complexBuffer1[i] = odd + evenI;
complexBuffer1[conjI] = conj(odd - evenI);
}
complexFft.ifft(complexBuffer1.data(), complexBuffer2.data());
for (size_t i = 0; i < hSize; ++i) {
complex v = complexBuffer2[i];
if (modified) v = _fft_impl::complexMul<true>(v, modifiedRotations[i]);
output[2*i] = v.real();
output[2*i + 1] = v.imag();
}
}
};
template<typename V>
struct ModifiedRealFFT : public RealFFT<V, FFTOptions::halfFreqShift> {
using RealFFT<V, FFTOptions::halfFreqShift>::RealFFT;
};
/// @}
}} // namespace
#endif // include guard

View File

@ -1,436 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_FILTERS_H
#define SIGNALSMITH_DSP_FILTERS_H
#include "./perf.h"
#include <cmath>
#include <complex>
namespace signalsmith {
namespace filters {
/** @defgroup Filters Basic filters
@brief Classes for some common filter types
@{
@file
*/
/** Filter design methods.
These differ mostly in how they handle frequency-warping near Nyquist:
\diagram{filters-lowpass.svg}
\diagram{filters-highpass.svg}
\diagram{filters-peak.svg}
\diagram{filters-bandpass.svg}
\diagram{filters-notch.svg}
\diagram{filters-high-shelf.svg}
\diagram{filters-low-shelf.svg}
\diagram{filters-allpass.svg}
*/
enum class BiquadDesign {
bilinear, ///< Bilinear transform, adjusting for centre frequency but not bandwidth
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.
oneSided, ///< Based on `bilinear`, adjusting bandwidth to preserve the lower boundary (leaving the upper one loose).
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.
};
/** A standard biquad.
This is not guaranteed to be stable if modulated at audio rate.
The default highpass/lowpass bandwidth (`defaultBandwidth`) produces a Butterworth filter when bandwidth-compensation is disabled.
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`).*/
template<typename Sample, bool cookbookBandwidth=false>
class BiquadStatic {
static constexpr BiquadDesign bwDesign = cookbookBandwidth ? BiquadDesign::cookbook : BiquadDesign::oneSided;
Sample a1 = 0, a2 = 0, b0 = 1, b1 = 0, b2 = 0;
Sample x1 = 0, x2 = 0, y1 = 0, y2 = 0;
enum class Type {highpass, lowpass, highShelf, lowShelf, bandpass, notch, peak, allpass};
struct FreqSpec {
double scaledFreq;
double w0, sinW0, cosW0;
double inv2Q;
FreqSpec(double freq, BiquadDesign design) {
scaledFreq = std::max(1e-6, std::min(0.4999, freq));
if (design == BiquadDesign::cookbook) {
scaledFreq = std::min(0.45, scaledFreq);
}
w0 = 2*M_PI*scaledFreq;
cosW0 = std::cos(w0);
sinW0 = std::sin(w0);
}
void oneSidedCompQ() {
// Ratio between our (digital) lower boundary f1 and centre f0
double f1Factor = std::sqrt(inv2Q*inv2Q + 1) - inv2Q;
// Bilinear means discrete-time freq f = continuous-time freq tan(pi*xf/pi)
double ctF1 = std::tan(M_PI*scaledFreq*f1Factor), invCtF0 = (1 + cosW0)/sinW0;
double ctF1Factor = ctF1*invCtF0;
inv2Q = 0.5/ctF1Factor - 0.5*ctF1Factor;
}
};
SIGNALSMITH_INLINE static FreqSpec octaveSpec(double scaledFreq, double octaves, BiquadDesign design) {
FreqSpec spec(scaledFreq, design);
if (design == BiquadDesign::cookbook) {
// Approximately preserves bandwidth between halfway points
octaves *= spec.w0/spec.sinW0;
}
spec.inv2Q = std::sinh(std::log(2)*0.5*octaves); // 1/(2Q)
if (design == BiquadDesign::oneSided) spec.oneSidedCompQ();
return spec;
}
SIGNALSMITH_INLINE static FreqSpec qSpec(double scaledFreq, double q, BiquadDesign design) {
FreqSpec spec(scaledFreq, design);
spec.inv2Q = 0.5/q;
if (design == BiquadDesign::oneSided) spec.oneSidedCompQ();
return spec;
}
SIGNALSMITH_INLINE double dbToSqrtGain(double db) {
return std::pow(10, db*0.025);
}
SIGNALSMITH_INLINE BiquadStatic & configure(Type type, FreqSpec calc, double sqrtGain, BiquadDesign design) {
double w0 = calc.w0;
if (design == BiquadDesign::vicanek) {
if (type == Type::notch) { // Heuristic for notches near Nyquist
calc.inv2Q *= (1 - calc.scaledFreq*0.5);
}
double Q = (type == Type::peak ? 0.5*sqrtGain : 0.5)/calc.inv2Q;
double q = (type == Type::peak ? 1/sqrtGain : 1)*calc.inv2Q;
double expmqw = std::exp(-q*w0);
double da1, da2;
if (q <= 1) {
a1 = da1 = -2*expmqw*std::cos(std::sqrt(1 - q*q)*w0);
} else {
a1 = da1 = -2*expmqw*std::cosh(std::sqrt(q*q - 1)*w0);
}
a2 = da2 = expmqw*expmqw;
double sinpd2 = std::sin(w0/2);
double p0 = 1 - sinpd2*sinpd2, p1 = sinpd2*sinpd2, p2 = 4*p0*p1;
double A0 = 1 + da1 + da2, A1 = 1 - da1 + da2, A2 = -4*da2;
A0 *= A0;
A1 *= A1;
if (type == Type::lowpass) {
double R1 = (A0*p0 + A1*p1 + A2*p2)*Q*Q;
double B0 = A0, B1 = (R1 - B0*p0)/p1;
b0 = 0.5*(std::sqrt(B0) + std::sqrt(std::max(0.0, B1)));
b1 = std::sqrt(B0) - b0;
b2 = 0;
return *this;
} else if (type == Type::highpass) {
b2 = b0 = std::sqrt(A0*p0 + A1*p1 + A2*p2)*Q/(4*p1);
b1 = -2*b0;
return *this;
} else if (type == Type::bandpass) {
double R1 = A0*p0 + A1*p1 + A2*p2;
double R2 = -A0 + A1 + 4*(p0 - p1)*A2;
double B2 = (R1 - R2*p1)/(4*p1*p1);
double B1 = R2 + 4*(p1 - p0)*B2;
b1 = -0.5*std::sqrt(std::max(0.0, B1));
b0 = 0.5*(std::sqrt(std::max(0.0, B2 + 0.25*B1)) - b1);
b2 = -b0 - b1;
return *this;
} else if (type == Type::notch) {
// The Vicanek paper doesn't cover notches (band-stop), but we know where the zeros should be:
b0 = 1;
double db1 = -2*std::cos(w0); // might be higher precision
b1 = db1;
b2 = 1;
// Scale so that B0 == A0 to get 0dB at f=0
double scale = std::sqrt(A0)/(b0 + db1 + b2);
b0 *= scale;
b1 *= scale;
b2 *= scale;
return *this;
} else if (type == Type::peak) {
double G2 = (sqrtGain*sqrtGain)*(sqrtGain*sqrtGain);
double R1 = (A0*p0 + A1*p1 + A2*p2)*G2;
double R2 = (-A0 + A1 + 4*(p0 - p1)*A2)*G2;
double B0 = A0;
double B2 = (R1 - R2*p1 - B0)/(4*p1*p1);
double B1 = R2 + B0 + 4*(p1 - p0)*B2;
double W = 0.5*(std::sqrt(B0) + std::sqrt(std::max(0.0, B1)));
b0 = 0.5*(W + std::sqrt(std::max(0.0, W*W + B2)));
b1 = 0.5*(std::sqrt(B0) - std::sqrt(std::max(0.0, B1)));
b2 = -B2/(4*b0);
return *this;
}
// All others fall back to `oneSided`
design = BiquadDesign::oneSided;
calc.oneSidedCompQ();
}
double alpha = calc.sinW0*calc.inv2Q;
double A = sqrtGain, sqrtA2alpha = 2*std::sqrt(A)*alpha;
double a0;
if (type == Type::highpass) {
b1 = -1 - calc.cosW0;
b0 = b2 = (1 + calc.cosW0)*0.5;
a0 = 1 + alpha;
a1 = -2*calc.cosW0;
a2 = 1 - alpha;
} else if (type == Type::lowpass) {
b1 = 1 - calc.cosW0;
b0 = b2 = b1*0.5;
a0 = 1 + alpha;
a1 = -2*calc.cosW0;
a2 = 1 - alpha;
} else if (type == Type::highShelf) {
b0 = A*((A+1)+(A-1)*calc.cosW0+sqrtA2alpha);
b2 = A*((A+1)+(A-1)*calc.cosW0-sqrtA2alpha);
b1 = -2*A*((A-1)+(A+1)*calc.cosW0);
a0 = (A+1)-(A-1)*calc.cosW0+sqrtA2alpha;
a2 = (A+1)-(A-1)*calc.cosW0-sqrtA2alpha;
a1 = 2*((A-1)-(A+1)*calc.cosW0);
} else if (type == Type::lowShelf) {
b0 = A*((A+1)-(A-1)*calc.cosW0+sqrtA2alpha);
b2 = A*((A+1)-(A-1)*calc.cosW0-sqrtA2alpha);
b1 = 2*A*((A-1)-(A+1)*calc.cosW0);
a0 = (A+1)+(A-1)*calc.cosW0+sqrtA2alpha;
a2 = (A+1)+(A-1)*calc.cosW0-sqrtA2alpha;
a1 = -2*((A-1)+(A+1)*calc.cosW0);
} else if (type == Type::bandpass) {
b0 = alpha;
b1 = 0;
b2 = -alpha;
a0 = 1 + alpha;
a1 = -2*calc.cosW0;
a2 = 1 - alpha;
} else if (type == Type::notch) {
b0 = 1;
b1 = -2*calc.cosW0;
b2 = 1;
a0 = 1 + alpha;
a1 = b1;
a2 = 1 - alpha;
} else if (type == Type::peak) {
b0 = 1 + alpha*A;
b1 = -2*calc.cosW0;
b2 = 1 - alpha*A;
a0 = 1 + alpha/A;
a1 = b1;
a2 = 1 - alpha/A;
} else if (type == Type::allpass) {
a0 = b2 = 1 + alpha;
a1 = b1 = -2*calc.cosW0;
a2 = b0 = 1 - alpha;
} else {
// reset to neutral
a1 = a2 = b1 = b2 = 0;
a0 = b0 = 1;
}
double invA0 = 1/a0;
b0 *= invA0;
b1 *= invA0;
b2 *= invA0;
a1 *= invA0;
a2 *= invA0;
return *this;
}
public:
static constexpr double defaultQ = 0.7071067811865476; // sqrt(0.5)
static constexpr double defaultBandwidth = 1.8999686269529916; // equivalent to above Q
Sample operator ()(Sample x0) {
Sample y0 = x0*b0 + x1*b1 + x2*b2 - y1*a1 - y2*a2;
y2 = y1;
y1 = y0;
x2 = x1;
x1 = x0;
return y0;
}
void reset() {
x1 = x2 = y1 = y2 = 0;
}
std::complex<Sample> response(Sample scaledFreq) const {
Sample w = scaledFreq*Sample(2*M_PI);
std::complex<Sample> invZ = {std::cos(w), -std::sin(w)}, invZ2 = invZ*invZ;
return (b0 + invZ*b1 + invZ2*b2)/(Sample(1) + invZ*a1 + invZ2*a2);
}
Sample responseDb(Sample scaledFreq) const {
Sample w = scaledFreq*Sample(2*M_PI);
std::complex<Sample> invZ = {std::cos(w), -std::sin(w)}, invZ2 = invZ*invZ;
Sample energy = std::norm(b0 + invZ*b1 + invZ2*b2)/std::norm(Sample(1) + invZ*a1 + invZ2*a2);
return 10*std::log10(energy);
}
/// @name Lowpass
/// @{
BiquadStatic & lowpass(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=BiquadDesign::bilinear) {
return configure(Type::lowpass, octaveSpec(scaledFreq, octaves, design), 0, design);
}
BiquadStatic & lowpassQ(double scaledFreq, double q, BiquadDesign design=BiquadDesign::bilinear) {
return configure(Type::lowpass, qSpec(scaledFreq, q, design), 0, design);
}
/// @deprecated use `BiquadDesign` instead
void lowpass(double scaledFreq, double octaves, bool correctBandwidth) {
lowpass(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @deprecated By the time you care about `design`, you should care about the bandwidth
BiquadStatic & lowpass(double scaledFreq, BiquadDesign design) {
return lowpass(scaledFreq, defaultBandwidth, design);
}
/// @}
/// @name Highpass
/// @{
BiquadStatic & highpass(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=BiquadDesign::bilinear) {
return configure(Type::highpass, octaveSpec(scaledFreq, octaves, design), 0, design);
}
BiquadStatic & highpassQ(double scaledFreq, double q, BiquadDesign design=BiquadDesign::bilinear) {
return configure(Type::highpass, qSpec(scaledFreq, q, design), 0, design);
}
/// @deprecated use `BiquadDesign` instead
void highpass(double scaledFreq, double octaves, bool correctBandwidth) {
highpass(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @deprecated By the time you care about `design`, you should care about the bandwidth
BiquadStatic & highpass(double scaledFreq, BiquadDesign design) {
return highpass(scaledFreq, defaultBandwidth, design);
}
/// @}
/// @name Bandpass
/// @{
BiquadStatic & bandpass(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) {
return configure(Type::bandpass, octaveSpec(scaledFreq, octaves, design), 0, design);
}
BiquadStatic & bandpassQ(double scaledFreq, double q, BiquadDesign design=bwDesign) {
return configure(Type::bandpass, qSpec(scaledFreq, q, design), 0, design);
}
/// @deprecated use `BiquadDesign` instead
void bandpass(double scaledFreq, double octaves, bool correctBandwidth) {
bandpass(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @deprecated By the time you care about `design`, you should care about the bandwidth
BiquadStatic & bandpass(double scaledFreq, BiquadDesign design) {
return bandpass(scaledFreq, defaultBandwidth, design);
}
/// @}
/// @name Notch
/// @{
BiquadStatic & notch(double scaledFreq, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) {
return configure(Type::notch, octaveSpec(scaledFreq, octaves, design), 0, design);
}
BiquadStatic & notchQ(double scaledFreq, double q, BiquadDesign design=bwDesign) {
return configure(Type::notch, qSpec(scaledFreq, q, design), 0, design);
}
/// @deprecated use `BiquadDesign` instead
void notch(double scaledFreq, double octaves, bool correctBandwidth) {
notch(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @deprecated By the time you care about `design`, you should care about the bandwidth
BiquadStatic & notch(double scaledFreq, BiquadDesign design) {
return notch(scaledFreq, defaultBandwidth, design);
}
/// @deprecated alias for `.notch()`
void bandStop(double scaledFreq, double octaves=1, bool correctBandwidth=true) {
notch(scaledFreq, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @}
/// @name Peak
/// @{
BiquadStatic & peak(double scaledFreq, double gain, double octaves=1, BiquadDesign design=bwDesign) {
return configure(Type::peak, octaveSpec(scaledFreq, octaves, design), std::sqrt(gain), design);
}
BiquadStatic & peakDb(double scaledFreq, double db, double octaves=1, BiquadDesign design=bwDesign) {
return configure(Type::peak, octaveSpec(scaledFreq, octaves, design), dbToSqrtGain(db), design);
}
BiquadStatic & peakQ(double scaledFreq, double gain, double q, BiquadDesign design=bwDesign) {
return configure(Type::peak, qSpec(scaledFreq, q, design), std::sqrt(gain), design);
}
BiquadStatic & peakDbQ(double scaledFreq, double db, double q, BiquadDesign design=bwDesign) {
return configure(Type::peak, qSpec(scaledFreq, q, design), dbToSqrtGain(db), design);
}
/// @deprecated By the time you care about `design`, you should care about the bandwidth
BiquadStatic & peak(double scaledFreq, double gain, BiquadDesign design) {
return peak(scaledFreq, gain, 1, design);
}
/// @}
/// @name High shelf
/// @{
BiquadStatic & highShelf(double scaledFreq, double gain, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) {
return configure(Type::highShelf, octaveSpec(scaledFreq, octaves, design), std::sqrt(gain), design);
}
BiquadStatic & highShelfDb(double scaledFreq, double db, double octaves=defaultBandwidth, BiquadDesign design=bwDesign) {
return configure(Type::highShelf, octaveSpec(scaledFreq, octaves, design), dbToSqrtGain(db), design);
}
BiquadStatic & highShelfQ(double scaledFreq, double gain, double q, BiquadDesign design=bwDesign) {
return configure(Type::highShelf, qSpec(scaledFreq, q, design), std::sqrt(gain), design);
}
BiquadStatic & highShelfDbQ(double scaledFreq, double db, double q, BiquadDesign design=bwDesign) {
return configure(Type::highShelf, qSpec(scaledFreq, q, design), dbToSqrtGain(db), design);
}
/// @deprecated use `BiquadDesign` instead
BiquadStatic & highShelf(double scaledFreq, double gain, double octaves, bool correctBandwidth) {
return highShelf(scaledFreq, gain, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @deprecated use `BiquadDesign` instead
BiquadStatic & highShelfDb(double scaledFreq, double db, double octaves, bool correctBandwidth) {
return highShelfDb(scaledFreq, db, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @}
/// @name Low shelf
/// @{
BiquadStatic & lowShelf(double scaledFreq, double gain, double octaves=2, BiquadDesign design=bwDesign) {
return configure(Type::lowShelf, octaveSpec(scaledFreq, octaves, design), std::sqrt(gain), design);
}
BiquadStatic & lowShelfDb(double scaledFreq, double db, double octaves=2, BiquadDesign design=bwDesign) {
return configure(Type::lowShelf, octaveSpec(scaledFreq, octaves, design), dbToSqrtGain(db), design);
}
BiquadStatic & lowShelfQ(double scaledFreq, double gain, double q, BiquadDesign design=bwDesign) {
return configure(Type::lowShelf, qSpec(scaledFreq, q, design), std::sqrt(gain), design);
}
BiquadStatic & lowShelfDbQ(double scaledFreq, double db, double q, BiquadDesign design=bwDesign) {
return configure(Type::lowShelf, qSpec(scaledFreq, q, design), dbToSqrtGain(db), design);
}
/// @deprecated use `BiquadDesign` instead
BiquadStatic & lowShelf(double scaledFreq, double gain, double octaves, bool correctBandwidth) {
return lowShelf(scaledFreq, gain, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @deprecated use `BiquadDesign` instead
BiquadStatic & lowShelfDb(double scaledFreq, double db, double octaves, bool correctBandwidth) {
return lowShelfDb(scaledFreq, db, octaves, correctBandwidth ? bwDesign : BiquadDesign::bilinear);
}
/// @}
/// @name Allpass
/// @{
BiquadStatic & allpass(double scaledFreq, double octaves=1, BiquadDesign design=bwDesign) {
return configure(Type::allpass, octaveSpec(scaledFreq, octaves, design), 0, design);
}
BiquadStatic & allpassQ(double scaledFreq, double q, BiquadDesign design=bwDesign) {
return configure(Type::allpass, qSpec(scaledFreq, q, design), 0, design);
}
/// @}
BiquadStatic & addGain(double factor) {
b0 *= factor;
b1 *= factor;
b2 *= factor;
return *this;
}
BiquadStatic & addGainDb(double db) {
return addGain(std::pow(10, db*0.05));
}
};
/** @} */
}} // signalsmith::filters::
#endif // include guard

218
dsp/mix.h
View File

@ -1,218 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_MULTI_CHANNEL_H
#define SIGNALSMITH_DSP_MULTI_CHANNEL_H
#include <array>
namespace signalsmith {
namespace mix {
/** @defgroup Mix Multichannel mixing
@brief Utilities for stereo/multichannel mixing operations
@{
@file
*/
/** @defgroup Matrices Orthogonal matrices
@brief Some common matrices used for audio
@ingroup Mix
@{ */
/// @brief Hadamard: high mixing levels, N log(N) operations
template<typename Sample, int size=-1>
class Hadamard {
public:
static_assert(size >= 0, "Size must be positive (or -1 for dynamic)");
/// Applies the matrix, scaled so it's orthogonal
template<class Data>
static void inPlace(Data &&data) {
unscaledInPlace(data);
Sample factor = scalingFactor();
for (int c = 0; c < size; ++c) {
data[c] *= factor;
}
}
/// Scaling factor applied to make it orthogonal
static Sample scalingFactor() {
/// TODO: test for C++20, or whatever makes this constexpr. Maybe a `#define` in `common.h`?
return std::sqrt(Sample(1)/(size ? size : 1));
}
/// Skips the scaling, so it's a matrix full of `1`s
template<class Data, int startIndex=0>
static void unscaledInPlace(Data &&data) {
if (size <= 1) return;
constexpr int hSize = size/2;
Hadamard<Sample, hSize>::template unscaledInPlace<Data, startIndex>(data);
Hadamard<Sample, hSize>::template unscaledInPlace<Data, startIndex + hSize>(data);
for (int i = 0; i < hSize; ++i) {
Sample a = data[i + startIndex], b = data[i + startIndex + hSize];
data[i + startIndex] = (a + b);
data[i + startIndex + hSize] = (a - b);
}
}
};
/// @brief Hadamard with dynamic size
template<typename Sample>
class Hadamard<Sample, -1> {
int size;
public:
Hadamard(int size) : size(size) {}
/// Applies the matrix, scaled so it's orthogonal
template<class Data>
void inPlace(Data &&data) const {
unscaledInPlace(data);
Sample factor = scalingFactor();
for (int c = 0; c < size; ++c) {
data[c] *= factor;
}
}
/// Scaling factor applied to make it orthogonal
Sample scalingFactor() const {
return std::sqrt(Sample(1)/(size ? size : 1));
}
/// Skips the scaling, so it's a matrix full of `1`s
template<class Data>
void unscaledInPlace(Data &&data) const {
int hSize = size/2;
while (hSize > 0) {
for (int startIndex = 0; startIndex < size; startIndex += hSize*2) {
for (int i = startIndex; i < startIndex + hSize; ++i) {
Sample a = data[i], b = data[i + hSize];
data[i] = (a + b);
data[i + hSize] = (a - b);
}
}
hSize /= 2;
}
}
};
/// @brief Householder: moderate mixing, 2N operations
template<typename Sample, int size=-1>
class Householder {
public:
static_assert(size >= 0, "Size must be positive (or -1 for dynamic)");
template<class Data>
static void inPlace(Data &&data) {
if (size < 1) return;
/// TODO: test for C++20, which makes `std::complex::operator/` constexpr
const Sample factor = Sample(-2)/Sample(size ? size : 1);
Sample sum = data[0];
for (int i = 1; i < size; ++i) {
sum += data[i];
}
sum *= factor;
for (int i = 0; i < size; ++i) {
data[i] += sum;
}
}
/// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard
constexpr static Sample scalingFactor() {
return 1;
}
};
/// @brief Householder with dynamic size
template<typename Sample>
class Householder<Sample, -1> {
int size;
public:
Householder(int size) : size(size) {}
template<class Data>
void inPlace(Data &&data) const {
if (size < 1) return;
const Sample factor = Sample(-2)/Sample(size ? size : 1);
Sample sum = data[0];
for (int i = 1; i < size; ++i) {
sum += data[i];
}
sum *= factor;
for (int i = 0; i < size; ++i) {
data[i] += sum;
}
}
/// @deprecated The matrix is already orthogonal, but this is here for compatibility with Hadamard
constexpr static Sample scalingFactor() {
return 1;
}
};
/// @}
/** @brief Upmix/downmix a stereo signal to an (even) multi-channel signal
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.
When mixing together, it uses the opposite rotations, such that upmix downmix produces the same stereo signal (when scaled by `.scalingFactor1()`.
*/
template<typename Sample, int channels>
class StereoMultiMixer {
static_assert((channels/2)*2 == channels, "StereoMultiMixer must have an even number of channels");
static_assert(channels > 0, "StereoMultiMixer must have a positive number of channels");
static constexpr int hChannels = channels/2;
std::array<Sample, channels> coeffs;
public:
StereoMultiMixer() {
coeffs[0] = 1;
coeffs[1] = 0;
for (int i = 1; i < hChannels; ++i) {
double phase = M_PI*i/channels;
coeffs[2*i] = std::cos(phase);
coeffs[2*i + 1] = std::sin(phase);
}
}
template<class In, class Out>
void stereoToMulti(In &input, Out &output) const {
output[0] = input[0];
output[1] = input[1];
for (int i = 2; i < channels; i += 2) {
output[i] = input[0]*coeffs[i] + input[1]*coeffs[i + 1];
output[i + 1] = input[1]*coeffs[i] - input[0]*coeffs[i + 1];
}
}
template<class In, class Out>
void multiToStereo(In &input, Out &output) const {
output[0] = input[0];
output[1] = input[1];
for (int i = 2; i < channels; i += 2) {
output[0] += input[i]*coeffs[i] - input[i + 1]*coeffs[i + 1];
output[1] += input[i + 1]*coeffs[i] + input[i]*coeffs[i + 1];
}
}
/// Scaling factor for the downmix, if channels are phase-aligned
static constexpr Sample scalingFactor1() {
return 2/Sample(channels);
}
/// Scaling factor for the downmix, if channels are independent
static Sample scalingFactor2() {
return std::sqrt(scalingFactor1());
}
};
/// A cheap (polynomial) almost-energy-preserving crossfade
/// Maximum energy error: 1.06%, average 0.64%, curves overshoot by 0.3%
/// See: http://signalsmith-audio.co.uk/writing/2021/cheap-energy-crossfade/
template<typename Sample, typename Result>
void cheapEnergyCrossfade(Sample x, Result &toCoeff, Result &fromCoeff) {
Sample x2 = 1 - x;
// Other powers p can be approximated by: k = -6.0026608 + p*(6.8773512 - 1.5838104*p)
Sample A = x*x2, B = A*(1 + (Sample)1.4186*A);
Sample C = (B + x), D = (B + x2);
toCoeff = C*C;
fromCoeff = D*D;
}
/** @} */
}} // signalsmith::delay::
#endif // include guard

View File

@ -1,84 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_PERF_H
#define SIGNALSMITH_DSP_PERF_H
#include <complex>
#if defined(__SSE__) || defined(_M_X64)
# include <xmmintrin.h>
#else
# include <cstdint> // for uintptr_t
#endif
namespace signalsmith {
namespace perf {
/** @defgroup Performance Performance helpers
@brief Nothing serious, just some `#defines` and helpers
@{
@file
*/
/// *Really* insist that a function/method is inlined (mostly for performance in DEBUG builds)
#ifndef SIGNALSMITH_INLINE
#ifdef __GNUC__
#define SIGNALSMITH_INLINE __attribute__((always_inline)) inline
#elif defined(__MSVC__)
#define SIGNALSMITH_INLINE __forceinline inline
#else
#define SIGNALSMITH_INLINE inline
#endif
#endif
/** @brief Complex-multiplication (with optional conjugate second-arg), without handling NaN/Infinity
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.
*/
template <bool conjugateSecond=false, typename V>
SIGNALSMITH_INLINE static std::complex<V> mul(const std::complex<V> &a, const std::complex<V> &b) {
return conjugateSecond ? std::complex<V>{
b.real()*a.real() + b.imag()*a.imag(),
b.real()*a.imag() - b.imag()*a.real()
} : std::complex<V>{
a.real()*b.real() - a.imag()*b.imag(),
a.real()*b.imag() + a.imag()*b.real()
};
}
#if defined(__SSE__) || defined(_M_X64)
class StopDenormals {
unsigned int controlStatusRegister;
public:
StopDenormals() : controlStatusRegister(_mm_getcsr()) {
_mm_setcsr(controlStatusRegister|0x8040); // Flush-to-Zero and Denormals-Are-Zero
}
~StopDenormals() {
_mm_setcsr(controlStatusRegister);
}
};
#elif (defined (__ARM_NEON) || defined (__ARM_NEON__))
class StopDenormals {
uintptr_t status;
public:
StopDenormals() {
uintptr_t asmStatus;
asm volatile("mrs %0, fpcr" : "=r"(asmStatus));
status = asmStatus = asmStatus|0x01000000U; // Flush to Zero
asm volatile("msr fpcr, %0" : : "ri"(asmStatus));
}
~StopDenormals() {
uintptr_t asmStatus = status;
asm volatile("msr fpcr, %0" : : "ri"(asmStatus));
}
};
#else
# if __cplusplus >= 202302L
# warning "The `StopDenormals` class doesn't do anything for this architecture"
# endif
class StopDenormals {}; // FIXME: add for other architectures
#endif
/** @} */
}} // signalsmith::perf::
#endif // include guard

View File

@ -1,184 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_RATES_H
#define SIGNALSMITH_DSP_RATES_H
#include "./windows.h"
#include "./delay.h"
namespace signalsmith {
namespace rates {
/** @defgroup Rates Multi-rate processing
@brief Classes for oversampling/upsampling/downsampling etc.
@{
@file
*/
/// @brief Fills a container with a Kaiser-windowed sinc for an FIR lowpass.
/// \diagram{rates-kaiser-sinc.svg,33-point results for various pass/stop frequencies}
template<class Data>
void fillKaiserSinc(Data &&data, int length, double passFreq, double stopFreq) {
if (length <= 0) return;
double kaiserBandwidth = (stopFreq - passFreq)*length;
kaiserBandwidth += 1.25/kaiserBandwidth; // heuristic for transition band, see `InterpolatorKaiserSincN`
auto kaiser = signalsmith::windows::Kaiser::withBandwidth(kaiserBandwidth);
kaiser.fill(data, length);
double centreIndex = (length - 1)*0.5;
double sincScale = M_PI*(passFreq + stopFreq);
double ampScale = (passFreq + stopFreq);
for (int i = 0; i < length; ++i) {
double x = (i - centreIndex), px = x*sincScale;
double sinc = (std::abs(px) > 1e-6) ? std::sin(px)*ampScale/px : ampScale;
data[i] *= sinc;
}
};
/// @brief If only the centre frequency is specified, a heuristic is used to balance ripples and transition width
/// \diagram{rates-kaiser-sinc-heuristic.svg,The transition width is set to: 0.9/sqrt(length)}
template<class Data>
void fillKaiserSinc(Data &&data, int length, double centreFreq) {
double halfWidth = 0.45/std::sqrt(length);
if (halfWidth > centreFreq) halfWidth = (halfWidth + centreFreq)*0.5;
fillKaiserSinc(data, length, centreFreq - halfWidth, centreFreq + halfWidth);
}
/** 2x FIR oversampling for block-based processing.
\diagram{rates-oversampler2xfir-responses-45.svg,Upsample response for various lengths}
The oversampled signal is stored inside this object, with channels accessed via `oversampler[c]`. For example, you might do:
\code{.cpp}
// Upsample from multi-channel input (inputBuffers[c][i] is a sample)
oversampler.up(inputBuffers, bufferLength)
// Modify the contents at the higher rate
for (int c = 0; c < 2; ++c) {
float *channel = oversampler[c];
for (int i = 0; i < bufferLength*2; ++i) {
channel[i] = std::abs(channel[i]);
}
}
// Downsample into the multi-channel output
oversampler.down(outputBuffers, bufferLength);
\endcode
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.
\diagram{rates-oversampler2xfir-lengths.svg,Resample error rates for different passband thresholds}
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.*/
template<typename Sample>
struct Oversampler2xFIR {
Oversampler2xFIR() : Oversampler2xFIR(0, 0) {}
Oversampler2xFIR(int channels, int maxBlock, int halfLatency=16, double passFreq=0.43) {
resize(channels, maxBlock, halfLatency, passFreq);
}
void resize(int nChannels, int maxBlockLength) {
resize(nChannels, maxBlockLength, oneWayLatency);
}
void resize(int nChannels, int maxBlockLength, int halfLatency, double passFreq=0.43) {
oneWayLatency = halfLatency;
kernelLength = oneWayLatency*2;
channels = nChannels;
halfSampleKernel.resize(kernelLength);
fillKaiserSinc(halfSampleKernel, kernelLength, passFreq, 1 - passFreq);
inputStride = kernelLength + maxBlockLength;
inputBuffer.resize(channels*inputStride);
stride = (maxBlockLength + kernelLength)*2;
buffer.resize(stride*channels);
}
void reset() {
inputBuffer.assign(inputBuffer.size(), 0);
buffer.assign(buffer.size(), 0);
}
/// @brief Round-trip latency (or equivalently: upsample latency at the higher rate).
/// This will be twice the value passed into the constructor or `.resize()`.
int latency() const {
return kernelLength;
}
/// Upsamples from a multi-channel input into the internal buffer
template<class Data>
void up(Data &&data, int lowSamples) {
for (int c = 0; c < channels; ++c) {
upChannel(c, data[c], lowSamples);
}
}
/// Upsamples a single-channel input into the internal buffer
template<class Data>
void upChannel(int c, Data &&data, int lowSamples) {
Sample *inputChannel = inputBuffer.data() + c*inputStride;
for (int i = 0; i < lowSamples; ++i) {
inputChannel[kernelLength + i] = data[i];
}
Sample *output = (*this)[c];
for (int i = 0; i < lowSamples; ++i) {
output[2*i] = inputChannel[i + oneWayLatency];
Sample *offsetInput = inputChannel + (i + 1);
Sample sum = 0;
for (int o = 0; o < kernelLength; ++o) {
sum += offsetInput[o]*halfSampleKernel[o];
}
output[2*i + 1] = sum;
}
// Copy the end of the buffer back to the beginning
for (int i = 0; i < kernelLength; ++i) {
inputChannel[i] = inputChannel[lowSamples + i];
}
}
/// Downsamples from the internal buffer to a multi-channel output
template<class Data>
void down(Data &&data, int lowSamples) {
for (int c = 0; c < channels; ++c) {
downChannel(c, data[c], lowSamples);
}
}
/// Downsamples a single channel from the internal buffer to a single-channel output
template<class Data>
void downChannel(int c, Data &&data, int lowSamples) {
Sample *input = buffer.data() + c*stride; // no offset for latency
for (int i = 0; i < lowSamples; ++i) {
Sample v1 = input[2*i + kernelLength];
Sample sum = 0;
for (int o = 0; o < kernelLength; ++o) {
Sample v2 = input[2*(i + o) + 1];
sum += v2*halfSampleKernel[o];
}
Sample v2 = sum;
Sample v = (v1 + v2)*Sample(0.5);
data[i] = v;
}
// Copy the end of the buffer back to the beginning
for (int i = 0; i < kernelLength*2; ++i) {
input[i] = input[lowSamples*2 + i];
}
}
/// Gets the samples for a single (higher-rate) channel. The valid length depends how many input samples were passed into `.up()`/`.upChannel()`.
Sample * operator[](int c) {
return buffer.data() + kernelLength*2 + stride*c;
}
const Sample * operator[](int c) const {
return buffer.data() + kernelLength*2 + stride*c;
}
private:
int oneWayLatency, kernelLength;
int channels;
int stride, inputStride;
std::vector<Sample> inputBuffer;
std::vector<Sample> halfSampleKernel;
std::vector<Sample> buffer;
};
/** @} */
}} // namespace
#endif // include guard

View File

@ -1,496 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_SPECTRAL_H
#define SIGNALSMITH_DSP_SPECTRAL_H
#include "./perf.h"
#include "./fft.h"
#include "./windows.h"
#include "./delay.h"
#include <cmath>
namespace signalsmith {
namespace spectral {
/** @defgroup Spectral Spectral Processing
@brief Tools for frequency-domain manipulation of audio signals
@{
@file
*/
/** @brief An FFT with built-in windowing and round-trip scaling
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`.
This avoids the awkward (real-valued) bands for DC-offset and Nyquist.
*/
template<typename Sample>
class WindowedFFT {
using MRFFT = signalsmith::fft::ModifiedRealFFT<Sample>;
using Complex = std::complex<Sample>;
MRFFT mrfft{2};
std::vector<Sample> fftWindow;
std::vector<Sample> timeBuffer;
int offsetSamples = 0;
public:
/// Returns a fast FFT size <= `size`
static int fastSizeAbove(int size, int divisor=1) {
return MRFFT::fastSizeAbove(size/divisor)*divisor;
}
/// Returns a fast FFT size >= `size`
static int fastSizeBelow(int size, int divisor=1) {
return MRFFT::fastSizeBelow(1 + (size - 1)/divisor)*divisor;
}
WindowedFFT() {}
WindowedFFT(int size, int rotateSamples=0) {
setSize(size, rotateSamples);
}
template<class WindowFn>
WindowedFFT(int size, WindowFn fn, Sample windowOffset=0.5, int rotateSamples=0) {
setSize(size, fn, windowOffset, rotateSamples);
}
/// Sets the size, returning the window for modification (initially all 1s)
std::vector<Sample> & setSizeWindow(int size, int rotateSamples=0) {
mrfft.setSize(size);
fftWindow.assign(size, 1);
timeBuffer.resize(size);
offsetSamples = rotateSamples;
if (offsetSamples < 0) offsetSamples += size; // TODO: for a negative rotation, the other half of the result is inverted
return fftWindow;
}
/// Sets the FFT size, with a user-defined functor for the window
template<class WindowFn>
void setSize(int size, WindowFn fn, Sample windowOffset=0.5, int rotateSamples=0) {
setSizeWindow(size, rotateSamples);
Sample invSize = 1/(Sample)size;
for (int i = 0; i < size; ++i) {
Sample r = (i + windowOffset)*invSize;
fftWindow[i] = fn(r);
}
}
/// Sets the size (using the default Blackman-Harris window)
void setSize(int size, int rotateSamples=0) {
setSize(size, [](double x) {
double phase = 2*M_PI*x;
// Blackman-Harris
return 0.35875 - 0.48829*std::cos(phase) + 0.14128*std::cos(phase*2) - 0.01168*std::cos(phase*3);
}, Sample(0.5), rotateSamples);
}
const std::vector<Sample> & window() const {
return this->fftWindow;
}
int size() const {
return mrfft.size();
}
/// Performs an FFT, with windowing and rotation (if enabled)
template<bool withWindow=true, bool withScaling=false, class Input, class Output>
void fft(Input &&input, Output &&output) {
int fftSize = size();
const Sample norm = (withScaling ? 1/(Sample)fftSize : 1);
for (int i = 0; i < offsetSamples; ++i) {
// Inverted polarity since we're using the MRFFT
timeBuffer[i + fftSize - offsetSamples] = -input[i]*norm*(withWindow ? fftWindow[i] : Sample(1));
}
for (int i = offsetSamples; i < fftSize; ++i) {
timeBuffer[i - offsetSamples] = input[i]*norm*(withWindow ? fftWindow[i] : Sample(1));
}
mrfft.fft(timeBuffer, output);
}
/// Performs an FFT (no windowing or rotation)
template<class Input, class Output>
void fftRaw(Input &&input, Output &&output) {
mrfft.fft(input, output);
}
/// Inverse FFT, with windowing, 1/N scaling and rotation (if enabled)
template<bool withWindow=true, bool withScaling=true, class Input, class Output>
void ifft(Input &&input, Output &&output) {
mrfft.ifft(input, timeBuffer);
int fftSize = mrfft.size();
const Sample norm = (withScaling ? 1/(Sample)fftSize : 1);
for (int i = 0; i < offsetSamples; ++i) {
// Inverted polarity since we're using the MRFFT
output[i] = -timeBuffer[i + fftSize - offsetSamples]*norm*(withWindow ? fftWindow[i] : Sample(1));
}
for (int i = offsetSamples; i < fftSize; ++i) {
output[i] = timeBuffer[i - offsetSamples]*norm*(withWindow ? fftWindow[i] : Sample(1));
}
}
/// Performs an IFFT (no windowing, scaling or rotation)
template<class Input, class Output>
void ifftRaw(Input &&input, Output &&output) {
mrfft.ifft(input, output);
}
};
/** STFT synthesis, built on a `MultiBuffer`.
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.
\diagram{stft-aliasing-simulated.svg,Simulated bad-case aliasing (random phase-shift for each band) for overlapping ratios}
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.
\diagram{stft-buffer-validity.svg}
You move the valid index along using `.ensureValid()`, passing in a functor which provides spectra (using `.analyse()` and/or direct modification through `.spectrum[c]`):
\code
void processSample(...) {
stft.ensureValid([&](int) {
// Here, we introduce (1 - windowSize) of latency
stft.analyse(inputBuffer.view(1 - windowSize))
});
// read as a MultiBuffer
auto result = stft.at(0);
++stft; // also moves the latest valid index
}
void processBlock(...) {
// assuming `historyLength` == blockSize
stft.ensureValid(blockSize, [&](int blockStartIndex) {
int inputStart = blockStartIndex + (1 - windowSize);
stft.analyse(inputBuffer.view(inputStart));
});
auto earliestValid = stft.at(0);
auto latestValid = stft.at(blockSize);
stft += blockSize;
}
\endcode
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`.
*/
template<typename Sample>
class STFT : public signalsmith::delay::MultiBuffer<Sample> {
using Super = signalsmith::delay::MultiBuffer<Sample>;
using Complex = std::complex<Sample>;
int channels = 0, _windowSize = 0, _fftSize = 0, _interval = 1;
int validUntilIndex = 0;
class MultiSpectrum {
int channels, stride;
std::vector<Complex> buffer;
public:
MultiSpectrum() : MultiSpectrum(0, 0) {}
MultiSpectrum(int channels, int bands) : channels(channels), stride(bands), buffer(channels*bands, 0) {}
void resize(int nChannels, int nBands) {
channels = nChannels;
stride = nBands;
buffer.assign(channels*stride, 0);
}
void reset() {
buffer.assign(buffer.size(), 0);
}
void swap(MultiSpectrum &other) {
using std::swap;
swap(buffer, other.buffer);
}
Complex * operator [](int channel) {
return buffer.data() + channel*stride;
}
const Complex * operator [](int channel) const {
return buffer.data() + channel*stride;
}
};
std::vector<Sample> timeBuffer;
bool rotate = false;
void resizeInternal(int newChannels, int windowSize, int newInterval, int historyLength, int zeroPadding) {
Super::resize(newChannels,
windowSize /* for output summing */
+ newInterval /* so we can read `windowSize` ahead (we'll be at most `interval-1` from the most recent block */
+ historyLength);
int fftSize = fft.fastSizeAbove(windowSize + zeroPadding);
this->channels = newChannels;
_windowSize = windowSize;
this->_fftSize = fftSize;
this->_interval = newInterval;
validUntilIndex = -1;
setWindow(windowShape, rotate);
spectrum.resize(channels, fftSize/2);
timeBuffer.resize(fftSize);
}
public:
enum class Window {kaiser, acg};
/// \deprecated use `.setWindow()` which actually updates the window when you change it
Window windowShape = Window::kaiser;
// for convenience
static constexpr Window kaiser = Window::kaiser;
static constexpr Window acg = Window::acg;
/** Swaps between the default (Kaiser) shape and Approximate Confined Gaussian (ACG).
\diagram{stft-windows.svg,Default (Kaiser) windows and partial cumulative sum}
The ACG has better rolloff since its edges go to 0:
\diagram{stft-windows-acg.svg,ACG windows and partial cumulative sum}
However, it generally has worse performance in terms of total sidelobe energy, affecting worst-case aliasing levels for (most) higher overlap ratios:
\diagram{stft-aliasing-simulated-acg.svg,Simulated bad-case aliasing for ACG windows - compare with above}*/
// TODO: these should both be set before resize()
void setWindow(Window shape, bool rotateToZero=false) {
windowShape = shape;
rotate = rotateToZero;
auto &window = fft.setSizeWindow(_fftSize, rotateToZero ? _windowSize/2 : 0);
if (windowShape == Window::kaiser) {
using Kaiser = ::signalsmith::windows::Kaiser;
/// Roughly optimal Kaiser for STFT analysis (forced to perfect reconstruction)
auto kaiser = Kaiser::withBandwidth(_windowSize/double(_interval), true);
kaiser.fill(window, _windowSize);
} else {
using Confined = ::signalsmith::windows::ApproximateConfinedGaussian;
auto confined = Confined::withBandwidth(_windowSize/double(_interval));
confined.fill(window, _windowSize);
}
::signalsmith::windows::forcePerfectReconstruction(window, _windowSize, _interval);
// 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)
for (int i = _windowSize; i < _fftSize; ++i) {
window[i] = 0;
}
}
using Spectrum = MultiSpectrum;
Spectrum spectrum;
WindowedFFT<Sample> fft;
STFT() {}
/// Parameters passed straight to `.resize()`
STFT(int channels, int windowSize, int interval, int historyLength=0, int zeroPadding=0) {
resize(channels, windowSize, interval, historyLength, zeroPadding);
}
/// Sets the channel-count, FFT size and interval.
void resize(int nChannels, int windowSize, int interval, int historyLength=0, int zeroPadding=0) {
resizeInternal(nChannels, windowSize, interval, historyLength, zeroPadding);
}
int windowSize() const {
return _windowSize;
}
int fftSize() const {
return _fftSize;
}
int interval() const {
return _interval;
}
/// Returns the (analysis and synthesis) window
decltype(fft.window()) window() const {
return fft.window();
}
/// Calculates the effective window for the partially-summed future output (relative to the most recent block)
std::vector<Sample> partialSumWindow(bool includeLatestBlock=true) const {
const auto &w = window();
std::vector<Sample> result(_windowSize, 0);
int firstOffset = (includeLatestBlock ? 0 : _interval);
for (int offset = firstOffset; offset < _windowSize; offset += _interval) {
for (int i = 0; i < _windowSize - offset; ++i) {
Sample value = w[i + offset];
result[i] += value*value;
}
}
return result;
}
/// Resets everything - since we clear the output sum, it will take `windowSize` samples to get proper output.
void reset() {
Super::reset();
spectrum.reset();
validUntilIndex = -1;
}
/** Generates valid output up to the specified index (or 0), using the callback as many times as needed.
The callback should be a functor accepting a single integer argument, which is the index for which a spectrum is required.
The block created from these spectra will start at this index in the output, plus `.latency()`.
*/
template<class AnalysisFn>
void ensureValid(int i, AnalysisFn fn) {
while (validUntilIndex < i) {
int blockIndex = validUntilIndex + 1;
fn(blockIndex);
auto output = this->view(blockIndex);
for (int c = 0; c < channels; ++c) {
auto channel = output[c];
// Clear out the future sum, a window-length and an interval ahead
for (int wi = _windowSize; wi < _windowSize + _interval; ++wi) {
channel[wi] = 0;
}
// Add in the IFFT'd result
fft.ifft(spectrum[c], timeBuffer);
for (int wi = 0; wi < _windowSize; ++wi) {
channel[wi] += timeBuffer[wi];
}
}
validUntilIndex += _interval;
}
}
/// The same as above, assuming index 0
template<class AnalysisFn>
void ensureValid(AnalysisFn fn) {
return ensureValid(0, fn);
}
/// Returns the next invalid index (a.k.a. the index of the next block)
int nextInvalid() const {
return validUntilIndex + 1;
}
/** Analyse a multi-channel input, for any type where `data[channel][index]` returns samples
Results can be read/edited using `.spectrum`. */
template<class Data>
void analyse(Data &&data) {
for (int c = 0; c < channels; ++c) {
fft.fft(data[c], spectrum[c]);
}
}
template<class Data>
void analyse(int c, Data &&data) {
fft.fft(data, spectrum[c]);
}
/// Analyse without windowing or zero-rotation
template<class Data>
void analyseRaw(Data &&data) {
for (int c = 0; c < channels; ++c) {
fft.fftRaw(data[c], spectrum[c]);
}
}
template<class Data>
void analyseRaw(int c, Data &&data) {
fft.fftRaw(data, spectrum[c]);
}
int bands() const {
return _fftSize/2;
}
/** Internal latency (between the block-index requested in `.ensureValid()` and its position in the output)
Currently unused, but it's in here to allow for a future implementation which spreads the FFT calculations out across each interval.*/
int latency() {
return 0;
}
// @name Shift the underlying buffer (moving the "valid" index accordingly)
// @{
STFT & operator ++() {
Super::operator ++();
validUntilIndex--;
return *this;
}
STFT & operator +=(int i) {
Super::operator +=(i);
validUntilIndex -= i;
return *this;
}
STFT & operator --() {
Super::operator --();
validUntilIndex++;
return *this;
}
STFT & operator -=(int i) {
Super::operator -=(i);
validUntilIndex += i;
return *this;
}
// @}
typename Super::MutableView operator ++(int postIncrement) {
auto result = Super::operator ++(postIncrement);
validUntilIndex--;
return result;
}
typename Super::MutableView operator --(int postIncrement) {
auto result = Super::operator --(postIncrement);
validUntilIndex++;
return result;
}
};
/** STFT processing, with input/output.
Before calling `.ensureValid(index)`, you should make sure the input is filled up to `index`.
*/
template<typename Sample>
class ProcessSTFT : public STFT<Sample> {
using Super = STFT<Sample>;
public:
signalsmith::delay::MultiBuffer<Sample> input;
ProcessSTFT(int inChannels, int outChannels, int windowSize, int interval, int historyLength=0) {
resize(inChannels, outChannels, windowSize, interval, historyLength);
}
/** Alter the spectrum, using input up to this point, for the output block starting from this point.
Sub-classes should replace this with whatever processing is desired. */
virtual void processSpectrum(int /*blockIndex*/) {}
/// Sets the input/output channels, FFT size and interval.
void resize(int inChannels, int outChannels, int windowSize, int interval, int historyLength=0) {
Super::resize(outChannels, windowSize, interval, historyLength);
input.resize(inChannels, windowSize + interval + historyLength);
}
void reset(Sample value=Sample()) {
Super::reset(value);
input.reset(value);
}
/// Internal latency, including buffering samples for analysis.
int latency() {
return Super::latency() + (this->windowSize() - 1);
}
void ensureValid(int i=0) {
Super::ensureValid(i, [&](int blockIndex) {
this->analyse(input.view(blockIndex - this->windowSize() + 1));
this->processSpectrum(blockIndex);
});
}
// @name Shift the output, input, and valid index.
// @{
ProcessSTFT & operator ++() {
Super::operator ++();
++input;
return *this;
}
ProcessSTFT & operator +=(int i) {
Super::operator +=(i);
input += i;
return *this;
}
ProcessSTFT & operator --() {
Super::operator --();
--input;
return *this;
}
ProcessSTFT & operator -=(int i) {
Super::operator -=(i);
input -= i;
return *this;
}
// @}
};
/** @} */
}} // signalsmith::spectral::
#endif // include guard

View File

@ -1,219 +0,0 @@
#include "./common.h"
#ifndef SIGNALSMITH_DSP_WINDOWS_H
#define SIGNALSMITH_DSP_WINDOWS_H
#include <cmath>
#include <algorithm>
namespace signalsmith {
namespace windows {
/** @defgroup Windows Window functions
@brief Windows for spectral analysis
These are generally double-precision, because they are mostly calculated during setup/reconfiguring, not real-time code.
@{
@file
*/
/** @brief The Kaiser window (almost) maximises the energy in the main-lobe compared to the side-lobes.
Kaiser windows can be constructing using the shape-parameter (beta) or using the static `with???()` methods.*/
class Kaiser {
// I_0(x)=\sum_{k=0}^{N}\frac{x^{2k}}{(k!)^2\cdot4^k}
inline static double bessel0(double x) {
const double significanceLimit = 1e-4;
double result = 0;
double term = 1;
double m = 0;
while (term > significanceLimit) {
result += term;
++m;
term *= (x*x)/(4*m*m);
}
return result;
}
double beta;
double invB0;
static double heuristicBandwidth(double bandwidth) {
// Good peaks
//return bandwidth + 8/((bandwidth + 3)*(bandwidth + 3));
// Good average
//return bandwidth + 14/((bandwidth + 2.5)*(bandwidth + 2.5));
// Compromise
return bandwidth + 8/((bandwidth + 3)*(bandwidth + 3)) + 0.25*std::max(3 - bandwidth, 0.0);
}
public:
/// Set up a Kaiser window with a given shape. `beta` is `pi*alpha` (since there is ambiguity about shape parameters)
Kaiser(double beta) : beta(beta), invB0(1/bessel0(beta)) {}
/// @name Bandwidth methods
/// @{
static Kaiser withBandwidth(double bandwidth, bool heuristicOptimal=false) {
return Kaiser(bandwidthToBeta(bandwidth, heuristicOptimal));
}
/** Returns the Kaiser shape where the main lobe has the specified bandwidth (as a factor of 1/window-length).
\diagram{kaiser-windows.svg,You can see that the main lobe matches the specified bandwidth.}
If `heuristicOptimal` is enabled, the main lobe width is _slightly_ wider, improving both the peak and total energy - see `bandwidthToEnergyDb()` and `bandwidthToPeakDb()`.
\diagram{kaiser-windows-heuristic.svg, The main lobe extends to ±bandwidth/2.} */
static double bandwidthToBeta(double bandwidth, bool heuristicOptimal=false) {
if (heuristicOptimal) { // Heuristic based on numerical search
bandwidth = heuristicBandwidth(bandwidth);
}
bandwidth = std::max(bandwidth, 2.0);
double alpha = std::sqrt(bandwidth*bandwidth*0.25 - 1);
return alpha*M_PI;
}
static double betaToBandwidth(double beta) {
double alpha = beta*(1.0/M_PI);
return 2*std::sqrt(alpha*alpha + 1);
}
/// @}
/// @name Performance methods
/// @{
/** @brief Total energy ratio (in dB) between side-lobes and the main lobe.
\diagram{windows-kaiser-sidelobe-energy.svg,Measured main/side lobe energy ratio. You can see that the heuristic improves performance for all bandwidth values.}
This function uses an approximation which is accurate to ±0.5dB for 2 bandwidth 10, or 1 bandwidth 10 when `heuristicOptimal`is enabled.
*/
static double bandwidthToEnergyDb(double bandwidth, bool heuristicOptimal=false) {
// Horrible heuristic fits
if (heuristicOptimal) {
if (bandwidth < 3) bandwidth += (3 - bandwidth)*0.5;
return 12.9 + -3/(bandwidth + 0.4) - 13.4*bandwidth + (bandwidth < 3)*-9.6*(bandwidth - 3);
}
return 10.5 + 15/(bandwidth + 0.4) - 13.25*bandwidth + (bandwidth < 2)*13*(bandwidth - 2);
}
static double energyDbToBandwidth(double energyDb, bool heuristicOptimal=false) {
double bw = 1;
while (bw < 20 && bandwidthToEnergyDb(bw, heuristicOptimal) > energyDb) {
bw *= 2;
}
double step = bw/2;
while (step > 0.0001) {
if (bandwidthToEnergyDb(bw, heuristicOptimal) > energyDb) {
bw += step;
} else {
bw -= step;
}
step *= 0.5;
}
return bw;
}
/** @brief Peak ratio (in dB) between side-lobes and the main lobe.
\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.}
This function uses an approximation which is accurate to ±0.5dB for 2 bandwidth 9, or 0.5 bandwidth 9 when `heuristicOptimal`is enabled.
*/
static double bandwidthToPeakDb(double bandwidth, bool heuristicOptimal=false) {
// Horrible heuristic fits
if (heuristicOptimal) {
return 14.2 - 20/(bandwidth + 1) - 13*bandwidth + (bandwidth < 3)*-6*(bandwidth - 3) + (bandwidth < 2.25)*5.8*(bandwidth - 2.25);
}
return 10 + 8/(bandwidth + 2) - 12.75*bandwidth + (bandwidth < 2)*4*(bandwidth - 2);
}
static double peakDbToBandwidth(double peakDb, bool heuristicOptimal=false) {
double bw = 1;
while (bw < 20 && bandwidthToPeakDb(bw, heuristicOptimal) > peakDb) {
bw *= 2;
}
double step = bw/2;
while (step > 0.0001) {
if (bandwidthToPeakDb(bw, heuristicOptimal) > peakDb) {
bw += step;
} else {
bw -= step;
}
step *= 0.5;
}
return bw;
}
/** @} */
/** Equivalent noise bandwidth (ENBW), a measure of frequency resolution.
\diagram{windows-kaiser-enbw.svg,Measured ENBW\, with and without the heuristic bandwidth adjustment.}
This approximation is accurate to ±0.05 up to a bandwidth of 22.
*/
static double bandwidthToEnbw(double bandwidth, bool heuristicOptimal=false) {
if (heuristicOptimal) bandwidth = heuristicBandwidth(bandwidth);
double b2 = std::max<double>(bandwidth - 2, 0);
return 1 + b2*(0.2 + b2*(-0.005 + b2*(-0.000005 + b2*0.0000022)));
}
/// Return the window's value for position in the range [0, 1]
double operator ()(double unit) {
double r = 2*unit - 1;
double arg = std::sqrt(1 - r*r);
return bessel0(beta*arg)*invB0;
}
/// Fills an arbitrary container with a Kaiser window
template<typename Data>
void fill(Data &&data, int size) const {
double invSize = 1.0/size;
for (int i = 0; i < size; ++i) {
double r = (2*i + 1)*invSize - 1;
double arg = std::sqrt(1 - r*r);
data[i] = bessel0(beta*arg)*invB0;
}
}
};
/** @brief The Approximate Confined Gaussian window is (almost) optimal
ACG windows can be constructing using the shape-parameter (sigma) or using the static `with???()` methods.*/
class ApproximateConfinedGaussian {
double gaussianFactor;
double gaussian(double x) const {
return std::exp(-x*x*gaussianFactor);
}
public:
/// Heuristic map from bandwidth to the appropriately-optimal sigma
static double bandwidthToSigma(double bandwidth) {
return 0.3/std::sqrt(bandwidth);
}
static ApproximateConfinedGaussian withBandwidth(double bandwidth) {
return ApproximateConfinedGaussian(bandwidthToSigma(bandwidth));
}
ApproximateConfinedGaussian(double sigma) : gaussianFactor(0.0625/(sigma*sigma)) {}
/// Fills an arbitrary container
template<typename Data>
void fill(Data &&data, int size) const {
double invSize = 1.0/size;
double offsetScale = gaussian(1)/(gaussian(3) + gaussian(-1));
double norm = 1/(gaussian(0) - 2*offsetScale*(gaussian(2)));
for (int i = 0; i < size; ++i) {
double r = (2*i + 1)*invSize - 1;
data[i] = norm*(gaussian(r) - offsetScale*(gaussian(r - 2) + gaussian(r + 2)));
}
}
};
/** Forces STFT perfect-reconstruction (WOLA) on an existing window, for a given STFT interval.
For example, here are perfect-reconstruction versions of the approximately-optimal @ref Kaiser windows:
\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.}
*/
template<typename Data>
void forcePerfectReconstruction(Data &&data, int windowLength, int interval) {
for (int i = 0; i < interval; ++i) {
double sum2 = 0;
for (int index = i; index < windowLength; index += interval) {
sum2 += data[index]*data[index];
}
double factor = 1/std::sqrt(sum2);
for (int index = i; index < windowLength; index += interval) {
data[index] *= factor;
}
}
}
/** @} */
}} // signalsmith::windows
#endif // include guard

View File

@ -0,0 +1 @@
#include "../../signalsmith-stretch.h"

View File

@ -3,10 +3,10 @@
//#include "dsp/spectral.h" //#include "dsp/spectral.h"
//#include "dsp/delay.h" //#include "dsp/delay.h"
#include "dsp/perf.h" //#include "dsp/perf.h"
SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 0); // Check version is compatible //SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 0); // Check version is compatible
#include "linear/stft.h" #include "signalsmith-linear/stft.h" // https://github.com/Signalsmith-Audio/linear
#include <vector> #include <vector>
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
@ -14,6 +14,19 @@ SIGNALSMITH_DSP_VERSION_CHECK(1, 6, 0); // Check version is compatible
namespace signalsmith { namespace stretch { namespace signalsmith { namespace stretch {
namespace _impl {
template <bool conjugateSecond=false, typename V>
static std::complex<V> mul(const std::complex<V> &a, const std::complex<V> &b) {
return conjugateSecond ? std::complex<V>{
b.real()*a.real() + b.imag()*a.imag(),
b.real()*a.imag() - b.imag()*a.real()
} : std::complex<V>{
a.real()*b.real() - a.imag()*b.imag(),
a.real()*b.imag() + a.imag()*b.real()
};
}
}
template<typename Sample=float, class RandomEngine=std::default_random_engine> template<typename Sample=float, class RandomEngine=std::default_random_engine>
struct SignalsmithStretch { struct SignalsmithStretch {
static constexpr size_t version[3] = {1, 1, 1}; static constexpr size_t version[3] = {1, 1, 1};
@ -385,9 +398,9 @@ private:
for (int b = 0; b < bands; ++b) { for (int b = 0; b < bands; ++b) {
auto &bin = bins[b]; auto &bin = bins[b];
bin.output = signalsmith::perf::mul(bin.output, rot); bin.output = _impl::mul(bin.output, rot);
bin.prevInput = signalsmith::perf::mul(bin.prevInput, rot); bin.prevInput = _impl::mul(bin.prevInput, rot);
rot = signalsmith::perf::mul(rot, rotStep); rot = _impl::mul(rot, rotStep);
} }
} }
} }
@ -426,8 +439,8 @@ private:
auto &outputBin = bins[b]; auto &outputBin = bins[b];
Complex prevInput = getFractional<&Band::prevInput>(c, lowIndex, fracIndex); Complex prevInput = getFractional<&Band::prevInput>(c, lowIndex, fracIndex);
Complex freqTwist = signalsmith::perf::mul<true>(prediction.input, prevInput); Complex freqTwist = _impl::mul<true>(prediction.input, prevInput);
Complex phase = signalsmith::perf::mul(outputBin.output, freqTwist); Complex phase = _impl::mul(outputBin.output, freqTwist);
outputBin.output = phase/(std::max(prevEnergy, prediction.energy) + noiseFloor); outputBin.output = phase/(std::max(prevEnergy, prediction.energy) + noiseFloor);
} }
} }
@ -457,17 +470,17 @@ private:
if (b > 0) { if (b > 0) {
Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor;
Complex downInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - binTimeFactor); Complex downInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - binTimeFactor);
Complex shortVerticalTwist = signalsmith::perf::mul<true>(prediction.input, downInput); Complex shortVerticalTwist = _impl::mul<true>(prediction.input, downInput);
auto &downBin = bins[b - 1]; auto &downBin = bins[b - 1];
phase += signalsmith::perf::mul(downBin.output, shortVerticalTwist); phase += _impl::mul(downBin.output, shortVerticalTwist);
if (b >= longVerticalStep) { if (b >= longVerticalStep) {
Complex longDownInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - longVerticalStep*binTimeFactor); Complex longDownInput = getFractional<&Band::input>(maxChannel, mapPoint.inputBin - longVerticalStep*binTimeFactor);
Complex longVerticalTwist = signalsmith::perf::mul<true>(prediction.input, longDownInput); Complex longVerticalTwist = _impl::mul<true>(prediction.input, longDownInput);
auto &longDownBin = bins[b - longVerticalStep]; auto &longDownBin = bins[b - longVerticalStep];
phase += signalsmith::perf::mul(longDownBin.output, longVerticalTwist); phase += _impl::mul(longDownBin.output, longVerticalTwist);
} }
} }
// Downwards vertical steps // Downwards vertical steps
@ -477,20 +490,20 @@ private:
Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor; Sample binTimeFactor = randomTimeFactor ? timeFactorDist(randomEngine) : timeFactor;
Complex downInput = getFractional<&Band::input>(maxChannel, upMapPoint.inputBin - binTimeFactor); Complex downInput = getFractional<&Band::input>(maxChannel, upMapPoint.inputBin - binTimeFactor);
Complex shortVerticalTwist = signalsmith::perf::mul<true>(upPrediction.input, downInput); Complex shortVerticalTwist = _impl::mul<true>(upPrediction.input, downInput);
auto &upBin = bins[b + 1]; auto &upBin = bins[b + 1];
phase += signalsmith::perf::mul<true>(upBin.output, shortVerticalTwist); phase += _impl::mul<true>(upBin.output, shortVerticalTwist);
if (b < bands - longVerticalStep) { if (b < bands - longVerticalStep) {
auto &longUpPrediction = predictions[b + longVerticalStep]; auto &longUpPrediction = predictions[b + longVerticalStep];
auto &longUpMapPoint = outputMap[b + longVerticalStep]; auto &longUpMapPoint = outputMap[b + longVerticalStep];
Complex longDownInput = getFractional<&Band::input>(maxChannel, longUpMapPoint.inputBin - longVerticalStep*binTimeFactor); Complex longDownInput = getFractional<&Band::input>(maxChannel, longUpMapPoint.inputBin - longVerticalStep*binTimeFactor);
Complex longVerticalTwist = signalsmith::perf::mul<true>(longUpPrediction.input, longDownInput); Complex longVerticalTwist = _impl::mul<true>(longUpPrediction.input, longDownInput);
auto &longUpBin = bins[b + longVerticalStep]; auto &longUpBin = bins[b + longVerticalStep];
phase += signalsmith::perf::mul<true>(longUpBin.output, longVerticalTwist); phase += _impl::mul<true>(longUpBin.output, longVerticalTwist);
} }
} }
@ -502,8 +515,8 @@ private:
auto &channelBin = bandsForChannel(c)[b]; auto &channelBin = bandsForChannel(c)[b];
auto &channelPrediction = predictionsForChannel(c)[b]; auto &channelPrediction = predictionsForChannel(c)[b];
Complex channelTwist = signalsmith::perf::mul<true>(channelPrediction.input, prediction.input); Complex channelTwist = _impl::mul<true>(channelPrediction.input, prediction.input);
Complex channelPhase = signalsmith::perf::mul(outputBin.output, channelTwist); Complex channelPhase = _impl::mul(outputBin.output, channelTwist);
channelBin.output = channelPrediction.makeOutput(channelPhase); channelBin.output = channelPrediction.makeOutput(channelPhase);
} }
} }