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.
Files changed (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. 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