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,306 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../IDspStage.h"
|
|
4
|
+
#include "../core/MovingAverageFilter.h"
|
|
5
|
+
#include "../utils/SimdOps.h"
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <string>
|
|
10
|
+
#include <algorithm>
|
|
11
|
+
#include <numeric> // For std::accumulate
|
|
12
|
+
|
|
13
|
+
namespace dsp::adapters
|
|
14
|
+
{
|
|
15
|
+
enum class AverageMode
|
|
16
|
+
{
|
|
17
|
+
Batch,
|
|
18
|
+
Moving
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class MovingAverageStage : public IDspStage
|
|
22
|
+
{
|
|
23
|
+
public:
|
|
24
|
+
/**
|
|
25
|
+
* @brief Constructs a new Moving Average Stage.
|
|
26
|
+
* @param mode The averaging mode (Batch or Moving).
|
|
27
|
+
* @param window_size The window size in samples (0 if using duration-based).
|
|
28
|
+
* @param window_duration_ms The window duration in milliseconds (0 if using size-based).
|
|
29
|
+
*/
|
|
30
|
+
explicit MovingAverageStage(AverageMode mode, size_t window_size = 0, double window_duration_ms = 0.0)
|
|
31
|
+
: m_mode(mode),
|
|
32
|
+
m_window_size(window_size),
|
|
33
|
+
m_window_duration_ms(window_duration_ms),
|
|
34
|
+
m_is_initialized(window_size > 0) // Initialized if windowSize was provided
|
|
35
|
+
{
|
|
36
|
+
if (m_mode == AverageMode::Moving && window_size == 0 && window_duration_ms == 0.0)
|
|
37
|
+
{
|
|
38
|
+
throw std::invalid_argument("MovingAverage: either window size or window duration must be greater than 0 for 'moving' mode");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Return the type identifier for this stage
|
|
43
|
+
const char *getType() const override
|
|
44
|
+
{
|
|
45
|
+
return "movingAverage";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// This is the implementation of the interface method
|
|
49
|
+
void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
|
|
50
|
+
{
|
|
51
|
+
if (m_mode == AverageMode::Batch)
|
|
52
|
+
{
|
|
53
|
+
processBatch(buffer, numSamples, numChannels);
|
|
54
|
+
}
|
|
55
|
+
else // AverageMode::Moving
|
|
56
|
+
{
|
|
57
|
+
processMoving(buffer, numSamples, numChannels, timestamps);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Serialize the stage's state to a Napi::Object
|
|
62
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
63
|
+
{
|
|
64
|
+
Napi::Object state = Napi::Object::New(env);
|
|
65
|
+
std::string modeStr = (m_mode == AverageMode::Moving) ? "moving" : "batch";
|
|
66
|
+
state.Set("mode", modeStr);
|
|
67
|
+
|
|
68
|
+
if (m_mode == AverageMode::Moving)
|
|
69
|
+
{
|
|
70
|
+
state.Set("windowSize", static_cast<uint32_t>(m_window_size));
|
|
71
|
+
state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
|
|
72
|
+
|
|
73
|
+
// Serialize each channel's filter state
|
|
74
|
+
Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
|
|
75
|
+
for (size_t i = 0; i < m_filters.size(); ++i)
|
|
76
|
+
{
|
|
77
|
+
Napi::Object channelState = Napi::Object::New(env);
|
|
78
|
+
|
|
79
|
+
// Get the filter's internal state
|
|
80
|
+
auto [bufferData, runningSum] = m_filters[i].getState();
|
|
81
|
+
|
|
82
|
+
// Convert buffer data to JavaScript array
|
|
83
|
+
Napi::Array bufferArray = Napi::Array::New(env, bufferData.size());
|
|
84
|
+
for (size_t j = 0; j < bufferData.size(); ++j)
|
|
85
|
+
{
|
|
86
|
+
bufferArray.Set(j, Napi::Number::New(env, bufferData[j]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
channelState.Set("buffer", bufferArray);
|
|
90
|
+
channelState.Set("runningSum", Napi::Number::New(env, runningSum));
|
|
91
|
+
|
|
92
|
+
channelsArray.Set(static_cast<uint32_t>(i), channelState);
|
|
93
|
+
}
|
|
94
|
+
state.Set("channels", channelsArray);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Deserialize and restore the stage's state
|
|
101
|
+
void deserializeState(const Napi::Object &state) override
|
|
102
|
+
{
|
|
103
|
+
std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
|
|
104
|
+
AverageMode newMode = (modeStr == "moving") ? AverageMode::Moving : AverageMode::Batch;
|
|
105
|
+
|
|
106
|
+
if (newMode != m_mode)
|
|
107
|
+
{
|
|
108
|
+
throw std::runtime_error("MovingAverage mode mismatch during deserialization");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (m_mode == AverageMode::Moving)
|
|
112
|
+
{
|
|
113
|
+
// Get window size and validate
|
|
114
|
+
size_t windowSize = state.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
115
|
+
if (windowSize != m_window_size)
|
|
116
|
+
{
|
|
117
|
+
throw std::runtime_error("Window size mismatch during deserialization");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get number of channels
|
|
121
|
+
uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
|
|
122
|
+
|
|
123
|
+
// Recreate filters
|
|
124
|
+
m_filters.clear();
|
|
125
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
126
|
+
{
|
|
127
|
+
m_filters.emplace_back(m_window_size);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Restore each channel's state
|
|
131
|
+
Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
|
|
132
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
133
|
+
{
|
|
134
|
+
Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
|
|
135
|
+
|
|
136
|
+
// Get buffer data
|
|
137
|
+
Napi::Array bufferArray = channelState.Get("buffer").As<Napi::Array>();
|
|
138
|
+
std::vector<float> bufferData;
|
|
139
|
+
bufferData.reserve(bufferArray.Length());
|
|
140
|
+
for (uint32_t j = 0; j < bufferArray.Length(); ++j)
|
|
141
|
+
{
|
|
142
|
+
bufferData.push_back(bufferArray.Get(j).As<Napi::Number>().FloatValue());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Get running sum
|
|
146
|
+
float runningSum = channelState.Get("runningSum").As<Napi::Number>().FloatValue();
|
|
147
|
+
|
|
148
|
+
// Validate that runningSum matches the actual sum of buffer values
|
|
149
|
+
float actualSum = 0.0f;
|
|
150
|
+
for (const auto &val : bufferData)
|
|
151
|
+
{
|
|
152
|
+
actualSum += val;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Allow small floating-point tolerance
|
|
156
|
+
const float tolerance = 0.0001f * std::max(1.0f, std::abs(actualSum));
|
|
157
|
+
if (std::abs(runningSum - actualSum) > tolerance)
|
|
158
|
+
{
|
|
159
|
+
throw std::runtime_error(
|
|
160
|
+
"Running sum validation failed: expected " +
|
|
161
|
+
std::to_string(actualSum) + " but got " +
|
|
162
|
+
std::to_string(runningSum));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Restore the filter's state
|
|
166
|
+
m_filters[i].setState(bufferData, runningSum);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Reset all filters to initial state
|
|
172
|
+
void reset() override
|
|
173
|
+
{
|
|
174
|
+
for (auto &filter : m_filters)
|
|
175
|
+
{
|
|
176
|
+
filter.clear();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private:
|
|
181
|
+
/**
|
|
182
|
+
* @brief Statelessly calculates the average for each channel
|
|
183
|
+
* and overwrites all samples in that channel with the result.
|
|
184
|
+
* Uses SIMD-optimized summation for better performance.
|
|
185
|
+
*/
|
|
186
|
+
void processBatch(float *buffer, size_t numSamples, int numChannels)
|
|
187
|
+
{
|
|
188
|
+
for (int c = 0; c < numChannels; ++c)
|
|
189
|
+
{
|
|
190
|
+
size_t numSamplesPerChannel = numSamples / numChannels;
|
|
191
|
+
if (numSamplesPerChannel == 0)
|
|
192
|
+
continue;
|
|
193
|
+
|
|
194
|
+
double sum = 0.0;
|
|
195
|
+
|
|
196
|
+
// For single-channel or well-aligned data, use SIMD sum
|
|
197
|
+
if (numChannels == 1)
|
|
198
|
+
{
|
|
199
|
+
// Fast path: contiguous memory
|
|
200
|
+
sum = dsp::simd::sum(buffer, numSamples);
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
{
|
|
204
|
+
// Multi-channel: strided access (compiler can still auto-vectorize)
|
|
205
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
206
|
+
{
|
|
207
|
+
sum += static_cast<double>(buffer[i]);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Calculate average
|
|
212
|
+
float average = static_cast<float>(sum / numSamplesPerChannel);
|
|
213
|
+
|
|
214
|
+
// Fill this channel's buffer with the single average value
|
|
215
|
+
// For single channel, memset equivalent is very fast
|
|
216
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
217
|
+
{
|
|
218
|
+
buffer[i] = average;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @brief Statefully processes samples using the moving average filters.
|
|
225
|
+
* @param buffer The interleaved audio buffer.
|
|
226
|
+
* @param numSamples The total number of samples.
|
|
227
|
+
* @param numChannels The number of channels.
|
|
228
|
+
* @param timestamps Optional timestamps for deriving sample rate on first call.
|
|
229
|
+
*/
|
|
230
|
+
void processMoving(float *buffer, size_t numSamples, int numChannels, const float *timestamps)
|
|
231
|
+
{
|
|
232
|
+
// Determine if we're in time-aware mode (pure duration without size, or both)
|
|
233
|
+
bool useTimeAware = (m_window_duration_ms > 0.0) && timestamps != nullptr;
|
|
234
|
+
|
|
235
|
+
// Lazy initialization: convert windowDuration to windowSize if needed
|
|
236
|
+
if (!m_is_initialized && m_window_duration_ms > 0.0)
|
|
237
|
+
{
|
|
238
|
+
if (timestamps != nullptr && numSamples > 1)
|
|
239
|
+
{
|
|
240
|
+
// Estimate sample rate from timestamps
|
|
241
|
+
size_t samples_to_check = std::min(numSamples, size_t(10));
|
|
242
|
+
double total_time_ms = timestamps[samples_to_check - 1] - timestamps[0];
|
|
243
|
+
double avg_sample_period_ms = total_time_ms / (samples_to_check - 1);
|
|
244
|
+
double estimated_sample_rate = 1000.0 / avg_sample_period_ms; // Hz
|
|
245
|
+
|
|
246
|
+
// Calculate window size: (duration_ms / 1000) * sample_rate
|
|
247
|
+
// For time-aware mode, use 3x the estimated size to ensure we never
|
|
248
|
+
// run out of buffer space before time-based expiration happens
|
|
249
|
+
size_t estimated_size = static_cast<size_t>((m_window_duration_ms / 1000.0) * estimated_sample_rate);
|
|
250
|
+
m_window_size = std::max(size_t(1), estimated_size * 3);
|
|
251
|
+
|
|
252
|
+
m_is_initialized = true;
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
{
|
|
256
|
+
throw std::runtime_error("MovingAverage: windowDuration was set, but timestamps are not available to derive sample rate");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Lazily initialize our filters, one for each channel
|
|
261
|
+
if (m_filters.size() != static_cast<size_t>(numChannels))
|
|
262
|
+
{
|
|
263
|
+
m_filters.clear();
|
|
264
|
+
for (int i = 0; i < numChannels; ++i)
|
|
265
|
+
{
|
|
266
|
+
if (useTimeAware)
|
|
267
|
+
{
|
|
268
|
+
// Create time-aware filter
|
|
269
|
+
m_filters.emplace_back(m_window_size, m_window_duration_ms);
|
|
270
|
+
}
|
|
271
|
+
else
|
|
272
|
+
{
|
|
273
|
+
// Create regular filter
|
|
274
|
+
m_filters.emplace_back(m_window_size);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Process the buffer sample by sample, de-interleaving
|
|
280
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
281
|
+
{
|
|
282
|
+
int channel = i % numChannels;
|
|
283
|
+
size_t sample_index = i / numChannels;
|
|
284
|
+
|
|
285
|
+
if (useTimeAware)
|
|
286
|
+
{
|
|
287
|
+
// Use time-aware processing
|
|
288
|
+
buffer[i] = m_filters[channel].addSampleWithTimestamp(buffer[i], timestamps[sample_index]);
|
|
289
|
+
}
|
|
290
|
+
else
|
|
291
|
+
{
|
|
292
|
+
// Use sample-count processing
|
|
293
|
+
buffer[i] = m_filters[channel].addSample(buffer[i]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
AverageMode m_mode;
|
|
299
|
+
size_t m_window_size;
|
|
300
|
+
double m_window_duration_ms;
|
|
301
|
+
bool m_is_initialized;
|
|
302
|
+
// We need a separate filter instance for each channel's state
|
|
303
|
+
std::vector<dsp::core::MovingAverageFilter<float>> m_filters;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
} // namespace dsp::adapters
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include "../IDspStage.h"
|
|
3
|
+
#include "../utils/SimdOps.h"
|
|
4
|
+
#include <cmath>
|
|
5
|
+
#include <stdexcept>
|
|
6
|
+
|
|
7
|
+
namespace dsp::adapters
|
|
8
|
+
{
|
|
9
|
+
enum class RectifyMode
|
|
10
|
+
{
|
|
11
|
+
FullWave,
|
|
12
|
+
HalfWave
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class RectifyStage : public IDspStage
|
|
16
|
+
{
|
|
17
|
+
public:
|
|
18
|
+
/**
|
|
19
|
+
* @brief Constructs a new Rectify Stage.
|
|
20
|
+
* @param mode The rectification mode (FULL_WAVE or HALF_WAVE).
|
|
21
|
+
*/
|
|
22
|
+
explicit RectifyStage(RectifyMode mode = RectifyMode::FullWave)
|
|
23
|
+
: m_mode(mode) {}
|
|
24
|
+
|
|
25
|
+
// Delete copy/move semantics
|
|
26
|
+
RectifyStage(const RectifyStage &) = delete;
|
|
27
|
+
RectifyStage &operator=(const RectifyStage &) = delete;
|
|
28
|
+
RectifyStage(RectifyStage &&) noexcept = delete;
|
|
29
|
+
RectifyStage &operator=(RectifyStage &&) noexcept = delete;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @brief Returns the type identifier of this stage.
|
|
33
|
+
* @return A string identifying the stage type ("rectify").
|
|
34
|
+
*/
|
|
35
|
+
const char *getType() const override
|
|
36
|
+
{
|
|
37
|
+
return "rectify";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @brief Applies in-place rectification based on the configured mode.
|
|
42
|
+
* Uses SIMD-optimized operations for better performance.
|
|
43
|
+
*/
|
|
44
|
+
void process(float *buffer, size_t numSamples, int /*numChannels*/, const float * /*timestamps*/ = nullptr) override
|
|
45
|
+
{
|
|
46
|
+
// Use SIMD-optimized operations for best performance
|
|
47
|
+
switch (m_mode)
|
|
48
|
+
{
|
|
49
|
+
case RectifyMode::FullWave:
|
|
50
|
+
dsp::simd::abs_inplace(buffer, numSamples);
|
|
51
|
+
break;
|
|
52
|
+
case RectifyMode::HalfWave:
|
|
53
|
+
dsp::simd::max_zero_inplace(buffer, numSamples);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @brief Serializes the stage's configured mode.
|
|
60
|
+
*/
|
|
61
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
62
|
+
{
|
|
63
|
+
Napi::Object state = Napi::Object::New(env);
|
|
64
|
+
state.Set("type", "rectify");
|
|
65
|
+
state.Set("mode", m_mode == RectifyMode::FullWave ? "full" : "half");
|
|
66
|
+
return state;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @brief Deserializes and restores the stage's configured mode.
|
|
71
|
+
*/
|
|
72
|
+
void deserializeState(const Napi::Object &state) override
|
|
73
|
+
{
|
|
74
|
+
std::string mode = state.Get("mode").As<Napi::String>().Utf8Value();
|
|
75
|
+
if (mode == "full")
|
|
76
|
+
m_mode = RectifyMode::FullWave;
|
|
77
|
+
else if (mode == "half")
|
|
78
|
+
m_mode = RectifyMode::HalfWave;
|
|
79
|
+
else
|
|
80
|
+
throw std::runtime_error("Invalid rectify mode");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
void reset() override {} // No internal buffers
|
|
84
|
+
|
|
85
|
+
private:
|
|
86
|
+
RectifyMode m_mode;
|
|
87
|
+
};
|
|
88
|
+
} // namespace dsp::adapters
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResamplerStage.h
|
|
3
|
+
*
|
|
4
|
+
* Rational resampling by factor L/M for arbitrary sample rate conversion.
|
|
5
|
+
* Combines interpolation (upsample by L) and decimation (downsample by M)
|
|
6
|
+
* using efficient polyphase FIR filtering.
|
|
7
|
+
*
|
|
8
|
+
* Algorithm:
|
|
9
|
+
* 1. Upsample by L (insert zeros)
|
|
10
|
+
* 2. Apply single combined anti-aliasing/anti-imaging filter
|
|
11
|
+
* 3. Downsample by M (keep every M-th sample)
|
|
12
|
+
*
|
|
13
|
+
* The filter cutoff is min(π/L, π/M) to satisfy both requirements.
|
|
14
|
+
*
|
|
15
|
+
* Example: Resample from 44.1 kHz to 48 kHz
|
|
16
|
+
* - L/M = 48000/44100 = 160/147 (after GCD reduction)
|
|
17
|
+
* - Upsample by 160, downsample by 147
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
#pragma once
|
|
21
|
+
|
|
22
|
+
#include "../IDspStage.h"
|
|
23
|
+
#include "../core/FirFilter.h"
|
|
24
|
+
#include <algorithm>
|
|
25
|
+
#include <cmath>
|
|
26
|
+
#include <numeric>
|
|
27
|
+
#include <stdexcept>
|
|
28
|
+
#include <vector>
|
|
29
|
+
|
|
30
|
+
namespace dsp
|
|
31
|
+
{
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resampler stage: Change sample rate by rational factor L/M
|
|
35
|
+
* Output rate = Input rate * (L/M)
|
|
36
|
+
*/
|
|
37
|
+
class ResamplerStage : public IDspStage
|
|
38
|
+
{
|
|
39
|
+
public:
|
|
40
|
+
/**
|
|
41
|
+
* Construct resampler
|
|
42
|
+
* @param upFactor Interpolation factor L
|
|
43
|
+
* @param downFactor Decimation factor M
|
|
44
|
+
* @param order FIR filter order (must be odd)
|
|
45
|
+
* @param sampleRate Input sample rate in Hz
|
|
46
|
+
*/
|
|
47
|
+
ResamplerStage(int upFactor, int downFactor, int order, double sampleRate)
|
|
48
|
+
: L_(upFactor), M_(downFactor), filterOrder_(order), inputSampleRate_(sampleRate), phaseAccumulator_(0)
|
|
49
|
+
{
|
|
50
|
+
if (upFactor < 1)
|
|
51
|
+
{
|
|
52
|
+
throw std::invalid_argument("Interpolation factor L must be >= 1");
|
|
53
|
+
}
|
|
54
|
+
if (downFactor < 1)
|
|
55
|
+
{
|
|
56
|
+
throw std::invalid_argument("Decimation factor M must be >= 1");
|
|
57
|
+
}
|
|
58
|
+
if (order < 3 || order % 2 == 0)
|
|
59
|
+
{
|
|
60
|
+
throw std::invalid_argument("Filter order must be odd and >= 3");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Reduce L/M to simplest form
|
|
64
|
+
int gcd = std::gcd(L_, M_);
|
|
65
|
+
L_ /= gcd;
|
|
66
|
+
M_ /= gcd;
|
|
67
|
+
|
|
68
|
+
// Calculate intermediate and output sample rates
|
|
69
|
+
intermediateSampleRate_ = inputSampleRate_ * L_;
|
|
70
|
+
outputSampleRate_ = inputSampleRate_ * L_ / M_;
|
|
71
|
+
|
|
72
|
+
// Design combined anti-aliasing/anti-imaging filter
|
|
73
|
+
// Cutoff at min(Fs_in/2, Fs_out/2) to prevent both aliasing and imaging
|
|
74
|
+
double cutoffFreq = std::min(inputSampleRate_ / 2.0, outputSampleRate_ / 2.0);
|
|
75
|
+
|
|
76
|
+
// Create polyphase filter bank
|
|
77
|
+
designPolyphaseFilter(cutoffFreq);
|
|
78
|
+
|
|
79
|
+
// State buffer for filtering
|
|
80
|
+
stateBuffer_.resize(filterOrder_, 0.0f);
|
|
81
|
+
stateIndex_ = 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Process input samples and produce resampled output
|
|
86
|
+
* Output size ≈ input size * (L/M)
|
|
87
|
+
*/
|
|
88
|
+
void process(const float *input, size_t inputSize,
|
|
89
|
+
float *output, size_t &outputSize) override
|
|
90
|
+
{
|
|
91
|
+
outputSize = 0;
|
|
92
|
+
|
|
93
|
+
if (inputSize == 0)
|
|
94
|
+
{
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Process each input sample
|
|
99
|
+
for (size_t i = 0; i < inputSize; ++i)
|
|
100
|
+
{
|
|
101
|
+
// Add input sample to state buffer
|
|
102
|
+
stateBuffer_[stateIndex_] = input[i];
|
|
103
|
+
stateIndex_ = (stateIndex_ + 1) % filterOrder_;
|
|
104
|
+
|
|
105
|
+
// Advance phase accumulator
|
|
106
|
+
phaseAccumulator_ += L_;
|
|
107
|
+
|
|
108
|
+
// Generate output samples when phase crosses M boundaries
|
|
109
|
+
while (phaseAccumulator_ >= M_)
|
|
110
|
+
{
|
|
111
|
+
// Calculate which polyphase filter to use
|
|
112
|
+
int phase = (phaseAccumulator_ - M_) % L_;
|
|
113
|
+
|
|
114
|
+
// Apply polyphase filter
|
|
115
|
+
float sum = 0.0f;
|
|
116
|
+
for (int tap = 0; tap < filterOrder_; ++tap)
|
|
117
|
+
{
|
|
118
|
+
int bufferIdx = (stateIndex_ - 1 - tap + filterOrder_) % filterOrder_;
|
|
119
|
+
int coeffIdx = phase + tap * L_;
|
|
120
|
+
|
|
121
|
+
if (coeffIdx < static_cast<int>(polyphaseCoeffs_.size()))
|
|
122
|
+
{
|
|
123
|
+
sum += stateBuffer_[bufferIdx] * polyphaseCoeffs_[coeffIdx];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
output[outputSize++] = sum * L_; // Gain correction for interpolation
|
|
128
|
+
phaseAccumulator_ -= M_;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
void reset() override
|
|
134
|
+
{
|
|
135
|
+
std::fill(stateBuffer_.begin(), stateBuffer_.end(), 0.0f);
|
|
136
|
+
stateIndex_ = 0;
|
|
137
|
+
phaseAccumulator_ = 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
std::string getName() const override
|
|
141
|
+
{
|
|
142
|
+
return "Resampler(L=" + std::to_string(L_) + ",M=" + std::to_string(M_) + ")";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get interpolation factor (reduced)
|
|
147
|
+
*/
|
|
148
|
+
int getUpFactor() const
|
|
149
|
+
{
|
|
150
|
+
return L_;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get decimation factor (reduced)
|
|
155
|
+
*/
|
|
156
|
+
int getDownFactor() const
|
|
157
|
+
{
|
|
158
|
+
return M_;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get output sample rate
|
|
163
|
+
*/
|
|
164
|
+
double getOutputSampleRate() const
|
|
165
|
+
{
|
|
166
|
+
return outputSampleRate_;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Get conversion ratio L/M
|
|
171
|
+
*/
|
|
172
|
+
double getRatio() const
|
|
173
|
+
{
|
|
174
|
+
return static_cast<double>(L_) / static_cast<double>(M_);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private:
|
|
178
|
+
/**
|
|
179
|
+
* Design polyphase filter bank for combined resampling
|
|
180
|
+
*/
|
|
181
|
+
void designPolyphaseFilter(double cutoffFreq)
|
|
182
|
+
{
|
|
183
|
+
const int M = filterOrder_;
|
|
184
|
+
const int totalTaps = M * L_;
|
|
185
|
+
|
|
186
|
+
polyphaseCoeffs_.resize(totalTaps);
|
|
187
|
+
|
|
188
|
+
const int center = totalTaps / 2;
|
|
189
|
+
const double fc = cutoffFreq / intermediateSampleRate_;
|
|
190
|
+
const double omega_c = 2.0 * M_PI * fc;
|
|
191
|
+
|
|
192
|
+
// Design prototype filter at intermediate rate
|
|
193
|
+
for (int n = 0; n < totalTaps; ++n)
|
|
194
|
+
{
|
|
195
|
+
// Sinc function
|
|
196
|
+
double t = n - center;
|
|
197
|
+
double sinc_val;
|
|
198
|
+
if (std::abs(t) < 1e-10)
|
|
199
|
+
{
|
|
200
|
+
sinc_val = omega_c / M_PI;
|
|
201
|
+
}
|
|
202
|
+
else
|
|
203
|
+
{
|
|
204
|
+
sinc_val = std::sin(omega_c * t) / (M_PI * t);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Hamming window
|
|
208
|
+
double window = 0.54 - 0.46 * std::cos(2.0 * M_PI * n / (totalTaps - 1));
|
|
209
|
+
|
|
210
|
+
polyphaseCoeffs_[n] = static_cast<float>(sinc_val * window);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Normalize coefficients
|
|
214
|
+
float sum = 0.0f;
|
|
215
|
+
for (float c : polyphaseCoeffs_)
|
|
216
|
+
{
|
|
217
|
+
sum += c;
|
|
218
|
+
}
|
|
219
|
+
for (float &c : polyphaseCoeffs_)
|
|
220
|
+
{
|
|
221
|
+
c /= sum;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
int L_; // Interpolation factor (reduced)
|
|
226
|
+
int M_; // Decimation factor (reduced)
|
|
227
|
+
int filterOrder_;
|
|
228
|
+
double inputSampleRate_;
|
|
229
|
+
double intermediateSampleRate_;
|
|
230
|
+
double outputSampleRate_;
|
|
231
|
+
|
|
232
|
+
std::vector<float> stateBuffer_;
|
|
233
|
+
std::vector<float> polyphaseCoeffs_;
|
|
234
|
+
size_t stateIndex_;
|
|
235
|
+
int phaseAccumulator_; // Tracks position in resampling cycle
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
} // namespace dsp
|