#include "./common.h" #ifndef SIGNALSMITH_DSP_DELAY_H #define SIGNALSMITH_DSP_DELAY_H #include #include #include // for std::ceil() #include #include #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 class Buffer { unsigned bufferIndex; unsigned bufferMask; std::vector 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 class View { using CBuffer = typename std::conditional::type; using CSample = typename std::conditional::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 void write(Data &&data, int length) { for (int i = 0; i < length; ++i) { (*this)[i] = data[i]; } } /// Read data out from the buffer template 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; using ConstView = View; 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 void write(Data &&data, int length) { for (int i = 0; i < length; ++i) { (*this)[i] = data[i]; } } /// Read data out from the buffer template 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 class MultiBuffer { int channels, stride; Buffer buffer; public: using ConstChannel = typename Buffer::ConstView; using MutableChannel = typename Buffer::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 class Stride { using CChannel = typename std::conditional::type; using CSample = typename std::conditional::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 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 void set(Data &&data) { for (int c = 0; c < channels; ++c) { view[c*stride] = data[c]; } } template Stride & operator =(const Data &data) { set(data); return *this; } Stride & operator =(const Stride &data) { set(data); return *this; } }; Stride at(int offset) { return {buffer.view(offset), channels, stride}; } Stride at(int offset) const { return {buffer.view(offset), channels, stride}; } /// Holds a particular position in the buffer template class View { using CChannel = typename std::conditional::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 at(int offset) { return {view + offset, channels, stride}; } Stride at(int offset) const { return {view + offset, channels, stride}; } }; using MutableView = View; using ConstView = View; 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 struct InterpolatorNearest { static constexpr int inputLength = 1; static constexpr Sample latency = -0.5; // Because we're truncating, which rounds down too often template 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 struct InterpolatorLinear { static constexpr int inputLength = 2; static constexpr int latency = 0; template 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 struct InterpolatorCubic { static constexpr int inputLength = 4; static constexpr int latency = 1; template 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 struct ProductRange { using Array = std::array; static constexpr int mid = (low + high)/2; using Left = ProductRange; using Right = ProductRange; Left left; Right right; const Sample total; ProductRange(Sample x) : left(x), right(x), total(left.total*right.total) {} template 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 struct ProductRange { using Array = std::array; const Sample total; ProductRange(Sample x) : total(x - index) {} template 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 struct InterpolatorLagrangeN { static constexpr int inputLength = n + 1; static constexpr int latency = (n - 1)/2; using Array = std::array; 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 Sample fractional(const Data &data, Sample fractional) const { constexpr int mid = n/2; using Left = _franck_impl::ProductRange; using Right = _franck_impl::ProductRange; Sample x = fractional + latency; Left left(x); Right right(x); return left.calculateResult(right.total, data, invDivisors) + right.calculateResult(left.total, data, invDivisors); } }; template using InterpolatorLagrange3 = InterpolatorLagrangeN; template using InterpolatorLagrange7 = InterpolatorLagrangeN; template using InterpolatorLagrange19 = InterpolatorLagrangeN; /** 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 struct InterpolatorKaiserSincN { static constexpr int inputLength = n; static constexpr Sample latency = minimumPhase ? 0 : (n*Sample(0.5) - 1); int subSampleSteps; std::vector 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 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 fft(windowedSinc.size()*2, 1); windowedSinc.resize(fft.size(), 0); std::vector> spectrum(fft.size()); std::vector> 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 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 using InterpolatorKaiserSinc20 = InterpolatorKaiserSincN; template using InterpolatorKaiserSinc8 = InterpolatorKaiserSincN; template using InterpolatorKaiserSinc4 = InterpolatorKaiserSincN; template using InterpolatorKaiserSinc20Min = InterpolatorKaiserSincN; template using InterpolatorKaiserSinc8Min = InterpolatorKaiserSincN; template using InterpolatorKaiserSinc4Min = InterpolatorKaiserSincN; /// @} /** @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 Interpolator=InterpolatorLinear> class Reader : public Interpolator /* so we can get the empty-base-class optimisation */ { using Super = Interpolator; public: Reader () {} /// Pass in a configured interpolator Reader (const Interpolator &interpolator) : Super(interpolator) {} template 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 Interpolator=InterpolatorLinear> class Delay : private Reader { using Super = Reader; Buffer 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 &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 Interpolator=InterpolatorLinear> class MultiDelay : private Reader { using Super = Reader; int channels; MultiBuffer 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::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::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 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 void readMulti(const Delays &delays, Output &output) { for (int c = 0; c < channels; ++c) { output[c] = Super::read(multiBuffer[c], delays[c]); } } template 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