dspx 1.2.3 → 1.3.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 (71) hide show
  1. package/README.md +40 -78
  2. package/binding.gyp +10 -0
  3. package/dist/FilterBankDesign.d.ts +233 -0
  4. package/dist/FilterBankDesign.d.ts.map +1 -0
  5. package/dist/FilterBankDesign.js +247 -0
  6. package/dist/FilterBankDesign.js.map +1 -0
  7. package/dist/advanced-dsp.d.ts +6 -6
  8. package/dist/advanced-dsp.d.ts.map +1 -1
  9. package/dist/advanced-dsp.js +35 -12
  10. package/dist/advanced-dsp.js.map +1 -1
  11. package/dist/backends.d.ts +0 -103
  12. package/dist/backends.d.ts.map +1 -1
  13. package/dist/backends.js +0 -217
  14. package/dist/backends.js.map +1 -1
  15. package/dist/bindings.d.ts +216 -17
  16. package/dist/bindings.d.ts.map +1 -1
  17. package/dist/bindings.js +503 -42
  18. package/dist/bindings.js.map +1 -1
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/types.d.ts +67 -8
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/utils.d.ts +38 -8
  26. package/dist/utils.d.ts.map +1 -1
  27. package/dist/utils.js +84 -26
  28. package/dist/utils.js.map +1 -1
  29. package/package.json +1 -2
  30. package/prebuilds/win32-x64/dspx.node +0 -0
  31. package/scripts/add-dispose-to-tests.js +145 -0
  32. package/src/native/DspPipeline.cc +777 -143
  33. package/src/native/DspPipeline.h +13 -0
  34. package/src/native/FilterBankDesignBindings.cc +241 -0
  35. package/src/native/IDspStage.h +24 -0
  36. package/src/native/UtilityBindings.cc +130 -0
  37. package/src/native/adapters/ClipDetectionStage.h +15 -4
  38. package/src/native/adapters/ConvolutionStage.h +101 -0
  39. package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
  40. package/src/native/adapters/DecimatorStage.h +80 -0
  41. package/src/native/adapters/DifferentiatorStage.h +13 -0
  42. package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
  43. package/src/native/adapters/FilterBankStage.cc +336 -0
  44. package/src/native/adapters/FilterBankStage.h +170 -0
  45. package/src/native/adapters/FilterStage.cc +139 -1
  46. package/src/native/adapters/FilterStage.h +4 -0
  47. package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
  48. package/src/native/adapters/IntegratorStage.h +15 -0
  49. package/src/native/adapters/InterpolatorStage.h +51 -0
  50. package/src/native/adapters/LinearRegressionStage.h +40 -0
  51. package/src/native/adapters/LmsStage.h +63 -0
  52. package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
  53. package/src/native/adapters/MovingAverageStage.h +119 -0
  54. package/src/native/adapters/PeakDetectionStage.h +53 -0
  55. package/src/native/adapters/RectifyStage.h +14 -0
  56. package/src/native/adapters/ResamplerStage.h +67 -0
  57. package/src/native/adapters/RlsStage.h +76 -0
  58. package/src/native/adapters/RmsStage.h +73 -1
  59. package/src/native/adapters/SnrStage.h +45 -0
  60. package/src/native/adapters/SscStage.h +65 -0
  61. package/src/native/adapters/StftStage.h +62 -0
  62. package/src/native/adapters/VarianceStage.h +60 -1
  63. package/src/native/adapters/WampStage.h +59 -0
  64. package/src/native/adapters/WaveformLengthStage.h +51 -0
  65. package/src/native/adapters/ZScoreNormalizeStage.h +65 -1
  66. package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
  67. package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
  68. package/src/native/core/FilterBankDesign.h +266 -0
  69. package/src/native/core/Policies.h +124 -0
  70. package/src/native/utils/CircularBufferArray.cc +2 -1
  71. package/src/native/utils/Toon.h +195 -0
@@ -0,0 +1,123 @@
1
+ #pragma once
2
+ #include "Policies.h"
3
+ #include <vector>
4
+ #include <stdexcept>
5
+
6
+ namespace dsp::core
7
+ {
8
+ /**
9
+ * @brief Implements a Cumulative Moving Average (CMA) filter.
10
+ *
11
+ * CMA is the average of all samples seen since initialization.
12
+ * Formula: CMA(n) = (CMA(n-1) * (n-1) + value(n)) / n
13
+ * or: CMA(n) = sum(values[1..n]) / n
14
+ *
15
+ * Unlike simple moving average (SMA), CMA considers ALL historical data:
16
+ * - SMA: Uses fixed window of recent N samples
17
+ * - CMA: Uses all samples from start to current
18
+ *
19
+ * Properties:
20
+ * - Memory-efficient: Only stores running sum and count
21
+ * - Stable convergence: Influence of new samples decreases over time
22
+ * - No window size needed: Adapts to any data length
23
+ *
24
+ * Use cases:
25
+ * - Long-term averages where all history matters
26
+ * - Baseline estimation from calibration data
27
+ * - Online mean estimation for statistics
28
+ *
29
+ * @tparam T The numeric type of the samples (e.g., float, double).
30
+ */
31
+ template <typename T>
32
+ class CumulativeMovingAverageFilter
33
+ {
34
+ public:
35
+ /**
36
+ * @brief Constructs a new CMA Filter.
37
+ */
38
+ CumulativeMovingAverageFilter()
39
+ : m_policy()
40
+ {
41
+ }
42
+
43
+ // Delete copy constructor and copy assignment
44
+ CumulativeMovingAverageFilter(const CumulativeMovingAverageFilter &) = delete;
45
+ CumulativeMovingAverageFilter &operator=(const CumulativeMovingAverageFilter &) = delete;
46
+
47
+ // Enable move semantics
48
+ CumulativeMovingAverageFilter(CumulativeMovingAverageFilter &&) noexcept = default;
49
+ CumulativeMovingAverageFilter &operator=(CumulativeMovingAverageFilter &&) noexcept = default;
50
+
51
+ /**
52
+ * @brief Adds a new sample and updates the CMA.
53
+ * @param newValue The new sample value.
54
+ * @return T The updated CMA value.
55
+ */
56
+ T addSample(T newValue)
57
+ {
58
+ m_policy.onAdd(newValue);
59
+ return m_policy.getResult(0); // Window count parameter unused for CMA
60
+ }
61
+
62
+ /**
63
+ * @brief Process array of samples in batch (optimized for throughput).
64
+ *
65
+ * @param input Input array of samples
66
+ * @param output Output array (same size as input)
67
+ * @param length Number of samples to process
68
+ */
69
+ void processArray(const T *input, T *output, size_t length)
70
+ {
71
+ for (size_t i = 0; i < length; ++i)
72
+ {
73
+ output[i] = addSample(input[i]);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @brief Gets the current CMA value.
79
+ * @return T The cumulative moving average.
80
+ */
81
+ T getCma() const { return m_policy.getResult(0); }
82
+
83
+ /**
84
+ * @brief Gets the number of samples processed.
85
+ * @return size_t The total count of samples.
86
+ */
87
+ size_t getCount() const { return m_policy.getCount(); }
88
+
89
+ /**
90
+ * @brief Clears the CMA state (resets to zero).
91
+ */
92
+ void clear() { m_policy.clear(); }
93
+
94
+ /**
95
+ * @brief Checks if any samples have been processed.
96
+ * @return true if count > 0, false otherwise.
97
+ */
98
+ bool hasData() const { return m_policy.getCount() > 0; }
99
+
100
+ /**
101
+ * @brief Exports the filter's internal state.
102
+ * @return A pair containing the running sum and sample count.
103
+ */
104
+ std::pair<T, size_t> getState() const
105
+ {
106
+ return m_policy.getState();
107
+ }
108
+
109
+ /**
110
+ * @brief Restores the filter's internal state.
111
+ * @param sum The running sum to restore.
112
+ * @param count The sample count to restore.
113
+ */
114
+ void setState(T sum, size_t count)
115
+ {
116
+ m_policy.setState(sum, count);
117
+ }
118
+
119
+ private:
120
+ CmaPolicy<T> m_policy;
121
+ };
122
+
123
+ } // namespace dsp::core
@@ -0,0 +1,129 @@
1
+ #pragma once
2
+ #include "Policies.h"
3
+ #include <vector>
4
+ #include <stdexcept>
5
+
6
+ namespace dsp::core
7
+ {
8
+ /**
9
+ * @brief Implements an Exponential Moving Average (EMA) filter.
10
+ *
11
+ * EMA gives more weight to recent samples and exponentially decaying weight to older samples.
12
+ * Formula: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
13
+ *
14
+ * where α (alpha) is the smoothing factor:
15
+ * - α close to 1: Fast response to changes (less smoothing)
16
+ * - α close to 0: Slow response to changes (more smoothing)
17
+ *
18
+ * Common conversions:
19
+ * - From N-period SMA: α = 2 / (N + 1)
20
+ * - From time constant: α = 1 - exp(-Δt / τ)
21
+ *
22
+ * Unlike simple moving average, EMA does not require a fixed window size,
23
+ * making it memory-efficient for very long averaging periods.
24
+ *
25
+ * @tparam T The numeric type of the samples (e.g., float, double).
26
+ */
27
+ template <typename T>
28
+ class ExponentialMovingAverageFilter
29
+ {
30
+ public:
31
+ /**
32
+ * @brief Constructs a new EMA Filter with specified alpha.
33
+ * @param alpha The smoothing factor (0 < α ≤ 1).
34
+ * @throws std::invalid_argument if alpha is outside valid range.
35
+ */
36
+ explicit ExponentialMovingAverageFilter(T alpha)
37
+ : m_policy(alpha)
38
+ {
39
+ if (alpha <= 0 || alpha > 1)
40
+ {
41
+ throw std::invalid_argument("EMA alpha must be in range (0, 1]");
42
+ }
43
+ }
44
+
45
+ // Delete copy constructor and copy assignment
46
+ ExponentialMovingAverageFilter(const ExponentialMovingAverageFilter &) = delete;
47
+ ExponentialMovingAverageFilter &operator=(const ExponentialMovingAverageFilter &) = delete;
48
+
49
+ // Enable move semantics
50
+ ExponentialMovingAverageFilter(ExponentialMovingAverageFilter &&) noexcept = default;
51
+ ExponentialMovingAverageFilter &operator=(ExponentialMovingAverageFilter &&) noexcept = default;
52
+
53
+ /**
54
+ * @brief Adds a new sample and updates the EMA.
55
+ * @param newValue The new sample value.
56
+ * @return T The updated EMA value.
57
+ */
58
+ T addSample(T newValue)
59
+ {
60
+ m_policy.onAdd(newValue);
61
+ return m_policy.getResult(0); // Count parameter unused for EMA
62
+ }
63
+
64
+ /**
65
+ * @brief Process array of samples in batch (optimized for throughput).
66
+ *
67
+ * @param input Input array of samples
68
+ * @param output Output array (same size as input)
69
+ * @param length Number of samples to process
70
+ */
71
+ void processArray(const T *input, T *output, size_t length)
72
+ {
73
+ for (size_t i = 0; i < length; ++i)
74
+ {
75
+ output[i] = addSample(input[i]);
76
+ }
77
+ }
78
+
79
+ /**
80
+ * @brief Gets the current EMA value.
81
+ * @return T The current exponential moving average.
82
+ */
83
+ T getEma() const { return m_policy.getResult(0); }
84
+
85
+ /**
86
+ * @brief Gets the alpha (smoothing factor).
87
+ * @return T The alpha value.
88
+ */
89
+ T getAlpha() const { return m_policy.getAlpha(); }
90
+
91
+ /**
92
+ * @brief Clears the EMA state.
93
+ */
94
+ void clear() { m_policy.clear(); }
95
+
96
+ /**
97
+ * @brief Checks if the filter has been initialized with at least one sample.
98
+ * @return true if initialized, false otherwise.
99
+ */
100
+ bool isInitialized() const
101
+ {
102
+ auto [ema, initialized] = m_policy.getState();
103
+ return initialized;
104
+ }
105
+
106
+ /**
107
+ * @brief Exports the filter's internal state.
108
+ * @return A pair containing the current EMA value and initialization flag.
109
+ */
110
+ std::pair<T, bool> getState() const
111
+ {
112
+ return m_policy.getState();
113
+ }
114
+
115
+ /**
116
+ * @brief Restores the filter's internal state.
117
+ * @param ema The EMA value to restore.
118
+ * @param initialized The initialization flag to restore.
119
+ */
120
+ void setState(T ema, bool initialized)
121
+ {
122
+ m_policy.setState(ema, initialized);
123
+ }
124
+
125
+ private:
126
+ EmaPolicy<T> m_policy;
127
+ };
128
+
129
+ } // namespace dsp::core
@@ -0,0 +1,266 @@
1
+ #pragma once
2
+
3
+ #include "IirFilter.h"
4
+ #include <vector>
5
+ #include <string>
6
+ #include <cmath>
7
+ #include <stdexcept>
8
+ #include <algorithm>
9
+
10
+ namespace dsp
11
+ {
12
+ namespace core
13
+ {
14
+ /**
15
+ * Filter coefficients structure for a single filter
16
+ */
17
+ struct FilterCoefficients
18
+ {
19
+ std::vector<float> b; // Numerator coefficients
20
+ std::vector<float> a; // Denominator coefficients
21
+ };
22
+
23
+ /**
24
+ * Filter Bank Design Engine
25
+ *
26
+ * Generates sets of bandpass filters covering a frequency range according to
27
+ * psychoacoustic (Mel, Bark) or mathematical (Linear, Log) scales.
28
+ *
29
+ * This is a stateless utility that performs frequency warping and filter design
30
+ * without maintaining any processing state.
31
+ */
32
+ class FilterBankDesign
33
+ {
34
+ public:
35
+ enum class Scale
36
+ {
37
+ Linear, // Linear spacing in Hz
38
+ Log, // Logarithmic spacing
39
+ Mel, // Mel scale (mimics human hearing)
40
+ Bark // Bark scale (critical band rate)
41
+ };
42
+
43
+ enum class Type
44
+ {
45
+ Butterworth, // Maximally flat passband
46
+ Chebyshev1 // Equiripple passband
47
+ };
48
+
49
+ /**
50
+ * Filter bank design options
51
+ */
52
+ struct DesignOptions
53
+ {
54
+ Scale scale; // Frequency spacing scale
55
+ Type type; // Filter topology
56
+ int count; // Number of bands
57
+ double sampleRate; // Sample rate in Hz
58
+ double minFreq; // Minimum frequency in Hz
59
+ double maxFreq; // Maximum frequency in Hz
60
+ int order; // Filter order per band (steepness)
61
+ double rippleDb = 0.5; // Passband ripple for Chebyshev (dB)
62
+ };
63
+
64
+ /**
65
+ * Design a filter bank with specified options
66
+ *
67
+ * @param opts Design options including scale, count, frequency range
68
+ * @return Vector of filter coefficients (one per band)
69
+ *
70
+ * @throws std::invalid_argument if options are invalid
71
+ *
72
+ * @example
73
+ * // Create 24-band Mel-spaced filter bank for speech analysis
74
+ * DesignOptions opts;
75
+ * opts.scale = Scale::Mel;
76
+ * opts.type = Type::Butterworth;
77
+ * opts.count = 24;
78
+ * opts.sampleRate = 44100;
79
+ * opts.minFreq = 20;
80
+ * opts.maxFreq = 8000;
81
+ * opts.order = 2;
82
+ * auto bank = FilterBankDesign::design(opts);
83
+ */
84
+ static std::vector<FilterCoefficients> design(const DesignOptions &opts)
85
+ {
86
+ // Validate inputs
87
+ if (opts.count <= 0)
88
+ {
89
+ throw std::invalid_argument("Band count must be positive");
90
+ }
91
+ if (opts.minFreq < 0)
92
+ {
93
+ throw std::invalid_argument("Minimum frequency cannot be negative");
94
+ }
95
+ if (opts.minFreq >= opts.maxFreq)
96
+ {
97
+ throw std::invalid_argument("Invalid frequency range: minFreq must be < maxFreq");
98
+ }
99
+ if (opts.maxFreq > opts.sampleRate / 2.0)
100
+ {
101
+ throw std::invalid_argument("Maximum frequency must be <= Nyquist frequency");
102
+ }
103
+ if (opts.order <= 0)
104
+ {
105
+ throw std::invalid_argument("Filter order must be positive");
106
+ }
107
+ if (opts.sampleRate <= 0)
108
+ {
109
+ throw std::invalid_argument("Sample rate must be positive");
110
+ }
111
+
112
+ // Step 1: Convert frequency boundaries to target scale
113
+ double minVal = toScale(opts.minFreq, opts.scale);
114
+ double maxVal = toScale(opts.maxFreq, opts.scale);
115
+ double step = (maxVal - minVal) / opts.count;
116
+
117
+ // Step 2: Generate band edges in target scale, then convert back to Hz
118
+ std::vector<double> boundaries;
119
+ boundaries.reserve(opts.count + 1);
120
+
121
+ for (int i = 0; i <= opts.count; ++i)
122
+ {
123
+ double val = minVal + (i * step);
124
+ double hz = fromScale(val, opts.scale);
125
+ boundaries.push_back(hz);
126
+ }
127
+
128
+ // Step 3: Create filters for each band
129
+ std::vector<FilterCoefficients> bank;
130
+ bank.reserve(opts.count);
131
+
132
+ for (int i = 0; i < opts.count; ++i)
133
+ {
134
+ double fLow = boundaries[i];
135
+ double fHigh = boundaries[i + 1];
136
+
137
+ // For the first band starting at 0 Hz, use a small positive value
138
+ // to avoid DC (bandpass filters can't have 0 Hz as lower bound)
139
+ if (fLow == 0.0)
140
+ {
141
+ fLow = 1.0; // 1 Hz minimum
142
+ }
143
+
144
+ // Normalize frequencies to [0, 0.5] range (0.5 = Nyquist)
145
+ double nLow = fLow / opts.sampleRate;
146
+ double nHigh = fHigh / opts.sampleRate;
147
+
148
+ // Safety clamping to avoid numerical issues
149
+ nLow = std::max(0.0001, std::min(nLow, 0.4999));
150
+ nHigh = std::max(0.0001, std::min(nHigh, 0.4999));
151
+
152
+ // Ensure proper ordering after clamping
153
+ if (nLow >= nHigh)
154
+ {
155
+ nHigh = nLow + 0.0001;
156
+ }
157
+
158
+ // Design bandpass filter using existing IirFilter factory
159
+ IirFilter<float> filter = (opts.type == Type::Chebyshev1)
160
+ ? IirFilter<float>::createChebyshevBandPass(
161
+ nLow, nHigh, opts.order, opts.rippleDb)
162
+ : IirFilter<float>::createButterworthBandPass(
163
+ nLow, nHigh, opts.order);
164
+
165
+ // Extract coefficients
166
+ FilterCoefficients coeffs;
167
+ coeffs.b = filter.getBCoefficients();
168
+ coeffs.a = filter.getACoefficients();
169
+
170
+ bank.push_back(coeffs);
171
+ }
172
+
173
+ return bank;
174
+ }
175
+
176
+ /**
177
+ * Get frequency boundaries for a filter bank design
178
+ * Useful for visualization and debugging
179
+ *
180
+ * @param opts Design options
181
+ * @return Vector of boundary frequencies in Hz
182
+ */
183
+ static std::vector<double> getBoundaries(const DesignOptions &opts)
184
+ {
185
+ if (opts.count <= 0)
186
+ {
187
+ throw std::invalid_argument("Band count must be positive");
188
+ }
189
+
190
+ double minVal = toScale(opts.minFreq, opts.scale);
191
+ double maxVal = toScale(opts.maxFreq, opts.scale);
192
+ double step = (maxVal - minVal) / opts.count;
193
+
194
+ std::vector<double> boundaries;
195
+ boundaries.reserve(opts.count + 1);
196
+
197
+ for (int i = 0; i <= opts.count; ++i)
198
+ {
199
+ double val = minVal + (i * step);
200
+ boundaries.push_back(fromScale(val, opts.scale));
201
+ }
202
+
203
+ return boundaries;
204
+ }
205
+
206
+ private:
207
+ /**
208
+ * Convert frequency from Hz to target scale
209
+ */
210
+ static double toScale(double hz, Scale scale)
211
+ {
212
+ switch (scale)
213
+ {
214
+ case Scale::Linear:
215
+ return hz;
216
+
217
+ case Scale::Log:
218
+ return std::log10(hz);
219
+
220
+ case Scale::Mel:
221
+ // Mel scale: f_mel = 2595 * log10(1 + f_hz / 700)
222
+ return 2595.0 * std::log10(1.0 + hz / 700.0);
223
+
224
+ case Scale::Bark:
225
+ // Bark scale (Traunmüller 1990)
226
+ // z = 26.81 * f / (1960 + f) - 0.53
227
+ return 26.81 * hz / (1960.0 + hz) - 0.53;
228
+
229
+ default:
230
+ return hz;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Convert from scale back to Hz
236
+ */
237
+ static double fromScale(double val, Scale scale)
238
+ {
239
+ switch (scale)
240
+ {
241
+ case Scale::Linear:
242
+ return val;
243
+
244
+ case Scale::Log:
245
+ return std::pow(10.0, val);
246
+
247
+ case Scale::Mel:
248
+ // Inverse Mel: f_hz = 700 * (10^(f_mel / 2595) - 1)
249
+ return 700.0 * (std::pow(10.0, val / 2595.0) - 1.0);
250
+
251
+ case Scale::Bark:
252
+ // Inverse Bark (Traunmüller 1990)
253
+ // f = 1960 * (z + 0.53) / (26.81 - (z + 0.53))
254
+ {
255
+ double adjusted = val + 0.53;
256
+ return 1960.0 * adjusted / (26.81 - adjusted);
257
+ }
258
+
259
+ default:
260
+ return val;
261
+ }
262
+ }
263
+ };
264
+
265
+ } // namespace core
266
+ } // namespace dsp
@@ -349,4 +349,128 @@ namespace dsp::core
349
349
  void setCoefficients(const std::vector<T> &coeffs) { m_coefficients = coeffs; }
350
350
  };
351
351
 
352
+ /**
353
+ * @brief Policy for Exponential Moving Average (EMA).
354
+ *
355
+ * Implements EMA: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
356
+ * where α (alpha) is the smoothing factor (0 < α ≤ 1).
357
+ *
358
+ * This policy is optimized for scalar operations and can be SIMD-accelerated
359
+ * in batch processing contexts.
360
+ */
361
+ template <typename T>
362
+ struct EmaPolicy
363
+ {
364
+ T m_ema = 0; // Current EMA value
365
+ T m_alpha; // Smoothing factor
366
+ bool m_initialized = false;
367
+
368
+ explicit EmaPolicy(T alpha)
369
+ : m_alpha(alpha)
370
+ {
371
+ if (alpha <= 0 || alpha > 1)
372
+ {
373
+ throw std::invalid_argument("EMA alpha must be in range (0, 1]");
374
+ }
375
+ }
376
+
377
+ void onAdd(T val)
378
+ {
379
+ if (!m_initialized)
380
+ {
381
+ // Initialize with first value
382
+ m_ema = val;
383
+ m_initialized = true;
384
+ }
385
+ else
386
+ {
387
+ // EMA formula: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
388
+ m_ema = m_alpha * val + (static_cast<T>(1) - m_alpha) * m_ema;
389
+ }
390
+ }
391
+
392
+ void onRemove(T val)
393
+ {
394
+ // EMA doesn't support removal in sliding window context
395
+ // This should not be called in typical EMA usage
396
+ }
397
+
398
+ void clear()
399
+ {
400
+ m_ema = 0;
401
+ m_initialized = false;
402
+ }
403
+
404
+ T getResult(size_t count) const
405
+ {
406
+ return m_ema;
407
+ }
408
+
409
+ // For state serialization
410
+ std::pair<T, bool> getState() const { return {m_ema, m_initialized}; }
411
+ void setState(T ema, bool initialized)
412
+ {
413
+ m_ema = ema;
414
+ m_initialized = initialized;
415
+ }
416
+
417
+ T getAlpha() const { return m_alpha; }
418
+ };
419
+
420
+ /**
421
+ * @brief Policy for Cumulative Moving Average (CMA).
422
+ *
423
+ * Implements CMA: CMA(n) = (CMA(n-1) * (n-1) + value(n)) / n
424
+ *
425
+ * Maintains the cumulative average over all samples seen since initialization.
426
+ * More efficient than recalculating from scratch each time.
427
+ */
428
+ template <typename T>
429
+ struct CmaPolicy
430
+ {
431
+ T m_sum = 0; // Running sum of all values
432
+ size_t m_count = 0; // Total number of samples seen
433
+
434
+ void onAdd(T val)
435
+ {
436
+ m_sum += val;
437
+ m_count++;
438
+ }
439
+
440
+ void onRemove(T val)
441
+ {
442
+ // CMA doesn't support removal in typical usage
443
+ // If called, decrement count and sum
444
+ if (m_count > 0)
445
+ {
446
+ m_sum -= val;
447
+ m_count--;
448
+ }
449
+ }
450
+
451
+ void clear()
452
+ {
453
+ m_sum = 0;
454
+ m_count = 0;
455
+ }
456
+
457
+ T getResult(size_t windowCount) const
458
+ {
459
+ // Use the policy's internal count, not the window count
460
+ if (m_count == 0)
461
+ return 0;
462
+ return m_sum / static_cast<T>(m_count);
463
+ }
464
+
465
+ // For state serialization
466
+ std::pair<T, size_t> getState() const { return {m_sum, m_count}; }
467
+ void setState(T sum, size_t count)
468
+ {
469
+ m_sum = sum;
470
+ m_count = count;
471
+ }
472
+
473
+ size_t getCount() const { return m_count; }
474
+ };
475
+
352
476
  } // namespace dsp::core
@@ -240,7 +240,8 @@ size_t CircularBufferArray<T>::expireOld(double currentTimestamp)
240
240
  double cutoff_time = currentTimestamp - windowDuration_ms;
241
241
 
242
242
  // Remove samples from tail while they're older than cutoff
243
- while (count > 0 && timestamps[tail] < cutoff_time)
243
+ // FIX: changed < to <= to correctly expire samples at the cutoff and avoid an additional windowduration delay
244
+ while (count > 0 && timestamps[tail] <= cutoff_time)
244
245
  {
245
246
  tail = (tail + 1) % capacity;
246
247
  --count;