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
package/README.md
ADDED
|
@@ -0,0 +1,1803 @@
|
|
|
1
|
+
# Work in Progress
|
|
2
|
+
|
|
3
|
+
> The project’s in heavy development.
|
|
4
|
+
> Once Phases 1 and 2 (core DSP + FFT/IIR/FIR) are stable, I’ll push the first npm release.
|
|
5
|
+
> Expect breaking changes until then!
|
|
6
|
+
|
|
7
|
+
# dspx
|
|
8
|
+
|
|
9
|
+
> **A high-performance DSP library with a built-in micro-framework for real-time pipelines, state persistence, and time-series signal processing. Powered by C++ (via N-API) and Redis for cross-session continuity — ideal for biosignals, IoT, and edge analytics.**
|
|
10
|
+
|
|
11
|
+
A modern DSP library built for Node.js backends processing real-time biosignals, audio streams, and sensor data. Features native C++ filters with full state serialization to Redis, enabling seamless processing across service restarts and distributed workers.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- 🚀 **Native C++ Performance** – Optimized circular buffers and filters with SIMD acceleration for real-time processing
|
|
18
|
+
- 🎯 **SIMD Acceleration** – AVX2/SSE2/NEON optimizations provide 2-8x speedup on batch operations and rectification
|
|
19
|
+
- 🔧 **TypeScript-First** – Full type safety with excellent IntelliSense
|
|
20
|
+
- 📡 **Redis State Persistence** – Fully implemented state serialization/deserialization including circular buffer contents and running sums
|
|
21
|
+
- ⏱️ **Time-Series Processing** – NEW! Support for irregular timestamps and time-based windows
|
|
22
|
+
- 🔗 **Fluent Pipeline API** – Chain filter operations with method chaining
|
|
23
|
+
- 🎯 **Zero-Copy Processing** – Direct TypedArray manipulation for minimal overhead
|
|
24
|
+
- 📊 **Multi-Channel Support** – Process multi-channel signals (EMG, EEG, audio) with independent filter states per channel
|
|
25
|
+
- ⚡ **Async Processing** – Background thread processing to avoid blocking the Node.js event loop
|
|
26
|
+
- 🛡️ **Crash Recovery** – Resume processing from exact state after service restarts
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🏗️ Architecture
|
|
31
|
+
|
|
32
|
+
The library uses a layered architecture with clear separation between TypeScript and C++ components:
|
|
33
|
+
|
|
34
|
+
```mermaid
|
|
35
|
+
graph TB
|
|
36
|
+
subgraph "TypeScript Layer (src/ts/)"
|
|
37
|
+
TS_API["TypeScript API<br/>bindings.ts"]
|
|
38
|
+
TS_TYPES["Type Definitions<br/>types.ts"]
|
|
39
|
+
TS_UTILS["Utilities<br/>CircularLogBuffer, TopicRouter"]
|
|
40
|
+
TS_REDIS["Redis Backend<br/>backends.ts"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
subgraph "N-API Bridge Layer"
|
|
44
|
+
NAPI["Node-API Bindings<br/>(native module)"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
subgraph "C++ Layer (src/native/)"
|
|
48
|
+
subgraph "dsp::adapters (N-API Adapters)"
|
|
49
|
+
ADAPTER_EXAMPLE["Example: MovingAverageStage<br/>(supports batch/moving modes)"]
|
|
50
|
+
ADAPTER_STATELESS["Example: RectifyStage<br/>(stateless only)"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
PIPELINE["DspPipeline.cc<br/>(Stage Orchestration)"]
|
|
54
|
+
|
|
55
|
+
subgraph "dsp::core (Pure C++ Algorithms)"
|
|
56
|
+
CORE_ENGINE["SlidingWindowFilter<br/>(Generic Stateful Engine)"]
|
|
57
|
+
CORE_FILTER["Example: MovingAverageFilter<br/>(wraps SlidingWindowFilter)<br/><b>Stateful - Moving Mode</b>"]
|
|
58
|
+
|
|
59
|
+
subgraph "Statistical Policies"
|
|
60
|
+
POLICY_EXAMPLE["Example: MeanPolicy<br/>(pure computation)<br/><b>Stateless - Batch Mode</b>"]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
subgraph "dsp::utils (Data Structures)"
|
|
65
|
+
CIRCULAR["CircularBuffer<br/>(Array & Vector)<br/><b>Stateful Storage</b>"]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph "External Services"
|
|
70
|
+
REDIS_DB[("Redis<br/>(State Persistence)")]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
%% Connections
|
|
74
|
+
TS_API --> NAPI
|
|
75
|
+
TS_REDIS --> REDIS_DB
|
|
76
|
+
NAPI --> PIPELINE
|
|
77
|
+
PIPELINE --> ADAPTER_EXAMPLE
|
|
78
|
+
PIPELINE --> ADAPTER_STATELESS
|
|
79
|
+
|
|
80
|
+
ADAPTER_EXAMPLE --> CORE_FILTER
|
|
81
|
+
ADAPTER_EXAMPLE --> POLICY_EXAMPLE
|
|
82
|
+
|
|
83
|
+
CORE_FILTER --> CORE_ENGINE
|
|
84
|
+
|
|
85
|
+
CORE_ENGINE --> POLICY_EXAMPLE
|
|
86
|
+
|
|
87
|
+
CORE_ENGINE --> CIRCULAR
|
|
88
|
+
|
|
89
|
+
%% Styling
|
|
90
|
+
classDef tsLayer fill:#3178c6,stroke:#235a97,color:#fff
|
|
91
|
+
classDef cppCore fill:#00599c,stroke:#003f6f,color:#fff
|
|
92
|
+
classDef cppEngine fill:#1a8cff,stroke:#0066cc,color:#fff
|
|
93
|
+
classDef cppPolicy fill:#66b3ff,stroke:#3399ff,color:#000
|
|
94
|
+
classDef cppAdapter fill:#659ad2,stroke:#4a7ba7,color:#fff
|
|
95
|
+
classDef cppUtils fill:#a8c5e2,stroke:#7a9fbe,color:#000
|
|
96
|
+
classDef external fill:#dc382d,stroke:#a82820,color:#fff
|
|
97
|
+
|
|
98
|
+
class TS_API,TS_TYPES,TS_UTILS,TS_REDIS tsLayer
|
|
99
|
+
class CORE_MA,CORE_RMS,CORE_MAV,CORE_VAR,CORE_ZSCORE cppCore
|
|
100
|
+
class CORE_ENGINE cppEngine
|
|
101
|
+
class POLICY_MEAN,POLICY_RMS,POLICY_MAV,POLICY_VAR,POLICY_ZSCORE cppPolicy
|
|
102
|
+
class ADAPTER_MA,ADAPTER_RMS,ADAPTER_RECT,ADAPTER_VAR,ADAPTER_ZSCORE cppAdapter
|
|
103
|
+
class CIRCULAR cppUtils
|
|
104
|
+
class REDIS_DB external
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Key Architectural Principles
|
|
108
|
+
|
|
109
|
+
**1. Namespace Separation**
|
|
110
|
+
|
|
111
|
+
- **`dsp::core`**: Pure C++ algorithms with no Node.js dependencies. Reusable in other C++ projects.
|
|
112
|
+
- **`dsp::adapters`**: N-API wrapper classes that expose core algorithms to JavaScript.
|
|
113
|
+
- **`dsp::utils`**: Shared data structures (circular buffers) used by core algorithms.
|
|
114
|
+
|
|
115
|
+
**2. Policy-Based Design (Zero-Cost Abstraction)**
|
|
116
|
+
|
|
117
|
+
The sliding window filters use **compile-time polymorphism** via template policies:
|
|
118
|
+
|
|
119
|
+
- **`SlidingWindowFilter<T, Policy>`**: Generic engine handling circular buffer and window logic
|
|
120
|
+
- **Statistical Policies**: Define specific computations (MeanPolicy, RmsPolicy, VariancePolicy, etc.)
|
|
121
|
+
- **Zero overhead**: Compiler inlines policy methods, achieving hand-written performance
|
|
122
|
+
- **Extensibility**: New statistical measures require only a new policy class
|
|
123
|
+
|
|
124
|
+
Example: `MovingAverageFilter` is a thin wrapper around `SlidingWindowFilter<float, MeanPolicy<float>>`
|
|
125
|
+
|
|
126
|
+
**2.1 Layered State Management**
|
|
127
|
+
|
|
128
|
+
State serialization follows a **3-tier delegation pattern** where each layer manages only its own state:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
Policy Layer → getState() returns policy-specific state (running sums, counters, etc.)
|
|
132
|
+
↓
|
|
133
|
+
SlidingWindowFilter → getState() returns {buffer contents, policy state}
|
|
134
|
+
↓
|
|
135
|
+
Filter Wrapper → getState() delegates to SlidingWindowFilter
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Example Implementation:**
|
|
139
|
+
|
|
140
|
+
```cpp
|
|
141
|
+
// Policy manages its own statistical state
|
|
142
|
+
template <typename T>
|
|
143
|
+
struct MeanPolicy {
|
|
144
|
+
T m_sum{}; // Policy-specific state
|
|
145
|
+
|
|
146
|
+
auto getState() const -> T { return m_sum; }
|
|
147
|
+
void setState(T sum) { m_sum = sum; }
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// SlidingWindowFilter coordinates buffer + policy state
|
|
151
|
+
template <typename T, typename Policy>
|
|
152
|
+
class SlidingWindowFilter {
|
|
153
|
+
auto getState() const {
|
|
154
|
+
return std::make_pair(m_buffer.toVector(), m_policy.getState());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
template <typename PolicyState>
|
|
158
|
+
void setState(const std::vector<T>& bufferData, const PolicyState& policyState) {
|
|
159
|
+
m_buffer.fromVector(bufferData);
|
|
160
|
+
m_policy.setState(policyState);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Filter delegates without knowledge of internals
|
|
165
|
+
class MovingAverageFilter {
|
|
166
|
+
auto getState() const { return m_filter.getState(); } // Clean delegation
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Benefits:**
|
|
171
|
+
|
|
172
|
+
- ✅ **Separation of concerns**: Each layer owns its state
|
|
173
|
+
- ✅ **Type safety**: Policies can have different state types
|
|
174
|
+
- ✅ **Extensibility**: Adding new policies doesn't change SlidingWindowFilter
|
|
175
|
+
- ✅ **DRY principle**: No duplicate state assembly code in each filter
|
|
176
|
+
|
|
177
|
+
**3. Layered Design**
|
|
178
|
+
|
|
179
|
+
- **TypeScript Layer**: User-facing API, type safety, Redis integration
|
|
180
|
+
- **N-API Bridge**: Zero-copy data marshaling between JS and C++
|
|
181
|
+
- **C++ Core**: High-performance DSP algorithms with optimized memory management
|
|
182
|
+
|
|
183
|
+
**4. State Management**
|
|
184
|
+
|
|
185
|
+
State serialization uses a **layered delegation pattern** (see section 2.1):
|
|
186
|
+
|
|
187
|
+
- **Policy Layer**: Manages statistical state (running sums, variance accumulators, etc.)
|
|
188
|
+
- **SlidingWindowFilter**: Coordinates buffer contents + policy state
|
|
189
|
+
- **Filter Wrapper**: Delegates to SlidingWindowFilter without knowledge of internals
|
|
190
|
+
|
|
191
|
+
Full state serialization to JSON enables:
|
|
192
|
+
|
|
193
|
+
- ✅ Redis persistence for distributed processing
|
|
194
|
+
- ✅ Process continuity across restarts
|
|
195
|
+
- ✅ State migration between workers
|
|
196
|
+
- ✅ Data integrity validation on deserialization
|
|
197
|
+
|
|
198
|
+
Each filter's `getState()` returns a tuple `{bufferData: T[], policyState: PolicyState}` that can be JSON-serialized through the TypeScript layer.
|
|
199
|
+
|
|
200
|
+
**5. Mode Architecture** (MovingAverage, RMS, Variance, ZScoreNormalize)
|
|
201
|
+
|
|
202
|
+
- **Batch Mode**: Stateless processing, computes over entire input
|
|
203
|
+
- **Moving Mode**: Stateful processing with sliding window continuity
|
|
204
|
+
|
|
205
|
+
This separation enables:
|
|
206
|
+
|
|
207
|
+
- ✅ Unit testing of C++ algorithms independently
|
|
208
|
+
- ✅ Reuse of core DSP code in other projects
|
|
209
|
+
- ✅ Type-safe TypeScript API with IntelliSense
|
|
210
|
+
- ✅ Zero-copy performance through N-API
|
|
211
|
+
- ✅ Distributed processing with Redis state sharing
|
|
212
|
+
|
|
213
|
+
**6. Native C++ Backend**
|
|
214
|
+
|
|
215
|
+
- **N-API Bindings**: Direct TypedArray access for zero-copy processing
|
|
216
|
+
- **Async Processing**: Uses `Napi::AsyncWorker` to avoid blocking the event loop
|
|
217
|
+
- **Optimized Data Structures**: Circular buffers with O(1) operations
|
|
218
|
+
- **Template-Based**: Generic implementation supports int, float, double
|
|
219
|
+
|
|
220
|
+
**7. Redis State Persistence**
|
|
221
|
+
|
|
222
|
+
The state serialization includes:
|
|
223
|
+
|
|
224
|
+
- **Circular buffer contents**: All samples in order (oldest to newest)
|
|
225
|
+
- **Running sums/squares**: Maintained for O(1) calculations (moving average uses `runningSum`, RMS uses `runningSumOfSquares`)
|
|
226
|
+
- **Per-channel state**: Independent state for each audio channel
|
|
227
|
+
- **Metadata**: Window size, channel count, timestamps, filter type
|
|
228
|
+
|
|
229
|
+
**State format examples:**
|
|
230
|
+
|
|
231
|
+
Moving Average state:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"timestamp": 1761156820,
|
|
236
|
+
"stages": [
|
|
237
|
+
{
|
|
238
|
+
"index": 0,
|
|
239
|
+
"type": "movingAverage",
|
|
240
|
+
"state": {
|
|
241
|
+
"windowSize": 3,
|
|
242
|
+
"numChannels": 1,
|
|
243
|
+
"channels": [
|
|
244
|
+
{
|
|
245
|
+
"buffer": [3, 4, 5],
|
|
246
|
+
"runningSum": 12
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
],
|
|
252
|
+
"stageCount": 1
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
RMS state:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"timestamp": 1761168608,
|
|
261
|
+
"stages": [
|
|
262
|
+
{
|
|
263
|
+
"index": 0,
|
|
264
|
+
"type": "rms",
|
|
265
|
+
"state": {
|
|
266
|
+
"windowSize": 3,
|
|
267
|
+
"numChannels": 1,
|
|
268
|
+
"channels": [
|
|
269
|
+
{
|
|
270
|
+
"buffer": [6, -7, 8],
|
|
271
|
+
"runningSumOfSquares": 149
|
|
272
|
+
}
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
],
|
|
277
|
+
"stageCount": 1
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**8. Multi-Channel Processing**
|
|
282
|
+
|
|
283
|
+
Each channel maintains its own independent filter state:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// 4-channel interleaved data: [ch1, ch2, ch3, ch4, ch1, ch2, ...]
|
|
287
|
+
const input = new Float32Array(4000); // 1000 samples × 4 channels
|
|
288
|
+
|
|
289
|
+
const pipeline = createDspPipeline();
|
|
290
|
+
pipeline.MovingAverage({ windowSize: 50 });
|
|
291
|
+
|
|
292
|
+
const output = await pipeline.process(input, {
|
|
293
|
+
sampleRate: 2000,
|
|
294
|
+
channels: 4,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Each channel has its own circular buffer and running sum
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## 📊 Comparison with Alternatives
|
|
303
|
+
|
|
304
|
+
| Feature | dspx | scipy/numpy | dsp.js | Web Audio API |
|
|
305
|
+
| ------------------ | ----------------- | ------------------- | ---------- | --------------- |
|
|
306
|
+
| TypeScript Support | ✅ Native | ❌ Python-only | ⚠️ Partial | ✅ Browser-only |
|
|
307
|
+
| Performance | ⚡⚡⚡ Native C++ | ⚡⚡⚡⚡ | ⚡ Pure JS | ⚡⚡⚡ |
|
|
308
|
+
| State Persistence | ✅ Redis | ❌ Manual | ❌ None | ❌ None |
|
|
309
|
+
| Multi-Channel | ✅ Built-in | ✅ NumPy arrays | ⚠️ Limited | ✅ AudioBuffer |
|
|
310
|
+
| Node.js Backend | ✅ Designed for | ❌ Context switch | ✅ Yes | ❌ Browser |
|
|
311
|
+
| Observability | ✅ Callbacks | ❌ Print statements | ❌ None | ⚠️ Limited |
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## 🎯 Is This Library Right For You?
|
|
316
|
+
|
|
317
|
+
### ✅ Great Fit For:
|
|
318
|
+
|
|
319
|
+
- Node.js backends processing real-time biosignals (EMG, EEG, ECG)
|
|
320
|
+
- Audio streaming services applying filters at scale
|
|
321
|
+
- IoT gateways processing sensor data
|
|
322
|
+
- Distributed signal processing across multiple workers
|
|
323
|
+
- Teams wanting native C++ performance without leaving TypeScript
|
|
324
|
+
|
|
325
|
+
### ❌ Not Ideal For:
|
|
326
|
+
|
|
327
|
+
- Browser-only applications (use Web Audio API)
|
|
328
|
+
- Python-based ML pipelines (use SciPy/NumPy)
|
|
329
|
+
- Hard real-time embedded systems (use bare C/C++)
|
|
330
|
+
- Ultra-low latency (<1ms) requirements (Redis adds ~1-5ms)
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## ⚡ Performance at a Glance
|
|
335
|
+
|
|
336
|
+
| Operation | Throughput | Production-Ready |
|
|
337
|
+
| -------------------------------- | ---------------- | --------------------------------- |
|
|
338
|
+
| Native processing (no callbacks) | 22M samples/sec | ✅ Maximum performance |
|
|
339
|
+
| Batched callbacks | 3.2M samples/sec | ✅ **Recommended** for production |
|
|
340
|
+
| Individual callbacks | 6.1M samples/sec | ⚠️ Development/debugging only |
|
|
341
|
+
|
|
342
|
+
**SIMD Acceleration:** Batch operations and rectification are 2-8x faster with AVX2/SSE2/NEON. See [SIMD_OPTIMIZATIONS.md](https://github.com/A-KGeorge/dsp_ts_redis/blob/main/docs/SIMD_OPTIMIZATIONS.md) for details.
|
|
343
|
+
|
|
344
|
+
**Recommendation:** Use batched callbacks in production. Individual callbacks benchmark faster but block the Node.js event loop and can't integrate with real telemetry systems (Kafka, Datadog, Loki).
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## 📦 Installation
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
npm install dspx redis
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Note:** You'll need a C++ compiler if prebuilt binaries aren't available for your platform:
|
|
355
|
+
|
|
356
|
+
- Windows: Visual Studio 2022 or Build Tools
|
|
357
|
+
- macOS: Xcode Command Line Tools
|
|
358
|
+
- Linux: GCC/G++ 7+
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 🚀 Quick Start
|
|
363
|
+
|
|
364
|
+
### Basic Usage (Sample-Based)
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { createDspPipeline } from "dspx";
|
|
368
|
+
|
|
369
|
+
// Create a processing pipeline
|
|
370
|
+
const pipeline = createDspPipeline();
|
|
371
|
+
|
|
372
|
+
// Add filters using method chaining
|
|
373
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: 100 });
|
|
374
|
+
|
|
375
|
+
// Process samples (modifies input in-place for performance)
|
|
376
|
+
const input = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
377
|
+
const output = await pipeline.process(input, {
|
|
378
|
+
sampleRate: 2000,
|
|
379
|
+
channels: 1,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
console.log(output); // Smoothed signal
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### NEW: Time-Series Processing with Timestamps
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { createDspPipeline } from "dspx";
|
|
389
|
+
|
|
390
|
+
// Create a pipeline with time-based window
|
|
391
|
+
const pipeline = createDspPipeline();
|
|
392
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 5000 }); // 5 seconds
|
|
393
|
+
|
|
394
|
+
// Process data with explicit timestamps (e.g., from IoT sensors with network jitter)
|
|
395
|
+
const samples = new Float32Array([1.2, 3.4, 2.1, 4.5, 3.3]);
|
|
396
|
+
const timestamps = new Float32Array([0, 100, 250, 400, 500]); // milliseconds
|
|
397
|
+
|
|
398
|
+
const smoothed = await pipeline.process(samples, timestamps, { channels: 1 });
|
|
399
|
+
console.log(smoothed); // Time-aware smoothing
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**📚 [Complete Time-Series Guide →](https://github.com/A-KGeorge/dsp_ts_redis/blob/main/docs/time-series-guide.md)**
|
|
403
|
+
|
|
404
|
+
### Processing Without Modifying Input
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
// Use processCopy to preserve the original input
|
|
408
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
409
|
+
const output = await pipeline.processCopy(input, {
|
|
410
|
+
sampleRate: 2000,
|
|
411
|
+
channels: 1,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
console.log(input); // [1, 2, 3, 4, 5] - unchanged
|
|
415
|
+
console.log(output); // [1, 1.5, 2, 3, 4] - smoothed
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### With Redis State Persistence
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
import { createDspPipeline } from "dspx";
|
|
422
|
+
import { createClient } from "redis";
|
|
423
|
+
|
|
424
|
+
const redis = await createClient({ url: "redis://localhost:6379" }).connect();
|
|
425
|
+
|
|
426
|
+
// Create pipeline with Redis config
|
|
427
|
+
const pipeline = createDspPipeline({
|
|
428
|
+
redisHost: "localhost",
|
|
429
|
+
redisPort: 6379,
|
|
430
|
+
stateKey: "dsp:user123:channel1",
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
pipeline.MovingAverage({ windowSize: 100 });
|
|
434
|
+
|
|
435
|
+
// Try to restore previous state from Redis
|
|
436
|
+
const savedState = await redis.get("dsp:user123:channel1");
|
|
437
|
+
if (savedState) {
|
|
438
|
+
await pipeline.loadState(savedState);
|
|
439
|
+
console.log("State restored!");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Process data - filter state is maintained
|
|
443
|
+
await pipeline.process(chunk1, { sampleRate: 2000, channels: 1 });
|
|
444
|
+
|
|
445
|
+
// Save state to Redis (includes circular buffer contents!)
|
|
446
|
+
const state = await pipeline.saveState();
|
|
447
|
+
await redis.set("dsp:user123:channel1", state);
|
|
448
|
+
|
|
449
|
+
// Continue processing - even after service restart!
|
|
450
|
+
await pipeline.process(chunk2, { sampleRate: 2000, channels: 1 });
|
|
451
|
+
|
|
452
|
+
// Clear state when starting fresh
|
|
453
|
+
pipeline.clearState();
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Multi-Channel Processing
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { createDspPipeline } from "dspx";
|
|
460
|
+
|
|
461
|
+
const pipeline = createDspPipeline();
|
|
462
|
+
pipeline.MovingAverage({ windowSize: 50 });
|
|
463
|
+
|
|
464
|
+
// Process 4-channel EMG data
|
|
465
|
+
// Data format: [ch1_s1, ch2_s1, ch3_s1, ch4_s1, ch1_s2, ch2_s2, ...]
|
|
466
|
+
const fourChannelData = new Float32Array(4000); // 1000 samples × 4 channels
|
|
467
|
+
|
|
468
|
+
const output = await pipeline.process(fourChannelData, {
|
|
469
|
+
sampleRate: 2000,
|
|
470
|
+
channels: 4, // Each channel maintains its own filter state
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Each channel is processed independently with its own circular buffer
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 🔗 API Reference
|
|
479
|
+
|
|
480
|
+
### Creating a Pipeline
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import { createDspPipeline, type RedisConfig } from "dspx";
|
|
484
|
+
|
|
485
|
+
interface RedisConfig {
|
|
486
|
+
redisHost?: string; // Redis server hostname (optional)
|
|
487
|
+
redisPort?: number; // Redis server port (optional)
|
|
488
|
+
stateKey?: string; // Key prefix for state storage (optional)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const pipeline = createDspPipeline(config?: RedisConfig);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Process Methods
|
|
495
|
+
|
|
496
|
+
The library supports three processing modes:
|
|
497
|
+
|
|
498
|
+
```typescript
|
|
499
|
+
// Mode 1: Legacy sample-based (auto-generates timestamps from sample rate)
|
|
500
|
+
await pipeline.process(samples: Float32Array, options: {
|
|
501
|
+
sampleRate: number; // Hz (required for timestamp generation)
|
|
502
|
+
channels: number;
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Mode 2: Time-based with explicit timestamps
|
|
506
|
+
await pipeline.process(samples: Float32Array, timestamps: Float32Array, options: {
|
|
507
|
+
channels: number; // sampleRate not needed
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// Mode 3: Auto-sequential (no sample rate, generates [0,1,2,3,...])
|
|
511
|
+
await pipeline.process(samples: Float32Array, options: {
|
|
512
|
+
channels: number; // sampleRate omitted
|
|
513
|
+
});
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
**See [Time-Series Guide](https://github.com/A-KGeorge/dsp_ts_redis/blob/main/docs/time-series-guide.md) for detailed examples.**
|
|
517
|
+
|
|
518
|
+
### Available Filters
|
|
519
|
+
|
|
520
|
+
#### Currently Implemented
|
|
521
|
+
|
|
522
|
+
##### Moving Average Filter
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
// Batch Mode - Stateless, computes average over entire input
|
|
526
|
+
pipeline.MovingAverage({ mode: "batch" });
|
|
527
|
+
|
|
528
|
+
// Moving Mode - Stateful, sliding window with continuity
|
|
529
|
+
pipeline.MovingAverage({ mode: "moving", windowSize: number });
|
|
530
|
+
|
|
531
|
+
// NEW: Time-Based Window
|
|
532
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: number }); // milliseconds
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Implements a simple moving average (SMA) filter with two modes:
|
|
536
|
+
|
|
537
|
+
**Modes:**
|
|
538
|
+
|
|
539
|
+
| Mode | Description | State | Output | Use Case |
|
|
540
|
+
| ---------- | ---------------------------------- | --------- | ------------------------------------------- | ----------------------------------- |
|
|
541
|
+
| `"batch"` | Computes average over entire input | Stateless | All samples have same value (mean of input) | Quality metrics, summary statistics |
|
|
542
|
+
| `"moving"` | Sliding window across samples | Stateful | Each sample smoothed by window | Real-time smoothing, trend analysis |
|
|
543
|
+
|
|
544
|
+
**Parameters:**
|
|
545
|
+
|
|
546
|
+
- `mode`: `"batch"` or `"moving"` - determines computation strategy
|
|
547
|
+
- `windowSize`: Number of samples to average over **(optional for moving mode)**
|
|
548
|
+
- `windowDuration`: Time-based window in milliseconds **(optional for moving mode)**
|
|
549
|
+
- At least one of `windowSize` or `windowDuration` must be specified for moving mode
|
|
550
|
+
|
|
551
|
+
**Features:**
|
|
552
|
+
|
|
553
|
+
- **Batch mode**: O(n) computation, no state between calls
|
|
554
|
+
- **Moving mode**: O(1) per sample with circular buffer and running sum
|
|
555
|
+
- Per-channel state for multi-channel processing
|
|
556
|
+
- Full state serialization to Redis
|
|
557
|
+
|
|
558
|
+
**Example:**
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// Batch mode: Average of [1,2,3,4,5] = 3
|
|
562
|
+
const batch = createDspPipeline().MovingAverage({ mode: "batch" });
|
|
563
|
+
const result1 = await batch.process(new Float32Array([1, 2, 3, 4, 5]));
|
|
564
|
+
// result1 = [3, 3, 3, 3, 3]
|
|
565
|
+
|
|
566
|
+
// Moving mode: Window size 3
|
|
567
|
+
const moving = createDspPipeline().MovingAverage({
|
|
568
|
+
mode: "moving",
|
|
569
|
+
windowSize: 3,
|
|
570
|
+
});
|
|
571
|
+
const result2 = await moving.process(new Float32Array([1, 2, 3, 4, 5]));
|
|
572
|
+
// result2 = [1, 1.5, 2, 3, 4] (sliding window averages)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**Use cases:**
|
|
576
|
+
|
|
577
|
+
- **Batch**: Quality control metrics, batch statistics, data summarization
|
|
578
|
+
- **Moving**: Signal smoothing, noise reduction, trend analysis, low-pass filtering
|
|
579
|
+
|
|
580
|
+
##### RMS (Root Mean Square) Filter
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
// Batch Mode - Stateless, computes RMS over entire input
|
|
584
|
+
pipeline.Rms({ mode: "batch" });
|
|
585
|
+
|
|
586
|
+
// Moving Mode - Stateful, sliding window with continuity
|
|
587
|
+
pipeline.Rms({ mode: "moving", windowSize: number });
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Implements an efficient RMS filter with two modes:
|
|
591
|
+
|
|
592
|
+
**Modes:**
|
|
593
|
+
|
|
594
|
+
| Mode | Description | State | Output | Use Case |
|
|
595
|
+
| ---------- | ------------------------------ | --------- | ------------------------------------------ | ----------------------------------- |
|
|
596
|
+
| `"batch"` | Computes RMS over entire input | Stateless | All samples have same value (RMS of input) | Power measurement, batch analysis |
|
|
597
|
+
| `"moving"` | Sliding window across samples | Stateful | Each sample is RMS of window | Envelope detection, real-time power |
|
|
598
|
+
|
|
599
|
+
**Parameters:**
|
|
600
|
+
|
|
601
|
+
- `mode`: `"batch"` or `"moving"` - determines computation strategy
|
|
602
|
+
- `windowSize`: Number of samples to calculate RMS over **(required for moving mode only)**
|
|
603
|
+
|
|
604
|
+
**Features:**
|
|
605
|
+
|
|
606
|
+
- **Batch mode**: O(n) computation, no state between calls
|
|
607
|
+
- **Moving mode**: O(1) per sample with circular buffer and running sum of squares
|
|
608
|
+
- Per-channel state for multi-channel processing
|
|
609
|
+
- Full state serialization to Redis
|
|
610
|
+
- Always positive output (magnitude-based)
|
|
611
|
+
|
|
612
|
+
**Example:**
|
|
613
|
+
|
|
614
|
+
```typescript
|
|
615
|
+
// Batch mode: RMS of [1, -2, 3, -4, 5] = sqrt((1² + 4 + 9 + 16 + 25)/5) = 3.31
|
|
616
|
+
const batch = createDspPipeline().Rms({ mode: "batch" });
|
|
617
|
+
const result1 = await batch.process(new Float32Array([1, -2, 3, -4, 5]));
|
|
618
|
+
// result1 = [3.31, 3.31, 3.31, 3.31, 3.31]
|
|
619
|
+
|
|
620
|
+
// Moving mode: Window size 3
|
|
621
|
+
const moving = createDspPipeline().Rms({ mode: "moving", windowSize: 3 });
|
|
622
|
+
const result2 = await moving.process(new Float32Array([1, -2, 3, -4, 5]));
|
|
623
|
+
// result2 = [1.0, 1.58, 2.16, 3.11, 4.08] - sliding window RMS
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Use cases:**
|
|
627
|
+
|
|
628
|
+
- **Batch**: Overall signal power, quality metrics, batch statistics
|
|
629
|
+
- **Moving**: Signal envelope detection, amplitude tracking, power measurement, feature extraction
|
|
630
|
+
|
|
631
|
+
##### Mean Absolute Value (MAV) Filter
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// Batch Mode - Stateless, computes MAV over entire input
|
|
635
|
+
pipeline.MeanAbsoluteValue({ mode: "batch" });
|
|
636
|
+
|
|
637
|
+
// Moving Mode - Stateful, sliding window with continuity
|
|
638
|
+
pipeline.MeanAbsoluteValue({ mode: "moving", windowSize: number });
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Implements an efficient Mean Absolute Value filter with two modes - commonly used in EMG signal analysis for muscle activity quantification.
|
|
642
|
+
|
|
643
|
+
**Modes:**
|
|
644
|
+
|
|
645
|
+
| Mode | Description | State | Output | Use Case |
|
|
646
|
+
| ---------- | ------------------------------ | --------- | ------------------------------------------ | ---------------------------------------- |
|
|
647
|
+
| `"batch"` | Computes MAV over entire input | Stateless | All samples have same value (MAV of input) | Global activity level, batch analysis |
|
|
648
|
+
| `"moving"` | Sliding window across samples | Stateful | Each sample is MAV of window | Real-time activity detection, transients |
|
|
649
|
+
|
|
650
|
+
**Parameters:**
|
|
651
|
+
|
|
652
|
+
- `mode`: `"batch"` or `"moving"` - determines computation strategy
|
|
653
|
+
- `windowSize`: Number of samples to calculate MAV over **(required for moving mode only)**
|
|
654
|
+
|
|
655
|
+
**Features:**
|
|
656
|
+
|
|
657
|
+
- **Batch mode**: O(n) computation, no state between calls
|
|
658
|
+
- **Moving mode**: O(1) per sample with circular buffer and running sum of absolute values
|
|
659
|
+
- Per-channel state for multi-channel EMG processing
|
|
660
|
+
- Full state serialization to Redis
|
|
661
|
+
- Always non-negative output
|
|
662
|
+
- Scale-invariant: MAV(k·x) = k·MAV(x)
|
|
663
|
+
|
|
664
|
+
**Example:**
|
|
665
|
+
|
|
666
|
+
```typescript
|
|
667
|
+
// Batch mode: MAV of [1, -2, 3, -4, 5] = (|1| + |-2| + |3| + |-4| + |5|)/5 = 3.0
|
|
668
|
+
const batch = createDspPipeline().MeanAbsoluteValue({ mode: "batch" });
|
|
669
|
+
const result1 = await batch.process(new Float32Array([1, -2, 3, -4, 5]));
|
|
670
|
+
// result1 = [3.0, 3.0, 3.0, 3.0, 3.0]
|
|
671
|
+
|
|
672
|
+
// Moving mode: Window size 3
|
|
673
|
+
const moving = createDspPipeline().MeanAbsoluteValue({
|
|
674
|
+
mode: "moving",
|
|
675
|
+
windowSize: 3,
|
|
676
|
+
});
|
|
677
|
+
const result2 = await moving.process(new Float32Array([1, -2, 3, -4, 5]));
|
|
678
|
+
// result2 = [1.0, 1.5, 2.0, 3.0, 4.0] - sliding window MAV
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
**Use cases:**
|
|
682
|
+
|
|
683
|
+
- **EMG Analysis**: Muscle activity quantification, fatigue detection, gesture recognition
|
|
684
|
+
- **Vibration Monitoring**: Equipment health monitoring, anomaly detection
|
|
685
|
+
- **Audio Processing**: Envelope detection, dynamic range analysis
|
|
686
|
+
- **Batch**: Overall signal activity level, quality metrics
|
|
687
|
+
- **Moving**: Real-time transient detection, activity onset/offset detection, prosthetic control
|
|
688
|
+
|
|
689
|
+
**Mathematical Properties:**
|
|
690
|
+
|
|
691
|
+
- **Non-negative**: MAV(x) ≥ 0 for all signals
|
|
692
|
+
- **Scale-invariant**: MAV(k·x) = k·MAV(x)
|
|
693
|
+
- **Bounded**: MAV(x) ≤ max(|x|) for any window
|
|
694
|
+
- **Reduces to mean**: For all positive signals, MAV = mean
|
|
695
|
+
|
|
696
|
+
##### Rectify Filter
|
|
697
|
+
|
|
698
|
+
```typescript
|
|
699
|
+
pipeline.Rectify(params?: { mode?: "full" | "half" });
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
Implements signal rectification with two modes: full-wave (absolute value) and half-wave (zero out negatives). Stateless operation with no internal buffers.
|
|
703
|
+
|
|
704
|
+
**Parameters:**
|
|
705
|
+
|
|
706
|
+
- `mode` (optional): Rectification mode
|
|
707
|
+
- `"full"` (default): Full-wave rectification (absolute value) - converts all samples to positive
|
|
708
|
+
- `"half"`: Half-wave rectification - positive samples unchanged, negative samples → 0
|
|
709
|
+
|
|
710
|
+
**Features:**
|
|
711
|
+
|
|
712
|
+
- Zero overhead - simple sample-by-sample transformation
|
|
713
|
+
- No internal state/buffers (stateless)
|
|
714
|
+
- Mode is serializable for pipeline persistence
|
|
715
|
+
- Works independently on each sample (no windowing)
|
|
716
|
+
|
|
717
|
+
**Use cases:**
|
|
718
|
+
|
|
719
|
+
- EMG signal pre-processing before envelope detection
|
|
720
|
+
- AC to DC conversion in audio/biosignal processing
|
|
721
|
+
- Preparing signals for RMS or moving average smoothing
|
|
722
|
+
- Feature extraction requiring positive-only values
|
|
723
|
+
|
|
724
|
+
**Examples:**
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
// Full-wave rectification (default) - converts to absolute value
|
|
728
|
+
const pipeline1 = createDspPipeline();
|
|
729
|
+
pipeline1.Rectify(); // or Rectify({ mode: "full" })
|
|
730
|
+
|
|
731
|
+
const bipolar = new Float32Array([1, -2, 3, -4, 5]);
|
|
732
|
+
const fullWave = await pipeline1.process(bipolar, {
|
|
733
|
+
sampleRate: 1000,
|
|
734
|
+
channels: 1,
|
|
735
|
+
});
|
|
736
|
+
console.log(fullWave); // [1, 2, 3, 4, 5] - all positive
|
|
737
|
+
|
|
738
|
+
// Half-wave rectification - zeros out negatives
|
|
739
|
+
const pipeline2 = createDspPipeline();
|
|
740
|
+
pipeline2.Rectify({ mode: "half" });
|
|
741
|
+
|
|
742
|
+
const halfWave = await pipeline2.process(new Float32Array([1, -2, 3, -4, 5]), {
|
|
743
|
+
sampleRate: 1000,
|
|
744
|
+
channels: 1,
|
|
745
|
+
});
|
|
746
|
+
console.log(halfWave); // [1, 0, 3, 0, 5] - negatives become zero
|
|
747
|
+
|
|
748
|
+
// Common pipeline: Rectify → RMS for EMG envelope
|
|
749
|
+
const emgPipeline = createDspPipeline();
|
|
750
|
+
emgPipeline
|
|
751
|
+
.Rectify({ mode: "full" }) // Convert to magnitude
|
|
752
|
+
.Rms({ windowSize: 50 }); // Calculate envelope
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
##### Variance Filter
|
|
756
|
+
|
|
757
|
+
```typescript
|
|
758
|
+
pipeline.Variance(params: { mode: "batch" | "moving"; windowSize?: number });
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Implements variance calculation to measure data spread and variability. Supports both stateless batch variance and stateful moving variance with a sliding window.
|
|
762
|
+
|
|
763
|
+
**Parameters:**
|
|
764
|
+
|
|
765
|
+
- `mode`: Variance calculation mode
|
|
766
|
+
- `"batch"`: Stateless - computes variance over entire batch, all output samples contain the same value
|
|
767
|
+
- `"moving"`: Stateful - computes variance over a sliding window, maintains state across process() calls
|
|
768
|
+
- `windowSize`: Required for `"moving"` mode - size of the sliding window
|
|
769
|
+
|
|
770
|
+
**Features:**
|
|
771
|
+
|
|
772
|
+
- **Batch mode**: O(1) space complexity, processes entire batch in two passes
|
|
773
|
+
- **Moving mode**: O(1) per-sample computation using circular buffer with running sums
|
|
774
|
+
- Maintains running sum and running sum of squares for efficient calculation
|
|
775
|
+
- Per-channel state for multi-channel processing
|
|
776
|
+
- Full state serialization to Redis including buffer contents and running values
|
|
777
|
+
- Variance is always non-negative (uses max(0, calculated) to handle floating-point errors)
|
|
778
|
+
|
|
779
|
+
**Mathematical Note:**
|
|
780
|
+
|
|
781
|
+
Variance is calculated as: `Var(X) = E[X²] - (E[X])²`
|
|
782
|
+
|
|
783
|
+
Where:
|
|
784
|
+
|
|
785
|
+
- `E[X]` is the mean (average) of values
|
|
786
|
+
- `E[X²]` is the mean of squared values
|
|
787
|
+
|
|
788
|
+
**Use cases:**
|
|
789
|
+
|
|
790
|
+
- Signal quality monitoring (detect signal stability)
|
|
791
|
+
- Anomaly detection (identify unusual variability)
|
|
792
|
+
- Feature extraction for machine learning (EMG, EEG analysis)
|
|
793
|
+
- Real-time variability tracking in biosignals
|
|
794
|
+
- Data consistency validation in sensor streams
|
|
795
|
+
|
|
796
|
+
**Examples:**
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
// Batch variance - stateless, entire batch → single variance value
|
|
800
|
+
const pipeline1 = createDspPipeline();
|
|
801
|
+
pipeline1.Variance({ mode: "batch" });
|
|
802
|
+
|
|
803
|
+
const data = new Float32Array([1, 2, 3, 4, 5]);
|
|
804
|
+
const output1 = await pipeline1.process(data, {
|
|
805
|
+
sampleRate: 1000,
|
|
806
|
+
channels: 1,
|
|
807
|
+
});
|
|
808
|
+
console.log(output1); // [2, 2, 2, 2, 2] - all values are the batch variance
|
|
809
|
+
|
|
810
|
+
// Moving variance - stateful, sliding window
|
|
811
|
+
const pipeline2 = createDspPipeline();
|
|
812
|
+
pipeline2.Variance({ mode: "moving", windowSize: 3 });
|
|
813
|
+
|
|
814
|
+
const output2 = await pipeline2.process(data, {
|
|
815
|
+
sampleRate: 1000,
|
|
816
|
+
channels: 1,
|
|
817
|
+
});
|
|
818
|
+
console.log(output2);
|
|
819
|
+
// [0, 0.25, 0.67, 0.67, 0.67] - variance evolves as window slides
|
|
820
|
+
// Window: [1] → [1,2] → [1,2,3] → [2,3,4] → [3,4,5]
|
|
821
|
+
|
|
822
|
+
// EMG variability monitoring pipeline
|
|
823
|
+
const emgPipeline = createDspPipeline()
|
|
824
|
+
.Rectify({ mode: "full" }) // Convert to magnitude
|
|
825
|
+
.Variance({ mode: "moving", windowSize: 100 }); // Track variability
|
|
826
|
+
|
|
827
|
+
// Multi-channel signal quality monitoring
|
|
828
|
+
const qualityPipeline = createDspPipeline().Variance({ mode: "batch" });
|
|
829
|
+
|
|
830
|
+
const fourChannelData = new Float32Array(4000); // 1000 samples × 4 channels
|
|
831
|
+
const variances = await qualityPipeline.process(fourChannelData, {
|
|
832
|
+
sampleRate: 2000,
|
|
833
|
+
channels: 4,
|
|
834
|
+
});
|
|
835
|
+
// Each channel gets its own variance value
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**Batch vs Moving Mode:**
|
|
839
|
+
|
|
840
|
+
| Mode | State | Output | Use Case |
|
|
841
|
+
| -------- | --------- | ----------------------------- | ------------------------------ |
|
|
842
|
+
| `batch` | Stateless | All samples = single variance | Per-chunk quality assessment |
|
|
843
|
+
| `moving` | Stateful | Variance per sample (sliding) | Real-time variability tracking |
|
|
844
|
+
|
|
845
|
+
**Performance:**
|
|
846
|
+
|
|
847
|
+
- Batch mode: Two-pass algorithm (sum calculation, then variance), O(n) time
|
|
848
|
+
- Moving mode: Single-pass with circular buffer, O(1) per sample after warmup
|
|
849
|
+
- State persistence includes full circular buffer + running sums (can be large for big windows)
|
|
850
|
+
|
|
851
|
+
##### Z-Score Normalization Filter
|
|
852
|
+
|
|
853
|
+
```typescript
|
|
854
|
+
pipeline.ZScoreNormalize(params: {
|
|
855
|
+
mode: "batch" | "moving";
|
|
856
|
+
windowSize?: number;
|
|
857
|
+
epsilon?: number;
|
|
858
|
+
});
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
Implements Z-Score normalization to standardize data to have mean 0 and standard deviation 1. Supports both stateless batch normalization and stateful moving normalization with a sliding window.
|
|
862
|
+
|
|
863
|
+
**Z-Score Formula:** `(Value - Mean) / StandardDeviation`
|
|
864
|
+
|
|
865
|
+
**Parameters:**
|
|
866
|
+
|
|
867
|
+
- `mode`: Normalization calculation mode
|
|
868
|
+
- `"batch"`: Stateless - computes mean and stddev over entire batch, normalizes all samples
|
|
869
|
+
- `"moving"`: Stateful - computes mean and stddev over sliding window, maintains state across process() calls
|
|
870
|
+
- `windowSize`: Required for `"moving"` mode - size of the sliding window
|
|
871
|
+
- `epsilon`: Small value to prevent division by zero when standard deviation is 0 (default: `1e-6`)
|
|
872
|
+
|
|
873
|
+
**Features:**
|
|
874
|
+
|
|
875
|
+
- **Batch mode**: Normalizes entire dataset to mean=0, stddev=1 using global statistics
|
|
876
|
+
- **Moving mode**: Adaptive normalization using local window statistics
|
|
877
|
+
- Maintains running sum and running sum of squares for O(1) mean/stddev calculation
|
|
878
|
+
- Per-channel state for multi-channel processing
|
|
879
|
+
- Full state serialization including buffer contents and running values
|
|
880
|
+
- Epsilon handling prevents NaN when processing constant signals
|
|
881
|
+
|
|
882
|
+
**Use cases:**
|
|
883
|
+
|
|
884
|
+
- Machine learning preprocessing (feature standardization)
|
|
885
|
+
- Anomaly detection (outlier identification using ±3σ thresholds)
|
|
886
|
+
- Neural network input normalization
|
|
887
|
+
- EEG/EMG signal standardization for multi-channel processing
|
|
888
|
+
- Real-time data stream normalization with adaptive statistics
|
|
889
|
+
- Removing baseline drift and amplitude variations
|
|
890
|
+
|
|
891
|
+
**Examples:**
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
// Batch normalization - standardize entire dataset
|
|
895
|
+
const pipeline1 = createDspPipeline();
|
|
896
|
+
pipeline1.ZScoreNormalize({ mode: "batch" });
|
|
897
|
+
|
|
898
|
+
const data = new Float32Array([10, 20, 30, 40, 50]);
|
|
899
|
+
const output1 = await pipeline1.process(data, {
|
|
900
|
+
sampleRate: 1000,
|
|
901
|
+
channels: 1,
|
|
902
|
+
});
|
|
903
|
+
// All samples normalized to mean=0, stddev=1
|
|
904
|
+
// Output: [-1.414, -0.707, 0, 0.707, 1.414] (approximately)
|
|
905
|
+
|
|
906
|
+
// Moving normalization - adaptive standardization
|
|
907
|
+
const pipeline2 = createDspPipeline();
|
|
908
|
+
pipeline2.ZScoreNormalize({ mode: "moving", windowSize: 50 });
|
|
909
|
+
|
|
910
|
+
const streamData = new Float32Array(200); // Continuous stream
|
|
911
|
+
const output2 = await pipeline2.process(streamData, {
|
|
912
|
+
sampleRate: 1000,
|
|
913
|
+
channels: 1,
|
|
914
|
+
});
|
|
915
|
+
// Each sample normalized using local window statistics
|
|
916
|
+
// Adapts to changes in mean and variance over time
|
|
917
|
+
|
|
918
|
+
// Anomaly detection with z-score thresholds
|
|
919
|
+
const pipeline3 = createDspPipeline();
|
|
920
|
+
pipeline3.ZScoreNormalize({ mode: "moving", windowSize: 100 });
|
|
921
|
+
|
|
922
|
+
const sensorData = new Float32Array(500);
|
|
923
|
+
const zScores = await pipeline3.process(sensorData, {
|
|
924
|
+
sampleRate: 100,
|
|
925
|
+
channels: 1,
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
// Detect outliers (z-score > ±3 indicates anomaly)
|
|
929
|
+
const anomalies = [];
|
|
930
|
+
for (let i = 0; i < zScores.length; i++) {
|
|
931
|
+
if (Math.abs(zScores[i]) > 3.0) {
|
|
932
|
+
anomalies.push({ index: i, zScore: zScores[i] });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Multi-channel EEG normalization
|
|
937
|
+
const eegPipeline = createDspPipeline();
|
|
938
|
+
eegPipeline.ZScoreNormalize({ mode: "moving", windowSize: 128 });
|
|
939
|
+
|
|
940
|
+
const fourChannelEEG = new Float32Array(2048); // 512 samples × 4 channels
|
|
941
|
+
const normalizedEEG = await eegPipeline.process(fourChannelEEG, {
|
|
942
|
+
sampleRate: 256,
|
|
943
|
+
channels: 4,
|
|
944
|
+
});
|
|
945
|
+
// Each EEG channel independently normalized to mean=0, stddev=1
|
|
946
|
+
|
|
947
|
+
// Custom epsilon for near-constant signals
|
|
948
|
+
const pipeline4 = createDspPipeline();
|
|
949
|
+
pipeline4.ZScoreNormalize({ mode: "batch", epsilon: 0.1 });
|
|
950
|
+
|
|
951
|
+
const nearConstant = new Float32Array([5.0, 5.001, 4.999, 5.0]);
|
|
952
|
+
const output4 = await pipeline4.process(nearConstant, {
|
|
953
|
+
sampleRate: 1000,
|
|
954
|
+
channels: 1,
|
|
955
|
+
});
|
|
956
|
+
// When stddev < epsilon, output is 0 (prevents division by tiny numbers)
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
**Batch vs Moving Mode:**
|
|
960
|
+
|
|
961
|
+
| Mode | State | Output | Use Case |
|
|
962
|
+
| -------- | --------- | ----------------------------------------------- | --------------------------------------------------- |
|
|
963
|
+
| `batch` | Stateless | All samples normalized using global mean/stddev | ML preprocessing, dataset standardization |
|
|
964
|
+
| `moving` | Stateful | Each sample normalized using local window stats | Real-time anomaly detection, adaptive normalization |
|
|
965
|
+
|
|
966
|
+
**Performance:**
|
|
967
|
+
|
|
968
|
+
- Batch mode: Two-pass algorithm (calculate mean, then stddev, then normalize), O(n) time
|
|
969
|
+
- Moving mode: Single-pass with circular buffer, O(1) per sample after warmup
|
|
970
|
+
- Anomaly detection: O(1) threshold comparison after normalization
|
|
971
|
+
- State persistence includes full circular buffer + running sums
|
|
972
|
+
|
|
973
|
+
**Mathematical Properties:**
|
|
974
|
+
|
|
975
|
+
- Normalized data has mean = 0 (centered)
|
|
976
|
+
- Normalized data has standard deviation = 1 (scaled)
|
|
977
|
+
- Z-scores > ±3 represent outliers (>99.7% of data within ±3σ in normal distribution)
|
|
978
|
+
- Preserves relative distances and relationships in the data
|
|
979
|
+
- Linear transformation (reversible if original mean/stddev are known)
|
|
980
|
+
|
|
981
|
+
##### Waveform Length (WL) Filter
|
|
982
|
+
|
|
983
|
+
```typescript
|
|
984
|
+
pipeline.WaveformLength({ windowSize: number });
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
Implements waveform length calculation to measure the total path length traveled by a signal over a sliding window. WL is the sum of absolute differences between consecutive samples, commonly used in EMG signal analysis.
|
|
988
|
+
|
|
989
|
+
**Parameters:**
|
|
990
|
+
|
|
991
|
+
- `windowSize`: Number of samples for the sliding window
|
|
992
|
+
|
|
993
|
+
**Features:**
|
|
994
|
+
|
|
995
|
+
- O(1) per-sample computation using circular buffer with running sum of differences
|
|
996
|
+
- Tracks previous sample to calculate absolute difference
|
|
997
|
+
- Per-channel state for multi-channel EMG processing
|
|
998
|
+
- Full state serialization including buffer and previous sample value
|
|
999
|
+
- Always non-negative output
|
|
1000
|
+
- Sensitive to signal complexity and frequency content
|
|
1001
|
+
|
|
1002
|
+
**Mathematical Definition:**
|
|
1003
|
+
|
|
1004
|
+
For window of samples `[x₁, x₂, ..., xₙ]`:
|
|
1005
|
+
`WL = Σ|xᵢ₊₁ - xᵢ|` for i = 1 to n-1
|
|
1006
|
+
|
|
1007
|
+
**Use cases:**
|
|
1008
|
+
|
|
1009
|
+
- EMG signal analysis and muscle activity quantification
|
|
1010
|
+
- Detecting signal complexity and variability
|
|
1011
|
+
- Gesture recognition and prosthetic control
|
|
1012
|
+
- Fatigue detection (WL decreases with muscle fatigue)
|
|
1013
|
+
- Feature extraction for classification algorithms
|
|
1014
|
+
- Vibration monitoring and equipment diagnostics
|
|
1015
|
+
|
|
1016
|
+
**Example:**
|
|
1017
|
+
|
|
1018
|
+
```typescript
|
|
1019
|
+
// Basic waveform length computation
|
|
1020
|
+
const pipeline = createDspPipeline();
|
|
1021
|
+
pipeline.WaveformLength({ windowSize: 100 });
|
|
1022
|
+
|
|
1023
|
+
const signal = new Float32Array([1, 3, 2, 5, 4, 6]);
|
|
1024
|
+
// Differences: |3-1|=2, |2-3|=1, |5-2|=3, |4-5|=1, |6-4|=2
|
|
1025
|
+
// Cumulative WL: 0, 2, 3, 6, 7, 9
|
|
1026
|
+
const result = await pipeline.process(signal, {
|
|
1027
|
+
sampleRate: 1000,
|
|
1028
|
+
channels: 1,
|
|
1029
|
+
});
|
|
1030
|
+
// result = [0, 2, 3, 6, 7, 9]
|
|
1031
|
+
|
|
1032
|
+
// EMG feature extraction pipeline
|
|
1033
|
+
const emgPipeline = createDspPipeline();
|
|
1034
|
+
emgPipeline
|
|
1035
|
+
.Rectify({ mode: "full" }) // Convert to magnitude
|
|
1036
|
+
.WaveformLength({ windowSize: 250 }); // 250ms window at 1kHz
|
|
1037
|
+
|
|
1038
|
+
// Multi-channel muscle monitoring
|
|
1039
|
+
const multiChannelEMG = new Float32Array(4000); // 1000 samples × 4 channels
|
|
1040
|
+
const wlFeatures = await emgPipeline.process(multiChannelEMG, {
|
|
1041
|
+
sampleRate: 1000,
|
|
1042
|
+
channels: 4,
|
|
1043
|
+
});
|
|
1044
|
+
// Each muscle gets independent WL calculation
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
**Comparison with other features:**
|
|
1048
|
+
|
|
1049
|
+
| Feature | Measures | Sensitivity | Use Case |
|
|
1050
|
+
| ------- | --------------------------- | ------------------ | ---------------------- |
|
|
1051
|
+
| WL | Total path length | High to complexity | Complexity/variability |
|
|
1052
|
+
| MAV | Average magnitude | Amplitude level | Activity level |
|
|
1053
|
+
| RMS | Root mean square | Power/energy | Signal strength |
|
|
1054
|
+
| SSC | Slope direction changes | Frequency content | Oscillation rate |
|
|
1055
|
+
| WAMP | Amplitude threshold crosses | Burst detection | Transient events |
|
|
1056
|
+
|
|
1057
|
+
**Mathematical Properties:**
|
|
1058
|
+
|
|
1059
|
+
- **Non-negative**: WL ≥ 0 for all signals
|
|
1060
|
+
- **Monotonic**: WL increases with window filling
|
|
1061
|
+
- **Frequency-dependent**: Higher frequency → higher WL for same amplitude
|
|
1062
|
+
- **Scale-variant**: WL(k·x) = k·WL(x) for constant k
|
|
1063
|
+
|
|
1064
|
+
##### Slope Sign Change (SSC) Filter
|
|
1065
|
+
|
|
1066
|
+
```typescript
|
|
1067
|
+
pipeline.SlopeSignChange({ windowSize: number; threshold?: number });
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
Implements slope sign change counting to measure frequency content by detecting how many times the signal changes direction (slope changes from positive to negative or vice versa). Commonly used in EMG analysis.
|
|
1071
|
+
|
|
1072
|
+
**Parameters:**
|
|
1073
|
+
|
|
1074
|
+
- `windowSize`: Number of samples for the sliding window
|
|
1075
|
+
- `threshold`: Minimum absolute difference to count as significant (default: 0)
|
|
1076
|
+
|
|
1077
|
+
**Features:**
|
|
1078
|
+
|
|
1079
|
+
- Counts direction changes (increasing→decreasing or decreasing→increasing)
|
|
1080
|
+
- Threshold filtering to ignore noise and minor fluctuations
|
|
1081
|
+
- Requires 2 previous samples for slope calculation
|
|
1082
|
+
- O(1) per-sample computation using circular buffer
|
|
1083
|
+
- Per-channel state for multi-channel processing
|
|
1084
|
+
- Full state serialization including filter state (previous 2 samples, init count)
|
|
1085
|
+
|
|
1086
|
+
**Mathematical Definition:**
|
|
1087
|
+
|
|
1088
|
+
A slope sign change occurs at sample `xᵢ` if:
|
|
1089
|
+
`[(xᵢ - xᵢ₋₁) × (xᵢ - xᵢ₊₁)] ≥ threshold²`
|
|
1090
|
+
|
|
1091
|
+
Where `xᵢ₋₁` and `xᵢ₊₁` are the previous and next samples.
|
|
1092
|
+
|
|
1093
|
+
**Use cases:**
|
|
1094
|
+
|
|
1095
|
+
- EMG frequency content analysis
|
|
1096
|
+
- Detecting oscillations and vibrations
|
|
1097
|
+
- Gesture recognition based on movement patterns
|
|
1098
|
+
- Signal complexity measurement
|
|
1099
|
+
- Muscle contraction rate estimation
|
|
1100
|
+
- Feature extraction for pattern recognition
|
|
1101
|
+
|
|
1102
|
+
**Example:**
|
|
1103
|
+
|
|
1104
|
+
```typescript
|
|
1105
|
+
// Basic SSC with zero threshold
|
|
1106
|
+
const pipeline = createDspPipeline();
|
|
1107
|
+
pipeline.SlopeSignChange({ windowSize: 100, threshold: 0 });
|
|
1108
|
+
|
|
1109
|
+
const signal = new Float32Array([1, 3, 2, 4, 3, 5]);
|
|
1110
|
+
// Slopes: + - + - +
|
|
1111
|
+
// Sign changes at indices: 2, 3, 4, 5
|
|
1112
|
+
const result = await pipeline.process(signal, {
|
|
1113
|
+
sampleRate: 1000,
|
|
1114
|
+
channels: 1,
|
|
1115
|
+
});
|
|
1116
|
+
// result = [0, 0, 1, 2, 3, 4]
|
|
1117
|
+
|
|
1118
|
+
// SSC with noise threshold
|
|
1119
|
+
const filteredPipeline = createDspPipeline();
|
|
1120
|
+
filteredPipeline.SlopeSignChange({ windowSize: 200, threshold: 0.1 });
|
|
1121
|
+
|
|
1122
|
+
const noisySignal = new Float32Array(500);
|
|
1123
|
+
// Only counts sign changes where |difference| > 0.1
|
|
1124
|
+
const sscCount = await filteredPipeline.process(noisySignal, {
|
|
1125
|
+
sampleRate: 1000,
|
|
1126
|
+
channels: 1,
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
// Multi-channel EMG analysis
|
|
1130
|
+
const emgPipeline = createDspPipeline();
|
|
1131
|
+
emgPipeline
|
|
1132
|
+
.Rectify({ mode: "full" })
|
|
1133
|
+
.SlopeSignChange({ windowSize: 250, threshold: 0.05 });
|
|
1134
|
+
|
|
1135
|
+
const fourChannelEMG = new Float32Array(4000);
|
|
1136
|
+
const sscFeatures = await emgPipeline.process(fourChannelEMG, {
|
|
1137
|
+
sampleRate: 1000,
|
|
1138
|
+
channels: 4,
|
|
1139
|
+
});
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
**Interpretation:**
|
|
1143
|
+
|
|
1144
|
+
- **Low SSC**: Smooth, slow-changing signal (low frequency)
|
|
1145
|
+
- **High SSC**: Rapidly oscillating signal (high frequency)
|
|
1146
|
+
- **Zero SSC**: Monotonic signal (constantly increasing or decreasing)
|
|
1147
|
+
- **Threshold effect**: Higher threshold → fewer counted changes (filters noise)
|
|
1148
|
+
|
|
1149
|
+
**Mathematical Properties:**
|
|
1150
|
+
|
|
1151
|
+
- **Integer-valued**: SSC is always a whole number (count)
|
|
1152
|
+
- **Non-negative**: SSC ≥ 0
|
|
1153
|
+
- **Bounded**: Max SSC = window_size - 2 (alternating signal)
|
|
1154
|
+
- **Frequency proxy**: Approximately proportional to signal frequency
|
|
1155
|
+
|
|
1156
|
+
##### Willison Amplitude (WAMP) Filter
|
|
1157
|
+
|
|
1158
|
+
```typescript
|
|
1159
|
+
pipeline.WillisonAmplitude({ windowSize: number; threshold?: number });
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
Implements Willison Amplitude calculation to count the number of times the absolute difference between consecutive samples exceeds a threshold. Useful for detecting burst activity and transient events in EMG signals.
|
|
1163
|
+
|
|
1164
|
+
**Parameters:**
|
|
1165
|
+
|
|
1166
|
+
- `windowSize`: Number of samples for the sliding window
|
|
1167
|
+
- `threshold`: Minimum absolute difference to count as significant (default: 0)
|
|
1168
|
+
|
|
1169
|
+
**Features:**
|
|
1170
|
+
|
|
1171
|
+
- Counts amplitude changes exceeding threshold
|
|
1172
|
+
- Tracks previous sample for difference calculation
|
|
1173
|
+
- O(1) per-sample computation using circular buffer
|
|
1174
|
+
- Per-channel state for multi-channel processing
|
|
1175
|
+
- Full state serialization including buffer and previous sample
|
|
1176
|
+
- Sensitive to amplitude variations and burst activity
|
|
1177
|
+
|
|
1178
|
+
**Mathematical Definition:**
|
|
1179
|
+
|
|
1180
|
+
WAMP counts samples where:
|
|
1181
|
+
`|xᵢ - xᵢ₋₁| > threshold`
|
|
1182
|
+
|
|
1183
|
+
Where `xᵢ` is the current sample and `xᵢ₋₁` is the previous sample.
|
|
1184
|
+
|
|
1185
|
+
**Use cases:**
|
|
1186
|
+
|
|
1187
|
+
- EMG burst detection and muscle activation counting
|
|
1188
|
+
- Transient event detection in sensor data
|
|
1189
|
+
- Activity level quantification
|
|
1190
|
+
- Feature extraction for gesture recognition
|
|
1191
|
+
- Equipment vibration monitoring
|
|
1192
|
+
- Signal quality assessment (detect dropouts/spikes)
|
|
1193
|
+
|
|
1194
|
+
**Example:**
|
|
1195
|
+
|
|
1196
|
+
```typescript
|
|
1197
|
+
// Basic WAMP with threshold
|
|
1198
|
+
const pipeline = createDspPipeline();
|
|
1199
|
+
pipeline.WillisonAmplitude({ windowSize: 100, threshold: 1.0 });
|
|
1200
|
+
|
|
1201
|
+
const signal = new Float32Array([0, 0.5, 2.5, 2.6, 1.0, 3.5]);
|
|
1202
|
+
// Diffs: 0.5, 2.0, 0.1, -1.6, 2.5
|
|
1203
|
+
// Exceeds: no, yes, no, yes, yes
|
|
1204
|
+
const result = await pipeline.process(signal, {
|
|
1205
|
+
sampleRate: 1000,
|
|
1206
|
+
channels: 1,
|
|
1207
|
+
});
|
|
1208
|
+
// result = [0, 0, 1, 1, 2, 3]
|
|
1209
|
+
|
|
1210
|
+
// EMG burst detection
|
|
1211
|
+
const burstPipeline = createDspPipeline();
|
|
1212
|
+
burstPipeline
|
|
1213
|
+
.Rectify({ mode: "full" })
|
|
1214
|
+
.WillisonAmplitude({ windowSize: 200, threshold: 0.1 });
|
|
1215
|
+
|
|
1216
|
+
const emgData = new Float32Array(1000);
|
|
1217
|
+
const burstCount = await burstPipeline.process(emgData, {
|
|
1218
|
+
sampleRate: 1000,
|
|
1219
|
+
channels: 1,
|
|
1220
|
+
});
|
|
1221
|
+
// High WAMP values indicate burst activity
|
|
1222
|
+
|
|
1223
|
+
// Multi-channel activity monitoring
|
|
1224
|
+
const activityPipeline = createDspPipeline();
|
|
1225
|
+
activityPipeline.WillisonAmplitude({ windowSize: 250, threshold: 0.05 });
|
|
1226
|
+
|
|
1227
|
+
const multiChannelData = new Float32Array(4000); // 4 channels
|
|
1228
|
+
const activityLevel = await activityPipeline.process(multiChannelData, {
|
|
1229
|
+
sampleRate: 1000,
|
|
1230
|
+
channels: 4,
|
|
1231
|
+
});
|
|
1232
|
+
// Each channel independently tracks activity bursts
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
**Interpretation:**
|
|
1236
|
+
|
|
1237
|
+
- **Low WAMP**: Smooth signal with gradual changes
|
|
1238
|
+
- **High WAMP**: Signal with frequent amplitude variations or bursts
|
|
1239
|
+
- **Zero WAMP**: Constant signal or all changes below threshold
|
|
1240
|
+
- **Threshold effect**: Higher threshold → fewer counted events
|
|
1241
|
+
|
|
1242
|
+
**Comparison with SSC:**
|
|
1243
|
+
|
|
1244
|
+
| Feature | Measures | Sensitivity | Best For |
|
|
1245
|
+
| ------- | ----------------- | ---------------------- | --------------------- |
|
|
1246
|
+
| WAMP | Amplitude changes | Large amplitude shifts | Burst detection |
|
|
1247
|
+
| SSC | Direction changes | Frequency content | Oscillation counting |
|
|
1248
|
+
| Both | Signal activity | Different aspects | Combined EMG features |
|
|
1249
|
+
|
|
1250
|
+
**Mathematical Properties:**
|
|
1251
|
+
|
|
1252
|
+
- **Integer-valued**: WAMP is a count (whole number)
|
|
1253
|
+
- **Non-negative**: WAMP ≥ 0
|
|
1254
|
+
- **Bounded**: Max WAMP = window_size - 1 (all samples exceed threshold)
|
|
1255
|
+
- **Threshold-dependent**: WAMP decreases as threshold increases
|
|
1256
|
+
|
|
1257
|
+
#### 🚧 Coming Very Soon
|
|
1258
|
+
|
|
1259
|
+
**Resampling Operations** (Expected in next few days):
|
|
1260
|
+
|
|
1261
|
+
- **`Decimate`**: Downsample by integer factor M with anti-aliasing filter
|
|
1262
|
+
- **`Interpolate`**: Upsample by integer factor L with anti-imaging filter
|
|
1263
|
+
- **`Resample`**: Rational resampling (L/M) for arbitrary rate conversion
|
|
1264
|
+
- All implemented with efficient polyphase FIR filtering in C++
|
|
1265
|
+
|
|
1266
|
+
**Other Planned Features:**
|
|
1267
|
+
|
|
1268
|
+
- **Transform Domain**: STFT, Hilbert transform, wavelet transforms
|
|
1269
|
+
- **Feature Extraction**: Zero-crossing rate, peak detection, autocorrelation
|
|
1270
|
+
|
|
1271
|
+
See the [project roadmap](https://github.com/A-KGeorge/dsp_ts_redis/blob/main/ROADMAP.md) for more details.
|
|
1272
|
+
|
|
1273
|
+
---
|
|
1274
|
+
|
|
1275
|
+
## 🔧 Advanced Features
|
|
1276
|
+
|
|
1277
|
+
For production deployments, the library provides comprehensive observability and monitoring capabilities:
|
|
1278
|
+
|
|
1279
|
+
### Available Features
|
|
1280
|
+
|
|
1281
|
+
- **Pipeline Callbacks** - Monitor performance, errors, and samples with batched or individual callbacks
|
|
1282
|
+
- **Topic-Based Logging** - Kafka-style hierarchical filtering for selective log subscription
|
|
1283
|
+
- **Topic Router** - Fan-out routing to multiple backends (PagerDuty, Prometheus, Loki, etc.)
|
|
1284
|
+
- **Priority-Based Routing** - 10-level priority system for fine-grained log filtering
|
|
1285
|
+
- **`.tap()` Debugging** - Inspect intermediate pipeline results without breaking the flow
|
|
1286
|
+
|
|
1287
|
+
### Quick Example
|
|
1288
|
+
|
|
1289
|
+
```typescript
|
|
1290
|
+
import { createDspPipeline, createTopicRouter } from "dspx";
|
|
1291
|
+
|
|
1292
|
+
// Production-grade routing to multiple backends
|
|
1293
|
+
const router = createTopicRouter()
|
|
1294
|
+
.errors(async (log) => await pagerDuty.alert(log))
|
|
1295
|
+
.performance(async (log) => await prometheus.record(log))
|
|
1296
|
+
.debug(async (log) => await loki.send(log))
|
|
1297
|
+
.build();
|
|
1298
|
+
|
|
1299
|
+
const pipeline = createDspPipeline()
|
|
1300
|
+
.pipeline({ onLogBatch: (logs) => router.routeBatch(logs) })
|
|
1301
|
+
.MovingAverage({ windowSize: 10 })
|
|
1302
|
+
.Rms({ windowSize: 5 });
|
|
1303
|
+
```
|
|
1304
|
+
|
|
1305
|
+
**📚 [Full Advanced Features Documentation](https://github.com/A-KGeorge/dsp_ts_redis/blob/main/docs/advanced.md)**
|
|
1306
|
+
|
|
1307
|
+
Key highlights:
|
|
1308
|
+
|
|
1309
|
+
- Batched callbacks: **3.2M samples/sec** (production-safe, non-blocking)
|
|
1310
|
+
- Individual callbacks: **6.1M samples/sec** (development only, blocks event loop)
|
|
1311
|
+
- Native processing: **22M samples/sec** (no callbacks)
|
|
1312
|
+
|
|
1313
|
+
---
|
|
1314
|
+
|
|
1315
|
+
### Core Methods
|
|
1316
|
+
|
|
1317
|
+
#### `process(input, options)`
|
|
1318
|
+
|
|
1319
|
+
Process data in-place (modifies input buffer for performance):
|
|
1320
|
+
|
|
1321
|
+
```typescript
|
|
1322
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
1323
|
+
const output = await pipeline.process(input, {
|
|
1324
|
+
sampleRate: 2000,
|
|
1325
|
+
channels: 1,
|
|
1326
|
+
});
|
|
1327
|
+
// input === output (same reference)
|
|
1328
|
+
```
|
|
1329
|
+
|
|
1330
|
+
#### `processCopy(input, options)`
|
|
1331
|
+
|
|
1332
|
+
Process a copy of the data (preserves original):
|
|
1333
|
+
|
|
1334
|
+
```typescript
|
|
1335
|
+
const input = new Float32Array([1, 2, 3, 4, 5]);
|
|
1336
|
+
const output = await pipeline.processCopy(input, {
|
|
1337
|
+
sampleRate: 2000,
|
|
1338
|
+
channels: 1,
|
|
1339
|
+
});
|
|
1340
|
+
// input !== output (different references)
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
#### `saveState()`
|
|
1344
|
+
|
|
1345
|
+
Serialize the current pipeline state to JSON:
|
|
1346
|
+
|
|
1347
|
+
```typescript
|
|
1348
|
+
const stateJson = await pipeline.saveState();
|
|
1349
|
+
// Returns: JSON string with all filter states
|
|
1350
|
+
await redis.set("dsp:state:key", stateJson);
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
#### `loadState(stateJson)`
|
|
1354
|
+
|
|
1355
|
+
Deserialize and restore pipeline state from JSON:
|
|
1356
|
+
|
|
1357
|
+
```typescript
|
|
1358
|
+
const stateJson = await redis.get("dsp:state:key");
|
|
1359
|
+
if (stateJson) {
|
|
1360
|
+
await pipeline.loadState(stateJson);
|
|
1361
|
+
}
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
#### `clearState()`
|
|
1365
|
+
|
|
1366
|
+
Reset all filter states to initial values:
|
|
1367
|
+
|
|
1368
|
+
```typescript
|
|
1369
|
+
pipeline.clearState();
|
|
1370
|
+
// All circular buffers cleared, running sums reset
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
#### `listState()`
|
|
1374
|
+
|
|
1375
|
+
Get a lightweight summary of the pipeline configuration (without full buffer data):
|
|
1376
|
+
|
|
1377
|
+
```typescript
|
|
1378
|
+
const pipeline = createDspPipeline()
|
|
1379
|
+
.MovingAverage({ windowSize: 100 })
|
|
1380
|
+
.Rectify({ mode: "full" })
|
|
1381
|
+
.Rms({ windowSize: 50 });
|
|
1382
|
+
|
|
1383
|
+
// After processing some data
|
|
1384
|
+
await pipeline.process(input, { sampleRate: 1000, channels: 1 });
|
|
1385
|
+
|
|
1386
|
+
const summary = pipeline.listState();
|
|
1387
|
+
console.log(summary);
|
|
1388
|
+
// {
|
|
1389
|
+
// stageCount: 3,
|
|
1390
|
+
// timestamp: 1761234567,
|
|
1391
|
+
// stages: [
|
|
1392
|
+
// {
|
|
1393
|
+
// index: 0,
|
|
1394
|
+
// type: 'movingAverage',
|
|
1395
|
+
// windowSize: 100,
|
|
1396
|
+
// numChannels: 1,
|
|
1397
|
+
// bufferSize: 100,
|
|
1398
|
+
// channelCount: 1
|
|
1399
|
+
// },
|
|
1400
|
+
// {
|
|
1401
|
+
// index: 1,
|
|
1402
|
+
// type: 'rectify',
|
|
1403
|
+
// mode: 'full'
|
|
1404
|
+
// },
|
|
1405
|
+
// {
|
|
1406
|
+
// index: 2,
|
|
1407
|
+
// type: 'rms',
|
|
1408
|
+
// windowSize: 50,
|
|
1409
|
+
// numChannels: 1,
|
|
1410
|
+
// bufferSize: 50,
|
|
1411
|
+
// channelCount: 1
|
|
1412
|
+
// }
|
|
1413
|
+
// ]
|
|
1414
|
+
// }
|
|
1415
|
+
```
|
|
1416
|
+
|
|
1417
|
+
**Use Cases:**
|
|
1418
|
+
|
|
1419
|
+
- **Monitoring dashboards**: Expose pipeline configuration via HTTP endpoint
|
|
1420
|
+
- **Health checks**: Verify pipeline structure and configuration
|
|
1421
|
+
- **Debugging**: Quick inspection without parsing full state JSON
|
|
1422
|
+
- **Logging**: Log pipeline configuration changes
|
|
1423
|
+
- **Size efficiency**: ~17-80% smaller than `saveState()` depending on buffer sizes
|
|
1424
|
+
|
|
1425
|
+
**Comparison with `saveState()`:**
|
|
1426
|
+
|
|
1427
|
+
| Method | Use Case | Contains Buffer Data | Size |
|
|
1428
|
+
| ------------- | --------------------------- | -------------------- | ------- |
|
|
1429
|
+
| `listState()` | Monitoring, debugging | No | Smaller |
|
|
1430
|
+
| `saveState()` | Redis persistence, recovery | Yes | Larger |
|
|
1431
|
+
|
|
1432
|
+
---
|
|
1433
|
+
|
|
1434
|
+
## 📊 Use Cases
|
|
1435
|
+
|
|
1436
|
+
### NEW: IoT Sensor Processing with Irregular Timestamps
|
|
1437
|
+
|
|
1438
|
+
```typescript
|
|
1439
|
+
import { createDspPipeline } from "dspx";
|
|
1440
|
+
|
|
1441
|
+
const pipeline = createDspPipeline();
|
|
1442
|
+
pipeline.MovingAverage({ mode: "moving", windowDuration: 10000 }); // 10 second window
|
|
1443
|
+
|
|
1444
|
+
// Sensor data with network jitter (irregular intervals)
|
|
1445
|
+
const sensorReadings = [
|
|
1446
|
+
{ value: 23.5, timestamp: 1698889200000 },
|
|
1447
|
+
{ value: 24.1, timestamp: 1698889200150 }, // 150ms later
|
|
1448
|
+
{ value: 23.8, timestamp: 1698889200380 }, // 230ms later (jitter!)
|
|
1449
|
+
{ value: 24.5, timestamp: 1698889200500 }, // 120ms later
|
|
1450
|
+
];
|
|
1451
|
+
|
|
1452
|
+
const samples = new Float32Array(sensorReadings.map((r) => r.value));
|
|
1453
|
+
const timestamps = new Float32Array(sensorReadings.map((r) => r.timestamp));
|
|
1454
|
+
|
|
1455
|
+
const smoothed = await pipeline.process(samples, timestamps, { channels: 1 });
|
|
1456
|
+
// Properly handles irregular sampling intervals!
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
**📚 [More Time-Series Examples →](https://github.com/A-KGeorge/dsp_ts_redis/blob/main/docs/time-series-guide.md#real-world-examples)**
|
|
1460
|
+
|
|
1461
|
+
### Streaming Data with Crash Recovery
|
|
1462
|
+
|
|
1463
|
+
```typescript
|
|
1464
|
+
import { createDspPipeline } from "dspx";
|
|
1465
|
+
import { createClient } from "redis";
|
|
1466
|
+
|
|
1467
|
+
const redis = await createClient({ url: "redis://localhost:6379" }).connect();
|
|
1468
|
+
const stateKey = "dsp:stream:sensor01";
|
|
1469
|
+
|
|
1470
|
+
const pipeline = createDspPipeline({
|
|
1471
|
+
redisHost: "localhost",
|
|
1472
|
+
redisPort: 6379,
|
|
1473
|
+
stateKey,
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
pipeline.MovingAverage({ windowSize: 100 });
|
|
1477
|
+
|
|
1478
|
+
// Restore state if processing was interrupted
|
|
1479
|
+
const savedState = await redis.get(stateKey);
|
|
1480
|
+
if (savedState) {
|
|
1481
|
+
await pipeline.loadState(savedState);
|
|
1482
|
+
console.log("Resumed from saved state");
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Process streaming chunks
|
|
1486
|
+
for await (const chunk of sensorStream) {
|
|
1487
|
+
const smoothed = await pipeline.process(chunk, {
|
|
1488
|
+
sampleRate: 1000,
|
|
1489
|
+
channels: 1,
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
// Save state after each chunk for crash recovery
|
|
1493
|
+
const state = await pipeline.saveState();
|
|
1494
|
+
await redis.set(stateKey, state);
|
|
1495
|
+
|
|
1496
|
+
await sendToAnalytics(smoothed);
|
|
1497
|
+
}
|
|
1498
|
+
```
|
|
1499
|
+
|
|
1500
|
+
### Multi-Channel EMG Processing
|
|
1501
|
+
|
|
1502
|
+
```typescript
|
|
1503
|
+
import { createDspPipeline } from "dspx";
|
|
1504
|
+
|
|
1505
|
+
// Process 4-channel EMG with rectification + RMS envelope detection
|
|
1506
|
+
const pipeline = createDspPipeline();
|
|
1507
|
+
pipeline
|
|
1508
|
+
.Rectify({ mode: "full" }) // Convert bipolar EMG to magnitude
|
|
1509
|
+
.Rms({ windowSize: 50 }); // Calculate RMS envelope
|
|
1510
|
+
|
|
1511
|
+
// Interleaved 4-channel data
|
|
1512
|
+
const emgData = new Float32Array(4000); // 1000 samples × 4 channels
|
|
1513
|
+
|
|
1514
|
+
const envelope = await pipeline.process(emgData, {
|
|
1515
|
+
sampleRate: 2000,
|
|
1516
|
+
channels: 4,
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
// Each channel maintains independent filter states
|
|
1520
|
+
// Output is smooth envelope tracking muscle activation
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
### Distributed Processing Across Workers
|
|
1524
|
+
|
|
1525
|
+
```typescript
|
|
1526
|
+
// Worker 1 processes first part
|
|
1527
|
+
const worker1 = createDspPipeline({
|
|
1528
|
+
redisHost: "redis.example.com",
|
|
1529
|
+
stateKey: "dsp:session:abc123",
|
|
1530
|
+
});
|
|
1531
|
+
worker1.MovingAverage({ windowSize: 100 });
|
|
1532
|
+
|
|
1533
|
+
await worker1.process(chunk1, { sampleRate: 2000, channels: 1 });
|
|
1534
|
+
const state = await worker1.saveState();
|
|
1535
|
+
await redis.set("dsp:session:abc123", state);
|
|
1536
|
+
|
|
1537
|
+
// Worker 2 continues exactly where Worker 1 left off
|
|
1538
|
+
const worker2 = createDspPipeline({
|
|
1539
|
+
redisHost: "redis.example.com",
|
|
1540
|
+
stateKey: "dsp:session:abc123",
|
|
1541
|
+
});
|
|
1542
|
+
worker2.MovingAverage({ windowSize: 100 });
|
|
1543
|
+
|
|
1544
|
+
const savedState = await redis.get("dsp:session:abc123");
|
|
1545
|
+
await worker2.loadState(savedState);
|
|
1546
|
+
await worker2.process(chunk2, { sampleRate: 2000, channels: 1 });
|
|
1547
|
+
// Processing continues seamlessly with exact buffer state
|
|
1548
|
+
```
|
|
1549
|
+
|
|
1550
|
+
---
|
|
1551
|
+
|
|
1552
|
+
## ⚠️ Important Disclaimers
|
|
1553
|
+
|
|
1554
|
+
### Not for Clinical Use
|
|
1555
|
+
|
|
1556
|
+
**This software has not been validated for medical diagnosis, treatment, or life-critical applications.**
|
|
1557
|
+
|
|
1558
|
+
- Not FDA/CE cleared or approved
|
|
1559
|
+
- No medical device certification
|
|
1560
|
+
- For research and development only
|
|
1561
|
+
- Consult regulatory experts before clinical deployment
|
|
1562
|
+
|
|
1563
|
+
### Performance Considerations
|
|
1564
|
+
|
|
1565
|
+
- **Redis overhead**: State save/load involves JSON serialization and network I/O (~1-10ms depending on state size and network latency).
|
|
1566
|
+
- **In-place processing**: Use `process()` instead of `processCopy()` when you don't need to preserve the input buffer.
|
|
1567
|
+
- **Async processing**: Filter processing runs on a background thread via `Napi::AsyncWorker` to avoid blocking the event loop.
|
|
1568
|
+
- **Batch sizes**: Process reasonable chunk sizes (e.g., 512-4096 samples) to balance latency and throughput.
|
|
1569
|
+
|
|
1570
|
+
---
|
|
1571
|
+
|
|
1572
|
+
## 🧪 Testing & Development
|
|
1573
|
+
|
|
1574
|
+
### Building from Source
|
|
1575
|
+
|
|
1576
|
+
```bash
|
|
1577
|
+
git clone https://github.com/A-KGeorge/dsp_ts_redis.git
|
|
1578
|
+
cd dspx
|
|
1579
|
+
npm install
|
|
1580
|
+
npm run build # Compile C++ bindings with cmake-js
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
### Running Examples
|
|
1584
|
+
|
|
1585
|
+
```bash
|
|
1586
|
+
# Make sure Redis is running
|
|
1587
|
+
redis-server
|
|
1588
|
+
|
|
1589
|
+
# Run the Redis persistence example
|
|
1590
|
+
npx tsx ./src/ts/examples/redis/redis-example.ts
|
|
1591
|
+
|
|
1592
|
+
# Time-series examples (NEW!)
|
|
1593
|
+
npx tsx ./src/ts/examples/timeseries/iot-sensor-example.ts
|
|
1594
|
+
npx tsx ./src/ts/examples/timeseries/redis-streaming-example.ts
|
|
1595
|
+
npx tsx ./src/ts/examples/timeseries/comparison-example.ts
|
|
1596
|
+
|
|
1597
|
+
# Moving Average examples
|
|
1598
|
+
npx tsx ./src/ts/examples/MovingAverage/test-state.ts
|
|
1599
|
+
npx tsx ./src/ts/examples/MovingAverage/test-streaming.ts
|
|
1600
|
+
|
|
1601
|
+
# RMS examples
|
|
1602
|
+
npx tsx ./src/ts/examples/RMS/test-state.ts
|
|
1603
|
+
npx tsx ./src/ts/examples/RMS/test-streaming.ts
|
|
1604
|
+
|
|
1605
|
+
# Rectify examples
|
|
1606
|
+
npx tsx ./src/ts/examples/Rectify/test-state.ts
|
|
1607
|
+
npx tsx ./src/ts/examples/Rectify/test-streaming.ts
|
|
1608
|
+
```
|
|
1609
|
+
|
|
1610
|
+
### Running Tests
|
|
1611
|
+
|
|
1612
|
+
```bash
|
|
1613
|
+
npm test # Run all tests
|
|
1614
|
+
```
|
|
1615
|
+
|
|
1616
|
+
**Test Coverage:**
|
|
1617
|
+
|
|
1618
|
+
- ✅ **260+ tests** across test suites
|
|
1619
|
+
- ✅ Moving Average (batch & moving modes)
|
|
1620
|
+
- ✅ RMS (batch & moving modes)
|
|
1621
|
+
- ✅ Variance (batch & moving modes)
|
|
1622
|
+
- ✅ Z-Score Normalization (batch & moving modes)
|
|
1623
|
+
- ✅ Mean Absolute Value (batch & moving modes)
|
|
1624
|
+
- ✅ Waveform Length (EMG feature)
|
|
1625
|
+
- ✅ Slope Sign Change (SSC - EMG feature)
|
|
1626
|
+
- ✅ Willison Amplitude (WAMP - EMG feature)
|
|
1627
|
+
- ✅ Rectify (full-wave & half-wave)
|
|
1628
|
+
- ✅ Time-Series Processing (18 tests)
|
|
1629
|
+
- ✅ Pipeline Chaining (17 tests)
|
|
1630
|
+
- ✅ State Management (save/load/clear)
|
|
1631
|
+
- ✅ Redis Persistence (15 tests)
|
|
1632
|
+
- ✅ Multi-Channel Processing
|
|
1633
|
+
- ✅ Topic Router & Logging (54 tests)
|
|
1634
|
+
- ✅ Tap Debugging (8 tests)
|
|
1635
|
+
|
|
1636
|
+
### Implementation Status
|
|
1637
|
+
|
|
1638
|
+
- ✅ **Moving Average Filter**: Fully implemented with state persistence
|
|
1639
|
+
- ✅ **RMS Filter**: Fully implemented with state persistence and envelope detection
|
|
1640
|
+
- ✅ **Rectify Filter**: Full-wave and half-wave rectification with mode persistence
|
|
1641
|
+
- ✅ **Variance Filter**: Batch and moving modes with state persistence
|
|
1642
|
+
- ✅ **Z-Score Normalization**: Batch and moving modes with adaptive normalization
|
|
1643
|
+
- ✅ **Mean Absolute Value**: Batch and moving modes for EMG/biosignal analysis
|
|
1644
|
+
- ✅ **Waveform Length**: EMG complexity and path length measurement
|
|
1645
|
+
- ✅ **Slope Sign Change (SSC)**: EMG frequency content and oscillation detection
|
|
1646
|
+
- ✅ **Willison Amplitude (WAMP)**: EMG burst detection and amplitude change counting
|
|
1647
|
+
- ✅ **Time-Series Processing**: Support for irregular timestamps and time-based windows
|
|
1648
|
+
- ✅ **Circular Buffer**: Optimized with O(1) operations
|
|
1649
|
+
- ✅ **Multi-Channel Support**: Independent state per channel
|
|
1650
|
+
- ✅ **Redis State Serialization**: Complete buffer and sum/sum-of-squares persistence
|
|
1651
|
+
- ✅ **Async Processing**: Background thread via Napi::AsyncWorker
|
|
1652
|
+
- ✅ **Pipeline Callbacks**: Batched and individual callbacks with topic routing
|
|
1653
|
+
- ✅ **Streaming Tests**: Comprehensive streaming validation with interruption recovery
|
|
1654
|
+
- ✅ **Core DSP**: IIR, FIR, FFT
|
|
1655
|
+
- 🚧 **Additional Filters**: polyphaseDecimate, interpolate, resample
|
|
1656
|
+
|
|
1657
|
+
---
|
|
1658
|
+
|
|
1659
|
+
## 📚 Examples
|
|
1660
|
+
|
|
1661
|
+
Check out the `/src/ts/examples` directory for complete working examples:
|
|
1662
|
+
|
|
1663
|
+
### Time-Series Processing (NEW!)
|
|
1664
|
+
|
|
1665
|
+
- [`timeseries/iot-sensor-example.ts`](./src/ts/examples/timeseries/iot-sensor-example.ts) - IoT sensor processing with network jitter and irregular timestamps
|
|
1666
|
+
- [`timeseries/redis-streaming-example.ts`](./src/ts/examples/timeseries/redis-streaming-example.ts) - Streaming data with Redis state persistence and recovery
|
|
1667
|
+
- [`timeseries/comparison-example.ts`](./src/ts/examples/timeseries/comparison-example.ts) - Sample-based vs time-based processing comparison
|
|
1668
|
+
|
|
1669
|
+
### Redis Integration
|
|
1670
|
+
|
|
1671
|
+
- [`redis/redis-example.ts`](./src/ts/examples/redis/redis-example.ts) - Full Redis integration with state persistence
|
|
1672
|
+
|
|
1673
|
+
### Moving Average Filter
|
|
1674
|
+
|
|
1675
|
+
- [`MovingAverage/test-state.ts`](./src/ts/examples/MovingAverage/test-state.ts) - State management (save/load/clear)
|
|
1676
|
+
- [`MovingAverage/test-streaming.ts`](./src/ts/examples/MovingAverage/test-streaming.ts) - Streaming data processing with interruption recovery
|
|
1677
|
+
|
|
1678
|
+
### RMS Filter
|
|
1679
|
+
|
|
1680
|
+
- [`RMS/test-state.ts`](./src/ts/examples/RMS/test-state.ts) - RMS state management with negative values
|
|
1681
|
+
- [`RMS/test-streaming.ts`](./src/ts/examples/RMS/test-streaming.ts) - Real-time envelope detection and multi-channel RMS
|
|
1682
|
+
|
|
1683
|
+
### Rectify Filter
|
|
1684
|
+
|
|
1685
|
+
- [`Rectify/test-state.ts`](./src/ts/examples/Rectify/test-state.ts) - Full-wave and half-wave rectification with state persistence
|
|
1686
|
+
- [`Rectify/test-streaming.ts`](./src/ts/examples/Rectify/test-streaming.ts) - EMG pre-processing and multi-channel rectification
|
|
1687
|
+
|
|
1688
|
+
---
|
|
1689
|
+
|
|
1690
|
+
## 🤝 Contributing
|
|
1691
|
+
|
|
1692
|
+
Contributions are welcome! This project is in active development.
|
|
1693
|
+
|
|
1694
|
+
### Priority Areas
|
|
1695
|
+
|
|
1696
|
+
1. **Transform Domain**: FFT, STFT, wavelet transforms
|
|
1697
|
+
2. **Performance**: SIMD optimizations, benchmarking
|
|
1698
|
+
3. **Testing**: Unit tests, validation against SciPy/NumPy
|
|
1699
|
+
4. **Documentation**: More examples, API docs, tutorials
|
|
1700
|
+
|
|
1701
|
+
### Development Workflow
|
|
1702
|
+
|
|
1703
|
+
```bash
|
|
1704
|
+
git clone https://github.com/A-KGeorge/dsp_ts_redis.git
|
|
1705
|
+
cd dspx
|
|
1706
|
+
npm install
|
|
1707
|
+
npm run build # Compile C++ with cmake-js
|
|
1708
|
+
npm run test # Run tests
|
|
1709
|
+
```
|
|
1710
|
+
|
|
1711
|
+
---
|
|
1712
|
+
|
|
1713
|
+
## 🌐 Browser Support (Roadmap)
|
|
1714
|
+
|
|
1715
|
+
WebAssembly port is planned to enable browser-based real-time audio processing:
|
|
1716
|
+
|
|
1717
|
+
- Audio Worklet integration for non-blocking DSP
|
|
1718
|
+
- WASM-accelerated spectral analysis
|
|
1719
|
+
- Potential integration with live coding platforms (Strudel.cc, Hydra)
|
|
1720
|
+
|
|
1721
|
+
Interested in collaborating? Open an issue!
|
|
1722
|
+
|
|
1723
|
+
---
|
|
1724
|
+
|
|
1725
|
+
## � Recent Bug Fixes & Improvements
|
|
1726
|
+
|
|
1727
|
+
### Critical Fixes (October 2025)
|
|
1728
|
+
|
|
1729
|
+
#### 1. **Fixed Precision Loss in Double Conversion** (C++)
|
|
1730
|
+
|
|
1731
|
+
- **Issue**: `NapiArrayToVector<double>` was using `FloatValue()` instead of `DoubleValue()`, causing 32-bit precision loss
|
|
1732
|
+
- **Impact**: Timestamps and high-precision data were truncated from 64-bit to 32-bit
|
|
1733
|
+
- **Fix**: Now uses `DoubleValue()` for `double` types and `FloatValue()` for `float` types
|
|
1734
|
+
- **File**: `src/native/utils/NapiUtils.cc`
|
|
1735
|
+
|
|
1736
|
+
#### 2. **Fixed DriftDetector Sample Rate Bug** (TypeScript)
|
|
1737
|
+
|
|
1738
|
+
- **Issue**: When `process()` was called with different `sampleRate` values, the existing `DriftDetector` (configured with the old sample rate) would be reused, causing incorrect drift detection
|
|
1739
|
+
- **Impact**: Drift detection would report false positives/negatives when processing streams with varying sample rates
|
|
1740
|
+
- **Fix**: Now checks if sample rate changed and recreates the detector when needed
|
|
1741
|
+
- **Files**: `src/ts/DriftDetector.ts`, `src/ts/bindings.ts`
|
|
1742
|
+
|
|
1743
|
+
#### 3. **Fixed Missing `<numeric>` Header** (C++)
|
|
1744
|
+
|
|
1745
|
+
- **Issue**: macOS/Linux builds failed with `error: no member named 'accumulate' in namespace 'std'`
|
|
1746
|
+
- **Impact**: CI builds on macOS and Ubuntu failed
|
|
1747
|
+
- **Fix**: Added `#include <numeric>` to `Policies.h`
|
|
1748
|
+
- **File**: `src/native/core/Policies.h`
|
|
1749
|
+
|
|
1750
|
+
#### 4. **Improved Build Reliability** (Build System)
|
|
1751
|
+
|
|
1752
|
+
- **Issue**: Dynamic file discovery in `binding.gyp` using `readdirSync()` wouldn't detect new `.cc` files without reconfiguration
|
|
1753
|
+
- **Impact**: New source files could be added but not compiled, causing mysterious build failures
|
|
1754
|
+
- **Fix**: Explicitly listed all source files in `binding.gyp`
|
|
1755
|
+
- **File**: `binding.gyp`
|
|
1756
|
+
|
|
1757
|
+
---
|
|
1758
|
+
|
|
1759
|
+
## ⚠️ Alpha Disclaimer
|
|
1760
|
+
|
|
1761
|
+
> **Important:** This library is currently in **alpha**.
|
|
1762
|
+
> It has **not been tested in production** and is primarily intended for
|
|
1763
|
+
> research, prototyping, and performance experimentation.
|
|
1764
|
+
>
|
|
1765
|
+
> - Use it **at your own discretion**.
|
|
1766
|
+
> - If you encounter bugs, crashes, or inconsistent behavior, please **open an issue**.
|
|
1767
|
+
> - Pull requests (PRs) are welcome — I’ll review and merge fixes or improvements as time allows.
|
|
1768
|
+
>
|
|
1769
|
+
> The goal is to build a **community-maintained** DSP framework.
|
|
1770
|
+
> Early adopters are encouraged to contribute benchmarks, feature requests,
|
|
1771
|
+
> and test results to help make this stable for real-world deployments.
|
|
1772
|
+
|
|
1773
|
+
---
|
|
1774
|
+
|
|
1775
|
+
### Testing
|
|
1776
|
+
|
|
1777
|
+
All 441 tests pass after these fixes. Run `npm test` to verify.
|
|
1778
|
+
|
|
1779
|
+
---
|
|
1780
|
+
|
|
1781
|
+
## �📄 License
|
|
1782
|
+
|
|
1783
|
+
MIT © Alan Kochukalam George
|
|
1784
|
+
|
|
1785
|
+
---
|
|
1786
|
+
|
|
1787
|
+
## 🙏 Acknowledgments
|
|
1788
|
+
|
|
1789
|
+
Built with:
|
|
1790
|
+
|
|
1791
|
+
- [N-API](https://nodejs.org/api/n-api.html) and [node-addon-api](https://github.com/nodejs/node-addon-api) for native bindings
|
|
1792
|
+
- [node-gyp](https://github.com/nodejs/node-gyp) for cross platform C++ compilation
|
|
1793
|
+
- [Redis](https://redis.io/) for state persistence
|
|
1794
|
+
- [TypeScript](https://www.typescriptlang.org/) for type safety
|
|
1795
|
+
|
|
1796
|
+
Inspired by:
|
|
1797
|
+
|
|
1798
|
+
- [SciPy](https://scipy.org/) signal processing
|
|
1799
|
+
- [librosa](https://librosa.org/) audio analysis
|
|
1800
|
+
|
|
1801
|
+
---
|
|
1802
|
+
|
|
1803
|
+
**Built for real-time signal processing in Node.js** 🚀
|