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,53 @@
1
+ #pragma once
2
+ #include <napi.h>
3
+
4
+ namespace dsp
5
+ {
6
+ // This abstract class is the key.
7
+ // Every filter you add will implement this.
8
+ class IDspStage
9
+ {
10
+ public:
11
+ virtual ~IDspStage() = default;
12
+
13
+ /**
14
+ * @brief Returns the type identifier of this stage.
15
+ *
16
+ * @return A string identifying the stage type (e.g., "movingAverage", "notchFilter").
17
+ */
18
+ virtual const char *getType() const = 0;
19
+
20
+ /**
21
+ * @brief Processes a chunk of audio data in-place.
22
+ *
23
+ * @param buffer The interleaved audio buffer.
24
+ * @param numSamples The total number of samples (e.g., 1024).
25
+ * @param numChannels The number of channels (e.g., 1, 2, 4).
26
+ * @param timestamps Optional array of timestamps (in milliseconds) for each sample.
27
+ * If nullptr, uses sample-based processing (legacy mode).
28
+ * If provided, must have length equal to numSamples.
29
+ */
30
+ virtual void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) = 0;
31
+
32
+ /**
33
+ * @brief Serializes the stage's internal state to a Napi::Object.
34
+ *
35
+ * @param env The N-API environment for creating JavaScript objects.
36
+ * @return Napi::Object containing the serialized state.
37
+ */
38
+ virtual Napi::Object serializeState(Napi::Env env) const = 0;
39
+
40
+ /**
41
+ * @brief Deserializes and restores the stage's internal state.
42
+ *
43
+ * @param state The Napi::Object containing the serialized state.
44
+ */
45
+ virtual void deserializeState(const Napi::Object &state) = 0;
46
+
47
+ /**
48
+ * @brief Resets the stage's internal state to initial values.
49
+ */
50
+ virtual void reset() = 0;
51
+ };
52
+
53
+ } // namespace dsp
@@ -0,0 +1,201 @@
1
+ /**
2
+ * InterpolatorStage.h
3
+ *
4
+ * Polyphase FIR interpolator for efficient upsampling by integer factor L.
5
+ * Inserts (L-1) zero samples between each input sample and applies anti-imaging
6
+ * low-pass filter to smooth the result.
7
+ *
8
+ * Algorithm:
9
+ * 1. Insert L-1 zeros between input samples (zero-stuffing)
10
+ * 2. Design anti-imaging FIR filter with cutoff at π/L
11
+ * 3. Use polyphase decomposition for efficiency (avoids computing on zeros)
12
+ * 4. Maintain state across process() calls for streaming
13
+ *
14
+ * Efficiency: Polyphase structure avoids multiplying by zeros, reducing
15
+ * computation significantly.
16
+ */
17
+
18
+ #pragma once
19
+
20
+ #include "../IDspStage.h"
21
+ #include "../core/FirFilter.h"
22
+ #include <algorithm>
23
+ #include <cmath>
24
+ #include <stdexcept>
25
+ #include <vector>
26
+
27
+ namespace dsp
28
+ {
29
+
30
+ /**
31
+ * Interpolator stage: Upsample signal by integer factor L
32
+ * Includes anti-imaging FIR low-pass filter
33
+ */
34
+ class InterpolatorStage : public IDspStage
35
+ {
36
+ public:
37
+ /**
38
+ * Construct interpolator
39
+ * @param factor Interpolation factor L (output rate = input rate * L)
40
+ * @param order FIR filter order (must be odd)
41
+ * @param sampleRate Input sample rate in Hz
42
+ */
43
+ InterpolatorStage(int factor, int order, double sampleRate)
44
+ : interpolationFactor_(factor), filterOrder_(order), sampleRate_(sampleRate), phaseIndex_(0)
45
+ {
46
+ if (factor < 2)
47
+ {
48
+ throw std::invalid_argument("Interpolation factor must be >= 2");
49
+ }
50
+ if (order < 3 || order % 2 == 0)
51
+ {
52
+ throw std::invalid_argument("Filter order must be odd and >= 3");
53
+ }
54
+
55
+ // Design anti-imaging filter: cutoff at Fs/(2*L) to prevent imaging
56
+ double outputSampleRate = sampleRate * factor;
57
+ double cutoffFreq = sampleRate / 2.0; // Nyquist of input rate
58
+
59
+ // Create FIR low-pass filter (operates at output rate)
60
+ filter_ = std::make_unique<FirFilter>(order, outputSampleRate);
61
+ designLowPassFilter(cutoffFreq, outputSampleRate);
62
+
63
+ // State buffer for polyphase filtering
64
+ stateBuffer_.resize(filterOrder_, 0.0f);
65
+ stateIndex_ = 0;
66
+ }
67
+
68
+ /**
69
+ * Process input samples and produce interpolated output
70
+ * Output size will be input size * L
71
+ */
72
+ void process(const float *input, size_t inputSize,
73
+ float *output, size_t &outputSize) override
74
+ {
75
+ outputSize = 0;
76
+
77
+ if (inputSize == 0)
78
+ {
79
+ return;
80
+ }
81
+
82
+ // Process each input sample, producing L output samples
83
+ for (size_t i = 0; i < inputSize; ++i)
84
+ {
85
+ // Add input sample to state buffer
86
+ stateBuffer_[stateIndex_] = input[i];
87
+ stateIndex_ = (stateIndex_ + 1) % filterOrder_;
88
+
89
+ // Generate L output samples using polyphase filter
90
+ for (int phase = 0; phase < interpolationFactor_; ++phase)
91
+ {
92
+ float sum = 0.0f;
93
+
94
+ // Apply polyphase filter coefficients
95
+ // Each phase uses every L-th coefficient
96
+ for (int tap = 0; tap < filterOrder_; ++tap)
97
+ {
98
+ int bufferIdx = (stateIndex_ - 1 - tap + filterOrder_) % filterOrder_;
99
+ int coeffIdx = phase + tap * interpolationFactor_;
100
+
101
+ if (coeffIdx < filterOrder_ * interpolationFactor_)
102
+ {
103
+ sum += stateBuffer_[bufferIdx] * polyphaseCoeffs_[coeffIdx];
104
+ }
105
+ }
106
+
107
+ output[outputSize++] = sum * interpolationFactor_; // Gain correction
108
+ }
109
+ }
110
+ }
111
+
112
+ void reset() override
113
+ {
114
+ std::fill(stateBuffer_.begin(), stateBuffer_.end(), 0.0f);
115
+ stateIndex_ = 0;
116
+ phaseIndex_ = 0;
117
+ }
118
+
119
+ std::string getName() const override
120
+ {
121
+ return "Interpolator(L=" + std::to_string(interpolationFactor_) + ")";
122
+ }
123
+
124
+ /**
125
+ * Get interpolation factor
126
+ */
127
+ int getFactor() const
128
+ {
129
+ return interpolationFactor_;
130
+ }
131
+
132
+ /**
133
+ * Get filter order
134
+ */
135
+ int getOrder() const
136
+ {
137
+ return filterOrder_;
138
+ }
139
+
140
+ private:
141
+ /**
142
+ * Design anti-imaging low-pass filter using windowed sinc method
143
+ * Creates polyphase decomposition of filter
144
+ */
145
+ void designLowPassFilter(double cutoffFreq, double outputSampleRate)
146
+ {
147
+ const int M = filterOrder_;
148
+ const int L = interpolationFactor_;
149
+ const int totalTaps = M * L;
150
+
151
+ polyphaseCoeffs_.resize(totalTaps);
152
+
153
+ const int center = totalTaps / 2;
154
+ const double fc = cutoffFreq / outputSampleRate;
155
+ const double omega_c = 2.0 * M_PI * fc;
156
+
157
+ // Design prototype filter at output rate
158
+ for (int n = 0; n < totalTaps; ++n)
159
+ {
160
+ // Sinc function
161
+ double t = n - center;
162
+ double sinc_val;
163
+ if (std::abs(t) < 1e-10)
164
+ {
165
+ sinc_val = omega_c / M_PI;
166
+ }
167
+ else
168
+ {
169
+ sinc_val = std::sin(omega_c * t) / (M_PI * t);
170
+ }
171
+
172
+ // Hamming window
173
+ double window = 0.54 - 0.46 * std::cos(2.0 * M_PI * n / (totalTaps - 1));
174
+
175
+ polyphaseCoeffs_[n] = static_cast<float>(sinc_val * window);
176
+ }
177
+
178
+ // Normalize coefficients
179
+ float sum = 0.0f;
180
+ for (float c : polyphaseCoeffs_)
181
+ {
182
+ sum += c;
183
+ }
184
+ for (float &c : polyphaseCoeffs_)
185
+ {
186
+ c /= sum;
187
+ }
188
+ }
189
+
190
+ int interpolationFactor_;
191
+ int filterOrder_;
192
+ double sampleRate_;
193
+
194
+ std::unique_ptr<FirFilter> filter_;
195
+ std::vector<float> stateBuffer_;
196
+ std::vector<float> polyphaseCoeffs_;
197
+ size_t stateIndex_;
198
+ int phaseIndex_;
199
+ };
200
+
201
+ } // namespace dsp
@@ -0,0 +1,289 @@
1
+ #pragma once
2
+
3
+ #include "../IDspStage.h"
4
+ #include "../core/MovingAbsoluteValueFilter.h" // Include the new core filter
5
+ #include <vector>
6
+ #include <stdexcept>
7
+ #include <cmath>
8
+ #include <string>
9
+ #include <algorithm>
10
+ #include <numeric> // For std::accumulate
11
+
12
+ namespace dsp::adapters
13
+ {
14
+ enum class MavMode
15
+ {
16
+ Batch,
17
+ Moving
18
+ };
19
+
20
+ class MeanAbsoluteValueStage : public IDspStage
21
+ {
22
+ public:
23
+ /**
24
+ * @brief Constructs a new Mean Absolute Value Stage.
25
+ * @param mode The MAV mode (Batch or Moving).
26
+ * @param window_size The window size in samples (0 if using duration-based).
27
+ * @param window_duration_ms The window duration in milliseconds (0 if using size-based).
28
+ */
29
+ explicit MeanAbsoluteValueStage(MavMode mode, size_t window_size = 0, double window_duration_ms = 0.0)
30
+ : m_mode(mode),
31
+ m_window_size(window_size),
32
+ m_window_duration_ms(window_duration_ms),
33
+ m_is_initialized(window_size > 0)
34
+ {
35
+ if (m_mode == MavMode::Moving && window_size == 0 && window_duration_ms == 0.0)
36
+ {
37
+ throw std::invalid_argument("MeanAbsoluteValue: either window size or window duration must be greater than 0 for 'moving' mode");
38
+ }
39
+ }
40
+
41
+ // Return the type identifier for this stage
42
+ const char *getType() const override
43
+ {
44
+ return "meanAbsoluteValue";
45
+ }
46
+
47
+ // Implementation of the interface method
48
+ void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
49
+ {
50
+ if (m_mode == MavMode::Batch)
51
+ {
52
+ processBatch(buffer, numSamples, numChannels);
53
+ }
54
+ else // MavMode::Moving
55
+ {
56
+ processMoving(buffer, numSamples, numChannels, timestamps);
57
+ }
58
+ }
59
+
60
+ // Serialize the stage's state
61
+ Napi::Object serializeState(Napi::Env env) const override
62
+ {
63
+ Napi::Object state = Napi::Object::New(env);
64
+ std::string modeStr = (m_mode == MavMode::Moving) ? "moving" : "batch";
65
+ state.Set("mode", modeStr);
66
+
67
+ if (m_mode == MavMode::Moving)
68
+ {
69
+ state.Set("windowSize", static_cast<uint32_t>(m_window_size));
70
+ state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
71
+
72
+ // Serialize each channel's filter state
73
+ Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
74
+ for (size_t i = 0; i < m_filters.size(); ++i)
75
+ {
76
+ Napi::Object channelState = Napi::Object::New(env);
77
+
78
+ // Get the filter's internal state
79
+ auto [bufferData, runningSumOfAbs] = m_filters[i].getState();
80
+
81
+ // Convert buffer data to JavaScript array
82
+ Napi::Array bufferArray = Napi::Array::New(env, bufferData.size());
83
+ for (size_t j = 0; j < bufferData.size(); ++j)
84
+ {
85
+ bufferArray.Set(j, Napi::Number::New(env, bufferData[j]));
86
+ }
87
+
88
+ channelState.Set("buffer", bufferArray);
89
+ channelState.Set("runningSum", Napi::Number::New(env, runningSumOfAbs));
90
+
91
+ channelsArray.Set(static_cast<uint32_t>(i), channelState);
92
+ }
93
+ state.Set("channels", channelsArray);
94
+ }
95
+
96
+ return state;
97
+ }
98
+
99
+ // Deserialize and restore the stage's state
100
+ void deserializeState(const Napi::Object &state) override
101
+ {
102
+ std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
103
+ MavMode newMode = (modeStr == "moving") ? MavMode::Moving : MavMode::Batch;
104
+
105
+ if (newMode != m_mode)
106
+ {
107
+ throw std::runtime_error("MeanAbsoluteValue mode mismatch during deserialization");
108
+ }
109
+
110
+ if (m_mode == MavMode::Moving)
111
+ {
112
+ // Get window size and validate
113
+ size_t windowSize = state.Get("windowSize").As<Napi::Number>().Uint32Value();
114
+ if (windowSize != m_window_size)
115
+ {
116
+ throw std::runtime_error("Window size mismatch during deserialization");
117
+ }
118
+
119
+ // Get number of channels
120
+ uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
121
+
122
+ // Recreate filters
123
+ m_filters.clear();
124
+ for (uint32_t i = 0; i < numChannels; ++i)
125
+ {
126
+ m_filters.emplace_back(m_window_size);
127
+ }
128
+
129
+ // Restore each channel's state
130
+ Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
131
+ for (uint32_t i = 0; i < numChannels; ++i)
132
+ {
133
+ Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
134
+
135
+ // Get buffer data
136
+ Napi::Array bufferArray = channelState.Get("buffer").As<Napi::Array>();
137
+ std::vector<float> bufferData;
138
+ bufferData.reserve(bufferArray.Length());
139
+ for (uint32_t j = 0; j < bufferArray.Length(); ++j)
140
+ {
141
+ bufferData.push_back(bufferArray.Get(j).As<Napi::Number>().FloatValue());
142
+ }
143
+
144
+ // Get running sum (of absolute values)
145
+ float runningSum = channelState.Get("runningSum").As<Napi::Number>().FloatValue();
146
+
147
+ // --- Validation ---
148
+ // We must re-calculate the sum of absolute values from the original buffer data
149
+ float actualSumOfAbs = 0.0f;
150
+ for (const auto &val : bufferData)
151
+ {
152
+ actualSumOfAbs += std::abs(val);
153
+ }
154
+
155
+ const float tolerance = 0.0001f * std::max(1.0f, std::abs(actualSumOfAbs));
156
+ if (std::abs(runningSum - actualSumOfAbs) > tolerance)
157
+ {
158
+ throw std::runtime_error(
159
+ "Running sum of absolute values validation failed: expected " +
160
+ std::to_string(actualSumOfAbs) + " but got " +
161
+ std::to_string(runningSum));
162
+ }
163
+ // --- End Validation ---
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 MAV for each channel
183
+ * and overwrites all samples in that channel with the result.
184
+ */
185
+ void processBatch(float *buffer, size_t numSamples, int numChannels)
186
+ {
187
+ for (int c = 0; c < numChannels; ++c)
188
+ {
189
+ size_t numSamplesPerChannel = numSamples / numChannels;
190
+ if (numSamplesPerChannel == 0)
191
+ continue;
192
+
193
+ double sum_abs = 0.0;
194
+
195
+ // First pass: Calculate sum of absolute values
196
+ for (size_t i = c; i < numSamples; i += numChannels)
197
+ {
198
+ sum_abs += static_cast<double>(std::abs(buffer[i]));
199
+ }
200
+
201
+ // Calculate MAV
202
+ float mav = static_cast<float>(sum_abs / numSamplesPerChannel);
203
+
204
+ // Second pass: Fill this channel's buffer with the single MAV value
205
+ for (size_t i = c; i < numSamples; i += numChannels)
206
+ {
207
+ buffer[i] = mav;
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * @brief Statefully processes samples using the moving MAV filters.
214
+ */
215
+ void processMoving(float *buffer, size_t numSamples, int numChannels, const float *timestamps)
216
+ {
217
+ // Determine if we're in time-aware mode
218
+ bool useTimeAware = (m_window_duration_ms > 0.0) && timestamps != nullptr;
219
+
220
+ // Lazy initialization: convert windowDuration to windowSize if needed
221
+ if (!m_is_initialized && m_window_duration_ms > 0.0)
222
+ {
223
+ if (timestamps != nullptr && numSamples > 1)
224
+ {
225
+ // Estimate sample rate from timestamps
226
+ size_t samples_to_check = std::min(numSamples, size_t(10));
227
+ double total_time_ms = timestamps[samples_to_check - 1] - timestamps[0];
228
+ double avg_sample_period_ms = total_time_ms / (samples_to_check - 1);
229
+ double estimated_sample_rate = 1000.0 / avg_sample_period_ms;
230
+
231
+ // Use 3x the estimated size for time-aware mode
232
+ size_t estimated_size = static_cast<size_t>((m_window_duration_ms / 1000.0) * estimated_sample_rate);
233
+ m_window_size = std::max(size_t(1), estimated_size * 3);
234
+
235
+ m_is_initialized = true;
236
+ }
237
+ else
238
+ {
239
+ throw std::runtime_error("MeanAbsoluteValue: windowDuration was set, but timestamps are not available to derive sample rate");
240
+ }
241
+ }
242
+
243
+ // Lazily initialize our filters, one for each channel
244
+ if (m_filters.size() != static_cast<size_t>(numChannels))
245
+ {
246
+ m_filters.clear();
247
+ for (int i = 0; i < numChannels; ++i)
248
+ {
249
+ if (useTimeAware)
250
+ {
251
+ // Create time-aware filter
252
+ m_filters.emplace_back(m_window_size, m_window_duration_ms);
253
+ }
254
+ else
255
+ {
256
+ // Create regular filter
257
+ m_filters.emplace_back(m_window_size);
258
+ }
259
+ }
260
+ }
261
+
262
+ // Process the buffer sample by sample, de-interleaving
263
+ for (size_t i = 0; i < numSamples; ++i)
264
+ {
265
+ int channel = i % numChannels;
266
+ size_t sample_index = i / numChannels;
267
+
268
+ if (useTimeAware)
269
+ {
270
+ // Use time-aware processing
271
+ buffer[i] = m_filters[channel].addSampleWithTimestamp(buffer[i], timestamps[sample_index]);
272
+ }
273
+ else
274
+ {
275
+ // Use sample-count processing
276
+ buffer[i] = m_filters[channel].addSample(buffer[i]);
277
+ }
278
+ }
279
+ }
280
+
281
+ MavMode m_mode;
282
+ size_t m_window_size;
283
+ double m_window_duration_ms;
284
+ bool m_is_initialized;
285
+ // We need a separate filter instance for each channel's state
286
+ std::vector<dsp::core::MovingAbsoluteValueFilter<float>> m_filters;
287
+ };
288
+
289
+ } // namespace dsp::adapters