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,53 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#include <napi.h>
|
|
3
|
+
|
|
4
|
+
namespace dsp
|
|
5
|
+
{
|
|
6
|
+
// This abstract class is the key.
|
|
7
|
+
// Every filter you add will implement this.
|
|
8
|
+
class IDspStage
|
|
9
|
+
{
|
|
10
|
+
public:
|
|
11
|
+
virtual ~IDspStage() = default;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @brief Returns the type identifier of this stage.
|
|
15
|
+
*
|
|
16
|
+
* @return A string identifying the stage type (e.g., "movingAverage", "notchFilter").
|
|
17
|
+
*/
|
|
18
|
+
virtual const char *getType() const = 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @brief Processes a chunk of audio data in-place.
|
|
22
|
+
*
|
|
23
|
+
* @param buffer The interleaved audio buffer.
|
|
24
|
+
* @param numSamples The total number of samples (e.g., 1024).
|
|
25
|
+
* @param numChannels The number of channels (e.g., 1, 2, 4).
|
|
26
|
+
* @param timestamps Optional array of timestamps (in milliseconds) for each sample.
|
|
27
|
+
* If nullptr, uses sample-based processing (legacy mode).
|
|
28
|
+
* If provided, must have length equal to numSamples.
|
|
29
|
+
*/
|
|
30
|
+
virtual void process(float *buffer, size_t numSamples, int numChannels, const float *timestamps = nullptr) = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @brief Serializes the stage's internal state to a Napi::Object.
|
|
34
|
+
*
|
|
35
|
+
* @param env The N-API environment for creating JavaScript objects.
|
|
36
|
+
* @return Napi::Object containing the serialized state.
|
|
37
|
+
*/
|
|
38
|
+
virtual Napi::Object serializeState(Napi::Env env) const = 0;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @brief Deserializes and restores the stage's internal state.
|
|
42
|
+
*
|
|
43
|
+
* @param state The Napi::Object containing the serialized state.
|
|
44
|
+
*/
|
|
45
|
+
virtual void deserializeState(const Napi::Object &state) = 0;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @brief Resets the stage's internal state to initial values.
|
|
49
|
+
*/
|
|
50
|
+
virtual void reset() = 0;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
} // namespace dsp
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InterpolatorStage.h
|
|
3
|
+
*
|
|
4
|
+
* Polyphase FIR interpolator for efficient upsampling by integer factor L.
|
|
5
|
+
* Inserts (L-1) zero samples between each input sample and applies anti-imaging
|
|
6
|
+
* low-pass filter to smooth the result.
|
|
7
|
+
*
|
|
8
|
+
* Algorithm:
|
|
9
|
+
* 1. Insert L-1 zeros between input samples (zero-stuffing)
|
|
10
|
+
* 2. Design anti-imaging FIR filter with cutoff at π/L
|
|
11
|
+
* 3. Use polyphase decomposition for efficiency (avoids computing on zeros)
|
|
12
|
+
* 4. Maintain state across process() calls for streaming
|
|
13
|
+
*
|
|
14
|
+
* Efficiency: Polyphase structure avoids multiplying by zeros, reducing
|
|
15
|
+
* computation significantly.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
#pragma once
|
|
19
|
+
|
|
20
|
+
#include "../IDspStage.h"
|
|
21
|
+
#include "../core/FirFilter.h"
|
|
22
|
+
#include <algorithm>
|
|
23
|
+
#include <cmath>
|
|
24
|
+
#include <stdexcept>
|
|
25
|
+
#include <vector>
|
|
26
|
+
|
|
27
|
+
namespace dsp
|
|
28
|
+
{
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Interpolator stage: Upsample signal by integer factor L
|
|
32
|
+
* Includes anti-imaging FIR low-pass filter
|
|
33
|
+
*/
|
|
34
|
+
class InterpolatorStage : public IDspStage
|
|
35
|
+
{
|
|
36
|
+
public:
|
|
37
|
+
/**
|
|
38
|
+
* Construct interpolator
|
|
39
|
+
* @param factor Interpolation factor L (output rate = input rate * L)
|
|
40
|
+
* @param order FIR filter order (must be odd)
|
|
41
|
+
* @param sampleRate Input sample rate in Hz
|
|
42
|
+
*/
|
|
43
|
+
InterpolatorStage(int factor, int order, double sampleRate)
|
|
44
|
+
: interpolationFactor_(factor), filterOrder_(order), sampleRate_(sampleRate), phaseIndex_(0)
|
|
45
|
+
{
|
|
46
|
+
if (factor < 2)
|
|
47
|
+
{
|
|
48
|
+
throw std::invalid_argument("Interpolation factor must be >= 2");
|
|
49
|
+
}
|
|
50
|
+
if (order < 3 || order % 2 == 0)
|
|
51
|
+
{
|
|
52
|
+
throw std::invalid_argument("Filter order must be odd and >= 3");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Design anti-imaging filter: cutoff at Fs/(2*L) to prevent imaging
|
|
56
|
+
double outputSampleRate = sampleRate * factor;
|
|
57
|
+
double cutoffFreq = sampleRate / 2.0; // Nyquist of input rate
|
|
58
|
+
|
|
59
|
+
// Create FIR low-pass filter (operates at output rate)
|
|
60
|
+
filter_ = std::make_unique<FirFilter>(order, outputSampleRate);
|
|
61
|
+
designLowPassFilter(cutoffFreq, outputSampleRate);
|
|
62
|
+
|
|
63
|
+
// State buffer for polyphase filtering
|
|
64
|
+
stateBuffer_.resize(filterOrder_, 0.0f);
|
|
65
|
+
stateIndex_ = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Process input samples and produce interpolated output
|
|
70
|
+
* Output size will be input size * L
|
|
71
|
+
*/
|
|
72
|
+
void process(const float *input, size_t inputSize,
|
|
73
|
+
float *output, size_t &outputSize) override
|
|
74
|
+
{
|
|
75
|
+
outputSize = 0;
|
|
76
|
+
|
|
77
|
+
if (inputSize == 0)
|
|
78
|
+
{
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Process each input sample, producing L output samples
|
|
83
|
+
for (size_t i = 0; i < inputSize; ++i)
|
|
84
|
+
{
|
|
85
|
+
// Add input sample to state buffer
|
|
86
|
+
stateBuffer_[stateIndex_] = input[i];
|
|
87
|
+
stateIndex_ = (stateIndex_ + 1) % filterOrder_;
|
|
88
|
+
|
|
89
|
+
// Generate L output samples using polyphase filter
|
|
90
|
+
for (int phase = 0; phase < interpolationFactor_; ++phase)
|
|
91
|
+
{
|
|
92
|
+
float sum = 0.0f;
|
|
93
|
+
|
|
94
|
+
// Apply polyphase filter coefficients
|
|
95
|
+
// Each phase uses every L-th coefficient
|
|
96
|
+
for (int tap = 0; tap < filterOrder_; ++tap)
|
|
97
|
+
{
|
|
98
|
+
int bufferIdx = (stateIndex_ - 1 - tap + filterOrder_) % filterOrder_;
|
|
99
|
+
int coeffIdx = phase + tap * interpolationFactor_;
|
|
100
|
+
|
|
101
|
+
if (coeffIdx < filterOrder_ * interpolationFactor_)
|
|
102
|
+
{
|
|
103
|
+
sum += stateBuffer_[bufferIdx] * polyphaseCoeffs_[coeffIdx];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
output[outputSize++] = sum * interpolationFactor_; // Gain correction
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
void reset() override
|
|
113
|
+
{
|
|
114
|
+
std::fill(stateBuffer_.begin(), stateBuffer_.end(), 0.0f);
|
|
115
|
+
stateIndex_ = 0;
|
|
116
|
+
phaseIndex_ = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
std::string getName() const override
|
|
120
|
+
{
|
|
121
|
+
return "Interpolator(L=" + std::to_string(interpolationFactor_) + ")";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get interpolation factor
|
|
126
|
+
*/
|
|
127
|
+
int getFactor() const
|
|
128
|
+
{
|
|
129
|
+
return interpolationFactor_;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get filter order
|
|
134
|
+
*/
|
|
135
|
+
int getOrder() const
|
|
136
|
+
{
|
|
137
|
+
return filterOrder_;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private:
|
|
141
|
+
/**
|
|
142
|
+
* Design anti-imaging low-pass filter using windowed sinc method
|
|
143
|
+
* Creates polyphase decomposition of filter
|
|
144
|
+
*/
|
|
145
|
+
void designLowPassFilter(double cutoffFreq, double outputSampleRate)
|
|
146
|
+
{
|
|
147
|
+
const int M = filterOrder_;
|
|
148
|
+
const int L = interpolationFactor_;
|
|
149
|
+
const int totalTaps = M * L;
|
|
150
|
+
|
|
151
|
+
polyphaseCoeffs_.resize(totalTaps);
|
|
152
|
+
|
|
153
|
+
const int center = totalTaps / 2;
|
|
154
|
+
const double fc = cutoffFreq / outputSampleRate;
|
|
155
|
+
const double omega_c = 2.0 * M_PI * fc;
|
|
156
|
+
|
|
157
|
+
// Design prototype filter at output rate
|
|
158
|
+
for (int n = 0; n < totalTaps; ++n)
|
|
159
|
+
{
|
|
160
|
+
// Sinc function
|
|
161
|
+
double t = n - center;
|
|
162
|
+
double sinc_val;
|
|
163
|
+
if (std::abs(t) < 1e-10)
|
|
164
|
+
{
|
|
165
|
+
sinc_val = omega_c / M_PI;
|
|
166
|
+
}
|
|
167
|
+
else
|
|
168
|
+
{
|
|
169
|
+
sinc_val = std::sin(omega_c * t) / (M_PI * t);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Hamming window
|
|
173
|
+
double window = 0.54 - 0.46 * std::cos(2.0 * M_PI * n / (totalTaps - 1));
|
|
174
|
+
|
|
175
|
+
polyphaseCoeffs_[n] = static_cast<float>(sinc_val * window);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Normalize coefficients
|
|
179
|
+
float sum = 0.0f;
|
|
180
|
+
for (float c : polyphaseCoeffs_)
|
|
181
|
+
{
|
|
182
|
+
sum += c;
|
|
183
|
+
}
|
|
184
|
+
for (float &c : polyphaseCoeffs_)
|
|
185
|
+
{
|
|
186
|
+
c /= sum;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
int interpolationFactor_;
|
|
191
|
+
int filterOrder_;
|
|
192
|
+
double sampleRate_;
|
|
193
|
+
|
|
194
|
+
std::unique_ptr<FirFilter> filter_;
|
|
195
|
+
std::vector<float> stateBuffer_;
|
|
196
|
+
std::vector<float> polyphaseCoeffs_;
|
|
197
|
+
size_t stateIndex_;
|
|
198
|
+
int phaseIndex_;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
} // namespace dsp
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "../IDspStage.h"
|
|
4
|
+
#include "../core/MovingAbsoluteValueFilter.h" // Include the new core filter
|
|
5
|
+
#include <vector>
|
|
6
|
+
#include <stdexcept>
|
|
7
|
+
#include <cmath>
|
|
8
|
+
#include <string>
|
|
9
|
+
#include <algorithm>
|
|
10
|
+
#include <numeric> // For std::accumulate
|
|
11
|
+
|
|
12
|
+
namespace dsp::adapters
|
|
13
|
+
{
|
|
14
|
+
enum class MavMode
|
|
15
|
+
{
|
|
16
|
+
Batch,
|
|
17
|
+
Moving
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class MeanAbsoluteValueStage : public IDspStage
|
|
21
|
+
{
|
|
22
|
+
public:
|
|
23
|
+
/**
|
|
24
|
+
* @brief Constructs a new Mean Absolute Value Stage.
|
|
25
|
+
* @param mode The MAV 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 MeanAbsoluteValueStage(MavMode 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 == MavMode::Moving && window_size == 0 && window_duration_ms == 0.0)
|
|
36
|
+
{
|
|
37
|
+
throw std::invalid_argument("MeanAbsoluteValue: 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 "meanAbsoluteValue";
|
|
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 == MavMode::Batch)
|
|
51
|
+
{
|
|
52
|
+
processBatch(buffer, numSamples, numChannels);
|
|
53
|
+
}
|
|
54
|
+
else // MavMode::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 == MavMode::Moving) ? "moving" : "batch";
|
|
65
|
+
state.Set("mode", modeStr);
|
|
66
|
+
|
|
67
|
+
if (m_mode == MavMode::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, runningSumOfAbs] = m_filters[i].getState();
|
|
80
|
+
|
|
81
|
+
// Convert buffer data to JavaScript array
|
|
82
|
+
Napi::Array bufferArray = Napi::Array::New(env, bufferData.size());
|
|
83
|
+
for (size_t j = 0; j < bufferData.size(); ++j)
|
|
84
|
+
{
|
|
85
|
+
bufferArray.Set(j, Napi::Number::New(env, bufferData[j]));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
channelState.Set("buffer", bufferArray);
|
|
89
|
+
channelState.Set("runningSum", Napi::Number::New(env, runningSumOfAbs));
|
|
90
|
+
|
|
91
|
+
channelsArray.Set(static_cast<uint32_t>(i), channelState);
|
|
92
|
+
}
|
|
93
|
+
state.Set("channels", channelsArray);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return state;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Deserialize and restore the stage's state
|
|
100
|
+
void deserializeState(const Napi::Object &state) override
|
|
101
|
+
{
|
|
102
|
+
std::string modeStr = state.Get("mode").As<Napi::String>().Utf8Value();
|
|
103
|
+
MavMode newMode = (modeStr == "moving") ? MavMode::Moving : MavMode::Batch;
|
|
104
|
+
|
|
105
|
+
if (newMode != m_mode)
|
|
106
|
+
{
|
|
107
|
+
throw std::runtime_error("MeanAbsoluteValue mode mismatch during deserialization");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (m_mode == MavMode::Moving)
|
|
111
|
+
{
|
|
112
|
+
// Get window size and validate
|
|
113
|
+
size_t windowSize = state.Get("windowSize").As<Napi::Number>().Uint32Value();
|
|
114
|
+
if (windowSize != m_window_size)
|
|
115
|
+
{
|
|
116
|
+
throw std::runtime_error("Window size mismatch during deserialization");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Get number of channels
|
|
120
|
+
uint32_t numChannels = state.Get("channels").As<Napi::Array>().Length();
|
|
121
|
+
|
|
122
|
+
// Recreate filters
|
|
123
|
+
m_filters.clear();
|
|
124
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
125
|
+
{
|
|
126
|
+
m_filters.emplace_back(m_window_size);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Restore each channel's state
|
|
130
|
+
Napi::Array channelsArray = state.Get("channels").As<Napi::Array>();
|
|
131
|
+
for (uint32_t i = 0; i < numChannels; ++i)
|
|
132
|
+
{
|
|
133
|
+
Napi::Object channelState = channelsArray.Get(i).As<Napi::Object>();
|
|
134
|
+
|
|
135
|
+
// Get buffer data
|
|
136
|
+
Napi::Array bufferArray = channelState.Get("buffer").As<Napi::Array>();
|
|
137
|
+
std::vector<float> bufferData;
|
|
138
|
+
bufferData.reserve(bufferArray.Length());
|
|
139
|
+
for (uint32_t j = 0; j < bufferArray.Length(); ++j)
|
|
140
|
+
{
|
|
141
|
+
bufferData.push_back(bufferArray.Get(j).As<Napi::Number>().FloatValue());
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Get running sum (of absolute values)
|
|
145
|
+
float runningSum = channelState.Get("runningSum").As<Napi::Number>().FloatValue();
|
|
146
|
+
|
|
147
|
+
// --- Validation ---
|
|
148
|
+
// We must re-calculate the sum of absolute values from the original buffer data
|
|
149
|
+
float actualSumOfAbs = 0.0f;
|
|
150
|
+
for (const auto &val : bufferData)
|
|
151
|
+
{
|
|
152
|
+
actualSumOfAbs += std::abs(val);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const float tolerance = 0.0001f * std::max(1.0f, std::abs(actualSumOfAbs));
|
|
156
|
+
if (std::abs(runningSum - actualSumOfAbs) > tolerance)
|
|
157
|
+
{
|
|
158
|
+
throw std::runtime_error(
|
|
159
|
+
"Running sum of absolute values validation failed: expected " +
|
|
160
|
+
std::to_string(actualSumOfAbs) + " but got " +
|
|
161
|
+
std::to_string(runningSum));
|
|
162
|
+
}
|
|
163
|
+
// --- End Validation ---
|
|
164
|
+
|
|
165
|
+
// Restore the filter's state
|
|
166
|
+
m_filters[i].setState(bufferData, runningSum);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Reset all filters to initial state
|
|
172
|
+
void reset() override
|
|
173
|
+
{
|
|
174
|
+
for (auto &filter : m_filters)
|
|
175
|
+
{
|
|
176
|
+
filter.clear();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private:
|
|
181
|
+
/**
|
|
182
|
+
* @brief Statelessly calculates the MAV for each channel
|
|
183
|
+
* and overwrites all samples in that channel with the result.
|
|
184
|
+
*/
|
|
185
|
+
void processBatch(float *buffer, size_t numSamples, int numChannels)
|
|
186
|
+
{
|
|
187
|
+
for (int c = 0; c < numChannels; ++c)
|
|
188
|
+
{
|
|
189
|
+
size_t numSamplesPerChannel = numSamples / numChannels;
|
|
190
|
+
if (numSamplesPerChannel == 0)
|
|
191
|
+
continue;
|
|
192
|
+
|
|
193
|
+
double sum_abs = 0.0;
|
|
194
|
+
|
|
195
|
+
// First pass: Calculate sum of absolute values
|
|
196
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
197
|
+
{
|
|
198
|
+
sum_abs += static_cast<double>(std::abs(buffer[i]));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Calculate MAV
|
|
202
|
+
float mav = static_cast<float>(sum_abs / numSamplesPerChannel);
|
|
203
|
+
|
|
204
|
+
// Second pass: Fill this channel's buffer with the single MAV value
|
|
205
|
+
for (size_t i = c; i < numSamples; i += numChannels)
|
|
206
|
+
{
|
|
207
|
+
buffer[i] = mav;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @brief Statefully processes samples using the moving MAV filters.
|
|
214
|
+
*/
|
|
215
|
+
void processMoving(float *buffer, size_t numSamples, int numChannels, const float *timestamps)
|
|
216
|
+
{
|
|
217
|
+
// Determine if we're in time-aware mode
|
|
218
|
+
bool useTimeAware = (m_window_duration_ms > 0.0) && timestamps != nullptr;
|
|
219
|
+
|
|
220
|
+
// Lazy initialization: convert windowDuration to windowSize if needed
|
|
221
|
+
if (!m_is_initialized && m_window_duration_ms > 0.0)
|
|
222
|
+
{
|
|
223
|
+
if (timestamps != nullptr && numSamples > 1)
|
|
224
|
+
{
|
|
225
|
+
// Estimate sample rate from timestamps
|
|
226
|
+
size_t samples_to_check = std::min(numSamples, size_t(10));
|
|
227
|
+
double total_time_ms = timestamps[samples_to_check - 1] - timestamps[0];
|
|
228
|
+
double avg_sample_period_ms = total_time_ms / (samples_to_check - 1);
|
|
229
|
+
double estimated_sample_rate = 1000.0 / avg_sample_period_ms;
|
|
230
|
+
|
|
231
|
+
// Use 3x the estimated size for time-aware mode
|
|
232
|
+
size_t estimated_size = static_cast<size_t>((m_window_duration_ms / 1000.0) * estimated_sample_rate);
|
|
233
|
+
m_window_size = std::max(size_t(1), estimated_size * 3);
|
|
234
|
+
|
|
235
|
+
m_is_initialized = true;
|
|
236
|
+
}
|
|
237
|
+
else
|
|
238
|
+
{
|
|
239
|
+
throw std::runtime_error("MeanAbsoluteValue: windowDuration was set, but timestamps are not available to derive sample rate");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Lazily initialize our filters, one for each channel
|
|
244
|
+
if (m_filters.size() != static_cast<size_t>(numChannels))
|
|
245
|
+
{
|
|
246
|
+
m_filters.clear();
|
|
247
|
+
for (int i = 0; i < numChannels; ++i)
|
|
248
|
+
{
|
|
249
|
+
if (useTimeAware)
|
|
250
|
+
{
|
|
251
|
+
// Create time-aware filter
|
|
252
|
+
m_filters.emplace_back(m_window_size, m_window_duration_ms);
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
{
|
|
256
|
+
// Create regular filter
|
|
257
|
+
m_filters.emplace_back(m_window_size);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Process the buffer sample by sample, de-interleaving
|
|
263
|
+
for (size_t i = 0; i < numSamples; ++i)
|
|
264
|
+
{
|
|
265
|
+
int channel = i % numChannels;
|
|
266
|
+
size_t sample_index = i / numChannels;
|
|
267
|
+
|
|
268
|
+
if (useTimeAware)
|
|
269
|
+
{
|
|
270
|
+
// Use time-aware processing
|
|
271
|
+
buffer[i] = m_filters[channel].addSampleWithTimestamp(buffer[i], timestamps[sample_index]);
|
|
272
|
+
}
|
|
273
|
+
else
|
|
274
|
+
{
|
|
275
|
+
// Use sample-count processing
|
|
276
|
+
buffer[i] = m_filters[channel].addSample(buffer[i]);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
MavMode m_mode;
|
|
282
|
+
size_t m_window_size;
|
|
283
|
+
double m_window_duration_ms;
|
|
284
|
+
bool m_is_initialized;
|
|
285
|
+
// We need a separate filter instance for each channel's state
|
|
286
|
+
std::vector<dsp::core::MovingAbsoluteValueFilter<float>> m_filters;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
} // namespace dsp::adapters
|