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,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FFT Examples - Demonstrating Radix-2 Handling and Windowing
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates:
|
|
5
|
+
* 1. Power-of-2 requirement and auto-padding
|
|
6
|
+
* 2. FFT vs DFT performance comparison
|
|
7
|
+
* 3. Windowing for spectral leakage reduction
|
|
8
|
+
* 4. Practical audio analysis scenarios
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
FftProcessor,
|
|
13
|
+
MovingFftProcessor,
|
|
14
|
+
FftUtils,
|
|
15
|
+
type ComplexArray,
|
|
16
|
+
} from "../fft.js";
|
|
17
|
+
|
|
18
|
+
console.log("=== FFT Examples: Radix-2 and Windowing ===\n");
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Example 1: Power-of-2 Requirement
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
console.log("--- Example 1: Power-of-2 Requirement ---");
|
|
25
|
+
|
|
26
|
+
// Generate a signal with non-power-of-2 length
|
|
27
|
+
const signalLength = 1000; // Not a power of 2!
|
|
28
|
+
const rawSignal = new Float32Array(signalLength);
|
|
29
|
+
for (let i = 0; i < signalLength; i++) {
|
|
30
|
+
rawSignal[i] = Math.sin((2 * Math.PI * 10 * i) / 1000); // 10 Hz sine wave
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`Original signal length: ${signalLength}`);
|
|
34
|
+
console.log(`Is power of 2: ${FftUtils.isPowerOfTwo(signalLength)}`);
|
|
35
|
+
|
|
36
|
+
// Try to use FFT directly (this will fail)
|
|
37
|
+
try {
|
|
38
|
+
const badFft = new FftProcessor(signalLength);
|
|
39
|
+
badFft.rfft(rawSignal);
|
|
40
|
+
console.log("❌ This should have thrown an error!");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.log(`✅ Expected error: ${(error as Error).message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Solution 1: Auto-padding (recommended)
|
|
46
|
+
console.log("\n✅ Solution 1: Auto-padding");
|
|
47
|
+
const padded = FftUtils.padToPowerOfTwo(rawSignal);
|
|
48
|
+
console.log(`Padded length: ${padded.length}`);
|
|
49
|
+
console.log(`Is power of 2: ${FftUtils.isPowerOfTwo(padded.length)}`);
|
|
50
|
+
|
|
51
|
+
const fftWithPadding = new FftProcessor(padded.length);
|
|
52
|
+
const spectrum1 = fftWithPadding.rfft(padded);
|
|
53
|
+
console.log(`Spectrum size: ${spectrum1.real.length} bins`);
|
|
54
|
+
|
|
55
|
+
// Solution 2: Use DFT (slower but exact)
|
|
56
|
+
console.log("\n✅ Solution 2: Use DFT");
|
|
57
|
+
const dftProcessor = new FftProcessor(signalLength);
|
|
58
|
+
const spectrum2 = dftProcessor.rdft(rawSignal);
|
|
59
|
+
console.log(`Spectrum size: ${spectrum2.real.length} bins`);
|
|
60
|
+
|
|
61
|
+
// ============================================================
|
|
62
|
+
// Example 2: FFT vs DFT Performance Comparison
|
|
63
|
+
// ============================================================
|
|
64
|
+
|
|
65
|
+
console.log("\n--- Example 2: FFT vs DFT Performance ---");
|
|
66
|
+
|
|
67
|
+
const sizes = [256, 1024, 4096];
|
|
68
|
+
|
|
69
|
+
for (const size of sizes) {
|
|
70
|
+
const testSignal = new Float32Array(size);
|
|
71
|
+
for (let i = 0; i < size; i++) {
|
|
72
|
+
testSignal[i] = Math.random();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// FFT timing
|
|
76
|
+
const fft = new FftProcessor(size);
|
|
77
|
+
const fftStart = performance.now();
|
|
78
|
+
for (let i = 0; i < 100; i++) {
|
|
79
|
+
fft.rfft(testSignal);
|
|
80
|
+
}
|
|
81
|
+
const fftTime = (performance.now() - fftStart) / 100;
|
|
82
|
+
|
|
83
|
+
// DFT timing
|
|
84
|
+
const dft = new FftProcessor(size);
|
|
85
|
+
const dftStart = performance.now();
|
|
86
|
+
for (let i = 0; i < 10; i++) {
|
|
87
|
+
// Only 10 iterations for DFT (it's slow!)
|
|
88
|
+
dft.rdft(testSignal);
|
|
89
|
+
}
|
|
90
|
+
const dftTime = (performance.now() - dftStart) / 10;
|
|
91
|
+
|
|
92
|
+
const speedup = dftTime / fftTime;
|
|
93
|
+
console.log(
|
|
94
|
+
`Size ${size}: FFT=${fftTime.toFixed(3)}ms, DFT=${dftTime.toFixed(
|
|
95
|
+
3
|
|
96
|
+
)}ms, Speedup=${speedup.toFixed(1)}x`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// Example 3: Windowing for Spectral Leakage Reduction
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
console.log("\n--- Example 3: Windowing Effects ---");
|
|
105
|
+
|
|
106
|
+
const fftSize = 1024;
|
|
107
|
+
const sampleRate = 8000; // 8 kHz
|
|
108
|
+
|
|
109
|
+
// Generate a pure tone (100 Hz) with some noise
|
|
110
|
+
const pureSignal = new Float32Array(fftSize);
|
|
111
|
+
const targetFreq = 100; // Hz
|
|
112
|
+
for (let i = 0; i < fftSize; i++) {
|
|
113
|
+
pureSignal[i] =
|
|
114
|
+
Math.sin((2 * Math.PI * targetFreq * i) / sampleRate) + 0.1 * Math.random(); // Add 10% noise
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(
|
|
118
|
+
`\nAnalyzing ${targetFreq} Hz tone at ${sampleRate} Hz sample rate`
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Test different window types
|
|
122
|
+
const windowTypes = ["none", "hann", "hamming", "blackman"] as const;
|
|
123
|
+
|
|
124
|
+
for (const windowType of windowTypes) {
|
|
125
|
+
const fft = new MovingFftProcessor({
|
|
126
|
+
fftSize,
|
|
127
|
+
windowType,
|
|
128
|
+
realInput: true,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Add all samples at once
|
|
132
|
+
fft.addSamples(pureSignal, () => {});
|
|
133
|
+
|
|
134
|
+
// Force computation
|
|
135
|
+
fft.computeSpectrum();
|
|
136
|
+
const magnitudes = fft.getMagnitudeSpectrum();
|
|
137
|
+
const freqs = fft.getFrequencyBins(sampleRate);
|
|
138
|
+
|
|
139
|
+
// Find peak
|
|
140
|
+
let maxIdx = 0;
|
|
141
|
+
let maxMag = 0;
|
|
142
|
+
for (let i = 0; i < magnitudes.length; i++) {
|
|
143
|
+
if (magnitudes[i] > maxMag) {
|
|
144
|
+
maxMag = magnitudes[i];
|
|
145
|
+
maxIdx = i;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const peakFreq = freqs[maxIdx];
|
|
150
|
+
|
|
151
|
+
// Calculate total power in neighboring bins (leakage indicator)
|
|
152
|
+
let leakagePower = 0;
|
|
153
|
+
const peakBin = Math.round((targetFreq * fftSize) / sampleRate);
|
|
154
|
+
for (let i = peakBin - 5; i <= peakBin + 5; i++) {
|
|
155
|
+
if (i >= 0 && i < magnitudes.length && i !== peakBin) {
|
|
156
|
+
leakagePower += magnitudes[i] * magnitudes[i];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(
|
|
161
|
+
`Window: ${windowType.padEnd(8)} | Peak: ${peakFreq.toFixed(
|
|
162
|
+
2
|
|
163
|
+
)} Hz | Leakage: ${leakagePower.toFixed(6)}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ============================================================
|
|
168
|
+
// Example 4: Practical Audio Spectral Analysis
|
|
169
|
+
// ============================================================
|
|
170
|
+
|
|
171
|
+
console.log("\n--- Example 4: Audio Spectral Analysis ---");
|
|
172
|
+
|
|
173
|
+
// Simulate audio with multiple frequencies
|
|
174
|
+
const audioLength = 4096;
|
|
175
|
+
const audioSampleRate = 44100; // 44.1 kHz
|
|
176
|
+
const audioSignal = new Float32Array(audioLength);
|
|
177
|
+
|
|
178
|
+
// Mix: 440 Hz (A4) + 880 Hz (A5) + 1320 Hz (E6)
|
|
179
|
+
const frequencies = [440, 880, 1320];
|
|
180
|
+
for (let i = 0; i < audioLength; i++) {
|
|
181
|
+
audioSignal[i] = 0;
|
|
182
|
+
for (const freq of frequencies) {
|
|
183
|
+
audioSignal[i] +=
|
|
184
|
+
Math.sin((2 * Math.PI * freq * i) / audioSampleRate) / frequencies.length;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Analyze with Hann windowing (standard for audio)
|
|
189
|
+
const audioFft = new MovingFftProcessor({
|
|
190
|
+
fftSize: audioLength,
|
|
191
|
+
windowType: "hann", // ✅ Recommended for audio
|
|
192
|
+
realInput: true,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
audioFft.addSamples(audioSignal, () => {});
|
|
196
|
+
audioFft.computeSpectrum();
|
|
197
|
+
|
|
198
|
+
const audioMagnitudes = audioFft.getMagnitudeSpectrum();
|
|
199
|
+
const audioFreqs = audioFft.getFrequencyBins(audioSampleRate);
|
|
200
|
+
const audioDb = FftUtils.toDecibels(audioMagnitudes);
|
|
201
|
+
|
|
202
|
+
// Find top 3 peaks
|
|
203
|
+
interface Peak {
|
|
204
|
+
frequency: number;
|
|
205
|
+
magnitude: number;
|
|
206
|
+
db: number;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const peaks: Peak[] = [];
|
|
210
|
+
for (let i = 1; i < audioMagnitudes.length - 1; i++) {
|
|
211
|
+
// Local maxima
|
|
212
|
+
if (
|
|
213
|
+
audioMagnitudes[i] > audioMagnitudes[i - 1] &&
|
|
214
|
+
audioMagnitudes[i] > audioMagnitudes[i + 1]
|
|
215
|
+
) {
|
|
216
|
+
if (audioMagnitudes[i] > 0.01) {
|
|
217
|
+
// Threshold
|
|
218
|
+
peaks.push({
|
|
219
|
+
frequency: audioFreqs[i],
|
|
220
|
+
magnitude: audioMagnitudes[i],
|
|
221
|
+
db: audioDb[i],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
peaks.sort((a, b) => b.magnitude - a.magnitude);
|
|
228
|
+
|
|
229
|
+
console.log("\nDetected frequencies:");
|
|
230
|
+
for (let i = 0; i < Math.min(3, peaks.length); i++) {
|
|
231
|
+
console.log(
|
|
232
|
+
`${i + 1}. ${peaks[i].frequency.toFixed(2)} Hz (${peaks[i].db.toFixed(
|
|
233
|
+
2
|
|
234
|
+
)} dB, mag=${peaks[i].magnitude.toFixed(4)})`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ============================================================
|
|
239
|
+
// Example 5: Streaming Spectrogram
|
|
240
|
+
// ============================================================
|
|
241
|
+
|
|
242
|
+
console.log("\n--- Example 5: Streaming Spectrogram ---");
|
|
243
|
+
|
|
244
|
+
const spectrogramFft = new MovingFftProcessor({
|
|
245
|
+
fftSize: 512,
|
|
246
|
+
hopSize: 128, // 75% overlap
|
|
247
|
+
mode: "batched",
|
|
248
|
+
windowType: "hann",
|
|
249
|
+
realInput: true,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Simulate streaming audio (2 seconds at 8 kHz)
|
|
253
|
+
const streamDuration = 2.0; // seconds
|
|
254
|
+
const streamSampleRate = 8000;
|
|
255
|
+
const totalSamples = streamDuration * streamSampleRate;
|
|
256
|
+
const chunkSize = 512;
|
|
257
|
+
|
|
258
|
+
console.log(
|
|
259
|
+
`\nStreaming ${streamDuration}s of audio at ${streamSampleRate} Hz`
|
|
260
|
+
);
|
|
261
|
+
console.log(`Chunk size: ${chunkSize}, FFT size: 512, Hop: 128 (75% overlap)`);
|
|
262
|
+
|
|
263
|
+
let frameCount = 0;
|
|
264
|
+
for (let offset = 0; offset < totalSamples; offset += chunkSize) {
|
|
265
|
+
const chunk = new Float32Array(chunkSize);
|
|
266
|
+
|
|
267
|
+
// Generate chunk: sweep from 100 Hz to 2000 Hz over 2 seconds
|
|
268
|
+
for (let i = 0; i < chunkSize; i++) {
|
|
269
|
+
const t = (offset + i) / streamSampleRate;
|
|
270
|
+
const freq = 100 + (1900 * t) / streamDuration; // Linear sweep
|
|
271
|
+
chunk[i] = Math.sin(2 * Math.PI * freq * t);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Process chunk
|
|
275
|
+
const numFrames = spectrogramFft.addSamples(
|
|
276
|
+
chunk,
|
|
277
|
+
(_spectrum: ComplexArray, _size: number) => {
|
|
278
|
+
frameCount++;
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (numFrames > 0) {
|
|
283
|
+
const mags = spectrogramFft.getMagnitudeSpectrum();
|
|
284
|
+
const freqs = spectrogramFft.getFrequencyBins(streamSampleRate);
|
|
285
|
+
|
|
286
|
+
// Find peak in current frame
|
|
287
|
+
let maxIdx = 0;
|
|
288
|
+
let maxMag = 0;
|
|
289
|
+
for (let i = 0; i < mags.length; i++) {
|
|
290
|
+
if (mags[i] > maxMag) {
|
|
291
|
+
maxMag = mags[i];
|
|
292
|
+
maxIdx = i;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const peakFreq = freqs[maxIdx];
|
|
297
|
+
|
|
298
|
+
// Only print every 10th frame
|
|
299
|
+
if (frameCount % 10 === 0) {
|
|
300
|
+
console.log(`Frame ${frameCount}: Peak at ${peakFreq.toFixed(2)} Hz`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
console.log(`Total spectrogram frames: ${frameCount}`);
|
|
306
|
+
|
|
307
|
+
// ============================================================
|
|
308
|
+
// Example 6: Zero-Padding Effects
|
|
309
|
+
// ============================================================
|
|
310
|
+
|
|
311
|
+
console.log("\n--- Example 6: Zero-Padding Effects ---");
|
|
312
|
+
|
|
313
|
+
// Short signal
|
|
314
|
+
const shortSignal = new Float32Array(100);
|
|
315
|
+
for (let i = 0; i < 100; i++) {
|
|
316
|
+
shortSignal[i] = Math.sin((2 * Math.PI * 5 * i) / 100);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log("\nOriginal signal: 100 samples");
|
|
320
|
+
const paddedSizes = [128, 256, 512, 1024];
|
|
321
|
+
|
|
322
|
+
for (const padSize of paddedSizes) {
|
|
323
|
+
const padded = FftUtils.zeroPad(shortSignal, padSize);
|
|
324
|
+
const fft = new FftProcessor(padded.length);
|
|
325
|
+
const spectrum = fft.rfft(padded);
|
|
326
|
+
|
|
327
|
+
console.log(
|
|
328
|
+
`Padded to ${padSize}: ${spectrum.real.length} frequency bins (spectral interpolation)`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
console.log(
|
|
333
|
+
"\n⚠️ Note: More bins ≠ better resolution! Resolution is determined by original signal length."
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
// ============================================================
|
|
337
|
+
// Summary
|
|
338
|
+
// ============================================================
|
|
339
|
+
|
|
340
|
+
console.log("\n=== Summary ===");
|
|
341
|
+
console.log("✅ Use FftUtils.padToPowerOfTwo() for non-power-of-2 signals");
|
|
342
|
+
console.log("✅ FFT is 100-1000x faster than DFT for large signals");
|
|
343
|
+
console.log(
|
|
344
|
+
"✅ Always use windowing (hann, hamming, blackman) to reduce spectral leakage"
|
|
345
|
+
);
|
|
346
|
+
console.log("✅ Hann window is recommended for general audio analysis");
|
|
347
|
+
console.log("✅ Use 50-75% overlap for smooth spectrograms");
|
|
348
|
+
console.log("✅ Zero-padding increases bins but NOT spectral resolution");
|
|
349
|
+
console.log("\n📚 See docs/FFT_USER_GUIDE.md for detailed explanations");
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Design Examples
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the filter API:
|
|
5
|
+
* - FIR filters (low-pass, high-pass, band-pass, band-stop)
|
|
6
|
+
* - IIR filters (Butterworth, Chebyshev, Biquad EQ)
|
|
7
|
+
* - Direct class methods for creating filters
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FirFilter, IirFilter } from "../filters.js";
|
|
11
|
+
|
|
12
|
+
console.log("=== Filter Design Examples ===\n");
|
|
13
|
+
|
|
14
|
+
const sampleRate = 8000; // 8 kHz
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Example 1: FIR Low-Pass Filter
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
console.log("--- Example 1: FIR Low-Pass Filter ---");
|
|
21
|
+
|
|
22
|
+
// Using class static method
|
|
23
|
+
const firLowpass1 = FirFilter.createLowPass({
|
|
24
|
+
cutoffFrequency: 1000,
|
|
25
|
+
sampleRate,
|
|
26
|
+
order: 51,
|
|
27
|
+
windowType: "hamming",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
console.log(`✅ FIR Low-pass (1000 Hz, order 51)`);
|
|
31
|
+
console.log(` Coefficients: ${firLowpass1.getCoefficients().length} taps`);
|
|
32
|
+
|
|
33
|
+
// Method 2: Using class static method (direct)
|
|
34
|
+
const firLowpass2 = FirFilter.createLowPass({
|
|
35
|
+
cutoffFrequency: 1000,
|
|
36
|
+
sampleRate,
|
|
37
|
+
order: 51,
|
|
38
|
+
windowType: "hamming",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(`✅ Same filter using FirFilter.createLowPass()`);
|
|
42
|
+
|
|
43
|
+
// Test with sample data
|
|
44
|
+
const testSignal = new Float32Array(100);
|
|
45
|
+
for (let i = 0; i < testSignal.length; i++) {
|
|
46
|
+
// Mix 500 Hz (pass) + 2000 Hz (reject)
|
|
47
|
+
testSignal[i] =
|
|
48
|
+
Math.sin((2 * Math.PI * 500 * i) / sampleRate) +
|
|
49
|
+
Math.sin((2 * Math.PI * 2000 * i) / sampleRate);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const filtered = await firLowpass1.process(testSignal);
|
|
53
|
+
console.log(
|
|
54
|
+
` Filtered ${testSignal.length} samples (2 kHz component attenuated)\n`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// ============================================================
|
|
58
|
+
// Example 2: FIR High-Pass Filter
|
|
59
|
+
// ============================================================
|
|
60
|
+
|
|
61
|
+
console.log("--- Example 2: FIR High-Pass Filter ---");
|
|
62
|
+
|
|
63
|
+
const firHighpass = FirFilter.createHighPass({
|
|
64
|
+
cutoffFrequency: 2000,
|
|
65
|
+
sampleRate,
|
|
66
|
+
order: 61,
|
|
67
|
+
windowType: "hann",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(`✅ FIR High-pass (2000 Hz, order 61, Hann window)`);
|
|
71
|
+
console.log(` Coefficients: ${firHighpass.getCoefficients().length} taps\n`);
|
|
72
|
+
|
|
73
|
+
// ============================================================
|
|
74
|
+
// Example 3: FIR Band-Pass Filter (Voice Band)
|
|
75
|
+
// ============================================================
|
|
76
|
+
|
|
77
|
+
console.log("--- Example 3: FIR Band-Pass Filter (Voice Band) ---");
|
|
78
|
+
|
|
79
|
+
const voiceBandpass = FirFilter.createBandPass({
|
|
80
|
+
lowCutoffFrequency: 300,
|
|
81
|
+
highCutoffFrequency: 3400,
|
|
82
|
+
sampleRate,
|
|
83
|
+
order: 101,
|
|
84
|
+
windowType: "blackman",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log(`✅ Voice band-pass (300-3400 Hz, order 101, Blackman window)`);
|
|
88
|
+
console.log(` Coefficients: ${voiceBandpass.getCoefficients().length} taps`);
|
|
89
|
+
console.log(` Use case: Telephone/voice communication\n`);
|
|
90
|
+
|
|
91
|
+
// ============================================================
|
|
92
|
+
// Example 4: FIR Band-Stop (Notch) Filter
|
|
93
|
+
// ============================================================
|
|
94
|
+
|
|
95
|
+
console.log("--- Example 4: FIR Band-Stop (Notch) Filter ---");
|
|
96
|
+
|
|
97
|
+
// Remove 50 Hz powerline hum
|
|
98
|
+
const notch50Hz = FirFilter.createBandStop({
|
|
99
|
+
lowCutoffFrequency: 48,
|
|
100
|
+
highCutoffFrequency: 52,
|
|
101
|
+
sampleRate,
|
|
102
|
+
order: 201,
|
|
103
|
+
windowType: "hamming",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(`✅ 50 Hz notch filter (48-52 Hz, order 201)`);
|
|
107
|
+
console.log(` Coefficients: ${notch50Hz.getCoefficients().length} taps`);
|
|
108
|
+
console.log(` Use case: Remove powerline interference\n`);
|
|
109
|
+
|
|
110
|
+
// ============================================================
|
|
111
|
+
// Example 5: Butterworth Low-Pass Filter
|
|
112
|
+
// ============================================================
|
|
113
|
+
|
|
114
|
+
console.log("--- Example 5: Butterworth Low-Pass Filter ---");
|
|
115
|
+
|
|
116
|
+
const butterworthLowpass = IirFilter.createButterworthLowPass({
|
|
117
|
+
cutoffFrequency: 1000,
|
|
118
|
+
sampleRate,
|
|
119
|
+
order: 4,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
console.log(`✅ Butterworth low-pass (1000 Hz, 4th-order)`);
|
|
123
|
+
console.log(
|
|
124
|
+
` B coefficients: ${butterworthLowpass.getBCoefficients().length}`
|
|
125
|
+
);
|
|
126
|
+
console.log(
|
|
127
|
+
` A coefficients: ${butterworthLowpass.getACoefficients().length}`
|
|
128
|
+
);
|
|
129
|
+
console.log(` Stable: ${butterworthLowpass.isStable()}`);
|
|
130
|
+
console.log(` Characteristics: Maximally flat passband\n`);
|
|
131
|
+
|
|
132
|
+
// ============================================================
|
|
133
|
+
// Example 6: Butterworth High-Pass Filter
|
|
134
|
+
// ============================================================
|
|
135
|
+
|
|
136
|
+
console.log("--- Example 6: Butterworth High-Pass Filter ---");
|
|
137
|
+
|
|
138
|
+
const butterworthHighpass = IirFilter.createButterworthHighPass({
|
|
139
|
+
cutoffFrequency: 500,
|
|
140
|
+
sampleRate,
|
|
141
|
+
order: 2,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
console.log(`✅ Butterworth high-pass (500 Hz, 2nd-order)`);
|
|
145
|
+
console.log(` Use case: Remove DC offset and low-frequency drift\n`);
|
|
146
|
+
|
|
147
|
+
// ============================================================
|
|
148
|
+
// Example 7: Butterworth Band-Pass Filter
|
|
149
|
+
// ============================================================
|
|
150
|
+
|
|
151
|
+
console.log("--- Example 7: Butterworth Band-Pass Filter ---");
|
|
152
|
+
|
|
153
|
+
const butterworthBandpass = IirFilter.createButterworthBandPass({
|
|
154
|
+
lowCutoffFrequency: 1000,
|
|
155
|
+
highCutoffFrequency: 2000,
|
|
156
|
+
sampleRate,
|
|
157
|
+
order: 3,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
console.log(`✅ Butterworth band-pass (1000-2000 Hz, 3rd-order)`);
|
|
161
|
+
console.log(
|
|
162
|
+
` Total order: ${butterworthBandpass.getOrder()} (2x3 = 6th-order)`
|
|
163
|
+
);
|
|
164
|
+
console.log(` Use case: Extract specific frequency band\n`);
|
|
165
|
+
|
|
166
|
+
// ============================================================
|
|
167
|
+
// Example 8: First-Order IIR Filters
|
|
168
|
+
// ============================================================
|
|
169
|
+
|
|
170
|
+
console.log("--- Example 8: First-Order IIR Filters ---");
|
|
171
|
+
|
|
172
|
+
const simpleLP = IirFilter.createFirstOrderLowPass({
|
|
173
|
+
cutoffFrequency: 1000,
|
|
174
|
+
sampleRate,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const simpleHP = IirFilter.createFirstOrderHighPass({
|
|
178
|
+
cutoffFrequency: 100,
|
|
179
|
+
sampleRate,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
console.log(`✅ First-order low-pass (1000 Hz)`);
|
|
183
|
+
console.log(` Simple RC filter, gentle rolloff (-20 dB/decade)`);
|
|
184
|
+
console.log(`✅ First-order high-pass (100 Hz)`);
|
|
185
|
+
console.log(` Use case: Fast, low-latency filtering\n`);
|
|
186
|
+
|
|
187
|
+
// ============================================================
|
|
188
|
+
// Example 9: Comparing FIR vs IIR Performance
|
|
189
|
+
// ============================================================
|
|
190
|
+
|
|
191
|
+
console.log("--- Example 9: FIR vs IIR Performance Comparison ---");
|
|
192
|
+
|
|
193
|
+
const fir = FirFilter.createLowPass({
|
|
194
|
+
cutoffFrequency: 1000,
|
|
195
|
+
sampleRate,
|
|
196
|
+
order: 51,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const iir = IirFilter.createButterworthLowPass({
|
|
200
|
+
cutoffFrequency: 1000,
|
|
201
|
+
sampleRate,
|
|
202
|
+
order: 4,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
console.log("FIR (51 taps):");
|
|
206
|
+
console.log(
|
|
207
|
+
` - Coefficients: ${
|
|
208
|
+
fir.getCoefficients().length
|
|
209
|
+
} (multiply-accumulates per sample)`
|
|
210
|
+
);
|
|
211
|
+
console.log(` - Always stable`);
|
|
212
|
+
console.log(` - Linear phase possible`);
|
|
213
|
+
|
|
214
|
+
console.log("\nIIR (4th-order Butterworth):");
|
|
215
|
+
console.log(
|
|
216
|
+
` - B coefficients: ${iir.getBCoefficients().length}, A coefficients: ${
|
|
217
|
+
iir.getACoefficients().length
|
|
218
|
+
}`
|
|
219
|
+
);
|
|
220
|
+
console.log(` - Much more efficient (fewer operations)`);
|
|
221
|
+
console.log(` - Non-linear phase`);
|
|
222
|
+
console.log(` - Stable: ${iir.isStable()}\n`);
|
|
223
|
+
|
|
224
|
+
// ============================================================
|
|
225
|
+
// Example 10: Real-Time Filtering
|
|
226
|
+
// ============================================================
|
|
227
|
+
|
|
228
|
+
console.log("--- Example 10: Real-Time Filtering ---");
|
|
229
|
+
|
|
230
|
+
const realtimeFilter = IirFilter.createButterworthLowPass({
|
|
231
|
+
cutoffFrequency: 1000,
|
|
232
|
+
sampleRate,
|
|
233
|
+
order: 4,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
console.log("Simulating real-time sample-by-sample processing:");
|
|
237
|
+
|
|
238
|
+
// Simulate streaming samples
|
|
239
|
+
for (let i = 0; i < 10; i++) {
|
|
240
|
+
const input = Math.sin((2 * Math.PI * 500 * i) / sampleRate);
|
|
241
|
+
const output = await realtimeFilter.processSample(input);
|
|
242
|
+
|
|
243
|
+
if (i < 5) {
|
|
244
|
+
console.log(
|
|
245
|
+
` Sample ${i}: in=${input.toFixed(4)}, out=${output.toFixed(4)}`
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
console.log(" ...");
|
|
250
|
+
console.log(`✅ Processed 10 samples sample-by-sample\n`);
|
|
251
|
+
|
|
252
|
+
// ============================================================
|
|
253
|
+
// Example 11: Filter State Management
|
|
254
|
+
// ============================================================
|
|
255
|
+
|
|
256
|
+
console.log("--- Example 11: Filter State Management ---");
|
|
257
|
+
|
|
258
|
+
const statefulFilter = FirFilter.createLowPass({
|
|
259
|
+
cutoffFrequency: 1000,
|
|
260
|
+
sampleRate,
|
|
261
|
+
order: 31,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Process first batch
|
|
265
|
+
const batch1 = new Float32Array(50);
|
|
266
|
+
for (let i = 0; i < batch1.length; i++) {
|
|
267
|
+
batch1[i] = Math.sin((2 * Math.PI * 500 * i) / sampleRate);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const out1 = await statefulFilter.process(batch1);
|
|
271
|
+
console.log(`✅ Processed batch 1: ${batch1.length} samples`);
|
|
272
|
+
|
|
273
|
+
// Process second batch (state is maintained)
|
|
274
|
+
const batch2 = new Float32Array(50);
|
|
275
|
+
for (let i = 0; i < batch2.length; i++) {
|
|
276
|
+
batch2[i] = Math.sin((2 * Math.PI * 500 * (i + 50)) / sampleRate);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const out2 = await statefulFilter.process(batch2);
|
|
280
|
+
console.log(
|
|
281
|
+
`✅ Processed batch 2: ${batch2.length} samples (state maintained)`
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Reset state
|
|
285
|
+
statefulFilter.reset();
|
|
286
|
+
console.log(`✅ Reset filter state\n`);
|
|
287
|
+
|
|
288
|
+
// ============================================================
|
|
289
|
+
// Summary
|
|
290
|
+
// ============================================================
|
|
291
|
+
|
|
292
|
+
console.log("=== Summary ===");
|
|
293
|
+
console.log("\n✅ FIR Filters:");
|
|
294
|
+
console.log(" - Low-pass, High-pass, Band-pass, Band-stop/Notch");
|
|
295
|
+
console.log(" - Window types: Hamming, Hann, Blackman, Bartlett");
|
|
296
|
+
console.log(" - Always stable, linear phase possible");
|
|
297
|
+
console.log(" - More taps = sharper transition, more computation");
|
|
298
|
+
|
|
299
|
+
console.log("\n✅ IIR Filters:");
|
|
300
|
+
console.log(" - Butterworth: Maximally flat passband");
|
|
301
|
+
console.log(" - Chebyshev: Sharper rolloff with passband ripple");
|
|
302
|
+
console.log(" - First-order: Simple, fast, gentle rolloff");
|
|
303
|
+
console.log(" - More efficient than FIR (fewer coefficients)");
|
|
304
|
+
console.log(" - Non-linear phase, can be unstable");
|
|
305
|
+
|
|
306
|
+
console.log("\n✅ Filter API:");
|
|
307
|
+
console.log(" - FirFilter.createLowPass/HighPass/BandPass/BandStop()");
|
|
308
|
+
console.log(" - IirFilter.createButterworthX() - Maximally flat");
|
|
309
|
+
console.log(" - IirFilter.createChebyshevX() - Sharp rolloff");
|
|
310
|
+
console.log(" - IirFilter.createPeakingEQ/LowShelf/HighShelf() - Biquad EQ");
|
|
311
|
+
|
|
312
|
+
console.log("\n✅ Use Cases:");
|
|
313
|
+
console.log(" - Audio: FIR with Hann/Hamming window");
|
|
314
|
+
console.log(" - Real-time: IIR Butterworth (low latency)");
|
|
315
|
+
console.log(" - Voice band: 300-3400 Hz band-pass");
|
|
316
|
+
console.log(" - Powerline removal: 50/60 Hz notch filter");
|
|
317
|
+
console.log(" - DC removal: High-pass filter");
|
|
318
|
+
|
|
319
|
+
console.log("\n📚 All filter design math is done in C++ for performance!");
|
|
320
|
+
console.log("🚀 SIMD-optimized convolution for FIR filters!");
|