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