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,336 @@
1
+ #include "FilterBankStage.h"
2
+ #include <iostream>
3
+
4
+ namespace dsp::adapters
5
+ {
6
+
7
+ FilterBankStage::FilterBankStage(const std::vector<FilterDefinition> &definitions, int numInputChannels)
8
+ : m_definitions(definitions), m_numInputChannels(numInputChannels)
9
+ {
10
+ if (definitions.empty())
11
+ {
12
+ throw std::runtime_error("FilterBank: definitions cannot be empty");
13
+ }
14
+ if (numInputChannels <= 0)
15
+ {
16
+ throw std::runtime_error("FilterBank: Invalid channel count");
17
+ }
18
+
19
+ initializeFilters();
20
+ }
21
+ FilterBankStage::~FilterBankStage()
22
+ {
23
+ try
24
+ {
25
+ // Explicitly clear filters first to ensure IirFilter destructors run
26
+ // before scratch buffers are destroyed
27
+ for (size_t ch = 0; ch < m_filters.size(); ++ch)
28
+ {
29
+ m_filters[ch].clear(); // Destroys all unique_ptrs in this channel
30
+ }
31
+ m_filters.clear(); // Clear outer vector
32
+
33
+ // Clear scratch buffers
34
+ m_planarInput.clear();
35
+ m_planarOutput.clear();
36
+ }
37
+ catch (const std::exception &e)
38
+ {
39
+ std::cerr << "[FilterBankStage] ERROR in destructor: " << e.what() << std::endl;
40
+ }
41
+ catch (...)
42
+ {
43
+ std::cerr << "[FilterBankStage] UNKNOWN ERROR in destructor" << std::endl;
44
+ }
45
+ }
46
+
47
+ void FilterBankStage::processResizing(const float *inputBuffer, size_t inputSize,
48
+ float *outputBuffer, size_t &outputSize,
49
+ int numChannels, const float *timestamps)
50
+ {
51
+ if (numChannels != m_numInputChannels)
52
+ {
53
+ throw std::runtime_error("FilterBank: Input channel mismatch");
54
+ }
55
+
56
+ size_t samplesPerChannel = inputSize / numChannels;
57
+ size_t numBands = m_definitions.size();
58
+ size_t totalOutputChannels = numChannels * numBands;
59
+
60
+ outputSize = samplesPerChannel * totalOutputChannels;
61
+
62
+ ensureScratchSize(samplesPerChannel);
63
+
64
+ std::vector<float *> inputPtrs(numChannels);
65
+ for (int i = 0; i < numChannels; ++i)
66
+ {
67
+ inputPtrs[i] = m_planarInput[i].data();
68
+ }
69
+
70
+ if (numChannels == 2)
71
+ {
72
+ dsp::simd::deinterleave2Ch(inputBuffer, inputPtrs[0], inputPtrs[1], samplesPerChannel);
73
+ }
74
+ else
75
+ {
76
+ dsp::simd::deinterleaveNCh(inputBuffer, inputPtrs.data(), numChannels, samplesPerChannel);
77
+ }
78
+
79
+ for (int ch = 0; ch < numChannels; ++ch)
80
+ {
81
+ for (size_t band = 0; band < numBands; ++band)
82
+ {
83
+ int outChIndex = (ch * static_cast<int>(numBands)) + static_cast<int>(band);
84
+
85
+ m_filters[ch][band]->process(
86
+ m_planarInput[ch].data(),
87
+ m_planarOutput[outChIndex].data(),
88
+ samplesPerChannel);
89
+ }
90
+ }
91
+
92
+ std::vector<const float *> outputPtrs(totalOutputChannels);
93
+ for (size_t i = 0; i < totalOutputChannels; ++i)
94
+ {
95
+ outputPtrs[i] = m_planarOutput[i].data();
96
+ }
97
+
98
+ dsp::simd::interleaveNCh(outputPtrs.data(), outputBuffer, static_cast<int>(totalOutputChannels), samplesPerChannel);
99
+ }
100
+
101
+ void FilterBankStage::initializeFilters()
102
+ {
103
+ m_filters.resize(m_numInputChannels);
104
+ for (int ch = 0; ch < m_numInputChannels; ++ch)
105
+ {
106
+ m_filters[ch].clear();
107
+ for (const auto &def : m_definitions)
108
+ {
109
+ // Convert double coefficients to float for IirFilter
110
+ std::vector<float> b_float(def.b.begin(), def.b.end());
111
+ std::vector<float> a_float(def.a.begin(), def.a.end());
112
+
113
+ // Create stateful IIR filter for each band
114
+ m_filters[ch].push_back(
115
+ std::make_unique<dsp::core::IirFilter<float>>(b_float, a_float, true));
116
+ }
117
+ }
118
+ }
119
+
120
+ void FilterBankStage::ensureScratchSize(size_t samplesPerChannel)
121
+ {
122
+ if (m_planarInput.size() != static_cast<size_t>(m_numInputChannels))
123
+ {
124
+ m_planarInput.resize(m_numInputChannels);
125
+ }
126
+ for (size_t i = 0; i < m_planarInput.size(); ++i)
127
+ {
128
+ if (m_planarInput[i].size() < samplesPerChannel)
129
+ {
130
+ m_planarInput[i].resize(samplesPerChannel, 0.0f);
131
+ }
132
+ }
133
+
134
+ size_t numOut = m_numInputChannels * m_definitions.size();
135
+
136
+ if (m_planarOutput.size() != numOut)
137
+ {
138
+ m_planarOutput.resize(numOut);
139
+ }
140
+ for (size_t i = 0; i < m_planarOutput.size(); ++i)
141
+ {
142
+ if (m_planarOutput[i].size() < samplesPerChannel)
143
+ {
144
+ m_planarOutput[i].resize(samplesPerChannel, 0.0f);
145
+ }
146
+ }
147
+ }
148
+
149
+ void FilterBankStage::reset()
150
+ {
151
+ for (auto &ch : m_filters)
152
+ {
153
+ for (auto &filter : ch)
154
+ {
155
+ filter->reset();
156
+ }
157
+ }
158
+ }
159
+
160
+ Napi::Object FilterBankStage::serializeState(Napi::Env env) const
161
+ {
162
+ Napi::Object state = Napi::Object::New(env);
163
+ state.Set("type", getType());
164
+ state.Set("numInputChannels", m_numInputChannels);
165
+ state.Set("numBands", m_definitions.size());
166
+
167
+ // Serialize filter states as nested arrays: channels[bands[state]]
168
+ Napi::Array channels = Napi::Array::New(env, m_filters.size());
169
+ for (size_t ch = 0; ch < m_filters.size(); ++ch)
170
+ {
171
+ Napi::Array bands = Napi::Array::New(env, m_filters[ch].size());
172
+ for (size_t band = 0; band < m_filters[ch].size(); ++band)
173
+ {
174
+ const auto &filter = m_filters[ch][band];
175
+
176
+ Napi::Object filterState = Napi::Object::New(env);
177
+
178
+ // Get filter state (input and output history)
179
+ auto [inputHistory, outputHistory] = filter->getState();
180
+
181
+ // Serialize input history
182
+ Napi::Array inputArray = Napi::Array::New(env, inputHistory.size());
183
+ for (size_t i = 0; i < inputHistory.size(); ++i)
184
+ {
185
+ inputArray.Set(i, inputHistory[i]);
186
+ }
187
+ filterState.Set("inputHistory", inputArray);
188
+
189
+ // Serialize output history
190
+ Napi::Array outputArray = Napi::Array::New(env, outputHistory.size());
191
+ for (size_t i = 0; i < outputHistory.size(); ++i)
192
+ {
193
+ outputArray.Set(i, outputHistory[i]);
194
+ }
195
+ filterState.Set("outputHistory", outputArray);
196
+
197
+ bands.Set(band, filterState);
198
+ }
199
+ channels.Set(ch, bands);
200
+ }
201
+ state.Set("filterStates", channels);
202
+
203
+ return state;
204
+ }
205
+
206
+ void FilterBankStage::deserializeState(const Napi::Object &state)
207
+ {
208
+ if (!state.Has("filterStates"))
209
+ {
210
+ throw std::runtime_error("FilterBank: Missing filterStates in serialized data");
211
+ }
212
+
213
+ Napi::Array channels = state.Get("filterStates").As<Napi::Array>();
214
+ if (channels.Length() != m_filters.size())
215
+ {
216
+ throw std::runtime_error("FilterBank: Channel count mismatch in deserialization");
217
+ }
218
+
219
+ for (size_t ch = 0; ch < channels.Length(); ++ch)
220
+ {
221
+ Napi::Array bands = channels.Get(ch).As<Napi::Array>();
222
+ if (bands.Length() != m_filters[ch].size())
223
+ {
224
+ throw std::runtime_error("FilterBank: Band count mismatch in deserialization");
225
+ }
226
+
227
+ for (size_t band = 0; band < bands.Length(); ++band)
228
+ {
229
+ Napi::Object filterState = bands.Get(band).As<Napi::Object>();
230
+
231
+ // Deserialize input history
232
+ std::vector<float> inputHistory;
233
+ if (filterState.Has("inputHistory"))
234
+ {
235
+ Napi::Array inputArray = filterState.Get("inputHistory").As<Napi::Array>();
236
+ inputHistory.reserve(inputArray.Length());
237
+ for (size_t i = 0; i < inputArray.Length(); ++i)
238
+ {
239
+ inputHistory.push_back(inputArray.Get(i).As<Napi::Number>().FloatValue());
240
+ }
241
+ }
242
+
243
+ // Deserialize output history
244
+ std::vector<float> outputHistory;
245
+ if (filterState.Has("outputHistory"))
246
+ {
247
+ Napi::Array outputArray = filterState.Get("outputHistory").As<Napi::Array>();
248
+ outputHistory.reserve(outputArray.Length());
249
+ for (size_t i = 0; i < outputArray.Length(); ++i)
250
+ {
251
+ outputHistory.push_back(outputArray.Get(i).As<Napi::Number>().FloatValue());
252
+ }
253
+ }
254
+
255
+ // Restore filter state
256
+ m_filters[ch][band]->setState(inputHistory, outputHistory);
257
+ }
258
+ }
259
+ }
260
+
261
+ void FilterBankStage::serializeToon(toon::Serializer &serializer) const
262
+ {
263
+ // Serialize configuration
264
+ serializer.writeInt32(m_numInputChannels);
265
+ serializer.writeInt32(static_cast<int32_t>(m_definitions.size()));
266
+
267
+ // Serialize each filter's state
268
+ for (const auto &ch : m_filters)
269
+ {
270
+ for (const auto &filter : ch)
271
+ {
272
+ // Get filter state (input and output history)
273
+ auto [inputHistory, outputHistory] = filter->getState();
274
+
275
+ // Write input history
276
+ serializer.writeInt32(static_cast<int32_t>(inputHistory.size()));
277
+ for (float val : inputHistory)
278
+ {
279
+ serializer.writeFloat(val);
280
+ }
281
+
282
+ // Write output history
283
+ serializer.writeInt32(static_cast<int32_t>(outputHistory.size()));
284
+ for (float val : outputHistory)
285
+ {
286
+ serializer.writeFloat(val);
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ void FilterBankStage::deserializeToon(toon::Deserializer &deserializer)
293
+ {
294
+ // Read and validate configuration
295
+ int numChannels = deserializer.readInt32();
296
+ int32_t numBands = deserializer.readInt32();
297
+
298
+ if (numChannels != m_numInputChannels)
299
+ {
300
+ throw std::runtime_error("FilterBank TOON: Channel count mismatch");
301
+ }
302
+ if (static_cast<size_t>(numBands) != m_definitions.size())
303
+ {
304
+ throw std::runtime_error("FilterBank TOON: Band count mismatch");
305
+ }
306
+
307
+ // Restore each filter's state
308
+ for (auto &ch : m_filters)
309
+ {
310
+ for (auto &filter : ch)
311
+ {
312
+ // Read input history
313
+ int32_t inputSize = deserializer.readInt32();
314
+ std::vector<float> inputHistory;
315
+ inputHistory.reserve(inputSize);
316
+ for (int32_t i = 0; i < inputSize; ++i)
317
+ {
318
+ inputHistory.push_back(deserializer.readFloat());
319
+ }
320
+
321
+ // Read output history
322
+ int32_t outputSize = deserializer.readInt32();
323
+ std::vector<float> outputHistory;
324
+ outputHistory.reserve(outputSize);
325
+ for (int32_t i = 0; i < outputSize; ++i)
326
+ {
327
+ outputHistory.push_back(deserializer.readFloat());
328
+ }
329
+
330
+ // Restore filter state
331
+ filter->setState(inputHistory, outputHistory);
332
+ }
333
+ }
334
+ }
335
+
336
+ } // namespace dsp::adapters
@@ -0,0 +1,170 @@
1
+ #pragma once
2
+ #include "../IDspStage.h"
3
+ #include "../core/IirFilter.h"
4
+ #include "../utils/SimdOps.h"
5
+ #include <vector>
6
+ #include <memory>
7
+ #include <stdexcept>
8
+
9
+ namespace dsp::adapters
10
+ {
11
+ /**
12
+ * @brief Lightweight struct to pass filter coefficients from JavaScript
13
+ */
14
+ struct FilterDefinition
15
+ {
16
+ std::vector<double> b; // Feedforward coefficients
17
+ std::vector<double> a; // Feedback coefficients
18
+ };
19
+
20
+ /**
21
+ * @brief Filter Bank Stage - Splits N input channels into N × M sub-bands
22
+ *
23
+ * Architecture:
24
+ * - Input: N channels (interleaved)
25
+ * - Output: N × M channels (interleaved, channel-major layout)
26
+ * - Layout: [ (Ch1_Band1, Ch1_Band2, ...), (Ch2_Band1, Ch2_Band2, ...) ]
27
+ *
28
+ * Optimization Strategy (Planar-Core):
29
+ * 1. De-interleave input to planar scratch buffers (cache-efficient)
30
+ * 2. Process filters on contiguous planar data (SIMD-friendly)
31
+ * 3. Interleave output back to standard format
32
+ *
33
+ * Use Cases:
34
+ * - Speech recognition: Mel-scale filter banks (20-40 bands)
35
+ * - Audio compression: Bark-scale for psychoacoustic analysis
36
+ * - Musical analysis: Octave bands (log scale)
37
+ * - Research: Linear-scale frequency analysis
38
+ *
39
+ * @example
40
+ * // 2 input channels → 20 output channels (10 bands per channel)
41
+ * // Input: [L, R, L, R, ...]
42
+ * // Output: [L_B1, L_B2, ..., L_B10, R_B1, R_B2, ..., R_B10, ...]
43
+ */
44
+ class FilterBankStage : public dsp::IDspStage
45
+ {
46
+ public:
47
+ /**
48
+ * @brief Construct a filter bank stage
49
+ * @param bandDefinitions Array of filter coefficient definitions (one per band)
50
+ * @param numInputChannels Number of input channels
51
+ */
52
+ FilterBankStage(const std::vector<FilterDefinition> &bandDefinitions, int numInputChannels);
53
+
54
+ /**
55
+ * @brief Destructor - ensures proper cleanup of filters and scratch buffers
56
+ */
57
+ virtual ~FilterBankStage();
58
+
59
+ const char *getType() const override { return "filterBank"; }
60
+
61
+ /**
62
+ * @brief This stage expands the buffer size (N channels → N×M channels)
63
+ * @return Always true
64
+ */
65
+ bool isResizing() const override { return true; }
66
+
67
+ /**
68
+ * @brief Returns output channel count = inputChannels × numBands
69
+ * @return Output channel count
70
+ */
71
+ int getOutputChannels() const override
72
+ {
73
+ return m_numInputChannels * static_cast<int>(m_definitions.size());
74
+ }
75
+
76
+ /**
77
+ * @brief Calculate output buffer size for resizing
78
+ * @param inputSize Input buffer size in samples
79
+ * @return Output buffer size in samples
80
+ */
81
+ size_t calculateOutputSize(size_t inputSize) const override
82
+ {
83
+ return (inputSize / m_numInputChannels) * getOutputChannels();
84
+ }
85
+
86
+ /**
87
+ * @brief Standard process() throws because this stage requires resizing
88
+ */
89
+ void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps) override
90
+ {
91
+ throw std::runtime_error("FilterBank requires processResizing");
92
+ }
93
+
94
+ /**
95
+ * @brief Process audio data with buffer resizing
96
+ *
97
+ * Algorithm:
98
+ * 1. De-interleave input to planar scratch buffers
99
+ * 2. Apply each band's filter to each input channel (planar processing)
100
+ * 3. Interleave results to output in channel-major order
101
+ *
102
+ * @param inputBuffer Source interleaved buffer
103
+ * @param inputSize Input size in samples
104
+ * @param outputBuffer Destination buffer (caller must allocate)
105
+ * @param outputSize Reference to store actual output size
106
+ * @param numChannels Number of input channels
107
+ * @param timestamps Optional timestamp array
108
+ */
109
+ void processResizing(const float *inputBuffer, size_t inputSize,
110
+ float *outputBuffer, size_t &outputSize,
111
+ int numChannels, const float *timestamps) override;
112
+
113
+ /**
114
+ * @brief Reset all filter states to initial conditions
115
+ */
116
+ void reset() override;
117
+
118
+ /**
119
+ * @brief Serialize filter bank state to JavaScript object
120
+ * @param env N-API environment
121
+ * @return Napi::Object containing all filter states
122
+ */
123
+ Napi::Object serializeState(Napi::Env env) const override;
124
+
125
+ /**
126
+ * @brief Restore filter bank state from JavaScript object
127
+ * @param state Napi::Object containing serialized filter states
128
+ */
129
+ void deserializeState(const Napi::Object &state) override;
130
+
131
+ /**
132
+ * @brief Serialize state to TOON binary format
133
+ * @param serializer TOON serializer instance
134
+ */
135
+ void serializeToon(toon::Serializer &serializer) const override;
136
+
137
+ /**
138
+ * @brief Deserialize state from TOON binary format
139
+ * @param deserializer TOON deserializer instance
140
+ */
141
+ void deserializeToon(toon::Deserializer &deserializer) override;
142
+
143
+ private:
144
+ /**
145
+ * @brief Initialize the 2D matrix of IIR filters [channel][band]
146
+ */
147
+ void initializeFilters();
148
+
149
+ /**
150
+ * @brief Ensure scratch buffers are allocated and sized correctly
151
+ * @param samplesPerChannel Number of samples per channel to process
152
+ */
153
+ void ensureScratchSize(size_t samplesPerChannel);
154
+
155
+ // Configuration
156
+ int m_numInputChannels;
157
+ std::vector<FilterDefinition> m_definitions;
158
+
159
+ // 2D Matrix of filters: m_filters[channelIndex][bandIndex]
160
+ std::vector<std::vector<std::unique_ptr<dsp::core::IirFilter<float>>>> m_filters;
161
+
162
+ // Optimization: Persistent scratch buffers in planar layout
163
+ // m_planarInput[channel][sample] - de-interleaved input
164
+ std::vector<std::vector<float>> m_planarInput;
165
+
166
+ // m_planarOutput[flatOutputChannelIndex][sample] - filtered planar data
167
+ std::vector<std::vector<float>> m_planarOutput;
168
+ };
169
+
170
+ } // namespace dsp::adapters
@@ -1,5 +1,6 @@
1
1
  #include "FilterStage.h"
2
2
  #include <stdexcept>
3
+ #include <iostream> // For optional debug logging
3
4
 
4
5
  namespace dsp::adapters
5
6
  {
@@ -153,6 +154,22 @@ namespace dsp::adapters
153
154
  Napi::Object state = Napi::Object::New(env);
154
155
  state.Set("filterType", m_isFir ? "fir" : "iir");
155
156
 
157
+ // --- SAVE COEFFICIENTS (New) ---
158
+ Napi::Array bArray = Napi::Array::New(env, m_bCoeffs.size());
159
+ for (size_t i = 0; i < m_bCoeffs.size(); ++i)
160
+ {
161
+ bArray.Set(i, Napi::Number::New(env, m_bCoeffs[i]));
162
+ }
163
+ state.Set("bCoeffs", bArray);
164
+
165
+ Napi::Array aArray = Napi::Array::New(env, m_aCoeffs.size());
166
+ for (size_t i = 0; i < m_aCoeffs.size(); ++i)
167
+ {
168
+ aArray.Set(i, Napi::Number::New(env, m_aCoeffs[i]));
169
+ }
170
+ state.Set("aCoeffs", aArray);
171
+ // --------------------------------
172
+
156
173
  if (m_isFir)
157
174
  {
158
175
  state.Set("numChannels", static_cast<uint32_t>(m_firFilters.size()));
@@ -287,4 +304,125 @@ namespace dsp::adapters
287
304
  }
288
305
  }
289
306
 
290
- } // namespace dsp::adapters
307
+ void FilterStage::serializeToon(dsp::toon::Serializer &s) const
308
+ {
309
+ // Serialize filter type
310
+ s.writeString(m_isFir ? "fir" : "iir");
311
+
312
+ // Serialize coefficients
313
+ s.writeInt32(static_cast<int32_t>(m_bCoeffs.size()));
314
+ for (double coeff : m_bCoeffs)
315
+ {
316
+ s.writeDouble(coeff);
317
+ }
318
+
319
+ s.writeInt32(static_cast<int32_t>(m_aCoeffs.size()));
320
+ for (double coeff : m_aCoeffs)
321
+ {
322
+ s.writeDouble(coeff);
323
+ }
324
+
325
+ // Serialize filter states
326
+ if (m_isFir)
327
+ {
328
+ s.writeInt32(static_cast<int32_t>(m_firFilters.size()));
329
+ for (const auto &filter : m_firFilters)
330
+ {
331
+ if (filter)
332
+ {
333
+ auto [stateBuffer, stateIndex] = filter->getState();
334
+ s.writeFloatArray(stateBuffer);
335
+ s.writeInt32(static_cast<int32_t>(stateIndex));
336
+ }
337
+ else
338
+ {
339
+ s.writeInt32(0); // Empty state buffer size
340
+ s.writeInt32(0); // stateIndex
341
+ }
342
+ }
343
+ }
344
+ else // IIR
345
+ {
346
+ s.writeInt32(static_cast<int32_t>(m_iirFilters.size()));
347
+ for (const auto &filter : m_iirFilters)
348
+ {
349
+ if (filter)
350
+ {
351
+ auto [xState, yState] = filter->getState();
352
+ s.writeFloatArray(xState);
353
+ s.writeFloatArray(yState);
354
+ }
355
+ else
356
+ {
357
+ s.writeInt32(0); // Empty xState size
358
+ s.writeInt32(0); // Empty yState size
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ void FilterStage::deserializeToon(dsp::toon::Deserializer &d)
365
+ {
366
+ // Deserialize filter type
367
+ std::string filterType = d.readString();
368
+ bool isFir = (filterType == "fir");
369
+
370
+ if (isFir != m_isFir)
371
+ {
372
+ throw std::runtime_error("FilterStage TOON load: filter type mismatch");
373
+ }
374
+
375
+ // Deserialize and validate coefficients
376
+ int32_t bSize = d.readInt32();
377
+ if (bSize != static_cast<int32_t>(m_bCoeffs.size()))
378
+ {
379
+ throw std::runtime_error("FilterStage TOON load: bCoeffs size mismatch");
380
+ }
381
+ for (int32_t i = 0; i < bSize; ++i)
382
+ {
383
+ d.readDouble(); // Skip validation for now
384
+ }
385
+
386
+ int32_t aSize = d.readInt32();
387
+ if (aSize != static_cast<int32_t>(m_aCoeffs.size()))
388
+ {
389
+ throw std::runtime_error("FilterStage TOON load: aCoeffs size mismatch");
390
+ }
391
+ for (int32_t i = 0; i < aSize; ++i)
392
+ {
393
+ d.readDouble(); // Skip validation for now
394
+ }
395
+
396
+ // Deserialize filter states
397
+ int32_t numChannels = d.readInt32();
398
+ initializeFilters(numChannels);
399
+
400
+ if (m_isFir)
401
+ {
402
+ for (int32_t i = 0; i < numChannels && i < static_cast<int32_t>(m_firFilters.size()); ++i)
403
+ {
404
+ std::vector<float> stateBuffer = d.readFloatArray();
405
+ int32_t stateIndex = d.readInt32();
406
+
407
+ if (m_firFilters[i] && !stateBuffer.empty())
408
+ {
409
+ m_firFilters[i]->setState(stateBuffer, static_cast<size_t>(stateIndex));
410
+ }
411
+ }
412
+ }
413
+ else // IIR
414
+ {
415
+ for (int32_t i = 0; i < numChannels && i < static_cast<int32_t>(m_iirFilters.size()); ++i)
416
+ {
417
+ std::vector<float> xState = d.readFloatArray();
418
+ std::vector<float> yState = d.readFloatArray();
419
+
420
+ if (m_iirFilters[i] && !xState.empty())
421
+ {
422
+ m_iirFilters[i]->setState(xState, yState);
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ } // namespace dsp::adapters
@@ -3,6 +3,7 @@
3
3
  #include "../IDspStage.h"
4
4
  #include "../core/IirFilter.h"
5
5
  #include "../core/FirFilter.h"
6
+ #include "../utils/Toon.h"
6
7
  #include <memory>
7
8
  #include <vector>
8
9
  #include <string>
@@ -23,6 +24,9 @@ namespace dsp::adapters
23
24
  Napi::Object serializeState(Napi::Env env) const override;
24
25
  void deserializeState(const Napi::Object &state) override;
25
26
 
27
+ void serializeToon(dsp::toon::Serializer &serializer) const override;
28
+ void deserializeToon(dsp::toon::Deserializer &deserializer) override;
29
+
26
30
  private:
27
31
  void initializeFilters(int numChannels);
28
32