2023-12-01 15:52:37 +00:00

185 lines
6.6 KiB
C++

#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