dspx 0.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +185 -0
- package/.vscode/c_cpp_properties.json +17 -0
- package/.vscode/settings.json +68 -0
- package/.vscode/tasks.json +28 -0
- package/DISCLAIMER.md +32 -0
- package/LICENSE +21 -0
- package/README.md +1803 -0
- package/ROADMAP.md +192 -0
- package/TECHNICAL_DEBT.md +165 -0
- package/binding.gyp +65 -0
- package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
- package/docs/AUTHENTICATION_SECURITY.md +396 -0
- package/docs/BACKEND_IMPROVEMENTS.md +399 -0
- package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
- package/docs/FFT_IMPLEMENTATION.md +490 -0
- package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
- package/docs/FFT_USER_GUIDE.md +494 -0
- package/docs/FILTERS_IMPLEMENTATION.md +260 -0
- package/docs/FILTER_API_GUIDE.md +418 -0
- package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
- package/docs/LOGGER_API_REFERENCE.md +350 -0
- package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
- package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
- package/docs/PHASES_5_7_SUMMARY.md +403 -0
- package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
- package/docs/SIMD_OPTIMIZATIONS.md +211 -0
- package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
- package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
- package/docs/TIMESERIES_QUICK_REF.md +85 -0
- package/docs/advanced.md +559 -0
- package/docs/time-series-guide.md +617 -0
- package/docs/time-series-migration.md +376 -0
- package/jest.config.js +37 -0
- package/package.json +42 -0
- package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
- package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
- package/scripts/test.js +24 -0
- package/src/build/dsp-ts-redis.node +0 -0
- package/src/native/DspPipeline.cc +675 -0
- package/src/native/DspPipeline.h +44 -0
- package/src/native/FftBindings.cc +817 -0
- package/src/native/FilterBindings.cc +1001 -0
- package/src/native/IDspStage.h +53 -0
- package/src/native/adapters/InterpolatorStage.h +201 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
- package/src/native/adapters/MovingAverageStage.h +306 -0
- package/src/native/adapters/RectifyStage.h +88 -0
- package/src/native/adapters/ResamplerStage.h +238 -0
- package/src/native/adapters/RmsStage.h +299 -0
- package/src/native/adapters/SscStage.h +121 -0
- package/src/native/adapters/VarianceStage.h +307 -0
- package/src/native/adapters/WampStage.h +114 -0
- package/src/native/adapters/WaveformLengthStage.h +115 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
- package/src/native/core/FftEngine.cc +441 -0
- package/src/native/core/FftEngine.h +224 -0
- package/src/native/core/FirFilter.cc +324 -0
- package/src/native/core/FirFilter.h +149 -0
- package/src/native/core/IirFilter.cc +576 -0
- package/src/native/core/IirFilter.h +210 -0
- package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
- package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
- package/src/native/core/MovingAverageFilter.cc +18 -0
- package/src/native/core/MovingAverageFilter.h +135 -0
- package/src/native/core/MovingFftFilter.cc +291 -0
- package/src/native/core/MovingFftFilter.h +203 -0
- package/src/native/core/MovingVarianceFilter.cc +194 -0
- package/src/native/core/MovingVarianceFilter.h +114 -0
- package/src/native/core/MovingZScoreFilter.cc +215 -0
- package/src/native/core/MovingZScoreFilter.h +113 -0
- package/src/native/core/Policies.h +352 -0
- package/src/native/core/RmsFilter.cc +18 -0
- package/src/native/core/RmsFilter.h +131 -0
- package/src/native/core/SscFilter.cc +16 -0
- package/src/native/core/SscFilter.h +137 -0
- package/src/native/core/WampFilter.cc +16 -0
- package/src/native/core/WampFilter.h +101 -0
- package/src/native/core/WaveformLengthFilter.cc +17 -0
- package/src/native/core/WaveformLengthFilter.h +98 -0
- package/src/native/utils/CircularBufferArray.cc +336 -0
- package/src/native/utils/CircularBufferArray.h +62 -0
- package/src/native/utils/CircularBufferVector.cc +145 -0
- package/src/native/utils/CircularBufferVector.h +45 -0
- package/src/native/utils/NapiUtils.cc +53 -0
- package/src/native/utils/NapiUtils.h +21 -0
- package/src/native/utils/SimdOps.h +870 -0
- package/src/native/utils/SlidingWindowFilter.cc +239 -0
- package/src/native/utils/SlidingWindowFilter.h +159 -0
- package/src/native/utils/TimeSeriesBuffer.cc +205 -0
- package/src/native/utils/TimeSeriesBuffer.h +140 -0
- package/src/ts/CircularLogBuffer.ts +87 -0
- package/src/ts/DriftDetector.ts +331 -0
- package/src/ts/TopicRouter.ts +428 -0
- package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
- package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
- package/src/ts/__tests__/Chaining.test.ts +387 -0
- package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
- package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
- package/src/ts/__tests__/DriftDetector.test.ts +389 -0
- package/src/ts/__tests__/Fft.test.ts +484 -0
- package/src/ts/__tests__/ListState.test.ts +153 -0
- package/src/ts/__tests__/Logger.test.ts +208 -0
- package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
- package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
- package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
- package/src/ts/__tests__/MovingAverage.test.ts +322 -0
- package/src/ts/__tests__/RMS.test.ts +315 -0
- package/src/ts/__tests__/Rectify.test.ts +272 -0
- package/src/ts/__tests__/Redis.test.ts +456 -0
- package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
- package/src/ts/__tests__/Tap.test.ts +164 -0
- package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
- package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
- package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
- package/src/ts/__tests__/TimeSeries.test.ts +254 -0
- package/src/ts/__tests__/TopicRouter.test.ts +332 -0
- package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
- package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
- package/src/ts/__tests__/Variance.test.ts +509 -0
- package/src/ts/__tests__/WaveformLength.test.ts +147 -0
- package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
- package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
- package/src/ts/advanced-dsp.ts +566 -0
- package/src/ts/backends.ts +1137 -0
- package/src/ts/bindings.ts +1225 -0
- package/src/ts/easter-egg.ts +42 -0
- package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
- package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
- package/src/ts/examples/MovingAverage/test-state.ts +85 -0
- package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
- package/src/ts/examples/RMS/test-state.ts +97 -0
- package/src/ts/examples/RMS/test-streaming.ts +253 -0
- package/src/ts/examples/Rectify/test-state.ts +107 -0
- package/src/ts/examples/Rectify/test-streaming.ts +242 -0
- package/src/ts/examples/Variance/test-state.ts +195 -0
- package/src/ts/examples/Variance/test-streaming.ts +260 -0
- package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
- package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
- package/src/ts/examples/advanced-dsp-examples.ts +397 -0
- package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
- package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
- package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
- package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
- package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
- package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
- package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
- package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
- package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
- package/src/ts/examples/chaining/test-chaining.ts +52 -0
- package/src/ts/examples/emg-features-example.ts +284 -0
- package/src/ts/examples/fft-example.ts +309 -0
- package/src/ts/examples/fft-examples.ts +349 -0
- package/src/ts/examples/filter-examples.ts +320 -0
- package/src/ts/examples/list-state-example.ts +131 -0
- package/src/ts/examples/logger-example.ts +91 -0
- package/src/ts/examples/notch-filter-examples.ts +243 -0
- package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
- package/src/ts/examples/phase6-7/production-observability.ts +476 -0
- package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
- package/src/ts/examples/redis/redis-example.ts +202 -0
- package/src/ts/examples/redis-example.ts +202 -0
- package/src/ts/examples/simd-benchmark.ts +126 -0
- package/src/ts/examples/tap-debugging.ts +230 -0
- package/src/ts/examples/timeseries/comparison-example.ts +290 -0
- package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
- package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
- package/src/ts/examples/waveform-length-example.ts +139 -0
- package/src/ts/fft.ts +722 -0
- package/src/ts/filters.ts +1078 -0
- package/src/ts/index.ts +120 -0
- package/src/ts/types.ts +589 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving/Batched FFT Filter Implementation with SIMD Optimizations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
#define _USE_MATH_DEFINES
|
|
6
|
+
#include "MovingFftFilter.h"
|
|
7
|
+
#include "../utils/SimdOps.h"
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <stdexcept>
|
|
10
|
+
|
|
11
|
+
#ifndef M_PI
|
|
12
|
+
#define M_PI 3.14159265358979323846
|
|
13
|
+
#endif
|
|
14
|
+
|
|
15
|
+
namespace dsp
|
|
16
|
+
{
|
|
17
|
+
namespace core
|
|
18
|
+
{
|
|
19
|
+
|
|
20
|
+
template <typename T>
|
|
21
|
+
MovingFftFilter<T>::MovingFftFilter(
|
|
22
|
+
size_t fftSize,
|
|
23
|
+
size_t hopSize,
|
|
24
|
+
FftMode mode,
|
|
25
|
+
WindowType windowType,
|
|
26
|
+
bool realInput)
|
|
27
|
+
: m_fftSize(fftSize), m_hopSize(hopSize == 0 ? fftSize : hopSize), m_mode(mode), m_windowType(windowType), m_realInput(realInput), m_fftEngine(std::make_unique<FftEngine<T>>(fftSize)), m_buffer(fftSize * 2) // Buffer size = 2x FFT size for overlap
|
|
28
|
+
,
|
|
29
|
+
m_sampleCounter(0)
|
|
30
|
+
{
|
|
31
|
+
if (fftSize == 0)
|
|
32
|
+
{
|
|
33
|
+
throw std::invalid_argument("FFT size must be > 0");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (m_hopSize > fftSize)
|
|
37
|
+
{
|
|
38
|
+
throw std::invalid_argument("Hop size cannot exceed FFT size");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Allocate working buffers
|
|
42
|
+
m_windowedSamples.resize(fftSize);
|
|
43
|
+
|
|
44
|
+
size_t spectrumSize = realInput ? m_fftEngine->getHalfSize() : fftSize;
|
|
45
|
+
m_spectrum.resize(spectrumSize);
|
|
46
|
+
|
|
47
|
+
// Initialize window
|
|
48
|
+
initWindow();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
template <typename T>
|
|
52
|
+
bool MovingFftFilter<T>::addSample(T sample, Complex *spectrum)
|
|
53
|
+
{
|
|
54
|
+
m_buffer.push(sample);
|
|
55
|
+
|
|
56
|
+
bool computed = false;
|
|
57
|
+
|
|
58
|
+
if (m_mode == FftMode::Moving)
|
|
59
|
+
{
|
|
60
|
+
// Moving mode: compute on every sample when buffer is full
|
|
61
|
+
if (m_buffer.getCount() >= m_fftSize)
|
|
62
|
+
{
|
|
63
|
+
computeSpectrum(m_spectrum.data());
|
|
64
|
+
|
|
65
|
+
if (spectrum)
|
|
66
|
+
{
|
|
67
|
+
size_t specSize = getSpectrumSize();
|
|
68
|
+
std::copy(m_spectrum.begin(), m_spectrum.begin() + specSize, spectrum);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
computed = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
{
|
|
76
|
+
// Batched mode: compute every hop samples
|
|
77
|
+
++m_sampleCounter;
|
|
78
|
+
|
|
79
|
+
if (m_buffer.getCount() >= m_fftSize && m_sampleCounter >= m_hopSize)
|
|
80
|
+
{
|
|
81
|
+
computeSpectrum(m_spectrum.data());
|
|
82
|
+
|
|
83
|
+
if (spectrum)
|
|
84
|
+
{
|
|
85
|
+
size_t specSize = getSpectrumSize();
|
|
86
|
+
std::copy(m_spectrum.begin(), m_spectrum.begin() + specSize, spectrum);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
m_sampleCounter = 0;
|
|
90
|
+
computed = true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return computed;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
template <typename T>
|
|
98
|
+
size_t MovingFftFilter<T>::addSamples(
|
|
99
|
+
const T *samples,
|
|
100
|
+
size_t count,
|
|
101
|
+
std::function<void(const Complex *, size_t)> callback)
|
|
102
|
+
{
|
|
103
|
+
size_t numSpectra = 0;
|
|
104
|
+
|
|
105
|
+
for (size_t i = 0; i < count; ++i)
|
|
106
|
+
{
|
|
107
|
+
if (addSample(samples[i], m_spectrum.data()))
|
|
108
|
+
{
|
|
109
|
+
if (callback)
|
|
110
|
+
{
|
|
111
|
+
callback(m_spectrum.data(), getSpectrumSize());
|
|
112
|
+
}
|
|
113
|
+
++numSpectra;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return numSpectra;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
template <typename T>
|
|
121
|
+
void MovingFftFilter<T>::computeSpectrum(Complex *spectrum)
|
|
122
|
+
{
|
|
123
|
+
if (m_buffer.getCount() < m_fftSize)
|
|
124
|
+
{
|
|
125
|
+
throw std::runtime_error("Insufficient samples for FFT");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Get latest samples from buffer
|
|
129
|
+
std::vector<T> allSamples = m_buffer.toVector();
|
|
130
|
+
std::vector<T> samples(m_fftSize);
|
|
131
|
+
|
|
132
|
+
// Extract the last fftSize samples
|
|
133
|
+
size_t startIdx = allSamples.size() - m_fftSize;
|
|
134
|
+
for (size_t i = 0; i < m_fftSize; ++i)
|
|
135
|
+
{
|
|
136
|
+
samples[i] = allSamples[startIdx + i];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Apply window
|
|
140
|
+
applyWindow(samples.data(), m_windowedSamples.data());
|
|
141
|
+
|
|
142
|
+
// Compute FFT
|
|
143
|
+
if (m_realInput)
|
|
144
|
+
{
|
|
145
|
+
if (m_fftEngine->isPowerOfTwo())
|
|
146
|
+
{
|
|
147
|
+
m_fftEngine->rfft(m_windowedSamples.data(), spectrum);
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
{
|
|
151
|
+
m_fftEngine->rdft(m_windowedSamples.data(), spectrum);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else
|
|
155
|
+
{
|
|
156
|
+
// Pack real samples as complex
|
|
157
|
+
std::vector<Complex> complexInput(m_fftSize);
|
|
158
|
+
for (size_t i = 0; i < m_fftSize; ++i)
|
|
159
|
+
{
|
|
160
|
+
complexInput[i] = Complex(m_windowedSamples[i], 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (m_fftEngine->isPowerOfTwo())
|
|
164
|
+
{
|
|
165
|
+
m_fftEngine->fft(complexInput.data(), spectrum);
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
{
|
|
169
|
+
m_fftEngine->dft(complexInput.data(), spectrum);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
template <typename T>
|
|
175
|
+
void MovingFftFilter<T>::reset()
|
|
176
|
+
{
|
|
177
|
+
m_buffer.clear();
|
|
178
|
+
m_sampleCounter = 0;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
template <typename T>
|
|
182
|
+
void MovingFftFilter<T>::setWindowType(WindowType type)
|
|
183
|
+
{
|
|
184
|
+
m_windowType = type;
|
|
185
|
+
initWindow();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
template <typename T>
|
|
189
|
+
void MovingFftFilter<T>::getMagnitudeSpectrum(T *magnitudes)
|
|
190
|
+
{
|
|
191
|
+
// MEDIUM WIN: Use SIMD-optimized magnitude calculation from FftEngine
|
|
192
|
+
m_fftEngine->getMagnitude(m_spectrum.data(), magnitudes, getSpectrumSize());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
template <typename T>
|
|
196
|
+
void MovingFftFilter<T>::getPowerSpectrum(T *power)
|
|
197
|
+
{
|
|
198
|
+
// MEDIUM WIN: Use SIMD-optimized power calculation from FftEngine
|
|
199
|
+
m_fftEngine->getPower(m_spectrum.data(), power, getSpectrumSize());
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
template <typename T>
|
|
203
|
+
void MovingFftFilter<T>::getPhaseSpectrum(T *phases)
|
|
204
|
+
{
|
|
205
|
+
m_fftEngine->getPhase(m_spectrum.data(), phases, getSpectrumSize());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
template <typename T>
|
|
209
|
+
void MovingFftFilter<T>::getFrequencyBins(T sampleRate, T *frequencies)
|
|
210
|
+
{
|
|
211
|
+
size_t specSize = getSpectrumSize();
|
|
212
|
+
T binWidth = sampleRate / static_cast<T>(m_fftSize);
|
|
213
|
+
|
|
214
|
+
for (size_t i = 0; i < specSize; ++i)
|
|
215
|
+
{
|
|
216
|
+
frequencies[i] = static_cast<T>(i) * binWidth;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ========== Private Methods ==========
|
|
221
|
+
|
|
222
|
+
template <typename T>
|
|
223
|
+
void MovingFftFilter<T>::initWindow()
|
|
224
|
+
{
|
|
225
|
+
m_window.resize(m_fftSize);
|
|
226
|
+
|
|
227
|
+
for (size_t i = 0; i < m_fftSize; ++i)
|
|
228
|
+
{
|
|
229
|
+
m_window[i] = getWindowCoefficient(i, m_windowType);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
template <typename T>
|
|
234
|
+
void MovingFftFilter<T>::applyWindow(const T *input, T *output)
|
|
235
|
+
{
|
|
236
|
+
// QUICK WIN: SIMD-optimized window application
|
|
237
|
+
if constexpr (std::is_same_v<T, float>)
|
|
238
|
+
{
|
|
239
|
+
dsp::simd::apply_window(input, m_window.data(), output, m_fftSize);
|
|
240
|
+
}
|
|
241
|
+
else
|
|
242
|
+
{
|
|
243
|
+
// Fallback for double precision
|
|
244
|
+
for (size_t i = 0; i < m_fftSize; ++i)
|
|
245
|
+
{
|
|
246
|
+
output[i] = input[i] * m_window[i];
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
template <typename T>
|
|
252
|
+
T MovingFftFilter<T>::getWindowCoefficient(size_t n, WindowType type)
|
|
253
|
+
{
|
|
254
|
+
if (type == WindowType::None)
|
|
255
|
+
{
|
|
256
|
+
return T(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const T pi = static_cast<T>(M_PI);
|
|
260
|
+
const T N = static_cast<T>(m_fftSize);
|
|
261
|
+
const T nf = static_cast<T>(n);
|
|
262
|
+
|
|
263
|
+
switch (type)
|
|
264
|
+
{
|
|
265
|
+
case WindowType::Hann:
|
|
266
|
+
// w[n] = 0.5 * (1 - cos(2πn/(N-1)))
|
|
267
|
+
return T(0.5) * (T(1) - std::cos(T(2) * pi * nf / (N - T(1))));
|
|
268
|
+
|
|
269
|
+
case WindowType::Hamming:
|
|
270
|
+
// w[n] = 0.54 - 0.46 * cos(2πn/(N-1))
|
|
271
|
+
return T(0.54) - T(0.46) * std::cos(T(2) * pi * nf / (N - T(1)));
|
|
272
|
+
|
|
273
|
+
case WindowType::Blackman:
|
|
274
|
+
// w[n] = 0.42 - 0.5*cos(2πn/(N-1)) + 0.08*cos(4πn/(N-1))
|
|
275
|
+
return T(0.42) - T(0.5) * std::cos(T(2) * pi * nf / (N - T(1))) + T(0.08) * std::cos(T(4) * pi * nf / (N - T(1)));
|
|
276
|
+
|
|
277
|
+
case WindowType::Bartlett:
|
|
278
|
+
// w[n] = 1 - |2n/(N-1) - 1|
|
|
279
|
+
return T(1) - std::abs(T(2) * nf / (N - T(1)) - T(1));
|
|
280
|
+
|
|
281
|
+
default:
|
|
282
|
+
return T(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Explicit template instantiations
|
|
287
|
+
template class MovingFftFilter<float>;
|
|
288
|
+
template class MovingFftFilter<double>;
|
|
289
|
+
|
|
290
|
+
} // namespace core
|
|
291
|
+
} // namespace dsp
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Moving/Batched FFT Filter
|
|
3
|
+
*
|
|
4
|
+
* Provides sliding-window and batched FFT processing:
|
|
5
|
+
* - Moving FFT: Updates spectrum as new samples arrive
|
|
6
|
+
* - Batched FFT: Processes complete frames with configurable overlap
|
|
7
|
+
* - Zero-padding support
|
|
8
|
+
* - Windowing functions (Hann, Hamming, Blackman)
|
|
9
|
+
*
|
|
10
|
+
* Uses CircularBufferArray for efficient sample buffering
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
#ifndef DSP_CORE_MOVING_FFT_FILTER_H
|
|
14
|
+
#define DSP_CORE_MOVING_FFT_FILTER_H
|
|
15
|
+
|
|
16
|
+
#include "FftEngine.h"
|
|
17
|
+
#include "../utils/CircularBufferArray.h"
|
|
18
|
+
#include <vector>
|
|
19
|
+
#include <functional>
|
|
20
|
+
|
|
21
|
+
namespace dsp
|
|
22
|
+
{
|
|
23
|
+
namespace core
|
|
24
|
+
{
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Window function types
|
|
28
|
+
*/
|
|
29
|
+
enum class WindowType
|
|
30
|
+
{
|
|
31
|
+
None, // Rectangular (no windowing)
|
|
32
|
+
Hann, // Hann window (cosine taper)
|
|
33
|
+
Hamming, // Hamming window
|
|
34
|
+
Blackman, // Blackman window (better sidelobe rejection)
|
|
35
|
+
Bartlett // Triangular window
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* FFT processing mode
|
|
40
|
+
*/
|
|
41
|
+
enum class FftMode
|
|
42
|
+
{
|
|
43
|
+
Moving, // Sliding window, updates on every sample
|
|
44
|
+
Batched // Process complete frames
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
template <typename T = float>
|
|
48
|
+
class MovingFftFilter
|
|
49
|
+
{
|
|
50
|
+
public:
|
|
51
|
+
using Complex = std::complex<T>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Constructor
|
|
55
|
+
*
|
|
56
|
+
* @param fftSize FFT size (must be power of 2)
|
|
57
|
+
* @param hopSize Hop size for batched mode (samples between frames)
|
|
58
|
+
* @param mode Processing mode (Moving or Batched)
|
|
59
|
+
* @param windowType Windowing function
|
|
60
|
+
* @param realInput True for real-input transforms (RFFT/RDFT)
|
|
61
|
+
*/
|
|
62
|
+
MovingFftFilter(
|
|
63
|
+
size_t fftSize,
|
|
64
|
+
size_t hopSize = 0,
|
|
65
|
+
FftMode mode = FftMode::Batched,
|
|
66
|
+
WindowType windowType = WindowType::Hann,
|
|
67
|
+
bool realInput = true);
|
|
68
|
+
|
|
69
|
+
~MovingFftFilter() = default;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Add sample and optionally compute FFT
|
|
73
|
+
*
|
|
74
|
+
* @param sample Input sample
|
|
75
|
+
* @param spectrum Output spectrum (nullptr if not ready)
|
|
76
|
+
* @return True if spectrum was computed
|
|
77
|
+
*/
|
|
78
|
+
bool addSample(T sample, Complex *spectrum);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Add batch of samples
|
|
82
|
+
*
|
|
83
|
+
* @param samples Input samples
|
|
84
|
+
* @param count Number of samples
|
|
85
|
+
* @param callback Called for each computed spectrum
|
|
86
|
+
* @return Number of spectra computed
|
|
87
|
+
*/
|
|
88
|
+
size_t addSamples(
|
|
89
|
+
const T *samples,
|
|
90
|
+
size_t count,
|
|
91
|
+
std::function<void(const Complex *, size_t)> callback);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Compute FFT from current buffer (force computation)
|
|
95
|
+
*
|
|
96
|
+
* @param spectrum Output spectrum
|
|
97
|
+
*/
|
|
98
|
+
void computeSpectrum(Complex *spectrum);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Reset filter state
|
|
102
|
+
*/
|
|
103
|
+
void reset();
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get FFT size
|
|
107
|
+
*/
|
|
108
|
+
size_t getFftSize() const { return m_fftSize; }
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get spectrum size (N for complex, N/2+1 for real)
|
|
112
|
+
*/
|
|
113
|
+
size_t getSpectrumSize() const
|
|
114
|
+
{
|
|
115
|
+
return m_realInput ? m_fftEngine->getHalfSize() : m_fftSize;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get hop size
|
|
120
|
+
*/
|
|
121
|
+
size_t getHopSize() const { return m_hopSize; }
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get current fill level
|
|
125
|
+
*/
|
|
126
|
+
size_t getFillLevel() const { return m_buffer.getCount(); }
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if ready to compute (buffer full)
|
|
130
|
+
*/
|
|
131
|
+
bool isReady() const { return m_buffer.getCount() >= m_fftSize; }
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Set window function
|
|
135
|
+
*/
|
|
136
|
+
void setWindowType(WindowType type);
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get magnitude spectrum
|
|
140
|
+
*/
|
|
141
|
+
void getMagnitudeSpectrum(T *magnitudes);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get power spectrum
|
|
145
|
+
*/
|
|
146
|
+
void getPowerSpectrum(T *power);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get phase spectrum
|
|
150
|
+
*/
|
|
151
|
+
void getPhaseSpectrum(T *phases);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get frequency bins (Hz)
|
|
155
|
+
*
|
|
156
|
+
* @param sampleRate Sample rate in Hz
|
|
157
|
+
* @param frequencies Output array (size = spectrum size)
|
|
158
|
+
*/
|
|
159
|
+
void getFrequencyBins(T sampleRate, T *frequencies);
|
|
160
|
+
|
|
161
|
+
private:
|
|
162
|
+
size_t m_fftSize;
|
|
163
|
+
size_t m_hopSize;
|
|
164
|
+
FftMode m_mode;
|
|
165
|
+
WindowType m_windowType;
|
|
166
|
+
bool m_realInput;
|
|
167
|
+
|
|
168
|
+
// FFT engine
|
|
169
|
+
std::unique_ptr<FftEngine<T>> m_fftEngine;
|
|
170
|
+
|
|
171
|
+
// Circular buffer for sample accumulation
|
|
172
|
+
utils::CircularBufferArray<T> m_buffer;
|
|
173
|
+
|
|
174
|
+
// Window coefficients
|
|
175
|
+
std::vector<T> m_window;
|
|
176
|
+
|
|
177
|
+
// Working buffers
|
|
178
|
+
std::vector<T> m_windowedSamples;
|
|
179
|
+
std::vector<Complex> m_spectrum;
|
|
180
|
+
|
|
181
|
+
// Sample counter for hop detection
|
|
182
|
+
size_t m_sampleCounter;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Initialize window function
|
|
186
|
+
*/
|
|
187
|
+
void initWindow();
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Apply window to samples
|
|
191
|
+
*/
|
|
192
|
+
void applyWindow(const T *input, T *output);
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Compute window function coefficient
|
|
196
|
+
*/
|
|
197
|
+
T getWindowCoefficient(size_t n, WindowType type);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
} // namespace core
|
|
201
|
+
} // namespace dsp
|
|
202
|
+
|
|
203
|
+
#endif // DSP_CORE_MOVING_FFT_FILTER_H
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#include "MovingVarianceFilter.h"
|
|
2
|
+
|
|
3
|
+
using namespace dsp::core;
|
|
4
|
+
|
|
5
|
+
// -----------------------------------------------------------------------------
|
|
6
|
+
// Constructor
|
|
7
|
+
// -----------------------------------------------------------------------------
|
|
8
|
+
template <typename T>
|
|
9
|
+
MovingVarianceFilter<T>::MovingVarianceFilter(size_t window_size)
|
|
10
|
+
: buffer(window_size), // Initialize the circular buffer
|
|
11
|
+
running_sum(0),
|
|
12
|
+
running_sum_of_squares(0),
|
|
13
|
+
window_size(window_size)
|
|
14
|
+
{
|
|
15
|
+
if (window_size == 0)
|
|
16
|
+
{
|
|
17
|
+
throw std::invalid_argument("Window size must be greater than 0");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// -----------------------------------------------------------------------------
|
|
22
|
+
// Time-aware Constructor
|
|
23
|
+
// -----------------------------------------------------------------------------
|
|
24
|
+
template <typename T>
|
|
25
|
+
MovingVarianceFilter<T>::MovingVarianceFilter(size_t window_size, double window_duration_ms)
|
|
26
|
+
: buffer(window_size, window_duration_ms), // Initialize time-aware circular buffer
|
|
27
|
+
running_sum(0),
|
|
28
|
+
running_sum_of_squares(0),
|
|
29
|
+
window_size(window_size)
|
|
30
|
+
{
|
|
31
|
+
if (window_size == 0)
|
|
32
|
+
{
|
|
33
|
+
throw std::invalid_argument("Window size must be greater than 0");
|
|
34
|
+
}
|
|
35
|
+
if (window_duration_ms <= 0.0)
|
|
36
|
+
{
|
|
37
|
+
throw std::invalid_argument("Window duration must be greater than 0");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// -----------------------------------------------------------------------------
|
|
42
|
+
// Method: addSample
|
|
43
|
+
// -----------------------------------------------------------------------------
|
|
44
|
+
template <typename T>
|
|
45
|
+
T MovingVarianceFilter<T>::addSample(T newValue)
|
|
46
|
+
{
|
|
47
|
+
T oldestValue = 0;
|
|
48
|
+
|
|
49
|
+
// If the buffer is full, get the oldest value
|
|
50
|
+
if (buffer.isFull())
|
|
51
|
+
{
|
|
52
|
+
oldestValue = buffer.peek();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Add the new value to the buffer
|
|
56
|
+
buffer.pushOverwrite(newValue);
|
|
57
|
+
|
|
58
|
+
// Update running sums
|
|
59
|
+
T oldestValueSquared = oldestValue * oldestValue;
|
|
60
|
+
T newValueSquared = newValue * newValue;
|
|
61
|
+
|
|
62
|
+
running_sum = running_sum - oldestValue + newValue;
|
|
63
|
+
running_sum_of_squares = running_sum_of_squares - oldestValueSquared + newValueSquared;
|
|
64
|
+
|
|
65
|
+
// Return the new variance
|
|
66
|
+
return getVariance();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// -----------------------------------------------------------------------------
|
|
70
|
+
// Method: addSampleWithTimestamp
|
|
71
|
+
// -----------------------------------------------------------------------------
|
|
72
|
+
template <typename T>
|
|
73
|
+
T MovingVarianceFilter<T>::addSampleWithTimestamp(T newValue, double timestamp)
|
|
74
|
+
{
|
|
75
|
+
// Expire old samples
|
|
76
|
+
size_t initial_count = buffer.getCount();
|
|
77
|
+
size_t expired_count = buffer.expireOld(timestamp);
|
|
78
|
+
|
|
79
|
+
// Rebuild running statistics if samples were expired
|
|
80
|
+
if (expired_count > 0)
|
|
81
|
+
{
|
|
82
|
+
running_sum = 0;
|
|
83
|
+
running_sum_of_squares = 0;
|
|
84
|
+
|
|
85
|
+
auto remaining = buffer.toVector();
|
|
86
|
+
for (const auto &value : remaining)
|
|
87
|
+
{
|
|
88
|
+
running_sum += value;
|
|
89
|
+
running_sum_of_squares += value * value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Add new sample
|
|
94
|
+
T oldestValue = 0;
|
|
95
|
+
if (buffer.isFull())
|
|
96
|
+
{
|
|
97
|
+
oldestValue = buffer.peek();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
buffer.pushOverwriteWithTimestamp(newValue, timestamp);
|
|
101
|
+
|
|
102
|
+
// Update running sums
|
|
103
|
+
T oldestValueSquared = oldestValue * oldestValue;
|
|
104
|
+
T newValueSquared = newValue * newValue;
|
|
105
|
+
|
|
106
|
+
running_sum = running_sum - oldestValue + newValue;
|
|
107
|
+
running_sum_of_squares = running_sum_of_squares - oldestValueSquared + newValueSquared;
|
|
108
|
+
|
|
109
|
+
return getVariance();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -----------------------------------------------------------------------------
|
|
113
|
+
// Method: getVariance
|
|
114
|
+
// -----------------------------------------------------------------------------
|
|
115
|
+
template <typename T>
|
|
116
|
+
T MovingVarianceFilter<T>::getVariance() const
|
|
117
|
+
{
|
|
118
|
+
size_t count = buffer.getCount();
|
|
119
|
+
if (count == 0)
|
|
120
|
+
{
|
|
121
|
+
return 0; // Avoid division by zero
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
T countT = static_cast<T>(count);
|
|
125
|
+
|
|
126
|
+
// Calculate mean
|
|
127
|
+
T mean = running_sum / countT;
|
|
128
|
+
|
|
129
|
+
// Calculate mean of squares
|
|
130
|
+
T mean_of_squares = running_sum_of_squares / countT;
|
|
131
|
+
|
|
132
|
+
// Variance = E[X^2] - (E[X])^2
|
|
133
|
+
// Use std::max to prevent negative values from floating point inaccuracies
|
|
134
|
+
T variance = std::max(static_cast<T>(0), mean_of_squares - (mean * mean));
|
|
135
|
+
|
|
136
|
+
return variance;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// -----------------------------------------------------------------------------
|
|
140
|
+
// Method: clear
|
|
141
|
+
// -----------------------------------------------------------------------------
|
|
142
|
+
template <typename T>
|
|
143
|
+
void MovingVarianceFilter<T>::clear()
|
|
144
|
+
{
|
|
145
|
+
buffer.clear();
|
|
146
|
+
running_sum = 0;
|
|
147
|
+
running_sum_of_squares = 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// -----------------------------------------------------------------------------
|
|
151
|
+
// Method: isFull
|
|
152
|
+
// -----------------------------------------------------------------------------
|
|
153
|
+
template <typename T>
|
|
154
|
+
bool MovingVarianceFilter<T>::isFull() const noexcept
|
|
155
|
+
{
|
|
156
|
+
return buffer.isFull();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// -----------------------------------------------------------------------------
|
|
160
|
+
// Method: isTimeAware
|
|
161
|
+
// -----------------------------------------------------------------------------
|
|
162
|
+
template <typename T>
|
|
163
|
+
bool MovingVarianceFilter<T>::isTimeAware() const noexcept
|
|
164
|
+
{
|
|
165
|
+
return buffer.isTimeAware();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// -----------------------------------------------------------------------------
|
|
169
|
+
// Method: getState
|
|
170
|
+
// -----------------------------------------------------------------------------
|
|
171
|
+
template <typename T>
|
|
172
|
+
std::pair<std::vector<T>, std::pair<T, T>> MovingVarianceFilter<T>::getState() const
|
|
173
|
+
{
|
|
174
|
+
return {buffer.toVector(), {running_sum, running_sum_of_squares}};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// -----------------------------------------------------------------------------
|
|
178
|
+
// Method: setState
|
|
179
|
+
// -----------------------------------------------------------------------------
|
|
180
|
+
template <typename T>
|
|
181
|
+
void MovingVarianceFilter<T>::setState(const std::vector<T> &bufferData, T sum, T sumOfSquares)
|
|
182
|
+
{
|
|
183
|
+
buffer.fromVector(bufferData);
|
|
184
|
+
running_sum = sum;
|
|
185
|
+
running_sum_of_squares = sumOfSquares;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Explicit template instantiation for common types
|
|
189
|
+
namespace dsp::core
|
|
190
|
+
{
|
|
191
|
+
template class MovingVarianceFilter<int>;
|
|
192
|
+
template class MovingVarianceFilter<float>;
|
|
193
|
+
template class MovingVarianceFilter<double>;
|
|
194
|
+
}
|