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,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EMG Feature Extraction Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to use SSC, WAMP, and Waveform Length together
|
|
5
|
+
* for comprehensive EMG signal analysis.
|
|
6
|
+
*
|
|
7
|
+
* These features are commonly used in:
|
|
8
|
+
* - Muscle activity detection
|
|
9
|
+
* - Gesture recognition
|
|
10
|
+
* - Prosthetic control
|
|
11
|
+
* - Rehabilitation monitoring
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createDspPipeline } from "../bindings";
|
|
15
|
+
|
|
16
|
+
async function emgFeaturesExample() {
|
|
17
|
+
console.log("=== EMG Feature Extraction Example ===\n");
|
|
18
|
+
|
|
19
|
+
// Example 1: Individual Feature Analysis
|
|
20
|
+
console.log("Example 1: Computing Individual EMG Features\n");
|
|
21
|
+
|
|
22
|
+
// Create pipelines for each feature
|
|
23
|
+
const sscPipeline = createDspPipeline();
|
|
24
|
+
const wampPipeline = createDspPipeline();
|
|
25
|
+
const wlPipeline = createDspPipeline();
|
|
26
|
+
|
|
27
|
+
// Window size: 250 samples (typical for EMG at 1kHz = 250ms window)
|
|
28
|
+
const windowSize = 250;
|
|
29
|
+
|
|
30
|
+
sscPipeline.SlopeSignChange({
|
|
31
|
+
windowSize,
|
|
32
|
+
threshold: 0.01, // Small threshold to filter noise
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
wampPipeline.WillisonAmplitude({
|
|
36
|
+
windowSize,
|
|
37
|
+
threshold: 0.05, // Threshold for significant amplitude changes
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
wlPipeline.WaveformLength({
|
|
41
|
+
windowSize,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Generate synthetic EMG-like signal (burst of activity)
|
|
45
|
+
const signal = new Float32Array(500);
|
|
46
|
+
for (let i = 0; i < 500; i++) {
|
|
47
|
+
// Resting phase (0-150)
|
|
48
|
+
if (i < 150) {
|
|
49
|
+
signal[i] = (Math.random() - 0.5) * 0.02; // Low noise
|
|
50
|
+
}
|
|
51
|
+
// Active phase (150-350)
|
|
52
|
+
else if (i < 350) {
|
|
53
|
+
const envelope = Math.sin(((i - 150) * Math.PI) / 200); // Smooth envelope
|
|
54
|
+
signal[i] = envelope * Math.sin(i * 0.5) + (Math.random() - 0.5) * 0.1;
|
|
55
|
+
}
|
|
56
|
+
// Recovery phase (350-500)
|
|
57
|
+
else {
|
|
58
|
+
signal[i] = (Math.random() - 0.5) * 0.03; // Low noise
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Process with each feature extractor
|
|
63
|
+
const sscResult = new Float32Array(signal);
|
|
64
|
+
const wampResult = new Float32Array(signal);
|
|
65
|
+
const wlResult = new Float32Array(signal);
|
|
66
|
+
|
|
67
|
+
await sscPipeline.process(sscResult, { channels: 1 });
|
|
68
|
+
await wampPipeline.process(wampResult, { channels: 1 });
|
|
69
|
+
await wlPipeline.process(wlResult, { channels: 1 });
|
|
70
|
+
|
|
71
|
+
// Analyze features at different phases
|
|
72
|
+
const restingIdx = 200; // Middle of resting phase
|
|
73
|
+
const activeIdx = 250; // Middle of active phase
|
|
74
|
+
const recoveryIdx = 400; // Middle of recovery phase
|
|
75
|
+
|
|
76
|
+
console.log("Resting Phase (low activity):");
|
|
77
|
+
console.log(` SSC: ${sscResult[restingIdx].toFixed(2)}`);
|
|
78
|
+
console.log(` WAMP: ${wampResult[restingIdx].toFixed(2)}`);
|
|
79
|
+
console.log(` WL: ${wlResult[restingIdx].toFixed(4)}\n`);
|
|
80
|
+
|
|
81
|
+
console.log("Active Phase (high activity):");
|
|
82
|
+
console.log(` SSC: ${sscResult[activeIdx].toFixed(2)}`);
|
|
83
|
+
console.log(` WAMP: ${wampResult[activeIdx].toFixed(2)}`);
|
|
84
|
+
console.log(` WL: ${wlResult[activeIdx].toFixed(4)}\n`);
|
|
85
|
+
|
|
86
|
+
console.log("Recovery Phase (returning to rest):");
|
|
87
|
+
console.log(` SSC: ${sscResult[recoveryIdx].toFixed(2)}`);
|
|
88
|
+
console.log(` WAMP: ${wampResult[recoveryIdx].toFixed(2)}`);
|
|
89
|
+
console.log(` WL: ${wlResult[recoveryIdx].toFixed(4)}\n`);
|
|
90
|
+
|
|
91
|
+
// Example 2: Multi-Channel EMG (e.g., multiple muscles)
|
|
92
|
+
console.log("Example 2: Multi-Channel EMG Analysis\n");
|
|
93
|
+
|
|
94
|
+
const multiPipeline = createDspPipeline();
|
|
95
|
+
multiPipeline.SlopeSignChange({
|
|
96
|
+
windowSize: 200,
|
|
97
|
+
threshold: 0.01,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 3 channels representing different muscles
|
|
101
|
+
const numChannels = 3;
|
|
102
|
+
const numSamples = 300;
|
|
103
|
+
const multiSignal = new Float32Array(numSamples * numChannels);
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < numSamples; i++) {
|
|
106
|
+
// Channel 0: Biceps (active early)
|
|
107
|
+
const bicepsActivity = i < 150 ? Math.sin(i * 0.3) * (i / 150) : 0.1;
|
|
108
|
+
multiSignal[i * numChannels] =
|
|
109
|
+
bicepsActivity + (Math.random() - 0.5) * 0.05;
|
|
110
|
+
|
|
111
|
+
// Channel 1: Triceps (active later)
|
|
112
|
+
const tricepsActivity =
|
|
113
|
+
i > 150 ? Math.sin(i * 0.3) * ((numSamples - i) / 150) : 0.1;
|
|
114
|
+
multiSignal[i * numChannels + 1] =
|
|
115
|
+
tricepsActivity + (Math.random() - 0.5) * 0.05;
|
|
116
|
+
|
|
117
|
+
// Channel 2: Forearm (continuously active)
|
|
118
|
+
const forearmActivity = Math.sin(i * 0.2) * 0.5;
|
|
119
|
+
multiSignal[i * numChannels + 2] =
|
|
120
|
+
forearmActivity + (Math.random() - 0.5) * 0.03;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
await multiPipeline.process(multiSignal, { channels: numChannels });
|
|
124
|
+
|
|
125
|
+
// Extract features at a specific time point
|
|
126
|
+
const timeIdx = 250;
|
|
127
|
+
console.log(`SSC at sample ${timeIdx}:`);
|
|
128
|
+
console.log(
|
|
129
|
+
` Biceps (Ch0): ${multiSignal[timeIdx * numChannels].toFixed(2)}`
|
|
130
|
+
);
|
|
131
|
+
console.log(
|
|
132
|
+
` Triceps (Ch1): ${multiSignal[timeIdx * numChannels + 1].toFixed(2)}`
|
|
133
|
+
);
|
|
134
|
+
console.log(
|
|
135
|
+
` Forearm (Ch2): ${multiSignal[timeIdx * numChannels + 2].toFixed(2)}\n`
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Example 3: Real-time Feature Extraction with Buffering
|
|
139
|
+
console.log("Example 3: Real-time Processing Simulation\n");
|
|
140
|
+
|
|
141
|
+
const realtimePipeline = createDspPipeline();
|
|
142
|
+
realtimePipeline.Rectify({ mode: "full" });
|
|
143
|
+
realtimePipeline.WillisonAmplitude({
|
|
144
|
+
windowSize: 100,
|
|
145
|
+
threshold: 0.05,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log("Processing 5 consecutive batches of 50 samples each:");
|
|
149
|
+
|
|
150
|
+
for (let batch = 0; batch < 5; batch++) {
|
|
151
|
+
const batchData = new Float32Array(50);
|
|
152
|
+
|
|
153
|
+
// Simulate different activity levels per batch
|
|
154
|
+
const intensity = batch === 2 ? 1.5 : 0.5; // High activity in batch 2
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < 50; i++) {
|
|
157
|
+
batchData[i] =
|
|
158
|
+
Math.sin(i * 0.4) * intensity + (Math.random() - 0.5) * 0.1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
await realtimePipeline.process(batchData, { channels: 1 });
|
|
162
|
+
|
|
163
|
+
// Get the last value (most recent feature value)
|
|
164
|
+
const currentFeature = batchData[batchData.length - 1];
|
|
165
|
+
console.log(` Batch ${batch + 1}: WAMP = ${currentFeature.toFixed(2)}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log();
|
|
169
|
+
|
|
170
|
+
// Example 4: Feature-based Activity Detection
|
|
171
|
+
console.log("Example 4: Activity Detection Threshold\n");
|
|
172
|
+
|
|
173
|
+
const detectorPipeline = createDspPipeline();
|
|
174
|
+
detectorPipeline.WaveformLength({ windowSize: 150 });
|
|
175
|
+
|
|
176
|
+
const testSignal = new Float32Array(400);
|
|
177
|
+
for (let i = 0; i < 400; i++) {
|
|
178
|
+
// Simulate muscle contraction from sample 150-250
|
|
179
|
+
const isActive = i >= 150 && i <= 250;
|
|
180
|
+
const amplitude = isActive ? 1.0 : 0.1;
|
|
181
|
+
testSignal[i] =
|
|
182
|
+
Math.sin(i * 0.3) * amplitude + (Math.random() - 0.5) * 0.05;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await detectorPipeline.process(testSignal, { channels: 1 });
|
|
186
|
+
|
|
187
|
+
// Define threshold for activity detection
|
|
188
|
+
const activityThreshold = 5.0;
|
|
189
|
+
|
|
190
|
+
let activityStart = -1;
|
|
191
|
+
let activityEnd = -1;
|
|
192
|
+
|
|
193
|
+
for (let i = 0; i < testSignal.length; i++) {
|
|
194
|
+
if (testSignal[i] > activityThreshold && activityStart === -1) {
|
|
195
|
+
activityStart = i;
|
|
196
|
+
}
|
|
197
|
+
if (
|
|
198
|
+
testSignal[i] < activityThreshold &&
|
|
199
|
+
activityStart !== -1 &&
|
|
200
|
+
activityEnd === -1
|
|
201
|
+
) {
|
|
202
|
+
activityEnd = i;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log(
|
|
207
|
+
`Activity detected from sample ${activityStart} to ${activityEnd}`
|
|
208
|
+
);
|
|
209
|
+
console.log(`Duration: ${activityEnd - activityStart} samples`);
|
|
210
|
+
console.log(`Expected activity: samples 150-250\n`);
|
|
211
|
+
|
|
212
|
+
// Example 5: Feature Comparison for Classification
|
|
213
|
+
console.log("Example 5: Feature Vector for Classification\n");
|
|
214
|
+
|
|
215
|
+
const featurePipeline1 = createDspPipeline();
|
|
216
|
+
const featurePipeline2 = createDspPipeline();
|
|
217
|
+
const featurePipeline3 = createDspPipeline();
|
|
218
|
+
|
|
219
|
+
featurePipeline1.SlopeSignChange({
|
|
220
|
+
windowSize: 200,
|
|
221
|
+
threshold: 0,
|
|
222
|
+
});
|
|
223
|
+
featurePipeline2.WillisonAmplitude({
|
|
224
|
+
windowSize: 200,
|
|
225
|
+
threshold: 0.03,
|
|
226
|
+
});
|
|
227
|
+
featurePipeline3.WaveformLength({ windowSize: 200 });
|
|
228
|
+
|
|
229
|
+
// Two different gesture patterns
|
|
230
|
+
const gesture1 = new Float32Array(300);
|
|
231
|
+
const gesture2 = new Float32Array(300);
|
|
232
|
+
|
|
233
|
+
// Gesture 1: Fast oscillation
|
|
234
|
+
for (let i = 0; i < 300; i++) {
|
|
235
|
+
gesture1[i] = Math.sin(i * 0.5) * 0.8;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Gesture 2: Slow oscillation with bursts
|
|
239
|
+
for (let i = 0; i < 300; i++) {
|
|
240
|
+
const burst = Math.floor(i / 60) % 2 === 0 ? 1.0 : 0.3;
|
|
241
|
+
gesture2[i] = Math.sin(i * 0.1) * burst;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Process each gesture
|
|
245
|
+
const g1ssc = new Float32Array(gesture1);
|
|
246
|
+
const g1wamp = new Float32Array(gesture1);
|
|
247
|
+
const g1wl = new Float32Array(gesture1);
|
|
248
|
+
|
|
249
|
+
await featurePipeline1.process(g1ssc, { channels: 1 });
|
|
250
|
+
await featurePipeline2.process(g1wamp, { channels: 1 });
|
|
251
|
+
await featurePipeline3.process(g1wl, { channels: 1 });
|
|
252
|
+
|
|
253
|
+
featurePipeline1.clearState();
|
|
254
|
+
featurePipeline2.clearState();
|
|
255
|
+
featurePipeline3.clearState();
|
|
256
|
+
|
|
257
|
+
const g2ssc = new Float32Array(gesture2);
|
|
258
|
+
const g2wamp = new Float32Array(gesture2);
|
|
259
|
+
const g2wl = new Float32Array(gesture2);
|
|
260
|
+
|
|
261
|
+
await featurePipeline1.process(g2ssc, { channels: 1 });
|
|
262
|
+
await featurePipeline2.process(g2wamp, { channels: 1 });
|
|
263
|
+
await featurePipeline3.process(g2wl, { channels: 1 });
|
|
264
|
+
|
|
265
|
+
// Extract features at middle of signals
|
|
266
|
+
const midIdx = 250;
|
|
267
|
+
|
|
268
|
+
console.log("Gesture 1 (fast oscillation) features:");
|
|
269
|
+
console.log(` SSC: ${g1ssc[midIdx].toFixed(2)}`);
|
|
270
|
+
console.log(` WAMP: ${g1wamp[midIdx].toFixed(2)}`);
|
|
271
|
+
console.log(` WL: ${g1wl[midIdx].toFixed(4)}\n`);
|
|
272
|
+
|
|
273
|
+
console.log("Gesture 2 (slow with bursts) features:");
|
|
274
|
+
console.log(` SSC: ${g2ssc[midIdx].toFixed(2)}`);
|
|
275
|
+
console.log(` WAMP: ${g2wamp[midIdx].toFixed(2)}`);
|
|
276
|
+
console.log(` WL: ${g2wl[midIdx].toFixed(4)}\n`);
|
|
277
|
+
|
|
278
|
+
console.log("These feature vectors can be used for ML classification!\n");
|
|
279
|
+
|
|
280
|
+
console.log("=== Example Complete ===");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Run the example
|
|
284
|
+
emgFeaturesExample().catch(console.error);
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FFT/DFT Examples
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates all 8 transforms and common use cases
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
FftProcessor,
|
|
9
|
+
MovingFftProcessor,
|
|
10
|
+
FftUtils,
|
|
11
|
+
type ComplexArray,
|
|
12
|
+
} from "../fft.js";
|
|
13
|
+
|
|
14
|
+
// ========== Example 1: Basic Spectral Analysis ==========
|
|
15
|
+
|
|
16
|
+
console.log("Example 1: Basic Spectral Analysis");
|
|
17
|
+
console.log("=====================================\n");
|
|
18
|
+
|
|
19
|
+
const fftSize = 1024;
|
|
20
|
+
const sampleRate = 44100; // 44.1 kHz
|
|
21
|
+
const fft = new FftProcessor(fftSize);
|
|
22
|
+
|
|
23
|
+
// Generate test signal: 440 Hz (A4) + 880 Hz (A5)
|
|
24
|
+
const signal = new Float32Array(fftSize);
|
|
25
|
+
for (let i = 0; i < fftSize; i++) {
|
|
26
|
+
signal[i] =
|
|
27
|
+
Math.sin((2 * Math.PI * 440 * i) / sampleRate) +
|
|
28
|
+
0.5 * Math.sin((2 * Math.PI * 880 * i) / sampleRate);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Compute FFT
|
|
32
|
+
const spectrum = fft.rfft(signal);
|
|
33
|
+
console.log(`Input size: ${signal.length}`);
|
|
34
|
+
console.log(`Spectrum size: ${spectrum.real.length} (half-spectrum)\n`);
|
|
35
|
+
|
|
36
|
+
// Get magnitude spectrum
|
|
37
|
+
const magnitudes = fft.getMagnitude(spectrum);
|
|
38
|
+
const frequencies = fft.getFrequencyBins(sampleRate);
|
|
39
|
+
|
|
40
|
+
// Find peaks
|
|
41
|
+
const peak1Freq = FftUtils.findPeakFrequency(magnitudes, sampleRate, fftSize);
|
|
42
|
+
console.log(`Peak frequency: ${peak1Freq.toFixed(2)} Hz`);
|
|
43
|
+
|
|
44
|
+
// Convert to decibels
|
|
45
|
+
const dB = FftUtils.toDecibels(magnitudes);
|
|
46
|
+
console.log(
|
|
47
|
+
`Peak level: ${dB[Math.round((440 * fftSize) / sampleRate)].toFixed(2)} dB\n`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// ========== Example 2: Inverse Transform ==========
|
|
51
|
+
|
|
52
|
+
console.log("Example 2: Inverse Transform (Perfect Reconstruction)");
|
|
53
|
+
console.log("======================================================\n");
|
|
54
|
+
|
|
55
|
+
// Original signal
|
|
56
|
+
const original = new Float32Array(256);
|
|
57
|
+
for (let i = 0; i < 256; i++) {
|
|
58
|
+
original[i] = Math.sin((2 * Math.PI * 5 * i) / 256);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const fft256 = new FftProcessor(256);
|
|
62
|
+
|
|
63
|
+
// Forward transform
|
|
64
|
+
const forwardSpectrum = fft256.rfft(original);
|
|
65
|
+
|
|
66
|
+
// Inverse transform
|
|
67
|
+
const reconstructed = fft256.irfft(forwardSpectrum);
|
|
68
|
+
|
|
69
|
+
// Check reconstruction error
|
|
70
|
+
let maxError = 0;
|
|
71
|
+
for (let i = 0; i < original.length; i++) {
|
|
72
|
+
const error = Math.abs(reconstructed[i] - original[i]);
|
|
73
|
+
maxError = Math.max(maxError, error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`Reconstruction max error: ${maxError.toExponential(2)}`);
|
|
77
|
+
console.log("✓ Perfect reconstruction (error < 1e-5)\n");
|
|
78
|
+
|
|
79
|
+
// ========== Example 3: Complex FFT ==========
|
|
80
|
+
|
|
81
|
+
console.log("Example 3: Complex FFT (Analytic Signal)");
|
|
82
|
+
console.log("=========================================\n");
|
|
83
|
+
|
|
84
|
+
const complexSize = 128;
|
|
85
|
+
const complexFft = new FftProcessor(complexSize);
|
|
86
|
+
|
|
87
|
+
// Create complex signal (analytic)
|
|
88
|
+
const complexInput: ComplexArray = {
|
|
89
|
+
real: new Float32Array(complexSize),
|
|
90
|
+
imag: new Float32Array(complexSize),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const freq = 10;
|
|
94
|
+
for (let i = 0; i < complexSize; i++) {
|
|
95
|
+
complexInput.real[i] = Math.cos((2 * Math.PI * freq * i) / complexSize);
|
|
96
|
+
complexInput.imag[i] = Math.sin((2 * Math.PI * freq * i) / complexSize);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const complexSpectrum = complexFft.fft(complexInput);
|
|
100
|
+
const complexMagnitudes = complexFft.getMagnitude(complexSpectrum);
|
|
101
|
+
|
|
102
|
+
// Find peak
|
|
103
|
+
let peakBin = 0;
|
|
104
|
+
let peakValue = 0;
|
|
105
|
+
for (let i = 0; i < complexMagnitudes.length; i++) {
|
|
106
|
+
if (complexMagnitudes[i] > peakValue) {
|
|
107
|
+
peakValue = complexMagnitudes[i];
|
|
108
|
+
peakBin = i;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`Complex signal frequency bin: ${peakBin} (expected: ${freq})`);
|
|
113
|
+
console.log(`Peak magnitude: ${peakValue.toFixed(2)}\n`);
|
|
114
|
+
|
|
115
|
+
// ========== Example 4: Moving FFT (Streaming) ==========
|
|
116
|
+
|
|
117
|
+
console.log("Example 4: Moving FFT (Streaming Audio)");
|
|
118
|
+
console.log("========================================\n");
|
|
119
|
+
|
|
120
|
+
const movingFft = new MovingFftProcessor({
|
|
121
|
+
fftSize: 2048,
|
|
122
|
+
hopSize: 512, // 75% overlap
|
|
123
|
+
mode: "batched",
|
|
124
|
+
windowType: "hann",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Simulate streaming audio
|
|
128
|
+
const streamSize = 8192;
|
|
129
|
+
const stream = new Float32Array(streamSize);
|
|
130
|
+
|
|
131
|
+
// Chirp signal (frequency sweep)
|
|
132
|
+
for (let i = 0; i < streamSize; i++) {
|
|
133
|
+
const instantFreq = 200 + (1000 * i) / streamSize; // 200 Hz -> 1200 Hz
|
|
134
|
+
stream[i] = Math.sin((2 * Math.PI * instantFreq * i) / sampleRate);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let frameCount = 0;
|
|
138
|
+
movingFft.addSamples(stream, (spectrum, size) => {
|
|
139
|
+
frameCount++;
|
|
140
|
+
|
|
141
|
+
if (frameCount === 1 || frameCount % 5 === 0) {
|
|
142
|
+
const mags = new Float32Array(size);
|
|
143
|
+
for (let i = 0; i < size; i++) {
|
|
144
|
+
mags[i] = Math.sqrt(spectrum.real[i] ** 2 + spectrum.imag[i] ** 2);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const peakFreq = FftUtils.findPeakFrequency(mags, sampleRate, 2048);
|
|
148
|
+
console.log(`Frame ${frameCount}: Peak at ${peakFreq.toFixed(2)} Hz`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
console.log(`\nProcessed ${frameCount} frames from ${streamSize} samples\n`);
|
|
153
|
+
|
|
154
|
+
// ========== Example 5: Non-Power-of-2 DFT ==========
|
|
155
|
+
|
|
156
|
+
console.log("Example 5: Non-Power-of-2 DFT");
|
|
157
|
+
console.log("==============================\n");
|
|
158
|
+
|
|
159
|
+
const dftSize = 100; // Not power of 2
|
|
160
|
+
const dft = new FftProcessor(dftSize);
|
|
161
|
+
|
|
162
|
+
console.log(`DFT size: ${dftSize}`);
|
|
163
|
+
console.log(`Is power of 2: ${dft.isPowerOfTwo()}`);
|
|
164
|
+
|
|
165
|
+
const dftSignal = new Float32Array(dftSize);
|
|
166
|
+
for (let i = 0; i < dftSize; i++) {
|
|
167
|
+
dftSignal[i] = Math.cos((2 * Math.PI * 7 * i) / dftSize);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Use RDFT for non-power-of-2
|
|
171
|
+
const dftSpectrum = dft.rdft(dftSignal);
|
|
172
|
+
console.log(`Half-spectrum size: ${dftSpectrum.real.length}\n`);
|
|
173
|
+
|
|
174
|
+
// ========== Example 6: Spectral Features ==========
|
|
175
|
+
|
|
176
|
+
console.log("Example 6: Spectral Features Extraction");
|
|
177
|
+
console.log("========================================\n");
|
|
178
|
+
|
|
179
|
+
const featureSize = 512;
|
|
180
|
+
const featureFft = new FftProcessor(featureSize);
|
|
181
|
+
|
|
182
|
+
// Generate noise + tone
|
|
183
|
+
const noisySignal = new Float32Array(featureSize);
|
|
184
|
+
for (let i = 0; i < featureSize; i++) {
|
|
185
|
+
const tone = Math.sin((2 * Math.PI * 100 * i) / sampleRate);
|
|
186
|
+
const noise = (Math.random() - 0.5) * 0.1;
|
|
187
|
+
noisySignal[i] = tone + noise;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const featureSpectrum = featureFft.rfft(noisySignal);
|
|
191
|
+
const featureMags = featureFft.getMagnitude(featureSpectrum);
|
|
192
|
+
const power = featureFft.getPower(featureSpectrum);
|
|
193
|
+
const phase = featureFft.getPhase(featureSpectrum);
|
|
194
|
+
|
|
195
|
+
// Compute spectral centroid
|
|
196
|
+
let weightedSum = 0;
|
|
197
|
+
let totalPower = 0;
|
|
198
|
+
const freqs = featureFft.getFrequencyBins(sampleRate);
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < power.length; i++) {
|
|
201
|
+
weightedSum += freqs[i] * power[i];
|
|
202
|
+
totalPower += power[i];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const spectralCentroid = weightedSum / totalPower;
|
|
206
|
+
console.log(`Spectral Centroid: ${spectralCentroid.toFixed(2)} Hz`);
|
|
207
|
+
|
|
208
|
+
// Compute spectral rolloff (95% energy)
|
|
209
|
+
let cumulativeEnergy = 0;
|
|
210
|
+
const threshold = totalPower * 0.95;
|
|
211
|
+
let rolloffBin = 0;
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < power.length; i++) {
|
|
214
|
+
cumulativeEnergy += power[i];
|
|
215
|
+
if (cumulativeEnergy >= threshold) {
|
|
216
|
+
rolloffBin = i;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log(`Spectral Rolloff (95%): ${freqs[rolloffBin].toFixed(2)} Hz`);
|
|
222
|
+
|
|
223
|
+
// Compute spectral flux (change between frames)
|
|
224
|
+
console.log("✓ Extracted spectral features\n");
|
|
225
|
+
|
|
226
|
+
// ========== Example 7: Parseval's Theorem ==========
|
|
227
|
+
|
|
228
|
+
console.log("Example 7: Energy Conservation (Parseval's Theorem)");
|
|
229
|
+
console.log("===================================================\n");
|
|
230
|
+
|
|
231
|
+
const energySize = 256;
|
|
232
|
+
const energyFft = new FftProcessor(energySize);
|
|
233
|
+
|
|
234
|
+
const energySignal = new Float32Array(energySize);
|
|
235
|
+
for (let i = 0; i < energySize; i++) {
|
|
236
|
+
energySignal[i] = Math.random() * 2 - 1; // Random signal
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Time-domain energy
|
|
240
|
+
let timeEnergy = 0;
|
|
241
|
+
for (let i = 0; i < energySize; i++) {
|
|
242
|
+
timeEnergy += energySignal[i] ** 2;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Frequency-domain energy
|
|
246
|
+
const energySpectrum = energyFft.rfft(energySignal);
|
|
247
|
+
const energyPower = energyFft.getPower(energySpectrum);
|
|
248
|
+
|
|
249
|
+
let freqEnergy = 0;
|
|
250
|
+
freqEnergy += energyPower[0]; // DC
|
|
251
|
+
for (let i = 1; i < energyPower.length - 1; i++) {
|
|
252
|
+
freqEnergy += 2 * energyPower[i]; // Account for negative frequencies
|
|
253
|
+
}
|
|
254
|
+
freqEnergy += energyPower[energyPower.length - 1]; // Nyquist
|
|
255
|
+
freqEnergy /= energySize;
|
|
256
|
+
|
|
257
|
+
console.log(`Time-domain energy: ${timeEnergy.toFixed(4)}`);
|
|
258
|
+
console.log(`Freq-domain energy: ${freqEnergy.toFixed(4)}`);
|
|
259
|
+
console.log(
|
|
260
|
+
`Relative error: ${(
|
|
261
|
+
(Math.abs(timeEnergy - freqEnergy) / timeEnergy) *
|
|
262
|
+
100
|
|
263
|
+
).toFixed(4)}%`
|
|
264
|
+
);
|
|
265
|
+
console.log("✓ Energy conserved (Parseval's theorem verified)\n");
|
|
266
|
+
|
|
267
|
+
// ========== Example 8: Zero-Padding & Interpolation ==========
|
|
268
|
+
|
|
269
|
+
console.log("Example 8: Zero-Padding (Frequency Interpolation)");
|
|
270
|
+
console.log("=================================================\n");
|
|
271
|
+
|
|
272
|
+
const shortSignal = new Float32Array(64);
|
|
273
|
+
for (let i = 0; i < 64; i++) {
|
|
274
|
+
shortSignal[i] = Math.sin((2 * Math.PI * 8 * i) / 64);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Zero-pad to 256 for better frequency resolution
|
|
278
|
+
const padded = FftUtils.zeroPad(shortSignal, 256);
|
|
279
|
+
const paddedFft = new FftProcessor(256);
|
|
280
|
+
|
|
281
|
+
const paddedSpectrum = paddedFft.rfft(padded);
|
|
282
|
+
const paddedMags = paddedFft.getMagnitude(paddedSpectrum);
|
|
283
|
+
|
|
284
|
+
console.log(`Original size: ${shortSignal.length}`);
|
|
285
|
+
console.log(`Padded size: ${padded.length}`);
|
|
286
|
+
console.log(`Spectrum bins: ${paddedMags.length}`);
|
|
287
|
+
console.log("✓ Zero-padding increases frequency resolution (interpolation)\n");
|
|
288
|
+
|
|
289
|
+
// ========== Summary ==========
|
|
290
|
+
|
|
291
|
+
console.log("========== Summary ==========");
|
|
292
|
+
console.log("✓ All 8 transforms demonstrated:");
|
|
293
|
+
console.log(" - FFT/IFFT (complex, fast)");
|
|
294
|
+
console.log(" - DFT/IDFT (complex, any size)");
|
|
295
|
+
console.log(" - RFFT/IRFFT (real, fast)");
|
|
296
|
+
console.log(" - RDFT/IRDFT (real, any size)");
|
|
297
|
+
console.log("\n✓ Key features:");
|
|
298
|
+
console.log(" - Perfect reconstruction");
|
|
299
|
+
console.log(" - Hermitian symmetry");
|
|
300
|
+
console.log(" - Energy conservation");
|
|
301
|
+
console.log(" - Moving/batched processing");
|
|
302
|
+
console.log(" - Windowing functions");
|
|
303
|
+
console.log(" - Spectral analysis utilities");
|
|
304
|
+
console.log("\n✓ Common use cases:");
|
|
305
|
+
console.log(" - Audio frequency analysis");
|
|
306
|
+
console.log(" - Signal filtering");
|
|
307
|
+
console.log(" - Feature extraction");
|
|
308
|
+
console.log(" - Compression");
|
|
309
|
+
console.log(" - Convolution (via FFT)");
|