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,307 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../IDspStage.h"
|
|
4
|
+
#include "../core/MovingVarianceFilter.h"
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <stdexcept>
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <numeric> // For std::accumulate
|
|
10
|
+
#include <algorithm> // For std::max
|
|
11
|
+
|
|
12
|
+
namespace dsp::adapters
|
|
13
|
+
{
|
|
14
|
+
enum class VarianceMode
|
|
15
|
+
{
|
|
16
|
+
Batch,
|
|
17
|
+
Moving
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class VarianceStage : public IDspStage
|
|
21
|
+
{
|
|
22
|
+
public:
|
|
23
|
+
/**
|
|
24
|
+
* @brief Constructs a new Variance Stage.
|
|
25
|
+
* @param mode The variance mode (Batch or Moving).
|
|
26
|
+
* @param window_size The window size in samples (0 if using duration-based).
|
|
27
|
+
* @param window_duration_ms The window duration in milliseconds (0 if using size-based).
|
|
28
|
+
*/
|
|
29
|
+
explicit VarianceStage(VarianceMode mode, size_t window_size = 0, double window_duration_ms = 0.0)
|
|
30
|
+
: m_mode(mode),
|
|
31
|
+
m_window_size(window_size),
|
|
32
|
+
m_window_duration_ms(window_duration_ms),
|
|
33
|
+
m_is_initialized(window_size > 0)
|
|
34
|
+
{
|
|
35
|
+
if (m_mode == VarianceMode::Moving && window_size == 0 && window_duration_ms == 0.0)
|
|
36
|
+
{
|
|
37
|
+
throw std::invalid_argument("Variance: either window size or window duration must be greater than 0 for 'moving' mode");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Return the type identifier for this stage
|
|
42
|
+
const char *getType() const override
|
|
43
|
+
{
|
|
44
|
+
return "variance";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Implementation of the interface method
|
|
48
|
+
void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
|
|
49
|
+
{
|
|
50
|
+
if (m_mode == VarianceMode::Batch)
|
|
51
|
+
{
|
|
52
|
+
processBatch(buffer, numSamples, numChannels);
|
|
53
|
+
}
|
|
54
|
+
else // VarianceMode::Moving
|
|
55
|
+
{
|
|
56
|
+
processMoving(buffer, numSamples, numChannels, timestamps);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Serialize the stage's state
|
|
61
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
62
|
+
{
|
|
63
|
+
Napi::Object state = Napi::Object::New(env);
|
|
64
|
+
std::string modeStr = (m_mode == VarianceMode::Moving) ? "moving" : "batch";
|
|
65
|
+
state.Set("mode", modeStr);
|
|
66
|
+
|
|
67
|
+
if (m_mode == VarianceMode::Moving)
|
|
68
|
+
{
|
|
69
|
+
state.Set("windowSize", static_cast<uint32_t>(m_window_size));
|
|
70
|
+
state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
|
|
71
|
+
|
|
72
|
+
// Serialize each channel's filter state
|
|
73
|
+
Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
|
|
74
|
+
for (size_t i = 0; i < m_filters.size(); ++i)
|
|
75
|
+
{
|
|
76
|
+
Napi::Object channelState = Napi::Object::New(env);
|
|
77
|
+
|
|
78
|
+
// Get the filter's internal state
|
|
79
|
+
auto [bufferData, sums] = m_filters[i].getState();
|
|
80
|
+
auto [runningSum, runningSumOfSquares] = sums;
|
|
81
|
+
|
|
82
|
+
// Convert buffer data to JavaScript array
|
|
83
|
+
Napi::Array bufferArray = Napi::Array::New(env, bufferData.size());
|
|
84
|
+
for (size_t j = 0; j < bufferData.size(); ++j)
|
|
85
|
+
{
|
|
86
|
+
bufferArray.Set(j, Napi::Number::New(env, bufferData[j]));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
channelState.Set("buffer", bufferArray);
|
|
90
|
+
channelState.Set("runningSum", Napi::Number::New(env, runningSum));
|
|
91
|
+
channelState.Set("runningSumOfSquares", Napi::Number::New(env, runningSumOfSquares));
|
|
92
|
+
|
|
93
|
+
channelsArray.Set(static_cast<uint32_t>(i), channelState);
|
|
94
|
+
}
|
|
95
|
+
state.Set("channels", channelsArray);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return state;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Deserialize and restore the stage's state
|
|
102
|
+
void deserializeState(const Napi::Object &state) override
|
|
103
|
+
{
|
|
104
|
+
std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
|
|
105
|
+
VarianceMode newMode = (modeStr == "moving") ? VarianceMode::Moving : VarianceMode::Batch;
|
|
106
|
+
|
|
107
|
+
if (newMode != m_mode)
|
|
108
|
+
{
|
|
109
|
+
throw std::runtime_error("Variance mode mismatch during deserialization");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (m_mode == VarianceMode::Moving)
|
|
113
|
+
{
|
|
114
|
+
// Get window size and validate
|
|
115
|
+
size_t windowSize = state.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
116
|
+
if (windowSize != m_window_size)
|
|
117
|
+
{
|
|
118
|
+
throw std::runtime_error("Window size mismatch during deserialization");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get number of channels
|
|
122
|
+
uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
|
|
123
|
+
|
|
124
|
+
// Recreate filters
|
|
125
|
+
m_filters.clear();
|
|
126
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
127
|
+
{
|
|
128
|
+
m_filters.emplace_back(m_window_size);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Restore each channel's state
|
|
132
|
+
Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
|
|
133
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
134
|
+
{
|
|
135
|
+
Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
|
|
136
|
+
|
|
137
|
+
// Get buffer data
|
|
138
|
+
Napi::Array bufferArray = channelState.Get("buffer").As<Napi::Array>();
|
|
139
|
+
std::vector<float> bufferData;
|
|
140
|
+
bufferData.reserve(bufferArray.Length());
|
|
141
|
+
for (uint32_t j = 0; j < bufferArray.Length(); ++j)
|
|
142
|
+
{
|
|
143
|
+
bufferData.push_back(bufferArray.Get(j).As<Napi::Number>().FloatValue());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get running sums
|
|
147
|
+
float runningSum = channelState.Get("runningSum").As<Napi::Number>().FloatValue();
|
|
148
|
+
float runningSumOfSquares = channelState.Get("runningSumOfSquares").As<Napi::Number>().FloatValue();
|
|
149
|
+
|
|
150
|
+
// --- Validation (similar to RmsStage and MovingAverageStage) ---
|
|
151
|
+
float actualSum = 0.0f;
|
|
152
|
+
float actualSumOfSquares = 0.0f;
|
|
153
|
+
for (const auto &val : bufferData)
|
|
154
|
+
{
|
|
155
|
+
actualSum += val;
|
|
156
|
+
actualSumOfSquares += val * val;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const float toleranceSum = 0.0001f * std::max(1.0f, std::abs(actualSum));
|
|
160
|
+
if (std::abs(runningSum - actualSum) > toleranceSum)
|
|
161
|
+
{
|
|
162
|
+
throw std::runtime_error(
|
|
163
|
+
"Running sum validation failed: expected " +
|
|
164
|
+
std::to_string(actualSum) + " but got " +
|
|
165
|
+
std::to_string(runningSum));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const float toleranceSq = 0.0001f * std::max(1.0f, std::abs(actualSumOfSquares));
|
|
169
|
+
if (std::abs(runningSumOfSquares - actualSumOfSquares) > toleranceSq)
|
|
170
|
+
{
|
|
171
|
+
throw std::runtime_error(
|
|
172
|
+
"Running sum of squares validation failed: expected " +
|
|
173
|
+
std::to_string(actualSumOfSquares) + " but got " +
|
|
174
|
+
std::to_string(runningSumOfSquares));
|
|
175
|
+
}
|
|
176
|
+
// --- End Validation ---
|
|
177
|
+
|
|
178
|
+
// Restore the filter's state
|
|
179
|
+
m_filters[i].setState(bufferData, runningSum, runningSumOfSquares);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Reset all filters to initial state
|
|
185
|
+
void reset() override
|
|
186
|
+
{
|
|
187
|
+
for (auto &filter : m_filters)
|
|
188
|
+
{
|
|
189
|
+
filter.clear();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private:
|
|
194
|
+
/**
|
|
195
|
+
* @brief Statelessly calculates the variance for each channel
|
|
196
|
+
* and overwrites all samples in that channel with the result.
|
|
197
|
+
*/
|
|
198
|
+
void processBatch(float *buffer, size_t numSamples, int numChannels)
|
|
199
|
+
{
|
|
200
|
+
for (int c = 0; c < numChannels; ++c)
|
|
201
|
+
{
|
|
202
|
+
size_t numSamplesPerChannel = numSamples / numChannels;
|
|
203
|
+
if (numSamplesPerChannel == 0)
|
|
204
|
+
continue;
|
|
205
|
+
|
|
206
|
+
double sum = 0.0;
|
|
207
|
+
double sum_sq = 0.0;
|
|
208
|
+
|
|
209
|
+
// First pass: Calculate sums
|
|
210
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
211
|
+
{
|
|
212
|
+
double val = static_cast<double>(buffer[i]);
|
|
213
|
+
sum += val;
|
|
214
|
+
sum_sq += val * val;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Calculate variance
|
|
218
|
+
double mean = sum / numSamplesPerChannel;
|
|
219
|
+
double mean_sq = sum_sq / numSamplesPerChannel;
|
|
220
|
+
float variance = static_cast<float>(std::max(0.0, mean_sq - (mean * mean)));
|
|
221
|
+
|
|
222
|
+
// Second pass: Fill this channel's buffer with the single variance value
|
|
223
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
224
|
+
{
|
|
225
|
+
buffer[i] = variance;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @brief Statefully processes samples using the moving variance filters.
|
|
232
|
+
*/
|
|
233
|
+
void processMoving(float *buffer, size_t numSamples, int numChannels, const float *timestamps)
|
|
234
|
+
{
|
|
235
|
+
// Determine if we're in time-aware mode
|
|
236
|
+
bool useTimeAware = (m_window_duration_ms > 0.0) && timestamps != nullptr;
|
|
237
|
+
|
|
238
|
+
// Lazy initialization: convert windowDuration to windowSize if needed
|
|
239
|
+
if (!m_is_initialized && m_window_duration_ms > 0.0)
|
|
240
|
+
{
|
|
241
|
+
if (timestamps != nullptr && numSamples > 1)
|
|
242
|
+
{
|
|
243
|
+
// Estimate sample rate from timestamps
|
|
244
|
+
size_t samples_to_check = std::min(numSamples, size_t(10));
|
|
245
|
+
double total_time_ms = timestamps[samples_to_check - 1] - timestamps[0];
|
|
246
|
+
double avg_sample_period_ms = total_time_ms / (samples_to_check - 1);
|
|
247
|
+
double estimated_sample_rate = 1000.0 / avg_sample_period_ms;
|
|
248
|
+
|
|
249
|
+
// Use 3x the estimated size for time-aware mode
|
|
250
|
+
size_t estimated_size = static_cast<size_t>((m_window_duration_ms / 1000.0) * estimated_sample_rate);
|
|
251
|
+
m_window_size = std::max(size_t(1), estimated_size * 3);
|
|
252
|
+
|
|
253
|
+
m_is_initialized = true;
|
|
254
|
+
}
|
|
255
|
+
else
|
|
256
|
+
{
|
|
257
|
+
throw std::runtime_error("Variance: windowDuration was set, but timestamps are not available to derive sample rate");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Lazily initialize our filters, one for each channel
|
|
262
|
+
if (m_filters.size() != static_cast<size_t>(numChannels))
|
|
263
|
+
{
|
|
264
|
+
m_filters.clear();
|
|
265
|
+
for (int i = 0; i < numChannels; ++i)
|
|
266
|
+
{
|
|
267
|
+
if (useTimeAware)
|
|
268
|
+
{
|
|
269
|
+
// Create time-aware filter
|
|
270
|
+
m_filters.emplace_back(m_window_size, m_window_duration_ms);
|
|
271
|
+
}
|
|
272
|
+
else
|
|
273
|
+
{
|
|
274
|
+
// Create regular filter
|
|
275
|
+
m_filters.emplace_back(m_window_size);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Process the buffer sample by sample, de-interleaving
|
|
281
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
282
|
+
{
|
|
283
|
+
int channel = i % numChannels;
|
|
284
|
+
size_t sample_index = i / numChannels;
|
|
285
|
+
|
|
286
|
+
if (useTimeAware)
|
|
287
|
+
{
|
|
288
|
+
// Use time-aware processing
|
|
289
|
+
buffer[i] = m_filters[channel].addSampleWithTimestamp(buffer[i], timestamps[sample_index]);
|
|
290
|
+
}
|
|
291
|
+
else
|
|
292
|
+
{
|
|
293
|
+
// Use sample-count processing
|
|
294
|
+
buffer[i] = m_filters[channel].addSample(buffer[i]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
VarianceMode m_mode;
|
|
300
|
+
size_t m_window_size;
|
|
301
|
+
double m_window_duration_ms;
|
|
302
|
+
bool m_is_initialized;
|
|
303
|
+
// We need a separate filter instance for each channel's state
|
|
304
|
+
std::vector<dsp::core::MovingVarianceFilter<float>> m_filters;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
} // namespace dsp::adapters
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../IDspStage.h"
|
|
4
|
+
#include "../core/WampFilter.h"
|
|
5
|
+
#include "../utils/NapiUtils.h"
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
|
|
10
|
+
namespace dsp::adapters
|
|
11
|
+
{
|
|
12
|
+
class WampStage : public IDspStage
|
|
13
|
+
{
|
|
14
|
+
public:
|
|
15
|
+
explicit WampStage(size_t window_size, float threshold)
|
|
16
|
+
: m_window_size(window_size), m_threshold(threshold)
|
|
17
|
+
{
|
|
18
|
+
if (window_size == 0)
|
|
19
|
+
{
|
|
20
|
+
throw std::invalid_argument("WAMP: window size must be greater than 0");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const char *getType() const override { return "willisonAmplitude"; }
|
|
25
|
+
|
|
26
|
+
void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
|
|
27
|
+
{
|
|
28
|
+
if (m_filters.size() != numChannels)
|
|
29
|
+
{
|
|
30
|
+
m_filters.clear();
|
|
31
|
+
for (int i = 0; i < numChannels; ++i)
|
|
32
|
+
{
|
|
33
|
+
m_filters.emplace_back(m_window_size, m_threshold);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
38
|
+
{
|
|
39
|
+
int channel = i % numChannels;
|
|
40
|
+
buffer[i] = m_filters[channel].addSample(buffer[i]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void reset() override
|
|
45
|
+
{
|
|
46
|
+
for (auto &filter : m_filters)
|
|
47
|
+
{
|
|
48
|
+
filter.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
53
|
+
{
|
|
54
|
+
Napi::Object state = Napi::Object::New(env);
|
|
55
|
+
state.Set("windowSize", static_cast<uint32_t>(m_window_size));
|
|
56
|
+
state.Set("threshold", Napi::Number::New(env, m_threshold));
|
|
57
|
+
state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
|
|
58
|
+
|
|
59
|
+
Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
|
|
60
|
+
for (size_t i = 0; i < m_filters.size(); ++i)
|
|
61
|
+
{
|
|
62
|
+
Napi::Object channelState = Napi::Object::New(env);
|
|
63
|
+
auto [internalState, prevSample] = m_filters[i].getState();
|
|
64
|
+
auto [bufferData, runningCount] = internalState;
|
|
65
|
+
|
|
66
|
+
channelState.Set("buffer", dsp::utils::VectorToNapiArray(env, bufferData));
|
|
67
|
+
channelState.Set("runningCount", static_cast<uint32_t>(runningCount));
|
|
68
|
+
channelState.Set("previousSample", Napi::Number::New(env, prevSample));
|
|
69
|
+
channelsArray.Set(static_cast<uint32_t>(i), channelState);
|
|
70
|
+
}
|
|
71
|
+
state.Set("channels", channelsArray);
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
void deserializeState(const Napi::Object &state) override
|
|
76
|
+
{
|
|
77
|
+
size_t windowSize = state.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
78
|
+
float threshold = state.Get("threshold").As<Napi::Number>().FloatValue();
|
|
79
|
+
if (windowSize != m_window_size || threshold != m_threshold)
|
|
80
|
+
{
|
|
81
|
+
throw std::runtime_error("WAMP parameter mismatch during deserialization");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
|
|
85
|
+
m_filters.clear();
|
|
86
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
87
|
+
{
|
|
88
|
+
m_filters.emplace_back(m_window_size, m_threshold);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
|
|
92
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
93
|
+
{
|
|
94
|
+
Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
|
|
95
|
+
std::vector<bool> bufferData = dsp::utils::NapiArrayToVector<bool>(channelState.Get("buffer").As<Napi::Array>());
|
|
96
|
+
size_t runningCount = channelState.Get("runningCount").As<Napi::Number>().Uint32Value();
|
|
97
|
+
float prevSample = channelState.Get("previousSample").As<Napi::Number>().FloatValue();
|
|
98
|
+
|
|
99
|
+
if (!dsp::core::CounterPolicy::validateState(runningCount, bufferData))
|
|
100
|
+
{
|
|
101
|
+
throw std::runtime_error("WAMP running count validation failed");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
m_filters[i].setState(bufferData, runningCount, prevSample);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private:
|
|
109
|
+
size_t m_window_size;
|
|
110
|
+
float m_threshold;
|
|
111
|
+
std::vector<dsp::core::WampFilter<float>> m_filters;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
} // namespace dsp::adapters
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../IDspStage.h"
|
|
4
|
+
#include "../core/WaveformLengthFilter.h"
|
|
5
|
+
#include "../utils/NapiUtils.h"
|
|
6
|
+
#include <vector>
|
|
7
|
+
#include <string>
|
|
8
|
+
#include <stdexcept>
|
|
9
|
+
|
|
10
|
+
namespace dsp::adapters
|
|
11
|
+
{
|
|
12
|
+
class WaveformLengthStage : public IDspStage
|
|
13
|
+
{
|
|
14
|
+
public:
|
|
15
|
+
explicit WaveformLengthStage(size_t window_size) : m_window_size(window_size)
|
|
16
|
+
{
|
|
17
|
+
if (window_size == 0)
|
|
18
|
+
{
|
|
19
|
+
throw std::invalid_argument("WaveformLength: window size must be greater than 0");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const char *getType() const override { return "waveformLength"; }
|
|
24
|
+
|
|
25
|
+
void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) override
|
|
26
|
+
{
|
|
27
|
+
if (m_filters.size() != numChannels)
|
|
28
|
+
{
|
|
29
|
+
m_filters.clear();
|
|
30
|
+
for (int i = 0; i < numChannels; ++i)
|
|
31
|
+
{
|
|
32
|
+
m_filters.emplace_back(m_window_size);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
37
|
+
{
|
|
38
|
+
int channel = i % numChannels;
|
|
39
|
+
buffer[i] = m_filters[channel].addSample(buffer[i]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void reset() override
|
|
44
|
+
{
|
|
45
|
+
for (auto &filter : m_filters)
|
|
46
|
+
{
|
|
47
|
+
filter.clear();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
Napi::Object serializeState(Napi::Env env) const override
|
|
52
|
+
{
|
|
53
|
+
Napi::Object state = Napi::Object::New(env);
|
|
54
|
+
state.Set("windowSize", static_cast<uint32_t>(m_window_size));
|
|
55
|
+
state.Set("numChannels", static_cast<uint32_t>(m_filters.size()));
|
|
56
|
+
|
|
57
|
+
Napi::Array channelsArray = Napi::Array::New(env, m_filters.size());
|
|
58
|
+
for (size_t i = 0; i < m_filters.size(); ++i)
|
|
59
|
+
{
|
|
60
|
+
Napi::Object channelState = Napi::Object::New(env);
|
|
61
|
+
// getState() returns: { {buffer, runningSum}, prevSample }
|
|
62
|
+
auto state = m_filters[i].getState();
|
|
63
|
+
const auto &internalState = state.first; // std::pair<std::vector<float>, double>
|
|
64
|
+
float prevSample = state.second;
|
|
65
|
+
|
|
66
|
+
const std::vector<float> &bufferData = internalState.first;
|
|
67
|
+
double runningSum = internalState.second;
|
|
68
|
+
|
|
69
|
+
channelState.Set("buffer", dsp::utils::VectorToNapiArray(env, bufferData));
|
|
70
|
+
channelState.Set("runningSum", Napi::Number::New(env, runningSum));
|
|
71
|
+
channelState.Set("previousSample", Napi::Number::New(env, prevSample));
|
|
72
|
+
channelsArray.Set(static_cast<uint32_t>(i), channelState);
|
|
73
|
+
}
|
|
74
|
+
state.Set("channels", channelsArray);
|
|
75
|
+
return state;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
void deserializeState(const Napi::Object &state) override
|
|
79
|
+
{
|
|
80
|
+
size_t windowSize = state.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
81
|
+
if (windowSize != m_window_size)
|
|
82
|
+
{
|
|
83
|
+
throw std::runtime_error("Window size mismatch during deserialization");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
|
|
87
|
+
m_filters.clear();
|
|
88
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
89
|
+
{
|
|
90
|
+
m_filters.emplace_back(m_window_size);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
|
|
94
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
95
|
+
{
|
|
96
|
+
Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
|
|
97
|
+
std::vector<float> bufferData = dsp::utils::NapiArrayToVector<float>(channelState.Get("buffer").As<Napi::Array>());
|
|
98
|
+
double runningSum = channelState.Get("runningSum").As<Napi::Number>().DoubleValue();
|
|
99
|
+
float prevSample = channelState.Get("previousSample").As<Napi::Number>().FloatValue();
|
|
100
|
+
|
|
101
|
+
if (!dsp::core::SumPolicy<float>::validateState(runningSum, bufferData))
|
|
102
|
+
{
|
|
103
|
+
throw std::runtime_error("WaveformLength running sum validation failed");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
m_filters[i].setState(bufferData, runningSum, prevSample);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private:
|
|
111
|
+
size_t m_window_size;
|
|
112
|
+
std::vector<dsp::core::WaveformLengthFilter<float>> m_filters;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
} // namespace dsp::adapters
|