dspx 1.2.4 → 1.3.1

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 (74) 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 +270 -17
  16. package/dist/bindings.d.ts.map +1 -1
  17. package/dist/bindings.js +566 -43
  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 +699 -126
  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/AmplifyStage.h +148 -0
  38. package/src/native/adapters/ClipDetectionStage.h +15 -4
  39. package/src/native/adapters/ConvolutionStage.h +101 -0
  40. package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
  41. package/src/native/adapters/DecimatorStage.h +80 -0
  42. package/src/native/adapters/DifferentiatorStage.h +13 -0
  43. package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
  44. package/src/native/adapters/FilterBankStage.cc +336 -0
  45. package/src/native/adapters/FilterBankStage.h +170 -0
  46. package/src/native/adapters/FilterStage.cc +122 -0
  47. package/src/native/adapters/FilterStage.h +4 -0
  48. package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
  49. package/src/native/adapters/IntegratorStage.h +15 -0
  50. package/src/native/adapters/InterpolatorStage.h +51 -0
  51. package/src/native/adapters/LinearRegressionStage.h +40 -0
  52. package/src/native/adapters/LmsStage.h +63 -0
  53. package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
  54. package/src/native/adapters/MovingAverageStage.h +119 -0
  55. package/src/native/adapters/PeakDetectionStage.h +53 -0
  56. package/src/native/adapters/RectifyStage.h +14 -0
  57. package/src/native/adapters/ResamplerStage.h +67 -0
  58. package/src/native/adapters/RlsStage.h +76 -0
  59. package/src/native/adapters/RmsStage.h +72 -0
  60. package/src/native/adapters/SnrStage.h +45 -0
  61. package/src/native/adapters/SquareStage.h +78 -0
  62. package/src/native/adapters/SscStage.h +65 -0
  63. package/src/native/adapters/StftStage.h +62 -0
  64. package/src/native/adapters/VarianceStage.h +59 -0
  65. package/src/native/adapters/WampStage.h +59 -0
  66. package/src/native/adapters/WaveformLengthStage.h +51 -0
  67. package/src/native/adapters/ZScoreNormalizeStage.h +64 -0
  68. package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
  69. package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
  70. package/src/native/core/FilterBankDesign.h +266 -0
  71. package/src/native/core/Policies.h +124 -0
  72. package/src/native/utils/CircularBufferArray.cc +2 -1
  73. package/src/native/utils/SimdOps.h +67 -0
  74. package/src/native/utils/Toon.h +195 -0
@@ -4,6 +4,7 @@
4
4
  #include <memory>
5
5
  #include <unordered_map>
6
6
  #include <functional>
7
+ #include <atomic>
7
8
  #include "IDspStage.h"
8
9
 
9
10
  namespace dsp
@@ -23,6 +24,12 @@ namespace dsp
23
24
  // This is the async "process" method called by the TS processor
24
25
  Napi::Value ProcessAsync(const Napi::CallbackInfo &info);
25
26
 
27
+ // The Synchronous method for JS workers/clusters
28
+ Napi::Value ProcessSync(const Napi::CallbackInfo &info);
29
+
30
+ // Dispose method for explicit teardown
31
+ Napi::Value Dispose(const Napi::CallbackInfo &info);
32
+
26
33
  // State management methods (for Redis persistence from TypeScript)
27
34
  Napi::Value SaveState(const Napi::CallbackInfo &info);
28
35
  Napi::Value LoadState(const Napi::CallbackInfo &info);
@@ -40,6 +47,12 @@ namespace dsp
40
47
 
41
48
  // This is the "pipeline": a vector of our abstract filter stages
42
49
  std::vector<std::unique_ptr<IDspStage>> m_stages;
50
+
51
+ // uses shared_ptr so the lock survices if the JS object is GC'd early
52
+ std::shared_ptr<std::atomic<bool>> m_isBusy;
53
+
54
+ // Flag to track if pipeline has been disposed
55
+ bool m_disposed = false;
43
56
  };
44
57
 
45
58
  } // namespace dsp
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Filter Bank Design Bindings
3
+ *
4
+ * N-API bindings for filter bank design utilities.
5
+ * Exposes stateless filter bank generation to TypeScript.
6
+ */
7
+
8
+ #include <napi.h>
9
+ #include "core/FilterBankDesign.h"
10
+
11
+ namespace dsp
12
+ {
13
+ /**
14
+ * Design a filter bank (N-API binding)
15
+ *
16
+ * @param info[0] - Options object with:
17
+ * - scale: 'linear' | 'log' | 'mel' | 'bark'
18
+ * - type: 'butterworth' | 'chebyshev'
19
+ * - count: number of bands
20
+ * - sampleRate: sample rate in Hz
21
+ * - frequencyRange: [minFreq, maxFreq]
22
+ * - order: filter order (default: 2)
23
+ * - rippleDb: Chebyshev ripple (default: 0.5)
24
+ *
25
+ * @return Array of { b: number[], a: number[] } coefficient objects
26
+ */
27
+ Napi::Value DesignFilterBank(const Napi::CallbackInfo &info)
28
+ {
29
+ Napi::Env env = info.Env();
30
+
31
+ // Validate input
32
+ if (info.Length() < 1 || !info[0].IsObject())
33
+ {
34
+ Napi::TypeError::New(env, "Options object required")
35
+ .ThrowAsJavaScriptException();
36
+ return env.Null();
37
+ }
38
+
39
+ Napi::Object options = info[0].As<Napi::Object>();
40
+
41
+ try
42
+ {
43
+ // Parse options
44
+ core::FilterBankDesign::DesignOptions opts;
45
+
46
+ // Parse scale
47
+ if (!options.Has("scale") || !options.Get("scale").IsString())
48
+ {
49
+ throw std::invalid_argument("scale (string) is required");
50
+ }
51
+ std::string scaleStr = options.Get("scale").As<Napi::String>().Utf8Value();
52
+
53
+ if (scaleStr == "mel")
54
+ {
55
+ opts.scale = core::FilterBankDesign::Scale::Mel;
56
+ }
57
+ else if (scaleStr == "bark")
58
+ {
59
+ opts.scale = core::FilterBankDesign::Scale::Bark;
60
+ }
61
+ else if (scaleStr == "log")
62
+ {
63
+ opts.scale = core::FilterBankDesign::Scale::Log;
64
+ }
65
+ else if (scaleStr == "linear")
66
+ {
67
+ opts.scale = core::FilterBankDesign::Scale::Linear;
68
+ }
69
+ else
70
+ {
71
+ throw std::invalid_argument("Invalid scale: must be 'linear', 'log', 'mel', or 'bark'");
72
+ }
73
+
74
+ // Parse type (optional, defaults to butterworth)
75
+ opts.type = core::FilterBankDesign::Type::Butterworth;
76
+ if (options.Has("type") && options.Get("type").IsString())
77
+ {
78
+ std::string typeStr = options.Get("type").As<Napi::String>().Utf8Value();
79
+ if (typeStr == "chebyshev" || typeStr == "chebyshev1")
80
+ {
81
+ opts.type = core::FilterBankDesign::Type::Chebyshev1;
82
+ }
83
+ else if (typeStr != "butterworth")
84
+ {
85
+ throw std::invalid_argument("Invalid type: must be 'butterworth' or 'chebyshev'");
86
+ }
87
+ }
88
+
89
+ // Parse count
90
+ if (!options.Has("count") || !options.Get("count").IsNumber())
91
+ {
92
+ throw std::invalid_argument("count (number) is required");
93
+ }
94
+ opts.count = options.Get("count").As<Napi::Number>().Int32Value();
95
+
96
+ // Parse sampleRate
97
+ if (!options.Has("sampleRate") || !options.Get("sampleRate").IsNumber())
98
+ {
99
+ throw std::invalid_argument("sampleRate (number) is required");
100
+ }
101
+ opts.sampleRate = options.Get("sampleRate").As<Napi::Number>().DoubleValue();
102
+
103
+ // Parse frequencyRange
104
+ if (!options.Has("frequencyRange") || !options.Get("frequencyRange").IsArray())
105
+ {
106
+ throw std::invalid_argument("frequencyRange ([min, max]) is required");
107
+ }
108
+ Napi::Array range = options.Get("frequencyRange").As<Napi::Array>();
109
+ if (range.Length() < 2)
110
+ {
111
+ throw std::invalid_argument("frequencyRange must have at least 2 elements");
112
+ }
113
+ opts.minFreq = range.Get((uint32_t)0).As<Napi::Number>().DoubleValue();
114
+ opts.maxFreq = range.Get((uint32_t)1).As<Napi::Number>().DoubleValue();
115
+
116
+ // Parse order (optional, defaults to 2)
117
+ opts.order = 2;
118
+ if (options.Has("order") && options.Get("order").IsNumber())
119
+ {
120
+ opts.order = options.Get("order").As<Napi::Number>().Int32Value();
121
+ }
122
+
123
+ // Parse rippleDb (optional, defaults to 0.5)
124
+ opts.rippleDb = 0.5;
125
+ if (options.Has("rippleDb") && options.Get("rippleDb").IsNumber())
126
+ {
127
+ opts.rippleDb = options.Get("rippleDb").As<Napi::Number>().DoubleValue();
128
+ }
129
+
130
+ // Design the filter bank
131
+ auto bank = core::FilterBankDesign::design(opts);
132
+
133
+ // Convert to JavaScript array
134
+ Napi::Array result = Napi::Array::New(env, bank.size());
135
+
136
+ for (size_t i = 0; i < bank.size(); ++i)
137
+ {
138
+ Napi::Object coeffs = Napi::Object::New(env);
139
+
140
+ // Convert b coefficients
141
+ Napi::Array b = Napi::Array::New(env, bank[i].b.size());
142
+ for (size_t j = 0; j < bank[i].b.size(); ++j)
143
+ {
144
+ b.Set(j, Napi::Number::New(env, bank[i].b[j]));
145
+ }
146
+ coeffs.Set("b", b);
147
+
148
+ // Convert a coefficients
149
+ Napi::Array a = Napi::Array::New(env, bank[i].a.size());
150
+ for (size_t j = 0; j < bank[i].a.size(); ++j)
151
+ {
152
+ a.Set(j, Napi::Number::New(env, bank[i].a[j]));
153
+ }
154
+ coeffs.Set("a", a);
155
+
156
+ result.Set(i, coeffs);
157
+ }
158
+
159
+ return result;
160
+ }
161
+ catch (const std::exception &e)
162
+ {
163
+ Napi::Error::New(env, std::string("Filter bank design failed: ") + e.what())
164
+ .ThrowAsJavaScriptException();
165
+ return env.Null();
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Get filter bank frequency boundaries (N-API binding)
171
+ *
172
+ * Useful for visualization and debugging
173
+ *
174
+ * @param info[0] - Same options object as designFilterBank
175
+ * @return Array of boundary frequencies in Hz
176
+ */
177
+ Napi::Value GetFilterBankBoundaries(const Napi::CallbackInfo &info)
178
+ {
179
+ Napi::Env env = info.Env();
180
+
181
+ if (info.Length() < 1 || !info[0].IsObject())
182
+ {
183
+ Napi::TypeError::New(env, "Options object required")
184
+ .ThrowAsJavaScriptException();
185
+ return env.Null();
186
+ }
187
+
188
+ Napi::Object options = info[0].As<Napi::Object>();
189
+
190
+ try
191
+ {
192
+ // Parse options (same as DesignFilterBank)
193
+ core::FilterBankDesign::DesignOptions opts;
194
+
195
+ std::string scaleStr = options.Get("scale").As<Napi::String>().Utf8Value();
196
+ if (scaleStr == "mel")
197
+ opts.scale = core::FilterBankDesign::Scale::Mel;
198
+ else if (scaleStr == "bark")
199
+ opts.scale = core::FilterBankDesign::Scale::Bark;
200
+ else if (scaleStr == "log")
201
+ opts.scale = core::FilterBankDesign::Scale::Log;
202
+ else
203
+ opts.scale = core::FilterBankDesign::Scale::Linear;
204
+
205
+ opts.count = options.Get("count").As<Napi::Number>().Int32Value();
206
+
207
+ Napi::Array range = options.Get("frequencyRange").As<Napi::Array>();
208
+ opts.minFreq = range.Get((uint32_t)0).As<Napi::Number>().DoubleValue();
209
+ opts.maxFreq = range.Get((uint32_t)1).As<Napi::Number>().DoubleValue();
210
+
211
+ // Get boundaries
212
+ auto boundaries = core::FilterBankDesign::getBoundaries(opts);
213
+
214
+ // Convert to JavaScript array
215
+ Napi::Array result = Napi::Array::New(env, boundaries.size());
216
+ for (size_t i = 0; i < boundaries.size(); ++i)
217
+ {
218
+ result.Set(i, Napi::Number::New(env, boundaries[i]));
219
+ }
220
+
221
+ return result;
222
+ }
223
+ catch (const std::exception &e)
224
+ {
225
+ Napi::Error::New(env, std::string("Get boundaries failed: ") + e.what())
226
+ .ThrowAsJavaScriptException();
227
+ return env.Null();
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Register filter bank design bindings
233
+ * Call this from your main addon Init function
234
+ */
235
+ void RegisterFilterBankDesignBindings(Napi::Env env, Napi::Object exports)
236
+ {
237
+ exports.Set("designFilterBank", Napi::Function::New(env, DesignFilterBank));
238
+ exports.Set("getFilterBankBoundaries", Napi::Function::New(env, GetFilterBankBoundaries));
239
+ }
240
+
241
+ } // namespace dsp
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
  #include <napi.h>
3
3
  #include <cstring> // for std::memcpy
4
+ #include "utils/Toon.h"
4
5
 
5
6
  namespace dsp
6
7
  {
@@ -98,6 +99,29 @@ namespace dsp
98
99
  */
99
100
  virtual void deserializeState(const Napi::Object &state) = 0;
100
101
 
102
+ /**
103
+ * @brief Serializes internal state to the efficient TOON binary format.
104
+ * Default implementation throws, forcing derived classes to implement it
105
+ * if TOON support is required.
106
+ *
107
+ * @param serializer The TOON serializer to write binary data to.
108
+ */
109
+ virtual void serializeToon(toon::Serializer &serializer) const
110
+ {
111
+ // Fallback: throw if not implemented for a specific stage
112
+ throw std::runtime_error("TOON serialization not implemented for this stage type");
113
+ }
114
+
115
+ /**
116
+ * @brief Deserializes internal state from TOON binary format.
117
+ *
118
+ * @param deserializer The TOON deserializer to read binary data from.
119
+ */
120
+ virtual void deserializeToon(toon::Deserializer &deserializer)
121
+ {
122
+ throw std::runtime_error("TOON deserialization not implemented for this stage type");
123
+ }
124
+
101
125
  /**
102
126
  * @brief Resets the stage's internal state to initial values.
103
127
  */
@@ -203,6 +203,134 @@ namespace dsp
203
203
  return output;
204
204
  }
205
205
 
206
+ /**
207
+ * @brief Interleaves an array of planar Float32Arrays into a single Float32Array.
208
+ *
209
+ * Takes an array of Float32Arrays (one per channel) and combines them into a
210
+ * single interleaved buffer.
211
+ *
212
+ * @param info[0] - An array of Float32Array objects.
213
+ * @returns A new Float32Array containing the interleaved data.
214
+ */
215
+ Napi::Value Interleave(const Napi::CallbackInfo &info)
216
+ {
217
+ Napi::Env env = info.Env();
218
+
219
+ if (info.Length() < 1 || !info[0].IsArray())
220
+ {
221
+ Napi::TypeError::New(env, "Expected an array of Float32Arrays").ThrowAsJavaScriptException();
222
+ return env.Null();
223
+ }
224
+
225
+ Napi::Array planarArray = info[0].As<Napi::Array>();
226
+ uint32_t numChannels = planarArray.Length();
227
+
228
+ if (numChannels == 0)
229
+ {
230
+ return Napi::Float32Array::New(env, 0);
231
+ }
232
+
233
+ std::vector<Napi::Float32Array> channels;
234
+ size_t channelLength = 0;
235
+
236
+ for (uint32_t i = 0; i < numChannels; ++i)
237
+ {
238
+ Napi::Value val = planarArray.Get(i);
239
+ if (!val.IsTypedArray() || val.As<Napi::TypedArray>().TypedArrayType() != napi_float32_array)
240
+ {
241
+ Napi::TypeError::New(env, "All elements in the array must be Float32Arrays").ThrowAsJavaScriptException();
242
+ return env.Null();
243
+ }
244
+ Napi::Float32Array channel = val.As<Napi::Float32Array>();
245
+ if (i == 0)
246
+ {
247
+ channelLength = channel.ElementLength();
248
+ }
249
+ else if (channel.ElementLength() != channelLength)
250
+ {
251
+ Napi::TypeError::New(env, "All channels must have the same length").ThrowAsJavaScriptException();
252
+ return env.Null();
253
+ }
254
+ channels.push_back(channel);
255
+ }
256
+
257
+ size_t interleavedLength = numChannels * channelLength;
258
+ Napi::Float32Array interleaved = Napi::Float32Array::New(env, interleavedLength);
259
+ float *outputData = interleaved.Data();
260
+
261
+ for (size_t i = 0; i < channelLength; ++i)
262
+ {
263
+ for (uint32_t c = 0; c < numChannels; ++c)
264
+ {
265
+ outputData[i * numChannels + c] = channels[c].Data()[i];
266
+ }
267
+ }
268
+
269
+ return interleaved;
270
+ }
271
+
272
+ /**
273
+ * @brief Deinterleaves a single Float32Array into an array of planar Float32Arrays.
274
+ *
275
+ * Takes an interleaved Float32Array and a channel count, and separates them
276
+ * into an array of Float32Arrays (one per channel).
277
+ *
278
+ * @param info[0] - The interleaved Float32Array.
279
+ * @param info[1] - The number of channels.
280
+ * @returns An array of new Float32Arrays, one for each channel.
281
+ */
282
+ Napi::Value Deinterleave(const Napi::CallbackInfo &info)
283
+ {
284
+ Napi::Env env = info.Env();
285
+
286
+ if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsNumber())
287
+ {
288
+ Napi::TypeError::New(env, "Expected an interleaved Float32Array and a channel count").ThrowAsJavaScriptException();
289
+ return env.Null();
290
+ }
291
+
292
+ Napi::TypedArray typedArray = info[0].As<Napi::TypedArray>();
293
+ if (typedArray.TypedArrayType() != napi_float32_array)
294
+ {
295
+ Napi::TypeError::New(env, "Input must be a Float32Array").ThrowAsJavaScriptException();
296
+ return env.Null();
297
+ }
298
+
299
+ Napi::Float32Array interleaved = typedArray.As<Napi::Float32Array>();
300
+ int numChannels = info[1].As<Napi::Number>().Int32Value();
301
+
302
+ if (numChannels <= 0)
303
+ {
304
+ Napi::TypeError::New(env, "Number of channels must be positive").ThrowAsJavaScriptException();
305
+ return env.Null();
306
+ }
307
+
308
+ size_t interleavedLength = interleaved.ElementLength();
309
+ if (interleavedLength % numChannels != 0)
310
+ {
311
+ Napi::RangeError::New(env, "Interleaved length is not divisible by the number of channels").ThrowAsJavaScriptException();
312
+ return env.Null();
313
+ }
314
+
315
+ size_t channelLength = interleavedLength / numChannels;
316
+ const float *inputData = interleaved.Data();
317
+
318
+ Napi::Array planarArray = Napi::Array::New(env, numChannels);
319
+
320
+ for (int c = 0; c < numChannels; ++c)
321
+ {
322
+ Napi::Float32Array channel = Napi::Float32Array::New(env, channelLength);
323
+ float *channelData = channel.Data();
324
+ for (size_t i = 0; i < channelLength; ++i)
325
+ {
326
+ channelData[i] = inputData[i * numChannels + c];
327
+ }
328
+ planarArray.Set(c, channel);
329
+ }
330
+
331
+ return planarArray;
332
+ }
333
+
206
334
  /**
207
335
  * @brief Initialize utility function bindings
208
336
  */
@@ -210,6 +338,8 @@ namespace dsp
210
338
  {
211
339
  exports.Set("dotProduct", Napi::Function::New(env, DotProduct));
212
340
  exports.Set("detrend", Napi::Function::New(env, Detrend));
341
+ exports.Set("interleave", Napi::Function::New(env, Interleave));
342
+ exports.Set("deinterleave", Napi::Function::New(env, Deinterleave));
213
343
  return exports;
214
344
  }
215
345
 
@@ -0,0 +1,148 @@
1
+ #ifndef DSP_ADAPTERS_AMPLIFYSTAGE_H
2
+ #define DSP_ADAPTERS_AMPLIFYSTAGE_H
3
+
4
+ #include "IDspStage.h"
5
+ #include "../utils/SimdOps.h"
6
+ #include <cstring>
7
+
8
+ // Platform detection for SIMD
9
+ #if defined(__AVX2__)
10
+ #include <immintrin.h>
11
+ #elif defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
12
+ #include <emmintrin.h>
13
+ #elif defined(__ARM_NEON) || defined(__aarch64__)
14
+ #include <arm_neon.h>
15
+ #endif
16
+
17
+ namespace dsp
18
+ {
19
+ namespace adapters
20
+ {
21
+ /**
22
+ * Amplify (Gain) Stage
23
+ * Multiplies all samples by a constant gain factor.
24
+ * SIMD-optimized for maximum throughput (AVX2/SSE2/NEON).
25
+ * Useful for scaling signals to appropriate amplitude ranges.
26
+ */
27
+ class AmplifyStage : public IDspStage
28
+ {
29
+ public:
30
+ explicit AmplifyStage(float gain = 1.0f)
31
+ : m_gain(gain)
32
+ {
33
+ }
34
+
35
+ void process(float *data, size_t numSamples, int channels, const float *timestamps = nullptr) override
36
+ {
37
+ // SIMD-optimized gain multiplication
38
+ #if defined(__AVX2__)
39
+ // AVX2: Process 8 floats at a time
40
+ const size_t simd_width = 8;
41
+ const size_t simd_count = numSamples / simd_width;
42
+ const size_t simd_end = simd_count * simd_width;
43
+
44
+ const __m256 gain_vec = _mm256_set1_ps(m_gain);
45
+
46
+ for (size_t i = 0; i < simd_end; i += simd_width)
47
+ {
48
+ __m256 samples = _mm256_loadu_ps(&data[i]);
49
+ samples = _mm256_mul_ps(samples, gain_vec);
50
+ _mm256_storeu_ps(&data[i], samples);
51
+ }
52
+
53
+ // Handle remainder
54
+ for (size_t i = simd_end; i < numSamples; ++i)
55
+ {
56
+ data[i] *= m_gain;
57
+ }
58
+
59
+ #elif defined(__SSE2__) || defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
60
+ // SSE2: Process 4 floats at a time
61
+ const size_t simd_width = 4;
62
+ const size_t simd_count = numSamples / simd_width;
63
+ const size_t simd_end = simd_count * simd_width;
64
+
65
+ const __m128 gain_vec = _mm_set1_ps(m_gain);
66
+
67
+ for (size_t i = 0; i < simd_end; i += simd_width)
68
+ {
69
+ __m128 samples = _mm_loadu_ps(&data[i]);
70
+ samples = _mm_mul_ps(samples, gain_vec);
71
+ _mm_storeu_ps(&data[i], samples);
72
+ }
73
+
74
+ // Handle remainder
75
+ for (size_t i = simd_end; i < numSamples; ++i)
76
+ {
77
+ data[i] *= m_gain;
78
+ }
79
+
80
+ #elif defined(__ARM_NEON) || defined(__aarch64__)
81
+ // ARM NEON: Process 4 floats at a time
82
+ const size_t simd_width = 4;
83
+ const size_t simd_count = numSamples / simd_width;
84
+ const size_t simd_end = simd_count * simd_width;
85
+
86
+ const float32x4_t gain_vec = vdupq_n_f32(m_gain);
87
+
88
+ for (size_t i = 0; i < simd_end; i += simd_width)
89
+ {
90
+ float32x4_t samples = vld1q_f32(&data[i]);
91
+ samples = vmulq_f32(samples, gain_vec);
92
+ vst1q_f32(&data[i], samples);
93
+ }
94
+
95
+ // Handle remainder
96
+ for (size_t i = simd_end; i < numSamples; ++i)
97
+ {
98
+ data[i] *= m_gain;
99
+ }
100
+
101
+ #else
102
+ // Scalar fallback (compiler may auto-vectorize)
103
+ for (size_t i = 0; i < numSamples; ++i)
104
+ {
105
+ data[i] *= m_gain;
106
+ }
107
+ #endif
108
+ }
109
+
110
+ const char *getType() const override
111
+ {
112
+ return "amplify";
113
+ }
114
+
115
+ Napi::Object serializeState(Napi::Env env) const override
116
+ {
117
+ // Stateless - return empty object
118
+ return Napi::Object::New(env);
119
+ }
120
+
121
+ void deserializeState(const Napi::Object &state) override
122
+ {
123
+ // Stateless - nothing to restore
124
+ }
125
+
126
+ // Serialization
127
+ void serializeToon(dsp::toon::Serializer &s) const override
128
+ {
129
+ // Stateless - no data to serialize
130
+ }
131
+
132
+ void deserializeToon(dsp::toon::Deserializer &d) override
133
+ {
134
+ // Stateless - no data to deserialize
135
+ }
136
+
137
+ void reset() override
138
+ {
139
+ // Stateless - no internal state to reset
140
+ }
141
+
142
+ private:
143
+ float m_gain;
144
+ };
145
+ } // namespace adapters
146
+ } // namespace dsp
147
+
148
+ #endif // DSP_ADAPTERS_AMPLIFYSTAGE_H
@@ -1,6 +1,7 @@
1
1
  #pragma once
2
2
 
3
3
  #include "../IDspStage.h"
4
+ #include "../utils/Toon.h"
4
5
  #include <cmath>
5
6
  #include <stdexcept>
6
7
  #include <string>
@@ -9,19 +10,19 @@ namespace dsp::adapters
9
10
  {
10
11
  /**
11
12
  * @brief Clip Detection Stage - Detects when samples exceed a threshold.
12
- *
13
+ *
13
14
  * This stage analyzes the input signal and outputs a binary (0.0 or 1.0) indicator
14
15
  * showing where clipping occurs. This is useful for:
15
16
  * - Audio clipping detection (overload prevention)
16
17
  * - Saturation detection in ADC signals
17
18
  * - Quality control in data acquisition
18
- *
19
+ *
19
20
  * **Output:**
20
21
  * - 1.0 when |sample| >= threshold (clipping detected)
21
22
  * - 0.0 when |sample| < threshold (no clipping)
22
- *
23
+ *
23
24
  * **Processing:** Stateless, processes each sample independently
24
- *
25
+ *
25
26
  * @example
26
27
  * ```cpp
27
28
  * // Detect audio clipping at 0.95 (5% headroom)
@@ -77,6 +78,16 @@ namespace dsp::adapters
77
78
  }
78
79
  }
79
80
 
81
+ inline void serializeToon(dsp::toon::Serializer &s) const override
82
+ {
83
+ s.writeFloat(m_threshold);
84
+ }
85
+
86
+ inline void deserializeToon(dsp::toon::Deserializer &d) override
87
+ {
88
+ m_threshold = d.readFloat();
89
+ }
90
+
80
91
  void reset() override
81
92
  {
82
93
  // No state to reset (stateless stage)