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,1078 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter Design Module
|
|
3
|
+
*
|
|
4
|
+
* Provides high-level API for creating digital filters:
|
|
5
|
+
* - FIR filters (Finite Impulse Response)
|
|
6
|
+
* - IIR filters (Infinite Impulse Response)
|
|
7
|
+
* - Butterworth, Chebyshev, Bessel, Biquad
|
|
8
|
+
* - Low-pass, High-pass, Band-pass, Band-stop/Notch
|
|
9
|
+
*
|
|
10
|
+
* All filter design math is done in C++ for performance.
|
|
11
|
+
* This module provides a clean TypeScript API.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
// Try multiple paths to find the native module
|
|
23
|
+
let DspAddon: any;
|
|
24
|
+
const possiblePaths = [
|
|
25
|
+
join(__dirname, "../build/dspx.node"),
|
|
26
|
+
join(__dirname, "../../build/Release/dspx.node"),
|
|
27
|
+
join(__dirname, "../../prebuilds/win32-x64/dsp-js-native.node"),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const path of possiblePaths) {
|
|
31
|
+
try {
|
|
32
|
+
DspAddon = require(path);
|
|
33
|
+
break;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
// Continue to next path
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!DspAddon) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Could not load native module. Tried paths:\n" + possiblePaths.join("\n")
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================================
|
|
46
|
+
// Types
|
|
47
|
+
// ============================================================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Filter type (topology/algorithm)
|
|
51
|
+
*/
|
|
52
|
+
export type FilterType =
|
|
53
|
+
| "fir"
|
|
54
|
+
| "iir"
|
|
55
|
+
| "butterworth"
|
|
56
|
+
| "chebyshev"
|
|
57
|
+
| "bessel"
|
|
58
|
+
| "biquad";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Filter mode (frequency response)
|
|
62
|
+
*/
|
|
63
|
+
export type FilterMode =
|
|
64
|
+
| "lowpass"
|
|
65
|
+
| "highpass"
|
|
66
|
+
| "bandpass"
|
|
67
|
+
| "bandstop"
|
|
68
|
+
| "notch";
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Window function for FIR filter design
|
|
72
|
+
*/
|
|
73
|
+
export type WindowType = "hamming" | "hann" | "blackman" | "bartlett";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Common filter options
|
|
77
|
+
*/
|
|
78
|
+
export interface BaseFilterOptions {
|
|
79
|
+
/** Sample rate in Hz (e.g., 44100) */
|
|
80
|
+
sampleRate: number;
|
|
81
|
+
|
|
82
|
+
/** Filter mode */
|
|
83
|
+
mode: FilterMode;
|
|
84
|
+
|
|
85
|
+
/** Cutoff frequency in Hz (for lowpass/highpass) */
|
|
86
|
+
cutoffFrequency?: number;
|
|
87
|
+
|
|
88
|
+
/** Low cutoff frequency in Hz (for bandpass/bandstop) */
|
|
89
|
+
lowCutoffFrequency?: number;
|
|
90
|
+
|
|
91
|
+
/** High cutoff frequency in Hz (for bandpass/bandstop) */
|
|
92
|
+
highCutoffFrequency?: number;
|
|
93
|
+
|
|
94
|
+
/** Whether filter maintains state between process calls (default: true) */
|
|
95
|
+
stateful?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* FIR filter specific options
|
|
100
|
+
*/
|
|
101
|
+
export interface FirFilterOptions extends BaseFilterOptions {
|
|
102
|
+
type: "fir";
|
|
103
|
+
|
|
104
|
+
/** Number of filter taps/coefficients (higher = sharper transition) */
|
|
105
|
+
order: number;
|
|
106
|
+
|
|
107
|
+
/** Window function (default: "hamming") */
|
|
108
|
+
windowType?: WindowType;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* IIR filter specific options (generic)
|
|
113
|
+
*/
|
|
114
|
+
export interface IirFilterOptions extends BaseFilterOptions {
|
|
115
|
+
type: "iir";
|
|
116
|
+
|
|
117
|
+
/** Filter order (1-8 recommended) */
|
|
118
|
+
order: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Butterworth filter options (maximally flat passband)
|
|
123
|
+
*/
|
|
124
|
+
export interface ButterworthFilterOptions extends BaseFilterOptions {
|
|
125
|
+
type: "butterworth";
|
|
126
|
+
|
|
127
|
+
/** Filter order (1-8 recommended, higher = sharper rolloff) */
|
|
128
|
+
order: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Chebyshev filter options (steeper rolloff than Butterworth)
|
|
133
|
+
*/
|
|
134
|
+
export interface ChebyshevFilterOptions extends BaseFilterOptions {
|
|
135
|
+
type: "chebyshev";
|
|
136
|
+
|
|
137
|
+
/** Filter order (1-8 recommended) */
|
|
138
|
+
order: number;
|
|
139
|
+
|
|
140
|
+
/** Passband ripple in dB (default: 0.5 dB) */
|
|
141
|
+
ripple?: number;
|
|
142
|
+
|
|
143
|
+
/** Chebyshev type: 1 (passband ripple) or 2 (stopband ripple) */
|
|
144
|
+
chebyshevType?: 1 | 2;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Biquad filter options (2nd-order IIR section)
|
|
149
|
+
*/
|
|
150
|
+
export interface BiquadFilterOptions extends BaseFilterOptions {
|
|
151
|
+
type: "biquad";
|
|
152
|
+
|
|
153
|
+
/** Q factor / bandwidth (default: 0.707 for Butterworth) */
|
|
154
|
+
q?: number;
|
|
155
|
+
|
|
156
|
+
/** Gain in dB (for peak/shelf filters, default: 0) */
|
|
157
|
+
gain?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Union of all filter option types
|
|
162
|
+
*/
|
|
163
|
+
export type FilterOptions =
|
|
164
|
+
| FirFilterOptions
|
|
165
|
+
| IirFilterOptions
|
|
166
|
+
| ButterworthFilterOptions
|
|
167
|
+
| ChebyshevFilterOptions
|
|
168
|
+
| BiquadFilterOptions;
|
|
169
|
+
|
|
170
|
+
// ============================================================
|
|
171
|
+
// Filter Classes (Wrappers around native code)
|
|
172
|
+
// ============================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* FIR (Finite Impulse Response) Filter
|
|
176
|
+
*
|
|
177
|
+
* - Always stable (no feedback)
|
|
178
|
+
* - Linear phase possible
|
|
179
|
+
* - Requires more coefficients than IIR for same frequency response
|
|
180
|
+
* - Uses SIMD-optimized convolution
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* ```ts
|
|
184
|
+
* const filter = FirFilter.createLowPass({
|
|
185
|
+
* cutoffFrequency: 1000,
|
|
186
|
+
* sampleRate: 8000,
|
|
187
|
+
* order: 51,
|
|
188
|
+
* windowType: "hamming"
|
|
189
|
+
* });
|
|
190
|
+
*
|
|
191
|
+
* const output = await filter.processSample(input);
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export class FirFilter {
|
|
195
|
+
private native: any;
|
|
196
|
+
|
|
197
|
+
private constructor(nativeFilter: any) {
|
|
198
|
+
this.native = nativeFilter;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Create low-pass FIR filter
|
|
203
|
+
*/
|
|
204
|
+
static createLowPass(options: {
|
|
205
|
+
cutoffFrequency: number;
|
|
206
|
+
sampleRate: number;
|
|
207
|
+
order: number;
|
|
208
|
+
windowType?: WindowType;
|
|
209
|
+
}): FirFilter {
|
|
210
|
+
const {
|
|
211
|
+
cutoffFrequency,
|
|
212
|
+
sampleRate,
|
|
213
|
+
order,
|
|
214
|
+
windowType = "hamming",
|
|
215
|
+
} = options;
|
|
216
|
+
|
|
217
|
+
// Normalize cutoff frequency: fc / (fs/2)
|
|
218
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
219
|
+
|
|
220
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Cutoff frequency must be between 0 and ${
|
|
223
|
+
sampleRate / 2
|
|
224
|
+
} Hz (Nyquist frequency)`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const nativeFilter = DspAddon.FirFilter.createLowPass(
|
|
229
|
+
normalizedCutoff,
|
|
230
|
+
order,
|
|
231
|
+
windowType
|
|
232
|
+
);
|
|
233
|
+
return new FirFilter(nativeFilter);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create high-pass FIR filter
|
|
238
|
+
*/
|
|
239
|
+
static createHighPass(options: {
|
|
240
|
+
cutoffFrequency: number;
|
|
241
|
+
sampleRate: number;
|
|
242
|
+
order: number;
|
|
243
|
+
windowType?: WindowType;
|
|
244
|
+
}): FirFilter {
|
|
245
|
+
const {
|
|
246
|
+
cutoffFrequency,
|
|
247
|
+
sampleRate,
|
|
248
|
+
order,
|
|
249
|
+
windowType = "hamming",
|
|
250
|
+
} = options;
|
|
251
|
+
|
|
252
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
253
|
+
|
|
254
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const nativeFilter = DspAddon.FirFilter.createHighPass(
|
|
261
|
+
normalizedCutoff,
|
|
262
|
+
order,
|
|
263
|
+
windowType
|
|
264
|
+
);
|
|
265
|
+
return new FirFilter(nativeFilter);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create band-pass FIR filter
|
|
270
|
+
*/
|
|
271
|
+
static createBandPass(options: {
|
|
272
|
+
lowCutoffFrequency: number;
|
|
273
|
+
highCutoffFrequency: number;
|
|
274
|
+
sampleRate: number;
|
|
275
|
+
order: number;
|
|
276
|
+
windowType?: WindowType;
|
|
277
|
+
}): FirFilter {
|
|
278
|
+
const {
|
|
279
|
+
lowCutoffFrequency,
|
|
280
|
+
highCutoffFrequency,
|
|
281
|
+
sampleRate,
|
|
282
|
+
order,
|
|
283
|
+
windowType = "hamming",
|
|
284
|
+
} = options;
|
|
285
|
+
|
|
286
|
+
const normalizedLow = lowCutoffFrequency / (sampleRate / 2);
|
|
287
|
+
const normalizedHigh = highCutoffFrequency / (sampleRate / 2);
|
|
288
|
+
|
|
289
|
+
if (
|
|
290
|
+
normalizedLow <= 0 ||
|
|
291
|
+
normalizedHigh >= 1 ||
|
|
292
|
+
normalizedLow >= normalizedHigh
|
|
293
|
+
) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Invalid band: low=${lowCutoffFrequency} Hz, high=${highCutoffFrequency} Hz (Nyquist=${
|
|
296
|
+
sampleRate / 2
|
|
297
|
+
} Hz)`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const nativeFilter = DspAddon.FirFilter.createBandPass(
|
|
302
|
+
normalizedLow,
|
|
303
|
+
normalizedHigh,
|
|
304
|
+
order,
|
|
305
|
+
windowType
|
|
306
|
+
);
|
|
307
|
+
return new FirFilter(nativeFilter);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Create band-stop (notch) FIR filter
|
|
312
|
+
*/
|
|
313
|
+
static createBandStop(options: {
|
|
314
|
+
lowCutoffFrequency: number;
|
|
315
|
+
highCutoffFrequency: number;
|
|
316
|
+
sampleRate: number;
|
|
317
|
+
order: number;
|
|
318
|
+
windowType?: WindowType;
|
|
319
|
+
}): FirFilter {
|
|
320
|
+
const {
|
|
321
|
+
lowCutoffFrequency,
|
|
322
|
+
highCutoffFrequency,
|
|
323
|
+
sampleRate,
|
|
324
|
+
order,
|
|
325
|
+
windowType = "hamming",
|
|
326
|
+
} = options;
|
|
327
|
+
|
|
328
|
+
const normalizedLow = lowCutoffFrequency / (sampleRate / 2);
|
|
329
|
+
const normalizedHigh = highCutoffFrequency / (sampleRate / 2);
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
normalizedLow <= 0 ||
|
|
333
|
+
normalizedHigh >= 1 ||
|
|
334
|
+
normalizedLow >= normalizedHigh
|
|
335
|
+
) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Invalid band: low=${lowCutoffFrequency} Hz, high=${highCutoffFrequency} Hz`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const nativeFilter = DspAddon.FirFilter.createBandStop(
|
|
342
|
+
normalizedLow,
|
|
343
|
+
normalizedHigh,
|
|
344
|
+
order,
|
|
345
|
+
windowType
|
|
346
|
+
);
|
|
347
|
+
return new FirFilter(nativeFilter);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Process single sample
|
|
352
|
+
*/
|
|
353
|
+
async processSample(input: number): Promise<number> {
|
|
354
|
+
return this.native.processSample(input);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Process batch of samples
|
|
359
|
+
*/
|
|
360
|
+
async process(input: Float32Array): Promise<Float32Array> {
|
|
361
|
+
return this.native.process(input);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Reset filter state
|
|
366
|
+
*/
|
|
367
|
+
reset(): void {
|
|
368
|
+
this.native.reset();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Get filter coefficients
|
|
373
|
+
*/
|
|
374
|
+
getCoefficients(): Float32Array {
|
|
375
|
+
return this.native.getCoefficients();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Get filter order
|
|
380
|
+
*/
|
|
381
|
+
getOrder(): number {
|
|
382
|
+
return this.native.getOrder();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* IIR (Infinite Impulse Response) Filter
|
|
388
|
+
*
|
|
389
|
+
* - Recursive structure (feedback)
|
|
390
|
+
* - More efficient than FIR (fewer coefficients needed)
|
|
391
|
+
* - Can be unstable if poles outside unit circle
|
|
392
|
+
* - Non-linear phase
|
|
393
|
+
*
|
|
394
|
+
* Common types: Butterworth, Chebyshev, Bessel, Biquad
|
|
395
|
+
*
|
|
396
|
+
* @example
|
|
397
|
+
* ```ts
|
|
398
|
+
* const filter = IirFilter.createButterworthLowPass({
|
|
399
|
+
* cutoffFrequency: 1000,
|
|
400
|
+
* sampleRate: 8000,
|
|
401
|
+
* order: 4
|
|
402
|
+
* });
|
|
403
|
+
*
|
|
404
|
+
* const output = await filter.processSample(input);
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
export class IirFilter {
|
|
408
|
+
private native: any;
|
|
409
|
+
|
|
410
|
+
private constructor(nativeFilter: any) {
|
|
411
|
+
this.native = nativeFilter;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Create Butterworth low-pass filter (maximally flat passband)
|
|
416
|
+
*/
|
|
417
|
+
static createButterworthLowPass(options: {
|
|
418
|
+
cutoffFrequency: number;
|
|
419
|
+
sampleRate: number;
|
|
420
|
+
order: number;
|
|
421
|
+
}): IirFilter {
|
|
422
|
+
const { cutoffFrequency, sampleRate, order } = options;
|
|
423
|
+
|
|
424
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
425
|
+
|
|
426
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
427
|
+
throw new Error(
|
|
428
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (order < 1 || order > 8) {
|
|
433
|
+
throw new Error("Order must be between 1 and 8");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const nativeFilter = DspAddon.IirFilter.createButterworthLowPass(
|
|
437
|
+
normalizedCutoff,
|
|
438
|
+
order
|
|
439
|
+
);
|
|
440
|
+
return new IirFilter(nativeFilter);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Create Butterworth high-pass filter
|
|
445
|
+
*/
|
|
446
|
+
static createButterworthHighPass(options: {
|
|
447
|
+
cutoffFrequency: number;
|
|
448
|
+
sampleRate: number;
|
|
449
|
+
order: number;
|
|
450
|
+
}): IirFilter {
|
|
451
|
+
const { cutoffFrequency, sampleRate, order } = options;
|
|
452
|
+
|
|
453
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
454
|
+
|
|
455
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (order < 1 || order > 8) {
|
|
462
|
+
throw new Error("Order must be between 1 and 8");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const nativeFilter = DspAddon.IirFilter.createButterworthHighPass(
|
|
466
|
+
normalizedCutoff,
|
|
467
|
+
order
|
|
468
|
+
);
|
|
469
|
+
return new IirFilter(nativeFilter);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Create Butterworth band-pass filter
|
|
474
|
+
*/
|
|
475
|
+
static createButterworthBandPass(options: {
|
|
476
|
+
lowCutoffFrequency: number;
|
|
477
|
+
highCutoffFrequency: number;
|
|
478
|
+
sampleRate: number;
|
|
479
|
+
order: number;
|
|
480
|
+
}): IirFilter {
|
|
481
|
+
const { lowCutoffFrequency, highCutoffFrequency, sampleRate, order } =
|
|
482
|
+
options;
|
|
483
|
+
|
|
484
|
+
const normalizedLow = lowCutoffFrequency / (sampleRate / 2);
|
|
485
|
+
const normalizedHigh = highCutoffFrequency / (sampleRate / 2);
|
|
486
|
+
|
|
487
|
+
if (
|
|
488
|
+
normalizedLow <= 0 ||
|
|
489
|
+
normalizedHigh >= 1 ||
|
|
490
|
+
normalizedLow >= normalizedHigh
|
|
491
|
+
) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`Invalid band: low=${lowCutoffFrequency} Hz, high=${highCutoffFrequency} Hz`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (order < 1 || order > 8) {
|
|
498
|
+
throw new Error("Order must be between 1 and 8");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const nativeFilter = DspAddon.IirFilter.createButterworthBandPass(
|
|
502
|
+
normalizedLow,
|
|
503
|
+
normalizedHigh,
|
|
504
|
+
order
|
|
505
|
+
);
|
|
506
|
+
return new IirFilter(nativeFilter);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Create first-order low-pass filter (simple RC filter)
|
|
511
|
+
*/
|
|
512
|
+
static createFirstOrderLowPass(options: {
|
|
513
|
+
cutoffFrequency: number;
|
|
514
|
+
sampleRate: number;
|
|
515
|
+
}): IirFilter {
|
|
516
|
+
const { cutoffFrequency, sampleRate } = options;
|
|
517
|
+
|
|
518
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
519
|
+
|
|
520
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
521
|
+
throw new Error(
|
|
522
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const nativeFilter =
|
|
527
|
+
DspAddon.IirFilter.createFirstOrderLowPass(normalizedCutoff);
|
|
528
|
+
return new IirFilter(nativeFilter);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Create first-order high-pass filter
|
|
533
|
+
*/
|
|
534
|
+
static createFirstOrderHighPass(options: {
|
|
535
|
+
cutoffFrequency: number;
|
|
536
|
+
sampleRate: number;
|
|
537
|
+
}): IirFilter {
|
|
538
|
+
const { cutoffFrequency, sampleRate } = options;
|
|
539
|
+
|
|
540
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
541
|
+
|
|
542
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const nativeFilter =
|
|
549
|
+
DspAddon.IirFilter.createFirstOrderHighPass(normalizedCutoff);
|
|
550
|
+
return new IirFilter(nativeFilter);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Create Chebyshev Type I low-pass filter (passband ripple)
|
|
555
|
+
*/
|
|
556
|
+
static createChebyshevLowPass(options: {
|
|
557
|
+
cutoffFrequency: number;
|
|
558
|
+
sampleRate: number;
|
|
559
|
+
order: number;
|
|
560
|
+
rippleDb?: number;
|
|
561
|
+
}): IirFilter {
|
|
562
|
+
const { cutoffFrequency, sampleRate, order, rippleDb = 0.5 } = options;
|
|
563
|
+
|
|
564
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
565
|
+
|
|
566
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
567
|
+
throw new Error(
|
|
568
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (order < 1 || order > 8) {
|
|
573
|
+
throw new Error("Order must be between 1 and 8");
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (rippleDb <= 0 || rippleDb > 3) {
|
|
577
|
+
throw new Error("Ripple must be between 0 and 3 dB");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const nativeFilter = DspAddon.IirFilter.createChebyshevLowPass(
|
|
581
|
+
normalizedCutoff,
|
|
582
|
+
order,
|
|
583
|
+
rippleDb
|
|
584
|
+
);
|
|
585
|
+
return new IirFilter(nativeFilter);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Create Chebyshev Type I high-pass filter (passband ripple)
|
|
590
|
+
*/
|
|
591
|
+
static createChebyshevHighPass(options: {
|
|
592
|
+
cutoffFrequency: number;
|
|
593
|
+
sampleRate: number;
|
|
594
|
+
order: number;
|
|
595
|
+
rippleDb?: number;
|
|
596
|
+
}): IirFilter {
|
|
597
|
+
const { cutoffFrequency, sampleRate, order, rippleDb = 0.5 } = options;
|
|
598
|
+
|
|
599
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
600
|
+
|
|
601
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (order < 1 || order > 8) {
|
|
608
|
+
throw new Error("Order must be between 1 and 8");
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (rippleDb <= 0 || rippleDb > 3) {
|
|
612
|
+
throw new Error("Ripple must be between 0 and 3 dB");
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const nativeFilter = DspAddon.IirFilter.createChebyshevHighPass(
|
|
616
|
+
normalizedCutoff,
|
|
617
|
+
order,
|
|
618
|
+
rippleDb
|
|
619
|
+
);
|
|
620
|
+
return new IirFilter(nativeFilter);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Create Chebyshev Type I band-pass filter (passband ripple)
|
|
625
|
+
*/
|
|
626
|
+
static createChebyshevBandPass(options: {
|
|
627
|
+
lowCutoffFrequency: number;
|
|
628
|
+
highCutoffFrequency: number;
|
|
629
|
+
sampleRate: number;
|
|
630
|
+
order: number;
|
|
631
|
+
rippleDb?: number;
|
|
632
|
+
}): IirFilter {
|
|
633
|
+
const {
|
|
634
|
+
lowCutoffFrequency,
|
|
635
|
+
highCutoffFrequency,
|
|
636
|
+
sampleRate,
|
|
637
|
+
order,
|
|
638
|
+
rippleDb = 0.5,
|
|
639
|
+
} = options;
|
|
640
|
+
|
|
641
|
+
const normalizedLow = lowCutoffFrequency / (sampleRate / 2);
|
|
642
|
+
const normalizedHigh = highCutoffFrequency / (sampleRate / 2);
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
normalizedLow <= 0 ||
|
|
646
|
+
normalizedHigh >= 1 ||
|
|
647
|
+
normalizedLow >= normalizedHigh
|
|
648
|
+
) {
|
|
649
|
+
throw new Error("Invalid cutoff frequencies");
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (order < 1 || order > 8) {
|
|
653
|
+
throw new Error("Order must be between 1 and 8");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const nativeFilter = DspAddon.IirFilter.createChebyshevBandPass(
|
|
657
|
+
normalizedLow,
|
|
658
|
+
normalizedHigh,
|
|
659
|
+
order,
|
|
660
|
+
rippleDb
|
|
661
|
+
);
|
|
662
|
+
return new IirFilter(nativeFilter);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Create peaking EQ biquad filter
|
|
667
|
+
* Useful for parametric EQ, boosting/cutting specific frequencies
|
|
668
|
+
*/
|
|
669
|
+
static createPeakingEQ(options: {
|
|
670
|
+
centerFrequency: number;
|
|
671
|
+
sampleRate: number;
|
|
672
|
+
Q: number;
|
|
673
|
+
gainDb: number;
|
|
674
|
+
}): IirFilter {
|
|
675
|
+
const { centerFrequency, sampleRate, Q, gainDb } = options;
|
|
676
|
+
|
|
677
|
+
const normalizedFreq = centerFrequency / (sampleRate / 2);
|
|
678
|
+
|
|
679
|
+
if (normalizedFreq <= 0 || normalizedFreq >= 1) {
|
|
680
|
+
throw new Error(
|
|
681
|
+
`Center frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (Q <= 0) {
|
|
686
|
+
throw new Error("Q must be positive");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const nativeFilter = DspAddon.IirFilter.createPeakingEQ(
|
|
690
|
+
normalizedFreq,
|
|
691
|
+
Q,
|
|
692
|
+
gainDb
|
|
693
|
+
);
|
|
694
|
+
return new IirFilter(nativeFilter);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Create low-shelf biquad filter
|
|
699
|
+
* Boosts or cuts all frequencies below cutoff
|
|
700
|
+
*/
|
|
701
|
+
static createLowShelf(options: {
|
|
702
|
+
cutoffFrequency: number;
|
|
703
|
+
sampleRate: number;
|
|
704
|
+
gainDb: number;
|
|
705
|
+
Q?: number;
|
|
706
|
+
}): IirFilter {
|
|
707
|
+
const { cutoffFrequency, sampleRate, gainDb, Q = 0.707 } = options;
|
|
708
|
+
|
|
709
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
710
|
+
|
|
711
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (Q <= 0) {
|
|
718
|
+
throw new Error("Q must be positive");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const nativeFilter = DspAddon.IirFilter.createLowShelf(
|
|
722
|
+
normalizedCutoff,
|
|
723
|
+
gainDb,
|
|
724
|
+
Q
|
|
725
|
+
);
|
|
726
|
+
return new IirFilter(nativeFilter);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Create high-shelf biquad filter
|
|
731
|
+
* Boosts or cuts all frequencies above cutoff
|
|
732
|
+
*/
|
|
733
|
+
static createHighShelf(options: {
|
|
734
|
+
cutoffFrequency: number;
|
|
735
|
+
sampleRate: number;
|
|
736
|
+
gainDb: number;
|
|
737
|
+
Q?: number;
|
|
738
|
+
}): IirFilter {
|
|
739
|
+
const { cutoffFrequency, sampleRate, gainDb, Q = 0.707 } = options;
|
|
740
|
+
|
|
741
|
+
const normalizedCutoff = cutoffFrequency / (sampleRate / 2);
|
|
742
|
+
|
|
743
|
+
if (normalizedCutoff <= 0 || normalizedCutoff >= 1) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`Cutoff frequency must be between 0 and ${sampleRate / 2} Hz`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (Q <= 0) {
|
|
750
|
+
throw new Error("Q must be positive");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const nativeFilter = DspAddon.IirFilter.createHighShelf(
|
|
754
|
+
normalizedCutoff,
|
|
755
|
+
gainDb,
|
|
756
|
+
Q
|
|
757
|
+
);
|
|
758
|
+
return new IirFilter(nativeFilter);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Process single sample
|
|
763
|
+
*/
|
|
764
|
+
async processSample(input: number): Promise<number> {
|
|
765
|
+
return this.native.processSample(input);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Process batch of samples
|
|
770
|
+
*/
|
|
771
|
+
async process(input: Float32Array): Promise<Float32Array> {
|
|
772
|
+
return this.native.process(input);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Reset filter state
|
|
777
|
+
*/
|
|
778
|
+
reset(): void {
|
|
779
|
+
this.native.reset();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Get feedforward (B) coefficients
|
|
784
|
+
*/
|
|
785
|
+
getBCoefficients(): Float32Array {
|
|
786
|
+
return this.native.getBCoefficients();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Get feedback (A) coefficients
|
|
791
|
+
*/
|
|
792
|
+
getACoefficients(): Float32Array {
|
|
793
|
+
return this.native.getACoefficients();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Get filter order
|
|
798
|
+
*/
|
|
799
|
+
getOrder(): number {
|
|
800
|
+
return this.native.getOrder();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Check if filter is stable
|
|
805
|
+
*/
|
|
806
|
+
isStable(): boolean {
|
|
807
|
+
return this.native.isStable();
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// ============================================================
|
|
812
|
+
// Unified Filter Design API
|
|
813
|
+
// ============================================================
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Create a digital filter with unified API
|
|
817
|
+
*
|
|
818
|
+
* Automatically dispatches to the appropriate filter type based on options.
|
|
819
|
+
* All filter design math is done in C++ for performance.
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```ts
|
|
823
|
+
* // FIR low-pass filter
|
|
824
|
+
* const fir = createFilter({
|
|
825
|
+
* type: "fir",
|
|
826
|
+
* mode: "lowpass",
|
|
827
|
+
* cutoffFrequency: 1000,
|
|
828
|
+
* sampleRate: 8000,
|
|
829
|
+
* order: 51,
|
|
830
|
+
* windowType: "hamming"
|
|
831
|
+
* });
|
|
832
|
+
*
|
|
833
|
+
* // Butterworth high-pass filter
|
|
834
|
+
* const butter = createFilter({
|
|
835
|
+
* type: "butterworth",
|
|
836
|
+
* mode: "highpass",
|
|
837
|
+
* cutoffFrequency: 500,
|
|
838
|
+
* sampleRate: 8000,
|
|
839
|
+
* order: 4
|
|
840
|
+
* });
|
|
841
|
+
*
|
|
842
|
+
* // Band-pass filter
|
|
843
|
+
* const bandpass = createFilter({
|
|
844
|
+
* type: "fir",
|
|
845
|
+
* mode: "bandpass",
|
|
846
|
+
* lowCutoffFrequency: 300,
|
|
847
|
+
* highCutoffFrequency: 3400,
|
|
848
|
+
* sampleRate: 8000,
|
|
849
|
+
* order: 101
|
|
850
|
+
* });
|
|
851
|
+
* ```
|
|
852
|
+
* @internal - Not exposed to users. Use specific filter constructors instead.
|
|
853
|
+
*/
|
|
854
|
+
function createFilter(options: FilterOptions): FirFilter | IirFilter {
|
|
855
|
+
const { type, mode } = options;
|
|
856
|
+
|
|
857
|
+
// FIR Filters
|
|
858
|
+
if (type === "fir") {
|
|
859
|
+
const firOpts = options as FirFilterOptions;
|
|
860
|
+
|
|
861
|
+
switch (mode) {
|
|
862
|
+
case "lowpass":
|
|
863
|
+
if (!firOpts.cutoffFrequency)
|
|
864
|
+
throw new Error("cutoffFrequency required for lowpass");
|
|
865
|
+
return FirFilter.createLowPass({
|
|
866
|
+
cutoffFrequency: firOpts.cutoffFrequency,
|
|
867
|
+
sampleRate: firOpts.sampleRate,
|
|
868
|
+
order: firOpts.order,
|
|
869
|
+
windowType: firOpts.windowType,
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
case "highpass":
|
|
873
|
+
if (!firOpts.cutoffFrequency)
|
|
874
|
+
throw new Error("cutoffFrequency required for highpass");
|
|
875
|
+
return FirFilter.createHighPass({
|
|
876
|
+
cutoffFrequency: firOpts.cutoffFrequency,
|
|
877
|
+
sampleRate: firOpts.sampleRate,
|
|
878
|
+
order: firOpts.order,
|
|
879
|
+
windowType: firOpts.windowType,
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
case "bandpass":
|
|
883
|
+
if (!firOpts.lowCutoffFrequency || !firOpts.highCutoffFrequency) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
"lowCutoffFrequency and highCutoffFrequency required for bandpass"
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
return FirFilter.createBandPass({
|
|
889
|
+
lowCutoffFrequency: firOpts.lowCutoffFrequency,
|
|
890
|
+
highCutoffFrequency: firOpts.highCutoffFrequency,
|
|
891
|
+
sampleRate: firOpts.sampleRate,
|
|
892
|
+
order: firOpts.order,
|
|
893
|
+
windowType: firOpts.windowType,
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
case "bandstop":
|
|
897
|
+
case "notch":
|
|
898
|
+
if (!firOpts.lowCutoffFrequency || !firOpts.highCutoffFrequency) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
"lowCutoffFrequency and highCutoffFrequency required for bandstop"
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
return FirFilter.createBandStop({
|
|
904
|
+
lowCutoffFrequency: firOpts.lowCutoffFrequency,
|
|
905
|
+
highCutoffFrequency: firOpts.highCutoffFrequency,
|
|
906
|
+
sampleRate: firOpts.sampleRate,
|
|
907
|
+
order: firOpts.order,
|
|
908
|
+
windowType: firOpts.windowType,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
default:
|
|
912
|
+
throw new Error(`Unsupported FIR mode: ${mode}`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Butterworth Filters
|
|
917
|
+
if (type === "butterworth") {
|
|
918
|
+
const butterOpts = options as ButterworthFilterOptions;
|
|
919
|
+
|
|
920
|
+
switch (mode) {
|
|
921
|
+
case "lowpass":
|
|
922
|
+
if (!butterOpts.cutoffFrequency)
|
|
923
|
+
throw new Error("cutoffFrequency required");
|
|
924
|
+
return IirFilter.createButterworthLowPass({
|
|
925
|
+
cutoffFrequency: butterOpts.cutoffFrequency,
|
|
926
|
+
sampleRate: butterOpts.sampleRate,
|
|
927
|
+
order: butterOpts.order,
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
case "highpass":
|
|
931
|
+
if (!butterOpts.cutoffFrequency)
|
|
932
|
+
throw new Error("cutoffFrequency required");
|
|
933
|
+
return IirFilter.createButterworthHighPass({
|
|
934
|
+
cutoffFrequency: butterOpts.cutoffFrequency,
|
|
935
|
+
sampleRate: butterOpts.sampleRate,
|
|
936
|
+
order: butterOpts.order,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
case "bandpass":
|
|
940
|
+
if (!butterOpts.lowCutoffFrequency || !butterOpts.highCutoffFrequency) {
|
|
941
|
+
throw new Error(
|
|
942
|
+
"lowCutoffFrequency and highCutoffFrequency required"
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return IirFilter.createButterworthBandPass({
|
|
946
|
+
lowCutoffFrequency: butterOpts.lowCutoffFrequency,
|
|
947
|
+
highCutoffFrequency: butterOpts.highCutoffFrequency,
|
|
948
|
+
sampleRate: butterOpts.sampleRate,
|
|
949
|
+
order: butterOpts.order,
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
default:
|
|
953
|
+
throw new Error(`Unsupported Butterworth mode: ${mode}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Chebyshev Filters
|
|
958
|
+
if (type === "chebyshev") {
|
|
959
|
+
const chebyOpts = options as ChebyshevFilterOptions;
|
|
960
|
+
const rippleDb = chebyOpts.ripple ?? 0.5;
|
|
961
|
+
|
|
962
|
+
switch (mode) {
|
|
963
|
+
case "lowpass":
|
|
964
|
+
if (!chebyOpts.cutoffFrequency)
|
|
965
|
+
throw new Error("cutoffFrequency required");
|
|
966
|
+
return IirFilter.createChebyshevLowPass({
|
|
967
|
+
cutoffFrequency: chebyOpts.cutoffFrequency,
|
|
968
|
+
sampleRate: chebyOpts.sampleRate,
|
|
969
|
+
order: chebyOpts.order,
|
|
970
|
+
rippleDb,
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
case "highpass":
|
|
974
|
+
if (!chebyOpts.cutoffFrequency)
|
|
975
|
+
throw new Error("cutoffFrequency required");
|
|
976
|
+
return IirFilter.createChebyshevHighPass({
|
|
977
|
+
cutoffFrequency: chebyOpts.cutoffFrequency,
|
|
978
|
+
sampleRate: chebyOpts.sampleRate,
|
|
979
|
+
order: chebyOpts.order,
|
|
980
|
+
rippleDb,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
case "bandpass":
|
|
984
|
+
if (!chebyOpts.lowCutoffFrequency || !chebyOpts.highCutoffFrequency) {
|
|
985
|
+
throw new Error(
|
|
986
|
+
"lowCutoffFrequency and highCutoffFrequency required"
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
return IirFilter.createChebyshevBandPass({
|
|
990
|
+
lowCutoffFrequency: chebyOpts.lowCutoffFrequency,
|
|
991
|
+
highCutoffFrequency: chebyOpts.highCutoffFrequency,
|
|
992
|
+
sampleRate: chebyOpts.sampleRate,
|
|
993
|
+
order: chebyOpts.order,
|
|
994
|
+
rippleDb,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
default:
|
|
998
|
+
throw new Error(`Unsupported Chebyshev mode: ${mode}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Biquad Filters (EQ, Shelf)
|
|
1003
|
+
if (type === "biquad") {
|
|
1004
|
+
const biquadOpts = options as BiquadFilterOptions;
|
|
1005
|
+
const Q = biquadOpts.q ?? 0.707;
|
|
1006
|
+
const gainDb = biquadOpts.gain ?? 0;
|
|
1007
|
+
|
|
1008
|
+
switch (mode) {
|
|
1009
|
+
case "lowpass":
|
|
1010
|
+
// Use Butterworth for biquad lowpass
|
|
1011
|
+
if (!biquadOpts.cutoffFrequency)
|
|
1012
|
+
throw new Error("cutoffFrequency required");
|
|
1013
|
+
return IirFilter.createButterworthLowPass({
|
|
1014
|
+
cutoffFrequency: biquadOpts.cutoffFrequency,
|
|
1015
|
+
sampleRate: biquadOpts.sampleRate,
|
|
1016
|
+
order: 2,
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
case "highpass":
|
|
1020
|
+
if (!biquadOpts.cutoffFrequency)
|
|
1021
|
+
throw new Error("cutoffFrequency required");
|
|
1022
|
+
return IirFilter.createButterworthHighPass({
|
|
1023
|
+
cutoffFrequency: biquadOpts.cutoffFrequency,
|
|
1024
|
+
sampleRate: biquadOpts.sampleRate,
|
|
1025
|
+
order: 2,
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
default:
|
|
1029
|
+
throw new Error(
|
|
1030
|
+
`Unsupported Biquad mode: ${mode}. Use IirFilter.createPeakingEQ/createLowShelf/createHighShelf for EQ/shelf filters.`
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Generic IIR (first-order for now)
|
|
1036
|
+
if (type === "iir") {
|
|
1037
|
+
const iirOpts = options as IirFilterOptions;
|
|
1038
|
+
|
|
1039
|
+
if (iirOpts.order === 1) {
|
|
1040
|
+
switch (mode) {
|
|
1041
|
+
case "lowpass":
|
|
1042
|
+
if (!iirOpts.cutoffFrequency)
|
|
1043
|
+
throw new Error("cutoffFrequency required");
|
|
1044
|
+
return IirFilter.createFirstOrderLowPass({
|
|
1045
|
+
cutoffFrequency: iirOpts.cutoffFrequency,
|
|
1046
|
+
sampleRate: iirOpts.sampleRate,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
case "highpass":
|
|
1050
|
+
if (!iirOpts.cutoffFrequency)
|
|
1051
|
+
throw new Error("cutoffFrequency required");
|
|
1052
|
+
return IirFilter.createFirstOrderHighPass({
|
|
1053
|
+
cutoffFrequency: iirOpts.cutoffFrequency,
|
|
1054
|
+
sampleRate: iirOpts.sampleRate,
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
default:
|
|
1058
|
+
throw new Error(`Unsupported IIR mode for order 1: ${mode}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
throw new Error(
|
|
1063
|
+
`Generic IIR filters with order > 1 not yet supported. Use 'butterworth' type.`
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
throw new Error(`Unsupported filter type: ${type}`);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ============================================================
|
|
1071
|
+
// Exports
|
|
1072
|
+
// ============================================================
|
|
1073
|
+
|
|
1074
|
+
export {
|
|
1075
|
+
// Classes
|
|
1076
|
+
FirFilter as Fir,
|
|
1077
|
+
IirFilter as Iir,
|
|
1078
|
+
};
|