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.
- package/README.md +40 -78
- package/binding.gyp +10 -0
- package/dist/FilterBankDesign.d.ts +233 -0
- package/dist/FilterBankDesign.d.ts.map +1 -0
- package/dist/FilterBankDesign.js +247 -0
- package/dist/FilterBankDesign.js.map +1 -0
- package/dist/advanced-dsp.d.ts +6 -6
- package/dist/advanced-dsp.d.ts.map +1 -1
- package/dist/advanced-dsp.js +35 -12
- package/dist/advanced-dsp.js.map +1 -1
- package/dist/backends.d.ts +0 -103
- package/dist/backends.d.ts.map +1 -1
- package/dist/backends.js +0 -217
- package/dist/backends.js.map +1 -1
- package/dist/bindings.d.ts +270 -17
- package/dist/bindings.d.ts.map +1 -1
- package/dist/bindings.js +566 -43
- package/dist/bindings.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +67 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +38 -8
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +84 -26
- package/dist/utils.js.map +1 -1
- package/package.json +1 -2
- package/prebuilds/win32-x64/dspx.node +0 -0
- package/scripts/add-dispose-to-tests.js +145 -0
- package/src/native/DspPipeline.cc +699 -126
- package/src/native/DspPipeline.h +13 -0
- package/src/native/FilterBankDesignBindings.cc +241 -0
- package/src/native/IDspStage.h +24 -0
- package/src/native/UtilityBindings.cc +130 -0
- package/src/native/adapters/AmplifyStage.h +148 -0
- package/src/native/adapters/ClipDetectionStage.h +15 -4
- package/src/native/adapters/ConvolutionStage.h +101 -0
- package/src/native/adapters/CumulativeMovingAverageStage.h +264 -0
- package/src/native/adapters/DecimatorStage.h +80 -0
- package/src/native/adapters/DifferentiatorStage.h +13 -0
- package/src/native/adapters/ExponentialMovingAverageStage.h +290 -0
- package/src/native/adapters/FilterBankStage.cc +336 -0
- package/src/native/adapters/FilterBankStage.h +170 -0
- package/src/native/adapters/FilterStage.cc +122 -0
- package/src/native/adapters/FilterStage.h +4 -0
- package/src/native/adapters/HilbertEnvelopeStage.h +55 -0
- package/src/native/adapters/IntegratorStage.h +15 -0
- package/src/native/adapters/InterpolatorStage.h +51 -0
- package/src/native/adapters/LinearRegressionStage.h +40 -0
- package/src/native/adapters/LmsStage.h +63 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +76 -0
- package/src/native/adapters/MovingAverageStage.h +119 -0
- package/src/native/adapters/PeakDetectionStage.h +53 -0
- package/src/native/adapters/RectifyStage.h +14 -0
- package/src/native/adapters/ResamplerStage.h +67 -0
- package/src/native/adapters/RlsStage.h +76 -0
- package/src/native/adapters/RmsStage.h +72 -0
- package/src/native/adapters/SnrStage.h +45 -0
- package/src/native/adapters/SquareStage.h +78 -0
- package/src/native/adapters/SscStage.h +65 -0
- package/src/native/adapters/StftStage.h +62 -0
- package/src/native/adapters/VarianceStage.h +59 -0
- package/src/native/adapters/WampStage.h +59 -0
- package/src/native/adapters/WaveformLengthStage.h +51 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +64 -0
- package/src/native/core/CumulativeMovingAverageFilter.h +123 -0
- package/src/native/core/ExponentialMovingAverageFilter.h +129 -0
- package/src/native/core/FilterBankDesign.h +266 -0
- package/src/native/core/Policies.h +124 -0
- package/src/native/utils/CircularBufferArray.cc +2 -1
- package/src/native/utils/SimdOps.h +67 -0
- package/src/native/utils/Toon.h +195 -0
|
@@ -1,37 +1,42 @@
|
|
|
1
1
|
#include "DspPipeline.h"
|
|
2
|
-
#include "adapters/MovingAverageStage.h"
|
|
3
|
-
#include "adapters/
|
|
4
|
-
#include "adapters/
|
|
5
|
-
#include "adapters/
|
|
6
|
-
#include "adapters/
|
|
7
|
-
#include "adapters/
|
|
8
|
-
#include "adapters/
|
|
9
|
-
#include "adapters/
|
|
10
|
-
#include "adapters/
|
|
11
|
-
#include "adapters/
|
|
12
|
-
#include "adapters/
|
|
13
|
-
#include "adapters/
|
|
14
|
-
#include "adapters/
|
|
15
|
-
#include "adapters/
|
|
16
|
-
#include "adapters/
|
|
17
|
-
#include "adapters/
|
|
18
|
-
#include "adapters/
|
|
19
|
-
#include "adapters/
|
|
20
|
-
#include "adapters/
|
|
21
|
-
#include "adapters/
|
|
22
|
-
#include "adapters/
|
|
23
|
-
#include "adapters/
|
|
24
|
-
#include "adapters/
|
|
25
|
-
#include "adapters/
|
|
26
|
-
#include "adapters/
|
|
27
|
-
#include "adapters/
|
|
28
|
-
#include "adapters/
|
|
29
|
-
#include "adapters/
|
|
30
|
-
#include "adapters/
|
|
31
|
-
#include "adapters/
|
|
32
|
-
#include "adapters/
|
|
33
|
-
#include "adapters/
|
|
34
|
-
#include "adapters/
|
|
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
|
-
//
|
|
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 ¶ms)
|
|
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 ¶ms)
|
|
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 ¶ms)
|
|
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 ¶ms)
|
|
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 ¶ms)
|
|
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 ¶ms)
|
|
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 ¶ms)
|
|
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> &×tampRef
|
|
1235
|
+
Napi::Reference<Napi::Float32Array> &×tampRef,
|
|
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
|
-
//
|
|
1106
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1322
|
+
|
|
1323
|
+
if (inputIdx >= (currentSize / m_channels))
|
|
1163
1324
|
{
|
|
1164
|
-
|
|
1165
|
-
timestamp = m_timestamps[
|
|
1166
|
-
static_cast<float>((inputTime -
|
|
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 >= (
|
|
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
|
|
1197
|
-
|
|
1198
|
-
|
|
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
|
-
|
|
1205
|
-
|
|
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(
|
|
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
|
-
//
|
|
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() >=
|
|
1468
|
+
if (info.Length() >= 3 && info[1].IsTypedArray())
|
|
1293
1469
|
{
|
|
1294
|
-
//
|
|
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
|
-
//
|
|
1310
|
-
|
|
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 =
|
|
1314
|
-
|
|
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
|
-
|
|
1329
|
-
|
|
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
|
-
//
|
|
1348
|
-
|
|
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
|
-
//
|
|
1351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1728
|
+
// --- Legacy JSON Path ---
|
|
1729
|
+
Napi::Object stateObj = Napi::Object::New(env);
|
|
1356
1730
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1731
|
+
// Save timestamp
|
|
1732
|
+
stateObj.Set("timestamp", static_cast<double>(std::time(nullptr)));
|
|
1359
1733
|
|
|
1360
|
-
//
|
|
1361
|
-
|
|
1734
|
+
// Save pipeline configuration and full state
|
|
1735
|
+
Napi::Array stagesArray = Napi::Array::New(env, m_stages.size());
|
|
1362
1736
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1737
|
+
for (size_t i = 0; i < m_stages.size(); ++i)
|
|
1738
|
+
{
|
|
1739
|
+
Napi::Object stageConfig = Napi::Object::New(env);
|
|
1365
1740
|
|
|
1366
|
-
|
|
1367
|
-
|
|
1741
|
+
stageConfig.Set("index", static_cast<uint32_t>(i));
|
|
1742
|
+
stageConfig.Set("type", m_stages[i]->getType());
|
|
1368
1743
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
*
|
|
1379
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|