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.
Files changed (172) hide show
  1. package/.github/workflows/ci.yml +185 -0
  2. package/.vscode/c_cpp_properties.json +17 -0
  3. package/.vscode/settings.json +68 -0
  4. package/.vscode/tasks.json +28 -0
  5. package/DISCLAIMER.md +32 -0
  6. package/LICENSE +21 -0
  7. package/README.md +1803 -0
  8. package/ROADMAP.md +192 -0
  9. package/TECHNICAL_DEBT.md +165 -0
  10. package/binding.gyp +65 -0
  11. package/docs/ADVANCED_LOGGER_FEATURES.md +598 -0
  12. package/docs/AUTHENTICATION_SECURITY.md +396 -0
  13. package/docs/BACKEND_IMPROVEMENTS.md +399 -0
  14. package/docs/CHEBYSHEV_BIQUAD_EQ_IMPLEMENTATION.md +405 -0
  15. package/docs/FFT_IMPLEMENTATION.md +490 -0
  16. package/docs/FFT_IMPROVEMENTS_SUMMARY.md +387 -0
  17. package/docs/FFT_USER_GUIDE.md +494 -0
  18. package/docs/FILTERS_IMPLEMENTATION.md +260 -0
  19. package/docs/FILTER_API_GUIDE.md +418 -0
  20. package/docs/FIR_SIMD_OPTIMIZATION.md +175 -0
  21. package/docs/LOGGER_API_REFERENCE.md +350 -0
  22. package/docs/NOTCH_FILTER_QUICK_REF.md +121 -0
  23. package/docs/PHASE2_TESTS_AND_NOTCH_FILTER.md +341 -0
  24. package/docs/PHASES_5_7_SUMMARY.md +403 -0
  25. package/docs/PIPELINE_FILTER_INTEGRATION.md +446 -0
  26. package/docs/SIMD_OPTIMIZATIONS.md +211 -0
  27. package/docs/TEST_MIGRATION_SUMMARY.md +173 -0
  28. package/docs/TIMESERIES_IMPLEMENTATION_SUMMARY.md +322 -0
  29. package/docs/TIMESERIES_QUICK_REF.md +85 -0
  30. package/docs/advanced.md +559 -0
  31. package/docs/time-series-guide.md +617 -0
  32. package/docs/time-series-migration.md +376 -0
  33. package/jest.config.js +37 -0
  34. package/package.json +42 -0
  35. package/prebuilds/linux-x64/dsp-ts-redis.node +0 -0
  36. package/prebuilds/win32-x64/dsp-ts-redis.node +0 -0
  37. package/scripts/test.js +24 -0
  38. package/src/build/dsp-ts-redis.node +0 -0
  39. package/src/native/DspPipeline.cc +675 -0
  40. package/src/native/DspPipeline.h +44 -0
  41. package/src/native/FftBindings.cc +817 -0
  42. package/src/native/FilterBindings.cc +1001 -0
  43. package/src/native/IDspStage.h +53 -0
  44. package/src/native/adapters/InterpolatorStage.h +201 -0
  45. package/src/native/adapters/MeanAbsoluteValueStage.h +289 -0
  46. package/src/native/adapters/MovingAverageStage.h +306 -0
  47. package/src/native/adapters/RectifyStage.h +88 -0
  48. package/src/native/adapters/ResamplerStage.h +238 -0
  49. package/src/native/adapters/RmsStage.h +299 -0
  50. package/src/native/adapters/SscStage.h +121 -0
  51. package/src/native/adapters/VarianceStage.h +307 -0
  52. package/src/native/adapters/WampStage.h +114 -0
  53. package/src/native/adapters/WaveformLengthStage.h +115 -0
  54. package/src/native/adapters/ZScoreNormalizeStage.h +326 -0
  55. package/src/native/core/FftEngine.cc +441 -0
  56. package/src/native/core/FftEngine.h +224 -0
  57. package/src/native/core/FirFilter.cc +324 -0
  58. package/src/native/core/FirFilter.h +149 -0
  59. package/src/native/core/IirFilter.cc +576 -0
  60. package/src/native/core/IirFilter.h +210 -0
  61. package/src/native/core/MovingAbsoluteValueFilter.cc +17 -0
  62. package/src/native/core/MovingAbsoluteValueFilter.h +135 -0
  63. package/src/native/core/MovingAverageFilter.cc +18 -0
  64. package/src/native/core/MovingAverageFilter.h +135 -0
  65. package/src/native/core/MovingFftFilter.cc +291 -0
  66. package/src/native/core/MovingFftFilter.h +203 -0
  67. package/src/native/core/MovingVarianceFilter.cc +194 -0
  68. package/src/native/core/MovingVarianceFilter.h +114 -0
  69. package/src/native/core/MovingZScoreFilter.cc +215 -0
  70. package/src/native/core/MovingZScoreFilter.h +113 -0
  71. package/src/native/core/Policies.h +352 -0
  72. package/src/native/core/RmsFilter.cc +18 -0
  73. package/src/native/core/RmsFilter.h +131 -0
  74. package/src/native/core/SscFilter.cc +16 -0
  75. package/src/native/core/SscFilter.h +137 -0
  76. package/src/native/core/WampFilter.cc +16 -0
  77. package/src/native/core/WampFilter.h +101 -0
  78. package/src/native/core/WaveformLengthFilter.cc +17 -0
  79. package/src/native/core/WaveformLengthFilter.h +98 -0
  80. package/src/native/utils/CircularBufferArray.cc +336 -0
  81. package/src/native/utils/CircularBufferArray.h +62 -0
  82. package/src/native/utils/CircularBufferVector.cc +145 -0
  83. package/src/native/utils/CircularBufferVector.h +45 -0
  84. package/src/native/utils/NapiUtils.cc +53 -0
  85. package/src/native/utils/NapiUtils.h +21 -0
  86. package/src/native/utils/SimdOps.h +870 -0
  87. package/src/native/utils/SlidingWindowFilter.cc +239 -0
  88. package/src/native/utils/SlidingWindowFilter.h +159 -0
  89. package/src/native/utils/TimeSeriesBuffer.cc +205 -0
  90. package/src/native/utils/TimeSeriesBuffer.h +140 -0
  91. package/src/ts/CircularLogBuffer.ts +87 -0
  92. package/src/ts/DriftDetector.ts +331 -0
  93. package/src/ts/TopicRouter.ts +428 -0
  94. package/src/ts/__tests__/AdvancedDsp.test.ts +585 -0
  95. package/src/ts/__tests__/AuthAndEdgeCases.test.ts +241 -0
  96. package/src/ts/__tests__/Chaining.test.ts +387 -0
  97. package/src/ts/__tests__/ChebyshevBiquad.test.ts +229 -0
  98. package/src/ts/__tests__/CircularLogBuffer.test.ts +158 -0
  99. package/src/ts/__tests__/DriftDetector.test.ts +389 -0
  100. package/src/ts/__tests__/Fft.test.ts +484 -0
  101. package/src/ts/__tests__/ListState.test.ts +153 -0
  102. package/src/ts/__tests__/Logger.test.ts +208 -0
  103. package/src/ts/__tests__/LoggerAdvanced.test.ts +319 -0
  104. package/src/ts/__tests__/LoggerMinor.test.ts +247 -0
  105. package/src/ts/__tests__/MeanAbsoluteValue.test.ts +398 -0
  106. package/src/ts/__tests__/MovingAverage.test.ts +322 -0
  107. package/src/ts/__tests__/RMS.test.ts +315 -0
  108. package/src/ts/__tests__/Rectify.test.ts +272 -0
  109. package/src/ts/__tests__/Redis.test.ts +456 -0
  110. package/src/ts/__tests__/SlopeSignChange.test.ts +166 -0
  111. package/src/ts/__tests__/Tap.test.ts +164 -0
  112. package/src/ts/__tests__/TimeBasedExpiration.test.ts +124 -0
  113. package/src/ts/__tests__/TimeBasedRmsAndMav.test.ts +231 -0
  114. package/src/ts/__tests__/TimeBasedVarianceAndZScore.test.ts +284 -0
  115. package/src/ts/__tests__/TimeSeries.test.ts +254 -0
  116. package/src/ts/__tests__/TopicRouter.test.ts +332 -0
  117. package/src/ts/__tests__/TopicRouterAdvanced.test.ts +483 -0
  118. package/src/ts/__tests__/TopicRouterPriority.test.ts +487 -0
  119. package/src/ts/__tests__/Variance.test.ts +509 -0
  120. package/src/ts/__tests__/WaveformLength.test.ts +147 -0
  121. package/src/ts/__tests__/WillisonAmplitude.test.ts +197 -0
  122. package/src/ts/__tests__/ZScoreNormalize.test.ts +459 -0
  123. package/src/ts/advanced-dsp.ts +566 -0
  124. package/src/ts/backends.ts +1137 -0
  125. package/src/ts/bindings.ts +1225 -0
  126. package/src/ts/easter-egg.ts +42 -0
  127. package/src/ts/examples/MeanAbsoluteValue/test-state.ts +99 -0
  128. package/src/ts/examples/MeanAbsoluteValue/test-streaming.ts +269 -0
  129. package/src/ts/examples/MovingAverage/test-state.ts +85 -0
  130. package/src/ts/examples/MovingAverage/test-streaming.ts +188 -0
  131. package/src/ts/examples/RMS/test-state.ts +97 -0
  132. package/src/ts/examples/RMS/test-streaming.ts +253 -0
  133. package/src/ts/examples/Rectify/test-state.ts +107 -0
  134. package/src/ts/examples/Rectify/test-streaming.ts +242 -0
  135. package/src/ts/examples/Variance/test-state.ts +195 -0
  136. package/src/ts/examples/Variance/test-streaming.ts +260 -0
  137. package/src/ts/examples/ZScoreNormalize/test-state.ts +277 -0
  138. package/src/ts/examples/ZScoreNormalize/test-streaming.ts +306 -0
  139. package/src/ts/examples/advanced-dsp-examples.ts +397 -0
  140. package/src/ts/examples/callbacks/advanced-router-features.ts +326 -0
  141. package/src/ts/examples/callbacks/benchmark-circular-buffer.ts +109 -0
  142. package/src/ts/examples/callbacks/monitoring-example.ts +265 -0
  143. package/src/ts/examples/callbacks/pipeline-callbacks-example.ts +137 -0
  144. package/src/ts/examples/callbacks/pooled-callbacks-example.ts +274 -0
  145. package/src/ts/examples/callbacks/priority-routing-example.ts +277 -0
  146. package/src/ts/examples/callbacks/production-topic-router.ts +214 -0
  147. package/src/ts/examples/callbacks/topic-based-logging.ts +161 -0
  148. package/src/ts/examples/chaining/test-chaining-redis.ts +113 -0
  149. package/src/ts/examples/chaining/test-chaining.ts +52 -0
  150. package/src/ts/examples/emg-features-example.ts +284 -0
  151. package/src/ts/examples/fft-example.ts +309 -0
  152. package/src/ts/examples/fft-examples.ts +349 -0
  153. package/src/ts/examples/filter-examples.ts +320 -0
  154. package/src/ts/examples/list-state-example.ts +131 -0
  155. package/src/ts/examples/logger-example.ts +91 -0
  156. package/src/ts/examples/notch-filter-examples.ts +243 -0
  157. package/src/ts/examples/phase5/drift-detection-example.ts +290 -0
  158. package/src/ts/examples/phase6-7/production-observability.ts +476 -0
  159. package/src/ts/examples/phase6-7/redis-timeseries-integration.ts +446 -0
  160. package/src/ts/examples/redis/redis-example.ts +202 -0
  161. package/src/ts/examples/redis-example.ts +202 -0
  162. package/src/ts/examples/simd-benchmark.ts +126 -0
  163. package/src/ts/examples/tap-debugging.ts +230 -0
  164. package/src/ts/examples/timeseries/comparison-example.ts +290 -0
  165. package/src/ts/examples/timeseries/iot-sensor-example.ts +143 -0
  166. package/src/ts/examples/timeseries/redis-streaming-example.ts +233 -0
  167. package/src/ts/examples/waveform-length-example.ts +139 -0
  168. package/src/ts/fft.ts +722 -0
  169. package/src/ts/filters.ts +1078 -0
  170. package/src/ts/index.ts +120 -0
  171. package/src/ts/types.ts +589 -0
  172. 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
+ };