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
@@ -1,37 +1,42 @@
1
1
  #include "DspPipeline.h"
2
- #include "adapters/MovingAverageStage.h" // Moving Average method
3
- #include "adapters/RmsStage.h" // RMS method
4
- #include "adapters/RectifyStage.h" // Rectify method
5
- #include "adapters/VarianceStage.h" // Variance method
6
- #include "adapters/ZScoreNormalizeStage.h" // Z-Score Normalize method
7
- #include "adapters/MeanAbsoluteValueStage.h" // Mean Absolute Value method
8
- #include "adapters/WaveformLengthStage.h" // Waveform Length method
9
- #include "adapters/SscStage.h" // Slope Sign Change method
10
- #include "adapters/WampStage.h" // Willison Amplitude method
11
- #include "adapters/FilterStage.h" // Filter stage (FIR/IIR)
12
- #include "adapters/InterpolatorStage.h" // Interpolator (upsample)
13
- #include "adapters/DecimatorStage.h" // Decimator (downsample)
14
- #include "adapters/ResamplerStage.h" // Resampler (rational rate conversion)
15
- #include "adapters/ConvolutionStage.h" // Convolution stage
16
- #include "adapters/LinearRegressionStage.h" // Linear Regression stage
17
- #include "adapters/LmsStage.h" // LMS Adaptive Filter stage
18
- #include "adapters/RlsStage.h" // RLS Adaptive Filter stage
19
- #include "adapters/WaveletTransformStage.h" // Wavelet Transform stage
20
- #include "adapters/HilbertEnvelopeStage.h" // Hilbert Envelope stage
21
- #include "adapters/StftStage.h" // STFT (Short-Time Fourier Transform) stage
22
- #include "adapters/FftStage.h" // FFT (Fast Fourier Transform) stage
23
- #include "adapters/MelSpectrogramStage.h" // Mel Spectrogram stage
24
- #include "adapters/MfccStage.h" // MFCC (Mel-Frequency Cepstral Coefficients) stage
25
- #include "adapters/MatrixTransformStage.h" // Matrix Transform stage (PCA/ICA/Whitening)
26
- #include "adapters/GscPreprocessorStage.h" // GSC Preprocessor for adaptive beamforming
27
- #include "adapters/ChannelSelectorStage.h" // Channel selector for reducing channel count
28
- #include "adapters/ChannelSelectStage.h" // Channel selector by indices (select/reorder)
29
- #include "adapters/ChannelMergeStage.h" // Channel merger/duplicator (merge/expand)
30
- #include "adapters/ClipDetectionStage.h" // Clip detection stage
31
- #include "adapters/PeakDetectionStage.h" // Peak detection stage
32
- #include "adapters/DifferentiatorStage.h" // Differentiator stage
33
- #include "adapters/IntegratorStage.h" // Integrator stage
34
- #include "adapters/SnrStage.h" // SNR stage
2
+ #include "adapters/MovingAverageStage.h" // Moving Average method
3
+ #include "adapters/ExponentialMovingAverageStage.h" // Exponential Moving Average method
4
+ #include "adapters/CumulativeMovingAverageStage.h" // Cumulative Moving Average method
5
+ #include "adapters/RmsStage.h" // RMS method
6
+ #include "adapters/RectifyStage.h" // Rectify method
7
+ #include "adapters/VarianceStage.h" // Variance method
8
+ #include "adapters/ZScoreNormalizeStage.h" // Z-Score Normalize method
9
+ #include "adapters/MeanAbsoluteValueStage.h" // Mean Absolute Value method
10
+ #include "adapters/WaveformLengthStage.h" // Waveform Length method
11
+ #include "adapters/SscStage.h" // Slope Sign Change method
12
+ #include "adapters/WampStage.h" // Willison Amplitude method
13
+ #include "adapters/FilterStage.h" // Filter stage (FIR/IIR)
14
+ #include "adapters/InterpolatorStage.h" // Interpolator (upsample)
15
+ #include "adapters/DecimatorStage.h" // Decimator (downsample)
16
+ #include "adapters/ResamplerStage.h" // Resampler (rational rate conversion)
17
+ #include "adapters/ConvolutionStage.h" // Convolution stage
18
+ #include "adapters/LinearRegressionStage.h" // Linear Regression stage
19
+ #include "adapters/LmsStage.h" // LMS Adaptive Filter stage
20
+ #include "adapters/RlsStage.h" // RLS Adaptive Filter stage
21
+ #include "adapters/WaveletTransformStage.h" // Wavelet Transform stage
22
+ #include "adapters/HilbertEnvelopeStage.h" // Hilbert Envelope stage
23
+ #include "adapters/StftStage.h" // STFT (Short-Time Fourier Transform) stage
24
+ #include "adapters/FftStage.h" // FFT (Fast Fourier Transform) stage
25
+ #include "adapters/MelSpectrogramStage.h" // Mel Spectrogram stage
26
+ #include "adapters/MfccStage.h" // MFCC (Mel-Frequency Cepstral Coefficients) stage
27
+ #include "adapters/MatrixTransformStage.h" // Matrix Transform stage (PCA/ICA/Whitening)
28
+ #include "adapters/GscPreprocessorStage.h" // GSC Preprocessor for adaptive beamforming
29
+ #include "adapters/ChannelSelectorStage.h" // Channel selector for reducing channel count
30
+ #include "adapters/ChannelSelectStage.h" // Channel selector by indices (select/reorder)
31
+ #include "adapters/ChannelMergeStage.h" // Channel merger/duplicator (merge/expand)
32
+ #include "adapters/FilterBankStage.h" // Filter Bank stage (split channels into frequency bands)
33
+ #include "adapters/ClipDetectionStage.h" // Clip detection stage
34
+ #include "adapters/PeakDetectionStage.h" // Peak detection stage
35
+ #include "adapters/DifferentiatorStage.h" // Differentiator stage
36
+ #include "adapters/SquareStage.h" // Square stage
37
+ #include "adapters/AmplifyStage.h" // Amplify (Gain) stage
38
+ #include "adapters/IntegratorStage.h" // Integrator stage
39
+ #include "adapters/SnrStage.h" // SNR stage
35
40
 
36
41
  namespace dsp
37
42
  {
@@ -42,6 +47,8 @@ namespace dsp
42
47
 
43
48
  #include <iostream>
44
49
  #include <ctime>
50
+ #include <cstdlib>
51
+ #include "utils/Toon.h"
45
52
 
46
53
  namespace dsp
47
54
  {
@@ -56,12 +63,16 @@ namespace dsp
56
63
 
57
64
  // Processing
58
65
  InstanceMethod("process", &DspPipeline::ProcessAsync),
66
+ InstanceMethod("processSync", &DspPipeline::ProcessSync),
59
67
 
60
68
  // State management (for Redis persistence from TypeScript)
61
69
  InstanceMethod("saveState", &DspPipeline::SaveState),
62
70
  InstanceMethod("loadState", &DspPipeline::LoadState),
63
71
  InstanceMethod("clearState", &DspPipeline::ClearState),
64
72
  InstanceMethod("listState", &DspPipeline::ListState),
73
+
74
+ // Lifecycle management
75
+ InstanceMethod("dispose", &DspPipeline::Dispose),
65
76
  });
66
77
 
67
78
  exports.Set("DspPipeline", func);
@@ -72,7 +83,8 @@ namespace dsp
72
83
  DspPipeline::DspPipeline(const Napi::CallbackInfo &info)
73
84
  : Napi::ObjectWrap<DspPipeline>(info)
74
85
  {
75
- // Config logic from TS (redis, stateKey) would go here
86
+ // Initialize the lock
87
+ m_isBusy = std::make_shared<std::atomic<bool>>(false);
76
88
  InitializeStageFactories();
77
89
  }
78
90
 
@@ -113,6 +125,35 @@ namespace dsp
113
125
  return std::make_unique<dsp::adapters::MovingAverageStage>(mode, windowSize, windowDurationMs);
114
126
  };
115
127
 
128
+ // Factory for Exponential Moving Average stage
129
+ m_stageFactories["exponentialMovingAverage"] = [](const Napi::Object &params)
130
+ {
131
+ std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
132
+ dsp::adapters::EmaMode mode = (modeStr == "moving") ? dsp::adapters::EmaMode::Moving : dsp::adapters::EmaMode::Batch;
133
+
134
+ // Parse alpha parameter (required, must be in range (0, 1])
135
+ if (!params.Has("alpha"))
136
+ {
137
+ throw std::invalid_argument("ExponentialMovingAverage: 'alpha' parameter is required");
138
+ }
139
+ double alpha = params.Get("alpha").As<Napi::Number>().DoubleValue();
140
+ if (alpha <= 0.0 || alpha > 1.0)
141
+ {
142
+ throw std::invalid_argument("ExponentialMovingAverage: 'alpha' must be in range (0, 1]");
143
+ }
144
+
145
+ return std::make_unique<dsp::adapters::ExponentialMovingAverageStage>(mode, static_cast<float>(alpha));
146
+ };
147
+
148
+ // Factory for Cumulative Moving Average stage
149
+ m_stageFactories["cumulativeMovingAverage"] = [](const Napi::Object &params)
150
+ {
151
+ std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
152
+ dsp::adapters::CmaMode mode = (modeStr == "moving") ? dsp::adapters::CmaMode::Moving : dsp::adapters::CmaMode::Batch;
153
+
154
+ return std::make_unique<dsp::adapters::CumulativeMovingAverageStage>(mode);
155
+ };
156
+
116
157
  // Factory for RMS stage
117
158
  m_stageFactories["rms"] = [](const Napi::Object &params)
118
159
  {
@@ -857,6 +898,60 @@ namespace dsp
857
898
  mapping, numInputChannels);
858
899
  };
859
900
 
901
+ // ===================================================================
902
+ // Filter Bank Stage
903
+ // ===================================================================
904
+ m_stageFactories["filterBank"] = [](const Napi::Object &params)
905
+ {
906
+ if (!params.Has("definitions") || !params.Has("inputChannels"))
907
+ {
908
+ throw std::invalid_argument("FilterBank: requires 'definitions' array and 'inputChannels'");
909
+ }
910
+
911
+ // Extract input channel count
912
+ int inputChannels = params.Get("inputChannels").As<Napi::Number>().Int32Value();
913
+
914
+ // Extract filter definitions array
915
+ Napi::Array defsArray = params.Get("definitions").As<Napi::Array>();
916
+ std::vector<dsp::adapters::FilterDefinition> definitions;
917
+ definitions.reserve(defsArray.Length());
918
+
919
+ for (uint32_t i = 0; i < defsArray.Length(); ++i)
920
+ {
921
+ Napi::Object defObj = defsArray.Get(i).As<Napi::Object>();
922
+
923
+ // Extract 'b' coefficients (feedforward)
924
+ if (!defObj.Has("b"))
925
+ {
926
+ throw std::invalid_argument("FilterBank: Each definition must have 'b' coefficients");
927
+ }
928
+ Napi::Array bArray = defObj.Get("b").As<Napi::Array>();
929
+ std::vector<double> b;
930
+ b.reserve(bArray.Length());
931
+ for (uint32_t j = 0; j < bArray.Length(); ++j)
932
+ {
933
+ b.push_back(bArray.Get(j).As<Napi::Number>().DoubleValue());
934
+ }
935
+
936
+ // Extract 'a' coefficients (feedback)
937
+ if (!defObj.Has("a"))
938
+ {
939
+ throw std::invalid_argument("FilterBank: Each definition must have 'a' coefficients");
940
+ }
941
+ Napi::Array aArray = defObj.Get("a").As<Napi::Array>();
942
+ std::vector<double> a;
943
+ a.reserve(aArray.Length());
944
+ for (uint32_t j = 0; j < aArray.Length(); ++j)
945
+ {
946
+ a.push_back(aArray.Get(j).As<Napi::Number>().DoubleValue());
947
+ }
948
+
949
+ definitions.push_back({b, a});
950
+ }
951
+
952
+ return std::make_unique<dsp::adapters::FilterBankStage>(definitions, inputChannels);
953
+ };
954
+
860
955
  // ===================================================================
861
956
  // Clip Detection Stage
862
957
  // ===================================================================
@@ -917,6 +1012,33 @@ namespace dsp
917
1012
  return std::make_unique<dsp::adapters::DifferentiatorStage>();
918
1013
  };
919
1014
 
1015
+ // ===================================================================
1016
+ // Square Stage
1017
+ // ===================================================================
1018
+ m_stageFactories["square"] = [](const Napi::Object &params)
1019
+ {
1020
+ // Stateless operation - no parameters needed
1021
+ return std::make_unique<dsp::adapters::SquareStage>();
1022
+ };
1023
+
1024
+ // Amplify (Gain) stage
1025
+ m_stageFactories["amplify"] = [](const Napi::Object &params)
1026
+ {
1027
+ float gain = 1.0f; // Default gain (no change)
1028
+
1029
+ if (params.Has("gain"))
1030
+ {
1031
+ gain = params.Get("gain").As<Napi::Number>().FloatValue();
1032
+
1033
+ if (gain <= 0.0f)
1034
+ {
1035
+ throw std::invalid_argument("Amplify gain must be positive");
1036
+ }
1037
+ }
1038
+
1039
+ return std::make_unique<dsp::adapters::AmplifyStage>(gain);
1040
+ };
1041
+
920
1042
  // Integrator stage (IIR leaky integrator)
921
1043
  m_stageFactories["integrator"] = [](const Napi::Object &params)
922
1044
  {
@@ -993,6 +1115,19 @@ namespace dsp
993
1115
  {
994
1116
  Napi::Env env = info.Env();
995
1117
 
1118
+ // Check if pipeline is disposed
1119
+ if (m_disposed)
1120
+ {
1121
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
1122
+ return env.Undefined();
1123
+ }
1124
+
1125
+ if (*m_isBusy)
1126
+ {
1127
+ Napi::Error::New(env, "Cannot add stage while processing").ThrowAsJavaScriptException();
1128
+ return env.Undefined();
1129
+ }
1130
+
996
1131
  // 1. Get arguments from TypeScript
997
1132
  std::string stageName = info[0].As<Napi::String>();
998
1133
  Napi::Object params = info[1].As<Napi::Object>();
@@ -1036,6 +1171,19 @@ namespace dsp
1036
1171
  {
1037
1172
  Napi::Env env = info.Env();
1038
1173
 
1174
+ // Check if pipeline is disposed
1175
+ if (m_disposed)
1176
+ {
1177
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
1178
+ return env.Undefined();
1179
+ }
1180
+
1181
+ if (*m_isBusy)
1182
+ {
1183
+ Napi::Error::New(env, "Cannot add filter stage while processing").ThrowAsJavaScriptException();
1184
+ return env.Undefined();
1185
+ }
1186
+
1039
1187
  if (info.Length() < 2 || !info[0].IsTypedArray() || !info[1].IsTypedArray())
1040
1188
  {
1041
1189
  Napi::TypeError::New(env, "Expected two Float64Arrays (b and a coefficients) as arguments").ThrowAsJavaScriptException();
@@ -1080,19 +1228,23 @@ namespace dsp
1080
1228
  std::vector<std::unique_ptr<IDspStage>> &stages,
1081
1229
  float *data,
1082
1230
  float *timestamps,
1231
+ double sampleRate,
1083
1232
  size_t numSamples,
1084
1233
  int channels,
1085
1234
  Napi::Reference<Napi::Float32Array> &&bufferRef,
1086
- Napi::Reference<Napi::Float32Array> &&timestampRef)
1235
+ Napi::Reference<Napi::Float32Array> &&timestampRef,
1236
+ std::shared_ptr<std::atomic<bool>> busyLock)
1087
1237
  : Napi::AsyncWorker(env),
1088
1238
  m_deferred(std::move(deferred)),
1089
1239
  m_stages(stages),
1090
1240
  m_data(data),
1091
1241
  m_timestamps(timestamps),
1242
+ m_sampleRate(sampleRate),
1092
1243
  m_numSamples(numSamples),
1093
1244
  m_channels(channels),
1094
1245
  m_bufferRef(std::move(bufferRef)),
1095
- m_timestampRef(std::move(timestampRef))
1246
+ m_timestampRef(std::move(timestampRef)),
1247
+ m_busyLock(busyLock)
1096
1248
  {
1097
1249
  }
1098
1250
 
@@ -1100,20 +1252,42 @@ namespace dsp
1100
1252
  // This runs on a worker thread (not blocking the event loop)
1101
1253
  void Execute() override
1102
1254
  {
1255
+ // Local storage for generated timestamps (RAII - automatically freed when function exits)
1256
+ std::vector<float> generatedTimestamps;
1257
+
1103
1258
  try
1104
1259
  {
1105
- // Process the buffer through all stages
1106
- // Handle both in-place and resizing stages
1260
+ // 1. Generate Timestamps if missing (Optimization)
1261
+ if (m_timestamps == nullptr)
1262
+ {
1263
+ generatedTimestamps.resize(m_numSamples);
1264
+
1265
+ // Calculate time step (dt) in milliseconds
1266
+ // If sampleRate is 0 or invalid, default to 1.0 (treating indices as time)
1267
+ double dt = (m_sampleRate > 0.0) ? (1000.0 / m_sampleRate) : 1.0;
1268
+
1269
+ // Fill timestamps linearly: t[i] = i * dt
1270
+ for (size_t i = 0; i < m_numSamples; ++i)
1271
+ {
1272
+ generatedTimestamps[i] = static_cast<float>(i * dt);
1273
+ }
1274
+
1275
+ // Point the main processing pointer to our locally generated data
1276
+ m_timestamps = generatedTimestamps.data();
1277
+ }
1278
+
1279
+ // 2. Process the buffer through all stages
1107
1280
  float *currentBuffer = m_data;
1108
1281
  size_t currentSize = m_numSamples;
1109
1282
  float *tempBuffer = nullptr;
1110
1283
  bool usingTempBuffer = false;
1111
1284
 
1285
+ const bool debugStageDumps = std::getenv("DSPX_DEBUG_STAGE_DUMPS") != nullptr;
1112
1286
  for (const auto &stage : m_stages)
1113
1287
  {
1114
1288
  if (stage->isResizing())
1115
1289
  {
1116
- // Resizing stage: allocate new buffer
1290
+ // Resizing logic (same as before)
1117
1291
  size_t outputSize = stage->calculateOutputSize(currentSize);
1118
1292
  float *outputBuffer = new float[outputSize];
1119
1293
 
@@ -1122,92 +1296,75 @@ namespace dsp
1122
1296
  outputBuffer, actualOutputSize,
1123
1297
  m_channels, m_timestamps);
1124
1298
 
1125
- // Free the previous temporary buffer if we allocated one
1126
1299
  if (usingTempBuffer)
1127
- {
1128
1300
  delete[] currentBuffer;
1129
- }
1130
-
1131
1301
  currentBuffer = outputBuffer;
1132
1302
  currentSize = actualOutputSize;
1133
1303
  usingTempBuffer = true;
1134
1304
 
1135
- // Update channel count if this stage changes it (e.g., ChannelSelector)
1136
1305
  int outputChannels = stage->getOutputChannels();
1137
1306
  if (outputChannels > 0)
1138
- {
1139
1307
  m_channels = outputChannels;
1140
- }
1141
1308
 
1142
- // Adjust timestamps for resampled data
1309
+ // Re-interpolate timestamps if needed (same as before)
1143
1310
  if (m_timestamps != nullptr)
1144
1311
  {
1145
1312
  double timeScale = stage->getTimeScaleFactor();
1146
1313
  size_t numOutputSamples = actualOutputSize / m_channels;
1147
-
1148
- // Allocate new timestamp buffer
1149
1314
  float *newTimestamps = new float[actualOutputSize];
1150
1315
 
1151
- // Interpolate timestamps based on time scale
1152
- // For upsampling (timeScale < 1): more samples, smaller time steps
1153
- // For downsampling (timeScale > 1): fewer samples, larger time steps
1154
1316
  for (size_t i = 0; i < numOutputSamples; ++i)
1155
1317
  {
1156
- // Map output sample index to input time domain
1157
1318
  double inputTime = i * timeScale;
1158
1319
  size_t inputIdx = static_cast<size_t>(inputTime);
1159
1320
  double frac = inputTime - inputIdx;
1160
-
1161
1321
  float timestamp;
1162
- if (inputIdx >= (m_numSamples / m_channels))
1322
+
1323
+ if (inputIdx >= (currentSize / m_channels))
1163
1324
  {
1164
- // Beyond input range, extrapolate
1165
- timestamp = m_timestamps[(m_numSamples / m_channels - 1) * m_channels] +
1166
- static_cast<float>((inputTime - (m_numSamples / m_channels - 1)) * timeScale);
1325
+ size_t lastIdx = (currentSize / m_channels) - 1;
1326
+ timestamp = m_timestamps[lastIdx * m_channels] +
1327
+ static_cast<float>((inputTime - lastIdx) * timeScale);
1167
1328
  }
1168
- else if (inputIdx + 1 >= (m_numSamples / m_channels))
1329
+ else if (inputIdx + 1 >= (currentSize / m_channels))
1169
1330
  {
1170
- // At boundary, use last timestamp
1171
1331
  timestamp = m_timestamps[inputIdx * m_channels];
1172
1332
  }
1173
1333
  else
1174
1334
  {
1175
- // Interpolate between two timestamps
1176
1335
  float t0 = m_timestamps[inputIdx * m_channels];
1177
1336
  float t1 = m_timestamps[(inputIdx + 1) * m_channels];
1178
1337
  timestamp = t0 + static_cast<float>(frac) * (t1 - t0);
1179
1338
  }
1180
1339
 
1181
- // Replicate timestamp for all channels
1182
1340
  for (int ch = 0; ch < m_channels; ++ch)
1183
1341
  {
1184
1342
  newTimestamps[i * m_channels + ch] = timestamp;
1185
1343
  }
1186
1344
  }
1187
-
1188
- // Replace old timestamps
1189
- // Note: We don't own the original m_timestamps, so don't delete it
1190
1345
  m_timestamps = newTimestamps;
1191
1346
  m_timestampBuffer.reset(newTimestamps);
1192
1347
  }
1193
1348
  }
1194
1349
  else
1195
1350
  {
1196
- // In-place stage: process directly
1197
- // If we're using a temp buffer, process it; otherwise process original
1198
- if (usingTempBuffer)
1199
- {
1200
- stage->process(currentBuffer, currentSize, m_channels, m_timestamps);
1201
- }
1202
- else
1351
+ // In-place processing
1352
+ stage->process(currentBuffer, currentSize, m_channels, m_timestamps);
1353
+
1354
+ if (debugStageDumps)
1203
1355
  {
1204
- // First in-place stage on original buffer
1205
- stage->process(currentBuffer, currentSize, m_channels, m_timestamps);
1356
+ const char *stype = stage->getType();
1357
+ size_t toShow = std::min<size_t>(8, currentSize);
1358
+ std::cout << "[DUMP] after '" << stype << "':";
1359
+ for (size_t i = 0; i < toShow; ++i)
1360
+ {
1361
+ std::cout << (i == 0 ? ' ' : ',') << currentBuffer[i];
1362
+ }
1363
+ std::cout << std::endl;
1206
1364
  }
1207
1365
  }
1208
1366
  }
1209
1367
 
1210
- // Store final result
1211
1368
  m_finalBuffer = currentBuffer;
1212
1369
  m_finalSize = currentSize;
1213
1370
  m_ownsBuffer = usingTempBuffer;
@@ -1221,14 +1378,15 @@ namespace dsp
1221
1378
  // This runs on the main thread after Execute() completes
1222
1379
  void OnOK() override
1223
1380
  {
1381
+ *m_busyLock = false; // unlock the pipeline
1382
+
1224
1383
  Napi::Env env = Env();
1225
1384
 
1226
1385
  // Create a new Float32Array with the final buffer size
1227
1386
  Napi::Float32Array outputArray = Napi::Float32Array::New(env, m_finalSize);
1228
- float *outputData = outputArray.Data();
1229
1387
 
1230
1388
  // Copy final data to the output array
1231
- std::memcpy(outputData, m_finalBuffer, m_finalSize * sizeof(float));
1389
+ std::memcpy(outputArray.Data(), m_finalBuffer, m_finalSize * sizeof(float));
1232
1390
 
1233
1391
  // Clean up temporary buffer if we allocated one
1234
1392
  if (m_ownsBuffer)
@@ -1243,6 +1401,7 @@ namespace dsp
1243
1401
  void OnError(const Napi::Error &error) override
1244
1402
  {
1245
1403
  m_deferred.Reject(error.Value());
1404
+ *m_busyLock = false; // unlock the pipeline
1246
1405
  }
1247
1406
 
1248
1407
  private:
@@ -1250,6 +1409,7 @@ namespace dsp
1250
1409
  std::vector<std::unique_ptr<IDspStage>> &m_stages;
1251
1410
  float *m_data;
1252
1411
  float *m_timestamps;
1412
+ double m_sampleRate;
1253
1413
  size_t m_numSamples;
1254
1414
  int m_channels;
1255
1415
  Napi::Reference<Napi::Float32Array> m_bufferRef;
@@ -1262,6 +1422,8 @@ namespace dsp
1262
1422
 
1263
1423
  // For managing allocated timestamp buffer
1264
1424
  std::unique_ptr<float[]> m_timestampBuffer;
1425
+
1426
+ std::shared_ptr<std::atomic<bool>> m_busyLock; // Pointer to the busy lock
1265
1427
  };
1266
1428
 
1267
1429
  /**
@@ -1276,48 +1438,75 @@ namespace dsp
1276
1438
  {
1277
1439
  Napi::Env env = info.Env();
1278
1440
 
1279
- // 1. Get buffer from TypeScript (zero-copy)
1441
+ // Check if pipeline is disposed
1442
+ if (m_disposed)
1443
+ {
1444
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
1445
+ return env.Undefined();
1446
+ }
1447
+
1448
+ if (*m_isBusy)
1449
+ {
1450
+ Napi::Error::New(env, "Pipeline is busy: Cannot call process() while another operation is running.").ThrowAsJavaScriptException();
1451
+ return env.Undefined();
1452
+ }
1453
+
1454
+ if (!info[0].IsTypedArray())
1455
+ {
1456
+ Napi::TypeError::New(env, "Argument 0 must be a Float32Array").ThrowAsJavaScriptException();
1457
+ return env.Undefined();
1458
+ }
1280
1459
  Napi::Float32Array jsBuffer = info[0].As<Napi::Float32Array>();
1281
1460
  float *data = jsBuffer.Data();
1282
1461
  size_t numSamples = jsBuffer.ElementLength();
1283
1462
 
1284
- // 2. Get timestamps and options
1285
- // TypeScript can pass either:
1286
- // process(buffer, timestamps, options) - new time-based API
1287
- // process(buffer, options) - legacy sample-based API (timestamps = nullptr)
1288
1463
  Napi::Float32Array jsTimestamps;
1289
1464
  float *timestamps = nullptr;
1290
1465
  Napi::Object options;
1466
+ double sampleRate = 0.0;
1291
1467
 
1292
- if (info.Length() >= 2 && info[1].IsTypedArray())
1468
+ if (info.Length() >= 3 && info[1].IsTypedArray())
1293
1469
  {
1294
- // New API: timestamps provided
1470
+ // Mode A: Explicit Timestamps
1295
1471
  jsTimestamps = info[1].As<Napi::Float32Array>();
1296
1472
  timestamps = jsTimestamps.Data();
1297
1473
  options = info[2].As<Napi::Object>();
1298
1474
 
1299
- // Validate timestamp length matches sample length
1300
1475
  if (jsTimestamps.ElementLength() != numSamples)
1301
1476
  {
1302
- Napi::TypeError::New(env, "Timestamp array length must match sample array length")
1303
- .ThrowAsJavaScriptException();
1477
+ Napi::TypeError::New(env, "Timestamp array length must match sample array length").ThrowAsJavaScriptException();
1304
1478
  return env.Undefined();
1305
1479
  }
1306
1480
  }
1307
1481
  else
1308
1482
  {
1309
- // Legacy API: no timestamps (will use sample indices)
1310
- options = info[1].As<Napi::Object>();
1483
+ // Mode B: Implicit Timestamps
1484
+ // If info[1] exists and is an object, use it as options.
1485
+ if (info.Length() >= 2 && info[1].IsObject())
1486
+ {
1487
+ options = info[1].As<Napi::Object>();
1488
+ }
1489
+ else
1490
+ {
1491
+ options = Napi::Object::New(env);
1492
+ }
1493
+ }
1494
+
1495
+ // Extract options safely
1496
+ if (options.Has("sampleRate"))
1497
+ {
1498
+ sampleRate = options.Get("sampleRate").As<Napi::Number>().DoubleValue();
1311
1499
  }
1312
1500
 
1313
- int channels = options.Get("channels").As<Napi::Number>().Uint32Value();
1314
- // int sampleRate = options.Get("sampleRate").As<Napi::Number>().Uint32Value();
1501
+ int channels = 1;
1502
+ if (options.Has("channels"))
1503
+ {
1504
+ channels = options.Get("channels").As<Napi::Number>().Uint32Value();
1505
+ }
1315
1506
 
1316
- // 3. Create a deferred promise and get the promise before moving
1317
1507
  Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
1318
1508
  Napi::Promise promise = deferred.Promise();
1319
1509
 
1320
- // 4. Create references to keep buffers alive during async operation
1321
1510
  Napi::Reference<Napi::Float32Array> bufferRef = Napi::Reference<Napi::Float32Array>::New(jsBuffer, 1);
1322
1511
  Napi::Reference<Napi::Float32Array> timestampRef;
1323
1512
  if (timestamps != nullptr)
@@ -1325,14 +1514,153 @@ namespace dsp
1325
1514
  timestampRef = Napi::Reference<Napi::Float32Array>::New(jsTimestamps, 1);
1326
1515
  }
1327
1516
 
1328
- // 5. Create and queue the worker
1329
- ProcessWorker *worker = new ProcessWorker(env, std::move(deferred), m_stages, data, timestamps, numSamples, channels, std::move(bufferRef), std::move(timestampRef));
1517
+ *m_isBusy = true; // lock the pipeline
1518
+
1519
+ ProcessWorker *worker = new ProcessWorker(env, std::move(deferred), m_stages, data, timestamps, sampleRate, numSamples, channels, std::move(bufferRef), std::move(timestampRef), m_isBusy);
1330
1520
  worker->Queue();
1331
1521
 
1332
- // 6. Return the promise immediately
1333
1522
  return promise;
1334
1523
  }
1335
1524
 
1525
+ /**
1526
+ * This is the "ProcessSync" method.
1527
+ * TS calls:
1528
+ * await native.processSync(buffer, timestamps, { channels: 4 })
1529
+ * or (legacy):
1530
+ * await native.processSync(buffer, { sampleRate: 2000, channels: 4 })
1531
+ * Returns the processed buffer directly.
1532
+ */
1533
+
1534
+ Napi::Value DspPipeline::ProcessSync(const Napi::CallbackInfo &info)
1535
+ {
1536
+ Napi::Env env = info.Env();
1537
+
1538
+ // Check if pipeline is disposed
1539
+ if (m_disposed)
1540
+ {
1541
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
1542
+ return env.Undefined();
1543
+ }
1544
+
1545
+ if (*m_isBusy)
1546
+ {
1547
+ Napi::Error::New(env, "Pipeline is busy: Cannot call processSync() while an async operation is running.").ThrowAsJavaScriptException();
1548
+ return env.Undefined();
1549
+ }
1550
+
1551
+ if (info.Length() < 1 || !info[0].IsTypedArray())
1552
+ {
1553
+ Napi::TypeError::New(env, "Buffer required (Float32Array)").ThrowAsJavaScriptException();
1554
+ return env.Undefined();
1555
+ }
1556
+
1557
+ Napi::Float32Array jsBuffer = info[0].As<Napi::Float32Array>();
1558
+ float *data = jsBuffer.Data();
1559
+ size_t numSamples = jsBuffer.ElementLength();
1560
+
1561
+ Napi::Float32Array jsTimestamps;
1562
+ float *timestamps = nullptr;
1563
+ Napi::Object options;
1564
+ double sampleRate = 0.0;
1565
+
1566
+ if (info.Length() >= 3 && info[1].IsTypedArray())
1567
+ {
1568
+ // Mode A: Explicit Timestamps
1569
+ jsTimestamps = info[1].As<Napi::Float32Array>();
1570
+ timestamps = jsTimestamps.Data();
1571
+ options = info[2].As<Napi::Object>();
1572
+
1573
+ if (jsTimestamps.ElementLength() != numSamples)
1574
+ {
1575
+ Napi::TypeError::New(env, "Timestamp array length must match sample array length").ThrowAsJavaScriptException();
1576
+ return env.Undefined();
1577
+ }
1578
+ }
1579
+ else
1580
+ {
1581
+ // Mode B: Implicit Timestamps
1582
+ // If info[1] exists and is an object, use it as options.
1583
+ if (info.Length() >= 2 && info[1].IsObject())
1584
+ {
1585
+ options = info[1].As<Napi::Object>();
1586
+ }
1587
+ else
1588
+ {
1589
+ options = Napi::Object::New(env);
1590
+ }
1591
+ }
1592
+
1593
+ // Extract options safely
1594
+ if (options.Has("sampleRate"))
1595
+ {
1596
+ sampleRate = options.Get("sampleRate").As<Napi::Number>().DoubleValue();
1597
+ }
1598
+
1599
+ int channels = 1;
1600
+ if (options.Has("channels"))
1601
+ {
1602
+ channels = options.Get("channels").As<Napi::Number>().Uint32Value();
1603
+ }
1604
+
1605
+ Napi::Reference<Napi::Float32Array> bufferRef = Napi::Reference<Napi::Float32Array>::New(jsBuffer, 1);
1606
+ Napi::Reference<Napi::Float32Array> timestampRef;
1607
+ if (timestamps != nullptr)
1608
+ {
1609
+ timestampRef = Napi::Reference<Napi::Float32Array>::New(jsTimestamps, 1);
1610
+ }
1611
+
1612
+ // --- Core Processing Logic (Direct Execution) ---
1613
+
1614
+ float *currentData = data;
1615
+ size_t currentSize = numSamples;
1616
+ std::vector<float> tempBuffer; // Safe RAII container
1617
+ bool isDetached = false;
1618
+
1619
+ try
1620
+ {
1621
+ for (const auto &stage : m_stages)
1622
+ {
1623
+ if (stage->isResizing())
1624
+ {
1625
+ size_t outputSize = stage->calculateOutputSize(currentSize);
1626
+ std::vector<float> nextBuffer(outputSize);
1627
+ size_t actualOutputSize = 0;
1628
+
1629
+ stage->processResizing(currentData, currentSize, nextBuffer.data(), actualOutputSize, channels, timestamps);
1630
+
1631
+ tempBuffer = std::move(nextBuffer);
1632
+ currentData = tempBuffer.data();
1633
+ currentSize = actualOutputSize;
1634
+ isDetached = true;
1635
+
1636
+ if (stage->getOutputChannels() > 0)
1637
+ channels = stage->getOutputChannels();
1638
+ }
1639
+ else
1640
+ {
1641
+ stage->process(currentData, currentSize, channels, timestamps);
1642
+ }
1643
+ }
1644
+ }
1645
+ catch (const std::exception &e)
1646
+ {
1647
+ Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
1648
+ return env.Undefined();
1649
+ }
1650
+
1651
+ // 4. Return
1652
+ if (isDetached)
1653
+ {
1654
+ Napi::Float32Array outputArray = Napi::Float32Array::New(env, currentSize);
1655
+ std::memcpy(outputArray.Data(), currentData, currentSize * sizeof(float));
1656
+ return outputArray;
1657
+ }
1658
+ else
1659
+ {
1660
+ return jsBuffer;
1661
+ }
1662
+ }
1663
+
1336
1664
  /**
1337
1665
  * Save current pipeline state as JSON string
1338
1666
  * TypeScript will handle storing this in Redis
@@ -1342,53 +1670,233 @@ namespace dsp
1342
1670
  Napi::Value DspPipeline::SaveState(const Napi::CallbackInfo &info)
1343
1671
  {
1344
1672
  Napi::Env env = info.Env();
1345
- Napi::Object stateObj = Napi::Object::New(env);
1346
1673
 
1347
- // Save timestamp
1348
- stateObj.Set("timestamp", static_cast<double>(std::time(nullptr)));
1674
+ // Check if pipeline is disposed
1675
+ if (m_disposed)
1676
+ {
1677
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
1678
+ return env.Undefined();
1679
+ }
1349
1680
 
1350
- // Save pipeline configuration and full state
1351
- Napi::Array stagesArray = Napi::Array::New(env, m_stages.size());
1681
+ // Check for format option
1682
+ bool useToon = false;
1683
+ if (info.Length() > 0 && info[0].IsObject())
1684
+ {
1685
+ Napi::Object options = info[0].As<Napi::Object>();
1686
+ if (options.Has("format"))
1687
+ {
1688
+ std::string fmt = options.Get("format").As<Napi::String>().Utf8Value();
1689
+ if (fmt == "toon")
1690
+ useToon = true;
1691
+ }
1692
+ }
1352
1693
 
1353
- for (size_t i = 0; i < m_stages.size(); ++i)
1694
+ if (useToon)
1695
+ {
1696
+ // --- Original compact binary TOON path ---
1697
+ try
1698
+ {
1699
+ dsp::toon::Serializer serializer;
1700
+ serializer.startObject();
1701
+ serializer.writeString("timestamp");
1702
+ serializer.writeDouble(static_cast<double>(std::time(nullptr)));
1703
+ serializer.writeString("stageCount");
1704
+ serializer.writeInt32(static_cast<int32_t>(m_stages.size()));
1705
+ serializer.writeString("stages");
1706
+ serializer.startArray();
1707
+ for (const auto &stage : m_stages)
1708
+ {
1709
+ serializer.startObject();
1710
+ serializer.writeString("type");
1711
+ serializer.writeString(stage->getType());
1712
+ serializer.writeString("state");
1713
+ stage->serializeToon(serializer);
1714
+ serializer.endObject();
1715
+ }
1716
+ serializer.endArray();
1717
+ serializer.endObject();
1718
+ return Napi::Buffer<uint8_t>::Copy(env, serializer.buffer.data(), serializer.buffer.size());
1719
+ }
1720
+ catch (const std::exception &e)
1721
+ {
1722
+ Napi::Error::New(env, std::string("TOON Save Failed: ") + e.what()).ThrowAsJavaScriptException();
1723
+ return env.Null();
1724
+ }
1725
+ }
1726
+ else
1354
1727
  {
1355
- Napi::Object stageConfig = Napi::Object::New(env);
1728
+ // --- Legacy JSON Path ---
1729
+ Napi::Object stateObj = Napi::Object::New(env);
1356
1730
 
1357
- stageConfig.Set("index", static_cast<uint32_t>(i));
1358
- stageConfig.Set("type", m_stages[i]->getType());
1731
+ // Save timestamp
1732
+ stateObj.Set("timestamp", static_cast<double>(std::time(nullptr)));
1359
1733
 
1360
- // Serialize the stage's internal state
1361
- stageConfig.Set("state", m_stages[i]->serializeState(env));
1734
+ // Save pipeline configuration and full state
1735
+ Napi::Array stagesArray = Napi::Array::New(env, m_stages.size());
1362
1736
 
1363
- stagesArray.Set(static_cast<uint32_t>(i), stageConfig);
1364
- }
1737
+ for (size_t i = 0; i < m_stages.size(); ++i)
1738
+ {
1739
+ Napi::Object stageConfig = Napi::Object::New(env);
1365
1740
 
1366
- stateObj.Set("stages", stagesArray);
1367
- stateObj.Set("stageCount", static_cast<uint32_t>(m_stages.size()));
1741
+ stageConfig.Set("index", static_cast<uint32_t>(i));
1742
+ stageConfig.Set("type", m_stages[i]->getType());
1368
1743
 
1369
- // Convert to JSON string using JavaScript's JSON.stringify
1370
- Napi::Object JSON = env.Global().Get("JSON").As<Napi::Object>();
1371
- Napi::Function stringify = JSON.Get("stringify").As<Napi::Function>();
1372
- return stringify.Call(JSON, {stateObj});
1744
+ // Serialize the stage's internal state
1745
+ stageConfig.Set("state", m_stages[i]->serializeState(env));
1746
+
1747
+ stagesArray.Set(static_cast<uint32_t>(i), stageConfig);
1748
+ }
1749
+
1750
+ stateObj.Set("stages", stagesArray);
1751
+ stateObj.Set("stageCount", static_cast<uint32_t>(m_stages.size()));
1752
+
1753
+ // Convert to JSON string using JavaScript's JSON.stringify
1754
+ Napi::Object JSON = env.Global().Get("JSON").As<Napi::Object>();
1755
+ Napi::Function stringify = JSON.Get("stringify").As<Napi::Function>();
1756
+ return stringify.Call(JSON, {stateObj});
1757
+ }
1373
1758
  }
1759
+
1374
1760
  /**
1375
- * Load pipeline state from JSON string with Smart Merge Strategy.
1376
- * * Logic:
1377
- * 1. Iterate through the saved state history.
1378
- * 2. If a saved stage matches the current pipeline's next stage (by type),
1379
- * restore the state into the existing stage instance.
1380
- * 3. If there is a mismatch (e.g. saved has 'Rectify' but current has 'Filter'),
1381
- * dynamically create the 'Rectify' stage using the factory and insert it.
1382
- * 4. Finally, append any remaining stages from the current pipeline definition.
1383
- * * Result: The pipeline becomes a fusion of [Restorable Saved Stages] + [Current Stages]
1761
+ * Load pipeline state from JSON string or TOON Buffer with inline validation.
1762
+ * NEW BEHAVIOR: Only loads state if saved stages match current stages EXACTLY.
1763
+ * This prevents pipeline corruption from incompatible state.
1764
+ *
1765
+ * If stage types or count don't match, throws an error instead of trying to merge.
1384
1766
  */
1385
1767
  Napi::Value DspPipeline::LoadState(const Napi::CallbackInfo &info)
1386
1768
  {
1387
1769
  Napi::Env env = info.Env();
1388
1770
 
1389
- if (info.Length() < 1 || !info[0].IsString())
1771
+ // Check if pipeline is disposed
1772
+ if (m_disposed)
1773
+ {
1774
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
1775
+ return env.Undefined();
1776
+ }
1777
+
1778
+ if (info.Length() < 1)
1779
+ {
1780
+ Napi::TypeError::New(env, "Expected state (String or Buffer) as first argument")
1781
+ .ThrowAsJavaScriptException();
1782
+ return env.Undefined();
1783
+ }
1784
+
1785
+ // --- TOON Path ---
1786
+ // Accept Node Buffer, Uint8Array, or ArrayBuffer
1787
+ const uint8_t *toonDataPtr = nullptr;
1788
+ size_t toonDataLen = 0;
1789
+
1790
+ if (info[0].IsBuffer())
1791
+ {
1792
+ Napi::Buffer<uint8_t> buffer = info[0].As<Napi::Buffer<uint8_t>>();
1793
+ toonDataPtr = buffer.Data();
1794
+ toonDataLen = buffer.Length();
1795
+ }
1796
+ else if (info[0].IsTypedArray())
1797
+ {
1798
+ Napi::TypedArray ta = info[0].As<Napi::TypedArray>();
1799
+ Napi::ArrayBuffer ab = ta.ArrayBuffer();
1800
+ toonDataPtr = static_cast<const uint8_t *>(ab.Data()) + ta.ByteOffset();
1801
+ toonDataLen = ta.ByteLength();
1802
+ }
1803
+ else if (info[0].IsArrayBuffer())
1804
+ {
1805
+ Napi::ArrayBuffer ab = info[0].As<Napi::ArrayBuffer>();
1806
+ toonDataPtr = static_cast<const uint8_t *>(ab.Data());
1807
+ toonDataLen = ab.ByteLength();
1808
+ }
1809
+
1810
+ if (toonDataPtr != nullptr && toonDataLen > 0)
1811
+ {
1812
+ try
1813
+ {
1814
+ const bool debugToon = std::getenv("DSPX_DEBUG_TOON") != nullptr;
1815
+ dsp::toon::Deserializer deserializer(toonDataPtr, toonDataLen);
1816
+ deserializer.consumeToken(dsp::toon::T_OBJECT_START);
1817
+ std::string key = deserializer.readString();
1818
+ double timestamp = deserializer.readDouble();
1819
+ key = deserializer.readString();
1820
+ int32_t savedStageCount = deserializer.readInt32();
1821
+ key = deserializer.readString();
1822
+ deserializer.consumeToken(dsp::toon::T_ARRAY_START);
1823
+
1824
+ // SIMPLER APPROACH: Validate and load in a single pass
1825
+ // For each saved stage, check type matches before deserializing
1826
+ size_t stageIdx = 0;
1827
+ while (deserializer.peekToken() != dsp::toon::T_ARRAY_END)
1828
+ {
1829
+ // Check we haven't exceeded current stage count
1830
+ if (stageIdx >= m_stages.size())
1831
+ {
1832
+ throw std::runtime_error("TOON Load: Stage count mismatch. Saved state has more stages than current pipeline (" +
1833
+ std::to_string(m_stages.size()) + ").");
1834
+ }
1835
+
1836
+ deserializer.consumeToken(dsp::toon::T_OBJECT_START);
1837
+ deserializer.readString(); // "type" key
1838
+ std::string savedType = deserializer.readString();
1839
+
1840
+ // Validate type matches BEFORE loading state
1841
+ std::string currentType = m_stages[stageIdx]->getType();
1842
+ if (currentType != savedType)
1843
+ {
1844
+ throw std::runtime_error("TOON Load: Stage type mismatch at index " +
1845
+ std::to_string(stageIdx) + ". Expected '" + currentType +
1846
+ "', got '" + savedType +
1847
+ "'. Cannot load incompatible state.");
1848
+ }
1849
+
1850
+ deserializer.readString(); // "state" key
1851
+
1852
+ if (debugToon)
1853
+ {
1854
+ std::cout << "[TOON] Loading state into stage[" << stageIdx
1855
+ << "]: type='" << savedType << "'" << std::endl;
1856
+ }
1857
+
1858
+ // Deserialize directly into the existing stage
1859
+ m_stages[stageIdx]->deserializeToon(deserializer);
1860
+
1861
+ deserializer.consumeToken(dsp::toon::T_OBJECT_END);
1862
+ stageIdx++;
1863
+ }
1864
+
1865
+ // Verify we loaded all current stages
1866
+ if (stageIdx != m_stages.size())
1867
+ {
1868
+ throw std::runtime_error("TOON Load: Stage count mismatch. Saved state has " +
1869
+ std::to_string(stageIdx) + " stages, current has " +
1870
+ std::to_string(m_stages.size()) + ".");
1871
+ }
1872
+
1873
+ if (debugToon)
1874
+ {
1875
+ std::cout << "[TOON] Validation passed and loaded " << stageIdx
1876
+ << " stages successfully" << std::endl;
1877
+ }
1878
+
1879
+ deserializer.consumeToken(dsp::toon::T_ARRAY_END);
1880
+ deserializer.consumeToken(dsp::toon::T_OBJECT_END);
1881
+
1882
+ if (debugToon)
1883
+ {
1884
+ std::cout << "[TOON] Load complete. Restored state into "
1885
+ << stageIdx << " stages" << std::endl;
1886
+ }
1887
+ return Napi::Boolean::New(env, true);
1888
+ }
1889
+ catch (const std::exception &e)
1890
+ {
1891
+ Napi::Error::New(env, std::string("TOON Load Failed: ") + e.what()).ThrowAsJavaScriptException();
1892
+ return Napi::Boolean::New(env, false);
1893
+ }
1894
+ }
1895
+
1896
+ // --- Legacy JSON Path ---
1897
+ if (!info[0].IsString())
1390
1898
  {
1391
- Napi::TypeError::New(env, "Expected state JSON string as first argument")
1899
+ Napi::TypeError::New(env, "Expected state JSON string or Buffer")
1392
1900
  .ThrowAsJavaScriptException();
1393
1901
  return env.Undefined();
1394
1902
  }
@@ -1507,6 +2015,13 @@ namespace dsp
1507
2015
  {
1508
2016
  Napi::Env env = info.Env();
1509
2017
 
2018
+ // Check if pipeline is disposed
2019
+ if (m_disposed)
2020
+ {
2021
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
2022
+ return env.Undefined();
2023
+ }
2024
+
1510
2025
  // Reset all stages
1511
2026
  for (auto &stage : m_stages)
1512
2027
  {
@@ -1528,6 +2043,14 @@ namespace dsp
1528
2043
  Napi::Value DspPipeline::ListState(const Napi::CallbackInfo &info)
1529
2044
  {
1530
2045
  Napi::Env env = info.Env();
2046
+
2047
+ // Check if pipeline is disposed
2048
+ if (m_disposed)
2049
+ {
2050
+ Napi::Error::New(env, "Pipeline is disposed").ThrowAsJavaScriptException();
2051
+ return env.Undefined();
2052
+ }
2053
+
1531
2054
  Napi::Object summary = Napi::Object::New(env);
1532
2055
 
1533
2056
  // Basic pipeline info
@@ -1588,6 +2111,52 @@ namespace dsp
1588
2111
  return summary;
1589
2112
  }
1590
2113
 
2114
+ /**
2115
+ * Dispose of the pipeline and free all resources
2116
+ * This method ensures safe cleanup and prevents further use of the pipeline
2117
+ *
2118
+ * Behavior:
2119
+ * - Blocks disposal if async processing is currently running
2120
+ * - Clears all stages (triggers RAII cleanup of all stage resources)
2121
+ * - Marks pipeline as disposed to prevent future operations
2122
+ * - Safe to call multiple times (idempotent)
2123
+ */
2124
+ Napi::Value DspPipeline::Dispose(const Napi::CallbackInfo &info)
2125
+ {
2126
+ Napi::Env env = info.Env();
2127
+
2128
+ // Already disposed - silently succeed (idempotent behavior)
2129
+ if (m_disposed)
2130
+ {
2131
+ return env.Undefined();
2132
+ }
2133
+
2134
+ // Cannot dispose while processing is in progress
2135
+ if (*m_isBusy)
2136
+ {
2137
+ Napi::Error::New(env, "Cannot dispose pipeline: process() is still running.")
2138
+ .ThrowAsJavaScriptException();
2139
+ return env.Undefined();
2140
+ }
2141
+
2142
+ // Clear all stages - triggers RAII cleanup of all stage resources
2143
+ // This will:
2144
+ // - Free all stage internal buffers
2145
+ // - Free all filter state memory
2146
+ // - Free all adaptive filter memory arenas
2147
+ // - Free all detachable buffers
2148
+ // - Free timestamp and resize buffers
2149
+ m_stages.clear();
2150
+
2151
+ // Reset busy flag (defensive programming)
2152
+ *m_isBusy = false;
2153
+
2154
+ // Mark as disposed to prevent further operations
2155
+ m_disposed = true;
2156
+
2157
+ return env.Undefined();
2158
+ }
2159
+
1591
2160
  } // namespace dsp
1592
2161
 
1593
2162
  // Forward declare FFT bindings init
@@ -1595,6 +2164,7 @@ namespace dsp
1595
2164
  {
1596
2165
  void InitFftBindings(Napi::Env env, Napi::Object exports);
1597
2166
  Napi::Object InitMatrixBindings(Napi::Env env, Napi::Object exports);
2167
+ void RegisterFilterBankDesignBindings(Napi::Env env, Napi::Object exports);
1598
2168
  namespace bindings
1599
2169
  {
1600
2170
  Napi::Object InitUtilityBindings(Napi::Env env, Napi::Object exports);
@@ -1613,6 +2183,9 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports)
1613
2183
  // Initialize FIR/IIR filter bindings
1614
2184
  dsp::InitFilterBindings(env, exports);
1615
2185
 
2186
+ // Initialize filter bank design utilities
2187
+ dsp::RegisterFilterBankDesignBindings(env, exports);
2188
+
1616
2189
  // Initialize matrix analysis bindings (PCA, ICA, Whitening)
1617
2190
  dsp::InitMatrixBindings(env, exports);
1618
2191