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,264 @@
1
+ #pragma once
2
+
3
+ #include "../IDspStage.h"
4
+ #include "../core/CumulativeMovingAverageFilter.h"
5
+ #include "../utils/SimdOps.h"
6
+ #include <vector>
7
+ #include <stdexcept>
8
+ #include <string>
9
+
10
+ namespace dsp::adapters
11
+ {
12
+ enum class CmaMode
13
+ {
14
+ Batch,
15
+ Moving
16
+ };
17
+
18
+ class CumulativeMovingAverageStage : public IDspStage
19
+ {
20
+ public:
21
+ /**
22
+ * @brief Constructs a new Cumulative Moving Average Stage.
23
+ * @param mode The averaging mode (Batch or Moving).
24
+ */
25
+ explicit CumulativeMovingAverageStage(CmaMode mode)
26
+ : m_mode(mode)
27
+ {
28
+ }
29
+
30
+ // Return the type identifier for this stage
31
+ const char *getType() const override
32
+ {
33
+ return "cumulativeMovingAverage";
34
+ }
35
+
36
+ // This is the implementation of the interface method
37
+ void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
38
+ {
39
+ if (m_mode == CmaMode::Batch)
40
+ {
41
+ processBatch(buffer, numSamples, numChannels);
42
+ }
43
+ else // CmaMode::Moving
44
+ {
45
+ processMoving(buffer, numSamples, numChannels);
46
+ }
47
+ }
48
+
49
+ // Serialize the stage's state to a Napi::Object
50
+ Napi::Object serializeState(Napi::Env env) const override
51
+ {
52
+ Napi::Object state = Napi::Object::New(env);
53
+ std::string modeStr = (m_mode == CmaMode::Moving) ? "moving" : "batch";
54
+ state.Set("mode", modeStr);
55
+
56
+ if (m_mode == CmaMode::Moving)
57
+ {
58
+ state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
59
+
60
+ // Serialize each channel's filter state
61
+ Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
62
+ for (size_t i = 0; i < m_filters.size(); ++i)
63
+ {
64
+ Napi::Object channelState = Napi::Object::New(env);
65
+
66
+ // Get the filter's internal state
67
+ auto [sum, count] = m_filters[i].getState();
68
+
69
+ channelState.Set("sum", Napi::Number::New(env, sum));
70
+ channelState.Set("count", Napi::Number::New(env, static_cast<uint32_t>(count)));
71
+
72
+ channelsArray.Set(static_cast<uint32_t>(i), channelState);
73
+ }
74
+ state.Set("channels", channelsArray);
75
+ }
76
+
77
+ return state;
78
+ }
79
+
80
+ // Deserialize and restore the stage's state
81
+ void deserializeState(const Napi::Object &state) override
82
+ {
83
+ std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
84
+ CmaMode newMode = (modeStr == "moving") ? CmaMode::Moving : CmaMode::Batch;
85
+
86
+ if (newMode != m_mode)
87
+ {
88
+ throw std::runtime_error("CumulativeMovingAverage mode mismatch during deserialization");
89
+ }
90
+
91
+ if (m_mode == CmaMode::Moving)
92
+ {
93
+ // Get number of channels
94
+ uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
95
+
96
+ // Recreate filters
97
+ m_filters.clear();
98
+ for (uint32_t i = 0; i < numChannels; ++i)
99
+ {
100
+ m_filters.emplace_back();
101
+ }
102
+
103
+ // Restore each channel's state
104
+ Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
105
+ for (uint32_t i = 0; i < numChannels; ++i)
106
+ {
107
+ Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
108
+
109
+ float sum = channelState.Get("sum").As<Napi::Number>().FloatValue();
110
+ size_t count = channelState.Get("count").As<Napi::Number>().Uint32Value();
111
+
112
+ // Restore the filter's state
113
+ m_filters[i].setState(sum, count);
114
+ }
115
+ }
116
+ }
117
+
118
+ // Reset all filters to initial state
119
+ void reset() override
120
+ {
121
+ for (auto &filter : m_filters)
122
+ {
123
+ filter.clear();
124
+ }
125
+ }
126
+
127
+ // TOON Binary Serialization
128
+ void serializeToon(dsp::toon::Serializer &serializer) const override
129
+ {
130
+ serializer.startObject();
131
+
132
+ // 1. Mode
133
+ serializer.writeString("mode");
134
+ serializer.writeString((m_mode == CmaMode::Moving) ? "moving" : "batch");
135
+
136
+ if (m_mode == CmaMode::Moving)
137
+ {
138
+ // 2. Channels
139
+ serializer.writeString("channels");
140
+ serializer.startArray();
141
+
142
+ for (const auto &filter : m_filters)
143
+ {
144
+ auto [sum, count] = filter.getState();
145
+
146
+ serializer.startObject();
147
+
148
+ serializer.writeString("sum");
149
+ serializer.writeFloat(sum);
150
+
151
+ serializer.writeString("count");
152
+ serializer.writeInt32(static_cast<int32_t>(count));
153
+
154
+ serializer.endObject();
155
+ }
156
+ serializer.endArray();
157
+ }
158
+
159
+ serializer.endObject();
160
+ }
161
+
162
+ // TOON Binary Deserialization
163
+ void deserializeToon(dsp::toon::Deserializer &deserializer) override
164
+ {
165
+ deserializer.consumeToken(dsp::toon::T_OBJECT_START);
166
+
167
+ // 1. Mode
168
+ std::string key = deserializer.readString(); // "mode"
169
+ std::string modeStr = deserializer.readString();
170
+
171
+ CmaMode newMode = (modeStr == "moving") ? CmaMode::Moving : CmaMode::Batch;
172
+ if (newMode != m_mode)
173
+ {
174
+ throw std::runtime_error("CumulativeMovingAverage mode mismatch during TOON deserialization");
175
+ }
176
+
177
+ if (modeStr == "moving")
178
+ {
179
+ // 2. Channels
180
+ key = deserializer.readString(); // "channels"
181
+ deserializer.consumeToken(dsp::toon::T_ARRAY_START);
182
+
183
+ m_filters.clear();
184
+
185
+ while (deserializer.peekToken() != dsp::toon::T_ARRAY_END)
186
+ {
187
+ deserializer.consumeToken(dsp::toon::T_OBJECT_START);
188
+
189
+ // Sum
190
+ deserializer.readString(); // "sum"
191
+ float sum = deserializer.readFloat();
192
+
193
+ // Count
194
+ deserializer.readString(); // "count"
195
+ int32_t count = deserializer.readInt32();
196
+
197
+ // Reconstruct filter
198
+ m_filters.emplace_back();
199
+ m_filters.back().setState(sum, static_cast<size_t>(count));
200
+
201
+ deserializer.consumeToken(dsp::toon::T_OBJECT_END);
202
+ }
203
+ deserializer.consumeToken(dsp::toon::T_ARRAY_END);
204
+ }
205
+
206
+ deserializer.consumeToken(dsp::toon::T_OBJECT_END);
207
+ }
208
+
209
+ private:
210
+ /**
211
+ * @brief Batch mode: Compute cumulative moving average over entire buffer per channel.
212
+ * Each sample in the channel is replaced by the cumulative average up to that point.
213
+ */
214
+ void processBatch(float *buffer, size_t numSamples, int numChannels)
215
+ {
216
+ // Process each channel independently
217
+ for (int c = 0; c < numChannels; ++c)
218
+ {
219
+ float sum = 0.0f;
220
+ size_t count = 0;
221
+
222
+ // Process all samples for this channel
223
+ for (size_t i = c; i < numSamples; i += numChannels)
224
+ {
225
+ sum += buffer[i];
226
+ count++;
227
+ // CMA = sum / count
228
+ buffer[i] = sum / static_cast<float>(count);
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * @brief Moving mode: Statefully process samples using CMA filters with continuity.
235
+ * @param buffer The interleaved audio buffer.
236
+ * @param numSamples The total number of samples.
237
+ * @param numChannels The number of channels.
238
+ */
239
+ void processMoving(float *buffer, size_t numSamples, int numChannels)
240
+ {
241
+ // Lazily initialize our filters, one for each channel
242
+ if (m_filters.size() != static_cast<size_t>(numChannels))
243
+ {
244
+ m_filters.clear();
245
+ for (int i = 0; i < numChannels; ++i)
246
+ {
247
+ m_filters.emplace_back();
248
+ }
249
+ }
250
+
251
+ // Process the buffer sample by sample, de-interleaving
252
+ for (size_t i = 0; i < numSamples; ++i)
253
+ {
254
+ int channel = i % numChannels;
255
+ buffer[i] = m_filters[channel].addSample(buffer[i]);
256
+ }
257
+ }
258
+
259
+ CmaMode m_mode;
260
+ // Separate filter instance for each channel's state
261
+ std::vector<dsp::core::CumulativeMovingAverageFilter<float>> m_filters;
262
+ };
263
+
264
+ } // namespace dsp::adapters
@@ -215,6 +215,86 @@ namespace dsp
215
215
  }
216
216
  }
217
217
 
218
+ void serializeToon(dsp::toon::Serializer &s) const override
219
+ {
220
+ s.startObject();
221
+
222
+ s.writeString("factor");
223
+ s.writeInt32(decimationFactor_);
224
+
225
+ s.writeString("order");
226
+ s.writeInt32(filterOrder_);
227
+
228
+ s.writeString("sampleRate");
229
+ s.writeDouble(sampleRate_);
230
+
231
+ s.writeString("phaseIndex");
232
+ s.writeInt32(phaseIndex_);
233
+
234
+ s.writeString("numChannels");
235
+ s.writeInt32(numChannels_);
236
+
237
+ s.writeString("stateBuffer");
238
+ s.writeFloatArray(stateBuffer_);
239
+
240
+ s.writeString("stateIndices");
241
+ s.startArray();
242
+ for (size_t idx : stateIndices_)
243
+ {
244
+ s.writeInt32(static_cast<int32_t>(idx));
245
+ }
246
+ s.endArray();
247
+
248
+ s.endObject();
249
+ }
250
+
251
+ void deserializeToon(dsp::toon::Deserializer &d) override
252
+ {
253
+ d.consumeToken(dsp::toon::T_OBJECT_START);
254
+
255
+ std::string key = d.readString(); // "factor"
256
+ int factor = d.readInt32();
257
+ if (factor != decimationFactor_)
258
+ {
259
+ throw std::runtime_error("Decimator factor mismatch during TOON deserialization");
260
+ }
261
+
262
+ key = d.readString(); // "order"
263
+ int order = d.readInt32();
264
+ if (order != filterOrder_)
265
+ {
266
+ throw std::runtime_error("Decimator order mismatch during TOON deserialization");
267
+ }
268
+
269
+ key = d.readString(); // "sampleRate"
270
+ d.readDouble(); // Skip sampleRate
271
+
272
+ key = d.readString(); // "phaseIndex"
273
+ phaseIndex_ = d.readInt32();
274
+
275
+ key = d.readString(); // "numChannels"
276
+ int numCh = d.readInt32();
277
+
278
+ if (numCh != numChannels_)
279
+ {
280
+ initializeStateBuffers(numCh);
281
+ }
282
+
283
+ key = d.readString(); // "stateBuffer"
284
+ stateBuffer_ = d.readFloatArray();
285
+
286
+ key = d.readString(); // "stateIndices"
287
+ d.consumeToken(dsp::toon::T_ARRAY_START);
288
+ stateIndices_.clear();
289
+ while (d.peekToken() != dsp::toon::T_ARRAY_END)
290
+ {
291
+ stateIndices_.push_back(static_cast<size_t>(d.readInt32()));
292
+ }
293
+ d.consumeToken(dsp::toon::T_ARRAY_END);
294
+
295
+ d.consumeToken(dsp::toon::T_OBJECT_END);
296
+ }
297
+
218
298
  /**
219
299
  * Get decimation factor
220
300
  */
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include "../IDspStage.h"
4
+ #include "../utils/Toon.h"
4
5
  #include <stdexcept>
5
6
  #include <string>
6
7
  #include <vector>
@@ -92,6 +93,18 @@ namespace dsp::adapters
92
93
  std::fill(m_prev_sample.begin(), m_prev_sample.end(), 0.0f);
93
94
  }
94
95
 
96
+ void serializeToon(dsp::toon::Serializer &s) const override
97
+ {
98
+ s.writeInt32(m_num_channels);
99
+ s.writeFloatArray(m_prev_sample);
100
+ }
101
+
102
+ void deserializeToon(dsp::toon::Deserializer &d) override
103
+ {
104
+ m_num_channels = d.readInt32();
105
+ m_prev_sample = d.readFloatArray();
106
+ }
107
+
95
108
  bool isResizing() const override { return false; }
96
109
 
97
110
  private:
@@ -0,0 +1,290 @@
1
+ #pragma once
2
+
3
+ #include "../IDspStage.h"
4
+ #include "../core/ExponentialMovingAverageFilter.h"
5
+ #include "../utils/SimdOps.h"
6
+ #include <vector>
7
+ #include <stdexcept>
8
+ #include <cmath>
9
+ #include <string>
10
+
11
+ namespace dsp::adapters
12
+ {
13
+ enum class EmaMode
14
+ {
15
+ Batch,
16
+ Moving
17
+ };
18
+
19
+ class ExponentialMovingAverageStage : public IDspStage
20
+ {
21
+ public:
22
+ /**
23
+ * @brief Constructs a new Exponential Moving Average Stage.
24
+ * @param mode The averaging mode (Batch or Moving).
25
+ * @param alpha The smoothing factor (0 < α ≤ 1).
26
+ */
27
+ explicit ExponentialMovingAverageStage(EmaMode mode, float alpha)
28
+ : m_mode(mode),
29
+ m_alpha(alpha)
30
+ {
31
+ if (alpha <= 0.0f || alpha > 1.0f)
32
+ {
33
+ throw std::invalid_argument("ExponentialMovingAverage: alpha must be in range (0, 1]");
34
+ }
35
+ }
36
+
37
+ // Return the type identifier for this stage
38
+ const char *getType() const override
39
+ {
40
+ return "exponentialMovingAverage";
41
+ }
42
+
43
+ // This is the implementation of the interface method
44
+ void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
45
+ {
46
+ if (m_mode == EmaMode::Batch)
47
+ {
48
+ processBatch(buffer, numSamples, numChannels);
49
+ }
50
+ else // EmaMode::Moving
51
+ {
52
+ processMoving(buffer, numSamples, numChannels);
53
+ }
54
+ }
55
+
56
+ // Serialize the stage's state to a Napi::Object
57
+ Napi::Object serializeState(Napi::Env env) const override
58
+ {
59
+ Napi::Object state = Napi::Object::New(env);
60
+ std::string modeStr = (m_mode == EmaMode::Moving) ? "moving" : "batch";
61
+ state.Set("mode", modeStr);
62
+ state.Set("alpha", Napi::Number::New(env, m_alpha));
63
+
64
+ if (m_mode == EmaMode::Moving)
65
+ {
66
+ state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
67
+
68
+ // Serialize each channel's filter state
69
+ Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
70
+ for (size_t i = 0; i < m_filters.size(); ++i)
71
+ {
72
+ Napi::Object channelState = Napi::Object::New(env);
73
+
74
+ // Get the filter's internal state
75
+ auto [ema, initialized] = m_filters[i].getState();
76
+
77
+ channelState.Set("ema", Napi::Number::New(env, ema));
78
+ channelState.Set("initialized", Napi::Boolean::New(env, initialized));
79
+
80
+ channelsArray.Set(static_cast<uint32_t>(i), channelState);
81
+ }
82
+ state.Set("channels", channelsArray);
83
+ }
84
+
85
+ return state;
86
+ }
87
+
88
+ // Deserialize and restore the stage's state
89
+ void deserializeState(const Napi::Object &state) override
90
+ {
91
+ std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
92
+ EmaMode newMode = (modeStr == "moving") ? EmaMode::Moving : EmaMode::Batch;
93
+
94
+ if (newMode != m_mode)
95
+ {
96
+ throw std::runtime_error("ExponentialMovingAverage mode mismatch during deserialization");
97
+ }
98
+
99
+ float alpha = state.Get("alpha").As<Napi::Number>().FloatValue();
100
+ if (std::abs(alpha - m_alpha) > 1e-6f)
101
+ {
102
+ throw std::runtime_error("ExponentialMovingAverage alpha mismatch during deserialization");
103
+ }
104
+
105
+ if (m_mode == EmaMode::Moving)
106
+ {
107
+ // Get number of channels
108
+ uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
109
+
110
+ // Recreate filters
111
+ m_filters.clear();
112
+ for (uint32_t i = 0; i < numChannels; ++i)
113
+ {
114
+ m_filters.emplace_back(m_alpha);
115
+ }
116
+
117
+ // Restore each channel's state
118
+ Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
119
+ for (uint32_t i = 0; i < numChannels; ++i)
120
+ {
121
+ Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
122
+
123
+ float ema = channelState.Get("ema").As<Napi::Number>().FloatValue();
124
+ bool initialized = channelState.Get("initialized").As<Napi::Boolean>().Value();
125
+
126
+ // Restore the filter's state
127
+ m_filters[i].setState(ema, initialized);
128
+ }
129
+ }
130
+ }
131
+
132
+ // Reset all filters to initial state
133
+ void reset() override
134
+ {
135
+ for (auto &filter : m_filters)
136
+ {
137
+ filter.clear();
138
+ }
139
+ }
140
+
141
+ // TOON Binary Serialization
142
+ void serializeToon(dsp::toon::Serializer &serializer) const override
143
+ {
144
+ serializer.startObject();
145
+
146
+ // 1. Mode
147
+ serializer.writeString("mode");
148
+ serializer.writeString((m_mode == EmaMode::Moving) ? "moving" : "batch");
149
+
150
+ // 2. Alpha
151
+ serializer.writeString("alpha");
152
+ serializer.writeFloat(m_alpha);
153
+
154
+ if (m_mode == EmaMode::Moving)
155
+ {
156
+ // 3. Channels
157
+ serializer.writeString("channels");
158
+ serializer.startArray();
159
+
160
+ for (const auto &filter : m_filters)
161
+ {
162
+ auto [ema, initialized] = filter.getState();
163
+
164
+ serializer.startObject();
165
+
166
+ serializer.writeString("ema");
167
+ serializer.writeFloat(ema);
168
+
169
+ serializer.writeString("initialized");
170
+ serializer.writeBool(initialized);
171
+
172
+ serializer.endObject();
173
+ }
174
+ serializer.endArray();
175
+ }
176
+
177
+ serializer.endObject();
178
+ }
179
+
180
+ // TOON Binary Deserialization
181
+ void deserializeToon(dsp::toon::Deserializer &deserializer) override
182
+ {
183
+ deserializer.consumeToken(dsp::toon::T_OBJECT_START);
184
+
185
+ // 1. Mode
186
+ std::string key = deserializer.readString(); // "mode"
187
+ std::string modeStr = deserializer.readString();
188
+
189
+ EmaMode newMode = (modeStr == "moving") ? EmaMode::Moving : EmaMode::Batch;
190
+ if (newMode != m_mode)
191
+ {
192
+ throw std::runtime_error("ExponentialMovingAverage mode mismatch during TOON deserialization");
193
+ }
194
+
195
+ // 2. Alpha
196
+ key = deserializer.readString(); // "alpha"
197
+ float alpha = deserializer.readFloat();
198
+ if (std::abs(alpha - m_alpha) > 1e-6f)
199
+ {
200
+ throw std::runtime_error("ExponentialMovingAverage alpha mismatch during TOON deserialization");
201
+ }
202
+
203
+ if (modeStr == "moving")
204
+ {
205
+ // 3. Channels
206
+ key = deserializer.readString(); // "channels"
207
+ deserializer.consumeToken(dsp::toon::T_ARRAY_START);
208
+
209
+ m_filters.clear();
210
+
211
+ while (deserializer.peekToken() != dsp::toon::T_ARRAY_END)
212
+ {
213
+ deserializer.consumeToken(dsp::toon::T_OBJECT_START);
214
+
215
+ // EMA value
216
+ deserializer.readString(); // "ema"
217
+ float ema = deserializer.readFloat();
218
+
219
+ // Initialized flag
220
+ deserializer.readString(); // "initialized"
221
+ bool initialized = deserializer.readBool();
222
+
223
+ // Reconstruct filter
224
+ m_filters.emplace_back(m_alpha);
225
+ m_filters.back().setState(ema, initialized);
226
+
227
+ deserializer.consumeToken(dsp::toon::T_OBJECT_END);
228
+ }
229
+ deserializer.consumeToken(dsp::toon::T_ARRAY_END);
230
+ }
231
+
232
+ deserializer.consumeToken(dsp::toon::T_OBJECT_END);
233
+ }
234
+
235
+ private:
236
+ /**
237
+ * @brief Batch mode: Compute exponential moving average over entire buffer per channel.
238
+ * Each sample in the channel is replaced by the progressive EMA.
239
+ */
240
+ void processBatch(float *buffer, size_t numSamples, int numChannels)
241
+ {
242
+ // Process each channel independently
243
+ for (int c = 0; c < numChannels; ++c)
244
+ {
245
+ // Initialize EMA with first sample of this channel
246
+ float ema = (c < numSamples) ? buffer[c] : 0.0f;
247
+
248
+ // Process all samples for this channel
249
+ for (size_t i = c; i < numSamples; i += numChannels)
250
+ {
251
+ // EMA formula: EMA(t) = α * value(t) + (1 - α) * EMA(t-1)
252
+ ema = m_alpha * buffer[i] + (1.0f - m_alpha) * ema;
253
+ buffer[i] = ema;
254
+ }
255
+ }
256
+ }
257
+
258
+ /**
259
+ * @brief Moving mode: Statefully process samples using EMA filters with continuity.
260
+ * @param buffer The interleaved audio buffer.
261
+ * @param numSamples The total number of samples.
262
+ * @param numChannels The number of channels.
263
+ */
264
+ void processMoving(float *buffer, size_t numSamples, int numChannels)
265
+ {
266
+ // Lazily initialize our filters, one for each channel
267
+ if (m_filters.size() != static_cast<size_t>(numChannels))
268
+ {
269
+ m_filters.clear();
270
+ for (int i = 0; i < numChannels; ++i)
271
+ {
272
+ m_filters.emplace_back(m_alpha);
273
+ }
274
+ }
275
+
276
+ // Process the buffer sample by sample, de-interleaving
277
+ for (size_t i = 0; i < numSamples; ++i)
278
+ {
279
+ int channel = i % numChannels;
280
+ buffer[i] = m_filters[channel].addSample(buffer[i]);
281
+ }
282
+ }
283
+
284
+ EmaMode m_mode;
285
+ float m_alpha;
286
+ // Separate filter instance for each channel's state
287
+ std::vector<dsp::core::ExponentialMovingAverageFilter<float>> m_filters;
288
+ };
289
+
290
+ } // namespace dsp::adapters