-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathrates.h
184 lines (160 loc) · 6.57 KB
/
rates.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#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