dspx 0.1.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +185 -0
- package/.vscode/c_cpp_properties.json +17 -0
- package/.vscode/settings.json +68 -0
- package/.vscode/tasks.json +28 -0
- package/DISCLAIMER.md +32 -0
- package/LICENSE +21 -0
- package/README.md +1803 -0
- package/ROADMAP.md +192 -0
- package/TECHNICAL_DEBT.md +165 -0
- package/binding.gyp +65 -0
- package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
- package/docs/AUTHENTICATION_SECURITY.md +396 -0
- package/docs/BACKEND_IMPROVEMENTS.md +399 -0
- package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
- package/docs/FFT_IMPLEMENTATION.md +490 -0
- package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
- package/docs/FFT_USER_GUIDE.md +494 -0
- package/docs/FILTERS_IMPLEMENTATION.md +260 -0
- package/docs/FILTER_API_GUIDE.md +418 -0
- package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
- package/docs/LOGGER_API_REFERENCE.md +350 -0
- package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
- package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
- package/docs/PHASES_5_7_SUMMARY.md +403 -0
- package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
- package/docs/SIMD_OPTIMIZATIONS.md +211 -0
- package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
- package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
- package/docs/TIMESERIES_QUICK_REF.md +85 -0
- package/docs/advanced.md +559 -0
- package/docs/time-series-guide.md +617 -0
- package/docs/time-series-migration.md +376 -0
- package/jest.config.js +37 -0
- package/package.json +42 -0
- package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
- package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
- package/scripts/test.js +24 -0
- package/src/build/dsp-ts-redis.node +0 -0
- package/src/native/DspPipeline.cc +675 -0
- package/src/native/DspPipeline.h +44 -0
- package/src/native/FftBindings.cc +817 -0
- package/src/native/FilterBindings.cc +1001 -0
- package/src/native/IDspStage.h +53 -0
- package/src/native/adapters/InterpolatorStage.h +201 -0
- package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
- package/src/native/adapters/MovingAverageStage.h +306 -0
- package/src/native/adapters/RectifyStage.h +88 -0
- package/src/native/adapters/ResamplerStage.h +238 -0
- package/src/native/adapters/RmsStage.h +299 -0
- package/src/native/adapters/SscStage.h +121 -0
- package/src/native/adapters/VarianceStage.h +307 -0
- package/src/native/adapters/WampStage.h +114 -0
- package/src/native/adapters/WaveformLengthStage.h +115 -0
- package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
- package/src/native/core/FftEngine.cc +441 -0
- package/src/native/core/FftEngine.h +224 -0
- package/src/native/core/FirFilter.cc +324 -0
- package/src/native/core/FirFilter.h +149 -0
- package/src/native/core/IirFilter.cc +576 -0
- package/src/native/core/IirFilter.h +210 -0
- package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
- package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
- package/src/native/core/MovingAverageFilter.cc +18 -0
- package/src/native/core/MovingAverageFilter.h +135 -0
- package/src/native/core/MovingFftFilter.cc +291 -0
- package/src/native/core/MovingFftFilter.h +203 -0
- package/src/native/core/MovingVarianceFilter.cc +194 -0
- package/src/native/core/MovingVarianceFilter.h +114 -0
- package/src/native/core/MovingZScoreFilter.cc +215 -0
- package/src/native/core/MovingZScoreFilter.h +113 -0
- package/src/native/core/Policies.h +352 -0
- package/src/native/core/RmsFilter.cc +18 -0
- package/src/native/core/RmsFilter.h +131 -0
- package/src/native/core/SscFilter.cc +16 -0
- package/src/native/core/SscFilter.h +137 -0
- package/src/native/core/WampFilter.cc +16 -0
- package/src/native/core/WampFilter.h +101 -0
- package/src/native/core/WaveformLengthFilter.cc +17 -0
- package/src/native/core/WaveformLengthFilter.h +98 -0
- package/src/native/utils/CircularBufferArray.cc +336 -0
- package/src/native/utils/CircularBufferArray.h +62 -0
- package/src/native/utils/CircularBufferVector.cc +145 -0
- package/src/native/utils/CircularBufferVector.h +45 -0
- package/src/native/utils/NapiUtils.cc +53 -0
- package/src/native/utils/NapiUtils.h +21 -0
- package/src/native/utils/SimdOps.h +870 -0
- package/src/native/utils/SlidingWindowFilter.cc +239 -0
- package/src/native/utils/SlidingWindowFilter.h +159 -0
- package/src/native/utils/TimeSeriesBuffer.cc +205 -0
- package/src/native/utils/TimeSeriesBuffer.h +140 -0
- package/src/ts/CircularLogBuffer.ts +87 -0
- package/src/ts/DriftDetector.ts +331 -0
- package/src/ts/TopicRouter.ts +428 -0
- package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
- package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
- package/src/ts/__tests__/Chaining.test.ts +387 -0
- package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
- package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
- package/src/ts/__tests__/DriftDetector.test.ts +389 -0
- package/src/ts/__tests__/Fft.test.ts +484 -0
- package/src/ts/__tests__/ListState.test.ts +153 -0
- package/src/ts/__tests__/Logger.test.ts +208 -0
- package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
- package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
- package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
- package/src/ts/__tests__/MovingAverage.test.ts +322 -0
- package/src/ts/__tests__/RMS.test.ts +315 -0
- package/src/ts/__tests__/Rectify.test.ts +272 -0
- package/src/ts/__tests__/Redis.test.ts +456 -0
- package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
- package/src/ts/__tests__/Tap.test.ts +164 -0
- package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
- package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
- package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
- package/src/ts/__tests__/TimeSeries.test.ts +254 -0
- package/src/ts/__tests__/TopicRouter.test.ts +332 -0
- package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
- package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
- package/src/ts/__tests__/Variance.test.ts +509 -0
- package/src/ts/__tests__/WaveformLength.test.ts +147 -0
- package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
- package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
- package/src/ts/advanced-dsp.ts +566 -0
- package/src/ts/backends.ts +1137 -0
- package/src/ts/bindings.ts +1225 -0
- package/src/ts/easter-egg.ts +42 -0
- package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
- package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
- package/src/ts/examples/MovingAverage/test-state.ts +85 -0
- package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
- package/src/ts/examples/RMS/test-state.ts +97 -0
- package/src/ts/examples/RMS/test-streaming.ts +253 -0
- package/src/ts/examples/Rectify/test-state.ts +107 -0
- package/src/ts/examples/Rectify/test-streaming.ts +242 -0
- package/src/ts/examples/Variance/test-state.ts +195 -0
- package/src/ts/examples/Variance/test-streaming.ts +260 -0
- package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
- package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
- package/src/ts/examples/advanced-dsp-examples.ts +397 -0
- package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
- package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
- package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
- package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
- package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
- package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
- package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
- package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
- package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
- package/src/ts/examples/chaining/test-chaining.ts +52 -0
- package/src/ts/examples/emg-features-example.ts +284 -0
- package/src/ts/examples/fft-example.ts +309 -0
- package/src/ts/examples/fft-examples.ts +349 -0
- package/src/ts/examples/filter-examples.ts +320 -0
- package/src/ts/examples/list-state-example.ts +131 -0
- package/src/ts/examples/logger-example.ts +91 -0
- package/src/ts/examples/notch-filter-examples.ts +243 -0
- package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
- package/src/ts/examples/phase6-7/production-observability.ts +476 -0
- package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
- package/src/ts/examples/redis/redis-example.ts +202 -0
- package/src/ts/examples/redis-example.ts +202 -0
- package/src/ts/examples/simd-benchmark.ts +126 -0
- package/src/ts/examples/tap-debugging.ts +230 -0
- package/src/ts/examples/timeseries/comparison-example.ts +290 -0
- package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
- package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
- package/src/ts/examples/waveform-length-example.ts +139 -0
- package/src/ts/fft.ts +722 -0
- package/src/ts/filters.ts +1078 -0
- package/src/ts/index.ts +120 -0
- package/src/ts/types.ts +589 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,675 @@
|
|
|
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
|
+
|
|
12
|
+
namespace dsp
|
|
13
|
+
{
|
|
14
|
+
// Forward declarations for bindings
|
|
15
|
+
extern void InitFftBindings(Napi::Env env, Napi::Object exports);
|
|
16
|
+
extern void InitFilterBindings(Napi::Env env, Napi::Object exports);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#include <iostream>
|
|
20
|
+
#include <ctime>
|
|
21
|
+
|
|
22
|
+
namespace dsp
|
|
23
|
+
{
|
|
24
|
+
|
|
25
|
+
// N-API Boilerplate: Init function
|
|
26
|
+
Napi::Object DspPipeline::Init(Napi::Env env, Napi::Object exports)
|
|
27
|
+
{
|
|
28
|
+
Napi::Function func = DefineClass(env, "DspPipeline", {
|
|
29
|
+
// Pipeline building
|
|
30
|
+
InstanceMethod("addStage", &DspPipeline::AddStage),
|
|
31
|
+
|
|
32
|
+
// Processing
|
|
33
|
+
InstanceMethod("process", &DspPipeline::ProcessAsync),
|
|
34
|
+
|
|
35
|
+
// State management (for Redis persistence from TypeScript)
|
|
36
|
+
InstanceMethod("saveState", &DspPipeline::SaveState),
|
|
37
|
+
InstanceMethod("loadState", &DspPipeline::LoadState),
|
|
38
|
+
InstanceMethod("clearState", &DspPipeline::ClearState),
|
|
39
|
+
InstanceMethod("listState", &DspPipeline::ListState),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
exports.Set("DspPipeline", func);
|
|
43
|
+
return exports;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// N-API Boilerplate: Constructor
|
|
47
|
+
DspPipeline::DspPipeline(const Napi::CallbackInfo &info)
|
|
48
|
+
: Napi::ObjectWrap<DspPipeline>(info)
|
|
49
|
+
{
|
|
50
|
+
// Config logic from TS (redis, stateKey) would go here
|
|
51
|
+
InitializeStageFactories();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize the stage factory map with all available stages
|
|
56
|
+
* This is where the methods get exposed to TypeScript
|
|
57
|
+
*/
|
|
58
|
+
void DspPipeline::InitializeStageFactories()
|
|
59
|
+
{
|
|
60
|
+
// Factory for Moving Average stage
|
|
61
|
+
m_stageFactories["movingAverage"] = [](const Napi::Object ¶ms)
|
|
62
|
+
{
|
|
63
|
+
std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
|
|
64
|
+
dsp::adapters::AverageMode mode = (modeStr == "moving") ? dsp::adapters::AverageMode::Moving : dsp::adapters::AverageMode::Batch;
|
|
65
|
+
|
|
66
|
+
size_t windowSize = 0;
|
|
67
|
+
double windowDurationMs = 0.0;
|
|
68
|
+
|
|
69
|
+
if (mode == dsp::adapters::AverageMode::Moving)
|
|
70
|
+
{
|
|
71
|
+
// Accept either windowSize or windowDuration
|
|
72
|
+
if (params.Has("windowSize"))
|
|
73
|
+
{
|
|
74
|
+
windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
75
|
+
}
|
|
76
|
+
else if (params.Has("windowDuration"))
|
|
77
|
+
{
|
|
78
|
+
// Store the duration - will be converted to windowSize on first process() call
|
|
79
|
+
// using the actual sample rate derived from timestamps
|
|
80
|
+
windowDurationMs = params.Get("windowDuration").As<Napi::Number>().DoubleValue();
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
{
|
|
84
|
+
throw std::invalid_argument("MovingAverage: either 'windowSize' or 'windowDuration' is required for 'moving' mode");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return std::make_unique<dsp::adapters::MovingAverageStage>(mode, windowSize, windowDurationMs);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Factory for RMS stage
|
|
92
|
+
m_stageFactories["rms"] = [](const Napi::Object ¶ms)
|
|
93
|
+
{
|
|
94
|
+
std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
|
|
95
|
+
dsp::adapters::RmsMode mode = (modeStr == "moving") ? dsp::adapters::RmsMode::Moving : dsp::adapters::RmsMode::Batch;
|
|
96
|
+
|
|
97
|
+
size_t windowSize = 0;
|
|
98
|
+
double windowDurationMs = 0.0;
|
|
99
|
+
|
|
100
|
+
if (mode == dsp::adapters::RmsMode::Moving)
|
|
101
|
+
{
|
|
102
|
+
if (params.Has("windowSize"))
|
|
103
|
+
{
|
|
104
|
+
windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
105
|
+
}
|
|
106
|
+
else if (params.Has("windowDuration"))
|
|
107
|
+
{
|
|
108
|
+
windowDurationMs = params.Get("windowDuration").As<Napi::Number>().DoubleValue();
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
{
|
|
112
|
+
throw std::invalid_argument("RMS: either 'windowSize' or 'windowDuration' is required for 'moving' mode");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return std::make_unique<dsp::adapters::RmsStage>(mode, windowSize, windowDurationMs);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Factory for Rectify stage
|
|
120
|
+
m_stageFactories["rectify"] = [](const Napi::Object ¶ms)
|
|
121
|
+
{
|
|
122
|
+
std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
|
|
123
|
+
dsp::adapters::RectifyMode mode = (modeStr == "half") ? dsp::adapters::RectifyMode::HalfWave : dsp::adapters::RectifyMode::FullWave;
|
|
124
|
+
return std::make_unique<dsp::adapters::RectifyStage>(mode);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Factory for Variance stage
|
|
128
|
+
m_stageFactories["variance"] = [](const Napi::Object ¶ms)
|
|
129
|
+
{
|
|
130
|
+
std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
|
|
131
|
+
dsp::adapters::VarianceMode mode = (modeStr == "moving") ? dsp::adapters::VarianceMode::Moving : dsp::adapters::VarianceMode::Batch;
|
|
132
|
+
|
|
133
|
+
size_t windowSize = 0;
|
|
134
|
+
double windowDurationMs = 0.0;
|
|
135
|
+
|
|
136
|
+
if (mode == dsp::adapters::VarianceMode::Moving)
|
|
137
|
+
{
|
|
138
|
+
if (params.Has("windowSize"))
|
|
139
|
+
{
|
|
140
|
+
windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
141
|
+
}
|
|
142
|
+
else if (params.Has("windowDuration"))
|
|
143
|
+
{
|
|
144
|
+
windowDurationMs = params.Get("windowDuration").As<Napi::Number>().DoubleValue();
|
|
145
|
+
}
|
|
146
|
+
else
|
|
147
|
+
{
|
|
148
|
+
throw std::invalid_argument("Variance: either 'windowSize' or 'windowDuration' is required for 'moving' mode");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return std::make_unique<dsp::adapters::VarianceStage>(mode, windowSize, windowDurationMs);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Factory for zScoreNormalize stage
|
|
156
|
+
m_stageFactories["zScoreNormalize"] = [](const Napi::Object ¶ms)
|
|
157
|
+
{
|
|
158
|
+
std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
|
|
159
|
+
dsp::adapters::ZScoreNormalizeMode mode = (modeStr == "moving") ? dsp::adapters::ZScoreNormalizeMode::Moving : dsp::adapters::ZScoreNormalizeMode::Batch;
|
|
160
|
+
|
|
161
|
+
size_t windowSize = 0;
|
|
162
|
+
double windowDurationMs = 0.0;
|
|
163
|
+
|
|
164
|
+
if (mode == dsp::adapters::ZScoreNormalizeMode::Moving)
|
|
165
|
+
{
|
|
166
|
+
if (params.Has("windowSize"))
|
|
167
|
+
{
|
|
168
|
+
windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
169
|
+
}
|
|
170
|
+
else if (params.Has("windowDuration"))
|
|
171
|
+
{
|
|
172
|
+
windowDurationMs = params.Get("windowDuration").As<Napi::Number>().DoubleValue();
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
{
|
|
176
|
+
throw std::invalid_argument("ZScoreNormalize: either 'windowSize' or 'windowDuration' is required for 'moving' mode");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Get optional epsilon, default to 1e-6
|
|
181
|
+
float epsilon = 1e-6f;
|
|
182
|
+
if (params.Has("epsilon"))
|
|
183
|
+
{
|
|
184
|
+
epsilon = params.Get("epsilon").As<Napi::Number>().FloatValue();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return std::make_unique<dsp::adapters::ZScoreNormalizeStage>(mode, windowSize, windowDurationMs, epsilon);
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Factory for Mean Absolute Value stage
|
|
191
|
+
m_stageFactories["meanAbsoluteValue"] = [](const Napi::Object ¶ms)
|
|
192
|
+
{
|
|
193
|
+
std::string modeStr = params.Get("mode").As<Napi::String>().Utf8Value();
|
|
194
|
+
dsp::adapters::MavMode mode = (modeStr == "moving") ? dsp::adapters::MavMode::Moving : dsp::adapters::MavMode::Batch;
|
|
195
|
+
|
|
196
|
+
size_t windowSize = 0;
|
|
197
|
+
double windowDurationMs = 0.0;
|
|
198
|
+
|
|
199
|
+
if (mode == dsp::adapters::MavMode::Moving)
|
|
200
|
+
{
|
|
201
|
+
if (params.Has("windowSize"))
|
|
202
|
+
{
|
|
203
|
+
windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
204
|
+
}
|
|
205
|
+
else if (params.Has("windowDuration"))
|
|
206
|
+
{
|
|
207
|
+
windowDurationMs = params.Get("windowDuration").As<Napi::Number>().DoubleValue();
|
|
208
|
+
}
|
|
209
|
+
else
|
|
210
|
+
{
|
|
211
|
+
throw std::invalid_argument("MeanAbsoluteValue: either 'windowSize' or 'windowDuration' is required for 'moving' mode");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return std::make_unique<dsp::adapters::MeanAbsoluteValueStage>(mode, windowSize, windowDurationMs);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Factory for Waveform Length stage
|
|
219
|
+
m_stageFactories["waveformLength"] = [](const Napi::Object ¶ms)
|
|
220
|
+
{
|
|
221
|
+
if (!params.Has("windowSize"))
|
|
222
|
+
{
|
|
223
|
+
throw std::invalid_argument("WaveformLength: 'windowSize' is required");
|
|
224
|
+
}
|
|
225
|
+
size_t windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
226
|
+
return std::make_unique<dsp::adapters::WaveformLengthStage>(windowSize);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Factory for Slope Sign Change (SSC) stage
|
|
230
|
+
m_stageFactories["slopeSignChange"] = [](const Napi::Object ¶ms)
|
|
231
|
+
{
|
|
232
|
+
if (!params.Has("windowSize"))
|
|
233
|
+
{
|
|
234
|
+
throw std::invalid_argument("SlopeSignChange: 'windowSize' is required");
|
|
235
|
+
}
|
|
236
|
+
size_t windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
237
|
+
|
|
238
|
+
float threshold = 0.0f;
|
|
239
|
+
if (params.Has("threshold"))
|
|
240
|
+
{
|
|
241
|
+
threshold = params.Get("threshold").As<Napi::Number>().FloatValue();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return std::make_unique<dsp::adapters::SscStage>(windowSize, threshold);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Factory for Willison Amplitude (WAMP) stage
|
|
248
|
+
m_stageFactories["willisonAmplitude"] = [](const Napi::Object ¶ms)
|
|
249
|
+
{
|
|
250
|
+
if (!params.Has("windowSize"))
|
|
251
|
+
{
|
|
252
|
+
throw std::invalid_argument("WillisonAmplitude: 'windowSize' is required");
|
|
253
|
+
}
|
|
254
|
+
size_t windowSize = params.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
255
|
+
|
|
256
|
+
float threshold = 0.0f;
|
|
257
|
+
if (params.Has("threshold"))
|
|
258
|
+
{
|
|
259
|
+
threshold = params.Get("threshold").As<Napi::Number>().FloatValue();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return std::make_unique<dsp::adapters::WampStage>(windowSize, threshold);
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* This is the "Factory" method.
|
|
268
|
+
* TS calls: native.addStage("movingAverage", { windowSize: 100 })
|
|
269
|
+
*/
|
|
270
|
+
Napi::Value DspPipeline::AddStage(const Napi::CallbackInfo &info)
|
|
271
|
+
{
|
|
272
|
+
Napi::Env env = info.Env();
|
|
273
|
+
|
|
274
|
+
// 1. Get arguments from TypeScript
|
|
275
|
+
std::string stageName = info[0].As<Napi::String>();
|
|
276
|
+
Napi::Object params = info[1].As<Napi::Object>();
|
|
277
|
+
|
|
278
|
+
// 2. Look up the stage factory in the map
|
|
279
|
+
auto it = m_stageFactories.find(stageName);
|
|
280
|
+
if (it != m_stageFactories.end())
|
|
281
|
+
{
|
|
282
|
+
try
|
|
283
|
+
{
|
|
284
|
+
// Factory found - create and add the stage
|
|
285
|
+
m_stages.push_back(it->second(params));
|
|
286
|
+
}
|
|
287
|
+
catch (const std::invalid_argument &e)
|
|
288
|
+
{
|
|
289
|
+
// Validation error in constructor - throw as JavaScript TypeError
|
|
290
|
+
Napi::TypeError::New(env, e.what()).ThrowAsJavaScriptException();
|
|
291
|
+
return env.Undefined();
|
|
292
|
+
}
|
|
293
|
+
catch (const std::exception &e)
|
|
294
|
+
{
|
|
295
|
+
// Other errors - throw as JavaScript Error
|
|
296
|
+
Napi::Error::New(env, e.what()).ThrowAsJavaScriptException();
|
|
297
|
+
return env.Undefined();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else
|
|
301
|
+
{
|
|
302
|
+
// Unknown stage type - throw error
|
|
303
|
+
Napi::TypeError::New(env, "Unknown stage type: " + stageName).ThrowAsJavaScriptException();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return env.Undefined();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* AsyncWorker for processing DSP pipeline in background thread
|
|
311
|
+
*/
|
|
312
|
+
class ProcessWorker : public Napi::AsyncWorker
|
|
313
|
+
{
|
|
314
|
+
public:
|
|
315
|
+
ProcessWorker(Napi::Env env,
|
|
316
|
+
Napi::Promise::Deferred deferred,
|
|
317
|
+
std::vector<std::unique_ptr<IDspStage>> &stages,
|
|
318
|
+
float *data,
|
|
319
|
+
float *timestamps,
|
|
320
|
+
size_t numSamples,
|
|
321
|
+
int channels,
|
|
322
|
+
Napi::Reference<Napi::Float32Array> &&bufferRef,
|
|
323
|
+
Napi::Reference<Napi::Float32Array> &×tampRef)
|
|
324
|
+
: Napi::AsyncWorker(env),
|
|
325
|
+
m_deferred(std::move(deferred)),
|
|
326
|
+
m_stages(stages),
|
|
327
|
+
m_data(data),
|
|
328
|
+
m_timestamps(timestamps),
|
|
329
|
+
m_numSamples(numSamples),
|
|
330
|
+
m_channels(channels),
|
|
331
|
+
m_bufferRef(std::move(bufferRef)),
|
|
332
|
+
m_timestampRef(std::move(timestampRef))
|
|
333
|
+
{
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
protected:
|
|
337
|
+
// This runs on a worker thread (not blocking the event loop)
|
|
338
|
+
void Execute() override
|
|
339
|
+
{
|
|
340
|
+
try
|
|
341
|
+
{
|
|
342
|
+
// Process the buffer through all stages
|
|
343
|
+
// Pass timestamps to stages that support time-based processing
|
|
344
|
+
for (const auto &stage : m_stages)
|
|
345
|
+
{
|
|
346
|
+
stage->process(m_data, m_numSamples, m_channels, m_timestamps);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (const std::exception &e)
|
|
350
|
+
{
|
|
351
|
+
SetError(e.what());
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// This runs on the main thread after Execute() completes
|
|
356
|
+
void OnOK() override
|
|
357
|
+
{
|
|
358
|
+
Napi::Env env = Env();
|
|
359
|
+
// Resolve the promise with the processed buffer
|
|
360
|
+
Napi::Float32Array buffer = m_bufferRef.Value();
|
|
361
|
+
m_deferred.Resolve(buffer);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
void OnError(const Napi::Error &error) override
|
|
365
|
+
{
|
|
366
|
+
m_deferred.Reject(error.Value());
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private:
|
|
370
|
+
Napi::Promise::Deferred m_deferred;
|
|
371
|
+
std::vector<std::unique_ptr<IDspStage>> &m_stages;
|
|
372
|
+
float *m_data;
|
|
373
|
+
float *m_timestamps;
|
|
374
|
+
size_t m_numSamples;
|
|
375
|
+
int m_channels;
|
|
376
|
+
Napi::Reference<Napi::Float32Array> m_bufferRef;
|
|
377
|
+
Napi::Reference<Napi::Float32Array> m_timestampRef;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* This is the "Process" method.
|
|
382
|
+
* TS calls:
|
|
383
|
+
* await native.process(buffer, timestamps, { channels: 4 })
|
|
384
|
+
* or (legacy):
|
|
385
|
+
* await native.process(buffer, { sampleRate: 2000, channels: 4 })
|
|
386
|
+
* Returns a Promise that resolves when processing is complete.
|
|
387
|
+
*/
|
|
388
|
+
Napi::Value DspPipeline::ProcessAsync(const Napi::CallbackInfo &info)
|
|
389
|
+
{
|
|
390
|
+
Napi::Env env = info.Env();
|
|
391
|
+
|
|
392
|
+
// 1. Get buffer from TypeScript (zero-copy)
|
|
393
|
+
Napi::Float32Array jsBuffer = info[0].As<Napi::Float32Array>();
|
|
394
|
+
float *data = jsBuffer.Data();
|
|
395
|
+
size_t numSamples = jsBuffer.ElementLength();
|
|
396
|
+
|
|
397
|
+
// 2. Get timestamps and options
|
|
398
|
+
// TypeScript can pass either:
|
|
399
|
+
// process(buffer, timestamps, options) - new time-based API
|
|
400
|
+
// process(buffer, options) - legacy sample-based API (timestamps = nullptr)
|
|
401
|
+
Napi::Float32Array jsTimestamps;
|
|
402
|
+
float *timestamps = nullptr;
|
|
403
|
+
Napi::Object options;
|
|
404
|
+
|
|
405
|
+
if (info.Length() >= 2 && info[1].IsTypedArray())
|
|
406
|
+
{
|
|
407
|
+
// New API: timestamps provided
|
|
408
|
+
jsTimestamps = info[1].As<Napi::Float32Array>();
|
|
409
|
+
timestamps = jsTimestamps.Data();
|
|
410
|
+
options = info[2].As<Napi::Object>();
|
|
411
|
+
|
|
412
|
+
// Validate timestamp length matches sample length
|
|
413
|
+
if (jsTimestamps.ElementLength() != numSamples)
|
|
414
|
+
{
|
|
415
|
+
Napi::TypeError::New(env, "Timestamp array length must match sample array length")
|
|
416
|
+
.ThrowAsJavaScriptException();
|
|
417
|
+
return env.Undefined();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else
|
|
421
|
+
{
|
|
422
|
+
// Legacy API: no timestamps (will use sample indices)
|
|
423
|
+
options = info[1].As<Napi::Object>();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
int channels = options.Get("channels").As<Napi::Number>().Uint32Value();
|
|
427
|
+
// int sampleRate = options.Get("sampleRate").As<Napi::Number>().Uint32Value();
|
|
428
|
+
|
|
429
|
+
// 3. Create a deferred promise and get the promise before moving
|
|
430
|
+
Napi::Promise::Deferred deferred = Napi::Promise::Deferred::New(env);
|
|
431
|
+
Napi::Promise promise = deferred.Promise();
|
|
432
|
+
|
|
433
|
+
// 4. Create references to keep buffers alive during async operation
|
|
434
|
+
Napi::Reference<Napi::Float32Array> bufferRef = Napi::Reference<Napi::Float32Array>::New(jsBuffer, 1);
|
|
435
|
+
Napi::Reference<Napi::Float32Array> timestampRef;
|
|
436
|
+
if (timestamps != nullptr)
|
|
437
|
+
{
|
|
438
|
+
timestampRef = Napi::Reference<Napi::Float32Array>::New(jsTimestamps, 1);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 5. Create and queue the worker
|
|
442
|
+
ProcessWorker *worker = new ProcessWorker(env, std::move(deferred), m_stages, data, timestamps, numSamples, channels, std::move(bufferRef), std::move(timestampRef));
|
|
443
|
+
worker->Queue();
|
|
444
|
+
|
|
445
|
+
// 6. Return the promise immediately
|
|
446
|
+
return promise;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Save current pipeline state as JSON string
|
|
451
|
+
* TypeScript will handle storing this in Redis
|
|
452
|
+
*
|
|
453
|
+
* Returns: JSON string with pipeline configuration and stage states
|
|
454
|
+
*/
|
|
455
|
+
Napi::Value DspPipeline::SaveState(const Napi::CallbackInfo &info)
|
|
456
|
+
{
|
|
457
|
+
Napi::Env env = info.Env();
|
|
458
|
+
Napi::Object stateObj = Napi::Object::New(env);
|
|
459
|
+
|
|
460
|
+
// Save timestamp
|
|
461
|
+
stateObj.Set("timestamp", static_cast<double>(std::time(nullptr)));
|
|
462
|
+
|
|
463
|
+
// Save pipeline configuration and full state
|
|
464
|
+
Napi::Array stagesArray = Napi::Array::New(env, m_stages.size());
|
|
465
|
+
|
|
466
|
+
for (size_t i = 0; i < m_stages.size(); ++i)
|
|
467
|
+
{
|
|
468
|
+
Napi::Object stageConfig = Napi::Object::New(env);
|
|
469
|
+
|
|
470
|
+
stageConfig.Set("index", static_cast<uint32_t>(i));
|
|
471
|
+
stageConfig.Set("type", m_stages[i]->getType());
|
|
472
|
+
|
|
473
|
+
// Serialize the stage's internal state
|
|
474
|
+
stageConfig.Set("state", m_stages[i]->serializeState(env));
|
|
475
|
+
|
|
476
|
+
stagesArray.Set(static_cast<uint32_t>(i), stageConfig);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
stateObj.Set("stages", stagesArray);
|
|
480
|
+
stateObj.Set("stageCount", static_cast<uint32_t>(m_stages.size()));
|
|
481
|
+
|
|
482
|
+
// Convert to JSON string using JavaScript's JSON.stringify
|
|
483
|
+
Napi::Object JSON = env.Global().Get("JSON").As<Napi::Object>();
|
|
484
|
+
Napi::Function stringify = JSON.Get("stringify").As<Napi::Function>();
|
|
485
|
+
return stringify.Call(JSON, {stateObj});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Load pipeline state from JSON string
|
|
490
|
+
* TypeScript retrieves this from Redis and passes it here
|
|
491
|
+
*
|
|
492
|
+
* Accepts: JSON string with pipeline configuration
|
|
493
|
+
*/
|
|
494
|
+
Napi::Value DspPipeline::LoadState(const Napi::CallbackInfo &info)
|
|
495
|
+
{
|
|
496
|
+
Napi::Env env = info.Env();
|
|
497
|
+
|
|
498
|
+
// Validate input
|
|
499
|
+
if (info.Length() < 1 || !info[0].IsString())
|
|
500
|
+
{
|
|
501
|
+
Napi::TypeError::New(env, "Expected state JSON string as first argument")
|
|
502
|
+
.ThrowAsJavaScriptException();
|
|
503
|
+
return env.Undefined();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
std::string stateJson = info[0].As<Napi::String>().Utf8Value();
|
|
507
|
+
|
|
508
|
+
try
|
|
509
|
+
{
|
|
510
|
+
// Parse JSON string using JavaScript's JSON.parse
|
|
511
|
+
Napi::Object JSON = env.Global().Get("JSON").As<Napi::Object>();
|
|
512
|
+
Napi::Function parse = JSON.Get("parse").As<Napi::Function>();
|
|
513
|
+
Napi::Object stateObj = parse.Call(JSON, {Napi::String::New(env, stateJson)}).As<Napi::Object>();
|
|
514
|
+
|
|
515
|
+
// Validate state object has required fields
|
|
516
|
+
if (!stateObj.Has("stages"))
|
|
517
|
+
{
|
|
518
|
+
Napi::Error::New(env, "Invalid state: missing 'stages' field")
|
|
519
|
+
.ThrowAsJavaScriptException();
|
|
520
|
+
return Napi::Boolean::New(env, false);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Get stages array
|
|
524
|
+
Napi::Array stagesArray = stateObj.Get("stages").As<Napi::Array>();
|
|
525
|
+
uint32_t stageCount = stagesArray.Length();
|
|
526
|
+
|
|
527
|
+
// Validate stage count matches
|
|
528
|
+
if (stageCount != m_stages.size())
|
|
529
|
+
{
|
|
530
|
+
Napi::Error::New(env, "Stage count mismatch: expected " +
|
|
531
|
+
std::to_string(m_stages.size()) + " but got " + std::to_string(stageCount))
|
|
532
|
+
.ThrowAsJavaScriptException();
|
|
533
|
+
return Napi::Boolean::New(env, false);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Log restoration
|
|
537
|
+
std::cout << "Restoring pipeline state with " << stageCount << " stages" << std::endl;
|
|
538
|
+
|
|
539
|
+
// Restore each stage's state
|
|
540
|
+
for (uint32_t i = 0; i < stageCount; ++i)
|
|
541
|
+
{
|
|
542
|
+
Napi::Object stageConfig = stagesArray.Get(i).As<Napi::Object>();
|
|
543
|
+
if (stageConfig.Has("state"))
|
|
544
|
+
{
|
|
545
|
+
Napi::Object stageState = stageConfig.Get("state").As<Napi::Object>();
|
|
546
|
+
m_stages[i]->deserializeState(stageState);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
std::cout << "State restoration complete!" << std::endl;
|
|
551
|
+
|
|
552
|
+
return Napi::Boolean::New(env, true);
|
|
553
|
+
}
|
|
554
|
+
catch (const std::exception &e)
|
|
555
|
+
{
|
|
556
|
+
Napi::Error::New(env, std::string("Failed to load state: ") + e.what())
|
|
557
|
+
.ThrowAsJavaScriptException();
|
|
558
|
+
return Napi::Boolean::New(env, false);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Clear all pipeline state (reset all stages)
|
|
564
|
+
* This resets filters to their initial state without removing them
|
|
565
|
+
*/
|
|
566
|
+
Napi::Value DspPipeline::ClearState(const Napi::CallbackInfo &info)
|
|
567
|
+
{
|
|
568
|
+
Napi::Env env = info.Env();
|
|
569
|
+
|
|
570
|
+
// Reset all stages
|
|
571
|
+
for (auto &stage : m_stages)
|
|
572
|
+
{
|
|
573
|
+
stage->reset();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
std::cout << "Pipeline state cleared (" << m_stages.size() << " stages reset)" << std::endl;
|
|
577
|
+
|
|
578
|
+
return env.Undefined();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* List current pipeline state (summary information)
|
|
583
|
+
* Returns a simplified view of the pipeline configuration
|
|
584
|
+
* Useful for debugging and monitoring without parsing full JSON
|
|
585
|
+
*
|
|
586
|
+
* Returns: Object with pipeline summary (stage count, types, window sizes, etc.)
|
|
587
|
+
*/
|
|
588
|
+
Napi::Value DspPipeline::ListState(const Napi::CallbackInfo &info)
|
|
589
|
+
{
|
|
590
|
+
Napi::Env env = info.Env();
|
|
591
|
+
Napi::Object summary = Napi::Object::New(env);
|
|
592
|
+
|
|
593
|
+
// Basic pipeline info
|
|
594
|
+
summary.Set("stageCount", static_cast<uint32_t>(m_stages.size()));
|
|
595
|
+
summary.Set("timestamp", static_cast<double>(std::time(nullptr)));
|
|
596
|
+
|
|
597
|
+
// Create array of stage summaries
|
|
598
|
+
Napi::Array stagesArray = Napi::Array::New(env, m_stages.size());
|
|
599
|
+
|
|
600
|
+
for (size_t i = 0; i < m_stages.size(); ++i)
|
|
601
|
+
{
|
|
602
|
+
Napi::Object stageSummary = Napi::Object::New(env);
|
|
603
|
+
|
|
604
|
+
// Basic stage info
|
|
605
|
+
stageSummary.Set("index", static_cast<uint32_t>(i));
|
|
606
|
+
stageSummary.Set("type", m_stages[i]->getType());
|
|
607
|
+
|
|
608
|
+
// Get full state to extract key info
|
|
609
|
+
Napi::Object fullState = m_stages[i]->serializeState(env);
|
|
610
|
+
|
|
611
|
+
// Extract common fields (windowSize, numChannels, mode)
|
|
612
|
+
if (fullState.Has("windowSize"))
|
|
613
|
+
{
|
|
614
|
+
stageSummary.Set("windowSize", fullState.Get("windowSize"));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (fullState.Has("numChannels"))
|
|
618
|
+
{
|
|
619
|
+
stageSummary.Set("numChannels", fullState.Get("numChannels"));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (fullState.Has("mode"))
|
|
623
|
+
{
|
|
624
|
+
stageSummary.Set("mode", fullState.Get("mode"));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Add buffer occupancy info for stateful filters
|
|
628
|
+
if (fullState.Has("channels"))
|
|
629
|
+
{
|
|
630
|
+
Napi::Array channels = fullState.Get("channels").As<Napi::Array>();
|
|
631
|
+
if (channels.Length() > 0)
|
|
632
|
+
{
|
|
633
|
+
Napi::Object firstChannel = channels.Get(uint32_t(0)).As<Napi::Object>();
|
|
634
|
+
if (firstChannel.Has("buffer"))
|
|
635
|
+
{
|
|
636
|
+
Napi::Array buffer = firstChannel.Get("buffer").As<Napi::Array>();
|
|
637
|
+
stageSummary.Set("bufferSize", buffer.Length());
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
stageSummary.Set("channelCount", channels.Length());
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
stagesArray.Set(static_cast<uint32_t>(i), stageSummary);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
summary.Set("stages", stagesArray);
|
|
647
|
+
|
|
648
|
+
return summary;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
} // namespace dsp
|
|
652
|
+
|
|
653
|
+
// Forward declare FFT bindings init
|
|
654
|
+
namespace dsp
|
|
655
|
+
{
|
|
656
|
+
void InitFftBindings(Napi::Env env, Napi::Object exports);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// This function is called by Node.js when the addon is loaded
|
|
660
|
+
Napi::Object InitAll(Napi::Env env, Napi::Object exports)
|
|
661
|
+
{
|
|
662
|
+
// Initialize DspPipeline class
|
|
663
|
+
dsp::DspPipeline::Init(env, exports);
|
|
664
|
+
|
|
665
|
+
// Initialize FFT/DFT bindings
|
|
666
|
+
dsp::InitFftBindings(env, exports);
|
|
667
|
+
|
|
668
|
+
// Initialize FIR/IIR filter bindings
|
|
669
|
+
dsp::InitFilterBindings(env, exports);
|
|
670
|
+
|
|
671
|
+
return exports;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// This line registers the module
|
|
675
|
+
NODE_API_MODULE(dsp_addon, InitAll)
|