echogarden 0.11.11 → 0.11.13

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 (130) hide show
  1. package/data/schemas/options.json +16 -0
  2. package/dist/alignment/DTWMfccSequenceAlignment.js +2 -2
  3. package/dist/alignment/DTWMfccSequenceAlignment.js.map +1 -1
  4. package/dist/alignment/SpeechAlignment.d.ts +1 -1
  5. package/dist/alignment/SpeechAlignment.js +15 -3
  6. package/dist/alignment/SpeechAlignment.js.map +1 -1
  7. package/dist/api/Alignment.js +3 -3
  8. package/dist/api/Alignment.js.map +1 -1
  9. package/dist/api/LanguageDetection.js +1 -1
  10. package/dist/api/LanguageDetection.js.map +1 -1
  11. package/dist/api/Recognition.js +3 -3
  12. package/dist/api/Recognition.js.map +1 -1
  13. package/dist/api/Synthesis.js +7 -6
  14. package/dist/api/Synthesis.js.map +1 -1
  15. package/dist/api/Translation.js +3 -3
  16. package/dist/api/Translation.js.map +1 -1
  17. package/dist/audio/AudioUtilities.d.ts +5 -2
  18. package/dist/audio/AudioUtilities.js +50 -22
  19. package/dist/audio/AudioUtilities.js.map +1 -1
  20. package/dist/cli/CLI.js +2 -2
  21. package/dist/cli/CLI.js.map +1 -1
  22. package/dist/dsp/SpeexResampler.js +1 -1
  23. package/dist/recognition/WhisperSTT.js +2 -2
  24. package/dist/recognition/WhisperSTT.js.map +1 -1
  25. package/dist/subtitles/Subtitles.d.ts +10 -7
  26. package/dist/subtitles/Subtitles.js +268 -207
  27. package/dist/subtitles/Subtitles.js.map +1 -1
  28. package/docs/Options.md +4 -2
  29. package/package.json +12 -11
  30. package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
  31. package/src/alignment/DTWSequenceAlignment.ts +121 -0
  32. package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
  33. package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
  34. package/src/alignment/SpeechAlignment.ts +488 -0
  35. package/src/api/API.ts +12 -0
  36. package/src/api/APIOptions.ts +15 -0
  37. package/src/api/Alignment.ts +329 -0
  38. package/src/api/Common.ts +16 -0
  39. package/src/api/Denoising.ts +120 -0
  40. package/src/api/LanguageDetection.ts +286 -0
  41. package/src/api/Recognition.ts +344 -0
  42. package/src/api/Synthesis.ts +1735 -0
  43. package/src/api/Translation.ts +143 -0
  44. package/src/api/Vad.ts +172 -0
  45. package/src/audio/AudioBufferConversion.ts +248 -0
  46. package/src/audio/AudioPlayer.ts +358 -0
  47. package/src/audio/AudioRecorder.ts +91 -0
  48. package/src/audio/AudioUtilities.ts +392 -0
  49. package/src/audio/SoxPath.ts +24 -0
  50. package/src/cli/CLI.ts +1360 -0
  51. package/src/cli/CLIConfigFile.ts +91 -0
  52. package/src/cli/CLILauncher.ts +26 -0
  53. package/src/cli/CLIOptionsSchema.ts +54 -0
  54. package/src/cli/CLIParser.ts +41 -0
  55. package/src/cli/CLIStarter.ts +40 -0
  56. package/src/codecs/FFMpegTranscoder.ts +214 -0
  57. package/src/codecs/TIMITCodec.ts +17 -0
  58. package/src/codecs/WaveCodec.ts +260 -0
  59. package/src/denoising/RNNoise.ts +95 -0
  60. package/src/dsp/BiquadFilter.ts +488 -0
  61. package/src/dsp/FFT.ts +187 -0
  62. package/src/dsp/MFCC.ts +227 -0
  63. package/src/dsp/MelSpectogram.ts +145 -0
  64. package/src/dsp/Rubberband.ts +249 -0
  65. package/src/dsp/Sonic.ts +59 -0
  66. package/src/dsp/SpeexResampler.ts +79 -0
  67. package/src/math/VectorMath.ts +812 -0
  68. package/src/nlp/ChineseSegmentation.ts +68 -0
  69. package/src/nlp/CompromiseNLP.ts +113 -0
  70. package/src/nlp/EspeakPhonemizer.ts +168 -0
  71. package/src/nlp/IPA.ts +139 -0
  72. package/src/nlp/JapaneseSegmentation.ts +53 -0
  73. package/src/nlp/Lexicon.ts +119 -0
  74. package/src/nlp/PhoneConversion.ts +508 -0
  75. package/src/nlp/Segmentation.ts +237 -0
  76. package/src/nlp/TextNormalizer.ts +160 -0
  77. package/src/recognition/AmazonTranscribeSTT.ts +112 -0
  78. package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
  79. package/src/recognition/GoogleCloudSTT.ts +92 -0
  80. package/src/recognition/SileroSTT.ts +173 -0
  81. package/src/recognition/VoskSTT.ts +112 -0
  82. package/src/recognition/WhisperSTT.ts +1518 -0
  83. package/src/server/Client.ts +297 -0
  84. package/src/server/Server.ts +178 -0
  85. package/src/server/ServerStarter.ts +12 -0
  86. package/src/server/Worker.ts +400 -0
  87. package/src/server/WorkerStarter.ts +38 -0
  88. package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
  89. package/src/subtitles/Subtitles.ts +478 -0
  90. package/src/synthesis/AwsPollyTTS.ts +78 -0
  91. package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
  92. package/src/synthesis/CoquiServerTTS.ts +29 -0
  93. package/src/synthesis/ElevenLabsTTS.ts +104 -0
  94. package/src/synthesis/EspeakTTS.ts +552 -0
  95. package/src/synthesis/FliteTTS.ts +387 -0
  96. package/src/synthesis/GoogleCloudTTS.ts +112 -0
  97. package/src/synthesis/GoogleTranslateTTS.ts +210 -0
  98. package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
  99. package/src/synthesis/SamTTS.ts +30 -0
  100. package/src/synthesis/SapiTTS.ts +222 -0
  101. package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
  102. package/src/synthesis/SvoxPicoTTS.ts +318 -0
  103. package/src/synthesis/VitsTTS.ts +734 -0
  104. package/src/tests/Test.ts +24 -0
  105. package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
  106. package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
  107. package/src/typings/Fillers.d.ts +41 -0
  108. package/src/utilities/BinaryArrayConversion.ts +159 -0
  109. package/src/utilities/Compression.ts +91 -0
  110. package/src/utilities/FileDownloader.ts +201 -0
  111. package/src/utilities/FileSystem.ts +265 -0
  112. package/src/utilities/Hashing.ts +230 -0
  113. package/src/utilities/Locale.ts +119 -0
  114. package/src/utilities/Logger.ts +72 -0
  115. package/src/utilities/NdArrayUtilities.ts +31 -0
  116. package/src/utilities/ObjectUtilities.ts +169 -0
  117. package/src/utilities/OpenPromise.ts +13 -0
  118. package/src/utilities/PackageManager.ts +97 -0
  119. package/src/utilities/Queue.ts +17 -0
  120. package/src/utilities/RandomGenerator.ts +237 -0
  121. package/src/utilities/SignalChannel.ts +22 -0
  122. package/src/utilities/TarballMaker.ts +68 -0
  123. package/src/utilities/Timeline.ts +231 -0
  124. package/src/utilities/Timer.ts +93 -0
  125. package/src/utilities/Utilities.ts +574 -0
  126. package/src/utilities/WasmMemoryManager.ts +516 -0
  127. package/src/utilities/WebReader.ts +55 -0
  128. package/src/utilities/WikipediaReader.ts +41 -0
  129. package/src/voice-activity-detection/SileroVAD.ts +86 -0
  130. package/src/voice-activity-detection/WebRtcVAD.ts +76 -0
@@ -0,0 +1,488 @@
1
+ // Based on the Chromium WebAudio source code at:
2
+ // https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/web_tests/webaudio/resources/biquad-filters.js
3
+ //
4
+ // A biquad filter has a z-transform of
5
+ // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
6
+ //
7
+ // The formulas for the various filters were taken from:
8
+ // https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
9
+ //
10
+ // MDN documentation:
11
+ // https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode
12
+
13
+ export function createLowpassFilter(sampleRate: number, cutoffFrequency: number, q = 0.7071) {
14
+ return createFilter("lowpass", sampleRate, cutoffFrequency, q, 0)
15
+ }
16
+
17
+ export function createHighpassFilter(sampleRate: number, cutoffFrequency: number, q = 0.7071) {
18
+ return createFilter("highpass", sampleRate, cutoffFrequency, q, 0)
19
+ }
20
+
21
+ export function createBandpassFilter(sampleRate: number, centerFrequency: number, q = 1) {
22
+ return createFilter("bandpass", sampleRate, centerFrequency, q, 0)
23
+ }
24
+
25
+ export function createLowshelfFilter(sampleRate: number, midpointFrequency: number, gain: number) {
26
+ return createFilter("lowshelf", sampleRate, midpointFrequency, 0, gain)
27
+ }
28
+
29
+ export function createHighshelfFilter(sampleRate: number, midpointFrequency: number, gain: number) {
30
+ return createFilter("highshelf", sampleRate, midpointFrequency, 0, gain)
31
+ }
32
+
33
+ export function createPeakingFilter(sampleRate: number, centerFrequency: number, q = 1, gain = 0) {
34
+ return createFilter("peaking", sampleRate, centerFrequency, q, gain)
35
+ }
36
+
37
+ export function createNotchFilter(sampleRate: number, centerFrequency: number, q = 1) {
38
+ return createFilter("notch", sampleRate, centerFrequency, q, 0)
39
+ }
40
+
41
+ export function createAllpassFilter(sampleRate: number, centerFrequency: number, q = 1) {
42
+ return createFilter("allpass", sampleRate, centerFrequency, q, 0)
43
+ }
44
+
45
+ export function createFilter(filterType: FilterType, sampleRate: number, frequency: number, q: number, gain: number) {
46
+ const coefficients = getFilterCoefficients(filterType, sampleRate, frequency, q, gain)
47
+
48
+ return new BiquadFilter(coefficients)
49
+ }
50
+
51
+ export class BiquadFilter {
52
+ private b0 = 0
53
+ private b1 = 0
54
+ private b2 = 0
55
+ private a1 = 0
56
+ private a2 = 0
57
+
58
+ private prevInput1 = 0
59
+ private prevInput2 = 0
60
+
61
+ private prevOutput1 = 0
62
+ private prevOutput2 = 0
63
+
64
+ constructor(coefficients: FilterCoefficients) {
65
+ this.setCoefficients(coefficients)
66
+ }
67
+
68
+ step(sample: number): number {
69
+ const filteredSample =
70
+ (this.b0 * sample) +
71
+ (this.b1 * this.prevInput1) +
72
+ (this.b2 * this.prevInput2) -
73
+ (this.a1 * this.prevOutput1) -
74
+ (this.a2 * this.prevOutput2)
75
+
76
+ this.prevInput2 = this.prevInput1
77
+ this.prevInput1 = sample
78
+
79
+ this.prevOutput2 = this.prevOutput1
80
+ this.prevOutput1 = filteredSample
81
+
82
+ return filteredSample
83
+ }
84
+
85
+ apply(samples: Float32Array) {
86
+ for (let i = 0; i < samples.length; i++) {
87
+ samples[i] = this.step(samples[i])
88
+ }
89
+ }
90
+
91
+ reset() {
92
+ this.prevInput1 = 0
93
+ this.prevInput2 = 0
94
+ this.prevOutput1 = 0
95
+ this.prevOutput2 = 0
96
+ }
97
+
98
+ setCoefficients(coefficients: FilterCoefficients) {
99
+ this.b0 = coefficients.b0
100
+ this.b1 = coefficients.b1
101
+ this.b2 = coefficients.b2
102
+ this.a1 = coefficients.a1
103
+ this.a2 = coefficients.a2
104
+ }
105
+ }
106
+
107
+ export function getFilterCoefficients(filterType: FilterType, sampleRate: number, centerFrequency: number, q: number, gain: number) {
108
+ const nyquistFrequency = sampleRate / 2
109
+ const freqRatio = clamp(centerFrequency / nyquistFrequency, 0, 1)
110
+
111
+ return filterCoefficientsFunction[filterType](freqRatio, q, gain) as FilterCoefficients
112
+ }
113
+
114
+ // Lowpass filter.
115
+ export function getLowpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
116
+ let b0: number
117
+ let b1: number
118
+ let b2: number
119
+ let a0: number
120
+ let a1: number
121
+ let a2: number
122
+
123
+ if (freqRatio == 1) {
124
+ // The formula below works, except for roundoff. When freqRatio = 1,
125
+ // the filter is just a wire, so hardwire the coefficients.
126
+ b0 = 1
127
+ b1 = 0
128
+ b2 = 0
129
+ a0 = 1
130
+ a1 = 0
131
+ a2 = 0
132
+ } else {
133
+ const theta = Math.PI * freqRatio
134
+ const alpha = Math.sin(theta) / (2 * Math.pow(10, q / 20))
135
+ const cosw = Math.cos(theta)
136
+ const beta = (1 - cosw) / 2
137
+
138
+ b0 = beta
139
+ b1 = 2 * beta
140
+ b2 = beta
141
+ a0 = 1 + alpha
142
+ a1 = -2 * cosw
143
+ a2 = 1 - alpha
144
+ }
145
+
146
+ return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
147
+ }
148
+
149
+ function getHighpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
150
+ let b0: number
151
+ let b1: number
152
+ let b2: number
153
+ let a0: number
154
+ let a1: number
155
+ let a2: number
156
+
157
+ if (freqRatio == 1) {
158
+ // The filter is 0
159
+ b0 = 0
160
+ b1 = 0
161
+ b2 = 0
162
+ a0 = 1
163
+ a1 = 0
164
+ a2 = 0
165
+ } else if (freqRatio == 0) {
166
+ // The filter is 1. Computation of coefficients below is ok, but
167
+ // there's a pole at 1 and a zero at 1, so round-off could make
168
+ // the filter unstable.
169
+ b0 = 1
170
+ b1 = 0
171
+ b2 = 0
172
+ a0 = 1
173
+ a1 = 0
174
+ a2 = 0
175
+ } else {
176
+ const theta = Math.PI * freqRatio
177
+ const alpha = Math.sin(theta) / (2 * Math.pow(10, q / 20))
178
+ const cosw = Math.cos(theta)
179
+ const beta = (1 + cosw) / 2
180
+
181
+ b0 = beta
182
+ b1 = -2 * beta
183
+ b2 = beta
184
+ a0 = 1 + alpha
185
+ a1 = -2 * cosw
186
+ a2 = 1 - alpha
187
+ }
188
+
189
+ return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
190
+ }
191
+
192
+ function getBandpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
193
+ let b0: number
194
+ let b1: number
195
+ let b2: number
196
+ let a0: number
197
+ let a1: number
198
+ let a2: number
199
+
200
+ let coefficients: FilterCoefficients
201
+
202
+ if (freqRatio > 0 && freqRatio < 1) {
203
+ const w0 = Math.PI * freqRatio
204
+
205
+ if (q > 0) {
206
+ const alpha = Math.sin(w0) / (2 * q)
207
+ const k = Math.cos(w0)
208
+
209
+ b0 = alpha
210
+ b1 = 0
211
+ b2 = -alpha
212
+ a0 = 1 + alpha
213
+ a1 = -2 * k
214
+ a2 = 1 - alpha
215
+
216
+ coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
217
+ } else {
218
+ // q = 0, and frequency ratio is not 0 or 1. The above formula has a
219
+ // divide by zero problem. The limit of the z-transform as q
220
+ // approaches 0 is 1, so set the filter that way.
221
+ coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
222
+ }
223
+ } else {
224
+ // When freqRatio = 0 or 1, the z-transform is identically 0,
225
+ // independent of q.
226
+ coefficients = { b0: 0, b1: 0, b2: 0, a1: 0, a2: 0 }
227
+ }
228
+
229
+ return coefficients
230
+ }
231
+
232
+ function getLowShelfFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
233
+ // q not used
234
+ let b0: number
235
+ let b1: number
236
+ let b2: number
237
+ let a0: number
238
+ let a1: number
239
+ let a2: number
240
+
241
+ let coefficients: FilterCoefficients
242
+
243
+ const S = 1
244
+ const A = Math.pow(10, gain / 40)
245
+
246
+ if (freqRatio == 1) {
247
+ // The filter is just a constant gain
248
+ coefficients = { b0: A * A, b1: 0, b2: 0, a1: 0, a2: 0 }
249
+ } else if (freqRatio == 0) {
250
+ // The filter is 1
251
+ coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
252
+ } else {
253
+ const w0 = Math.PI * freqRatio
254
+ const alpha = 1 / 2 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2)
255
+ const k = Math.cos(w0)
256
+ const k2 = 2 * Math.sqrt(A) * alpha
257
+ const Ap1 = A + 1
258
+ const Am1 = A - 1
259
+
260
+ b0 = A * (Ap1 - Am1 * k + k2)
261
+ b1 = 2 * A * (Am1 - Ap1 * k)
262
+ b2 = A * (Ap1 - Am1 * k - k2)
263
+ a0 = Ap1 + Am1 * k + k2
264
+ a1 = -2 * (Am1 + Ap1 * k)
265
+ a2 = Ap1 + Am1 * k - k2
266
+
267
+ coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
268
+ }
269
+
270
+ return coefficients
271
+ }
272
+
273
+ function getHighShelfFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
274
+ // q not used
275
+ let b0: number
276
+ let b1: number
277
+ let b2: number
278
+ let a0: number
279
+ let a1: number
280
+ let a2: number
281
+
282
+ let coefficients: FilterCoefficients
283
+
284
+ const A = Math.pow(10, gain / 40)
285
+
286
+ if (freqRatio == 1) {
287
+ // When freqRatio = 1, the z-transform is 1
288
+ coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
289
+ } else if (freqRatio > 0) {
290
+ const w0 = Math.PI * freqRatio
291
+ const S = 1
292
+ const alpha = 0.5 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2)
293
+ const k = Math.cos(w0)
294
+ const k2 = 2 * Math.sqrt(A) * alpha
295
+ const Ap1 = A + 1
296
+ const Am1 = A - 1
297
+
298
+ b0 = A * (Ap1 + Am1 * k + k2)
299
+ b1 = -2 * A * (Am1 + Ap1 * k)
300
+ b2 = A * (Ap1 + Am1 * k - k2)
301
+ a0 = Ap1 - Am1 * k + k2
302
+ a1 = 2 * (Am1 - Ap1 * k)
303
+ a2 = Ap1 - Am1 * k - k2
304
+
305
+ coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
306
+ } else {
307
+ // When freqRatio = 0, the filter is just a gain
308
+ coefficients = { b0: A * A, b1: 0, b2: 0, a1: 0, a2: 0 }
309
+ }
310
+
311
+ return coefficients
312
+ }
313
+
314
+ function getPeakingFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
315
+ let b0: number
316
+ let b1: number
317
+ let b2: number
318
+ let a0: number
319
+ let a1: number
320
+ let a2: number
321
+
322
+ let coefficients: FilterCoefficients
323
+
324
+ const A = Math.pow(10, gain / 40)
325
+
326
+ if (freqRatio > 0 && freqRatio < 1) {
327
+ if (q > 0) {
328
+ const w0 = Math.PI * freqRatio
329
+ const alpha = Math.sin(w0) / (2 * q)
330
+ const k = Math.cos(w0)
331
+
332
+ b0 = 1 + alpha * A
333
+ b1 = -2 * k
334
+ b2 = 1 - alpha * A
335
+ a0 = 1 + alpha / A
336
+ a1 = -2 * k
337
+ a2 = 1 - alpha / A
338
+
339
+ coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
340
+ } else {
341
+ // q = 0, we have a divide by zero problem in the formulas
342
+ // above. But if we look at the z-transform, we see that the
343
+ // limit as q approaches 0 is A^2.
344
+ coefficients = { b0: A * A, b1: 0, b2: 0, a1: 0, a2: 0 }
345
+ }
346
+ } else {
347
+ // freqRatio = 0 or 1, the z-transform is 1
348
+ coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
349
+ }
350
+
351
+ return coefficients
352
+ }
353
+
354
+ function getNotchFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
355
+ let b0: number
356
+ let b1: number
357
+ let b2: number
358
+ let a0: number
359
+ let a1: number
360
+ let a2: number
361
+
362
+ let coefficients: FilterCoefficients
363
+
364
+ if (freqRatio > 0 && freqRatio < 1) {
365
+ if (q > 0) {
366
+ const w0 = Math.PI * freqRatio
367
+ const alpha = Math.sin(w0) / (2 * q)
368
+ const k = Math.cos(w0)
369
+
370
+ b0 = 1
371
+ b1 = -2 * k
372
+ b2 = 1
373
+ a0 = 1 + alpha
374
+ a1 = -2 * k
375
+ a2 = 1 - alpha
376
+
377
+ coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
378
+ } else {
379
+ // When q = 0, we get a divide by zero above. The limit of the
380
+ // z-transform as q approaches 0 is 0, so set the coefficients
381
+ // appropriately.
382
+ coefficients = { b0: 0, b1: 0, b2: 0, a1: 0, a2: 0 }
383
+ }
384
+ } else {
385
+ // When freqRatio = 0 or 1, the z-transform is 1
386
+ coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
387
+ }
388
+
389
+ return coefficients
390
+ }
391
+
392
+ function getAllpassFilterCoefficients(freqRatio: number, q: number, gain: number): FilterCoefficients {
393
+ let b0: number
394
+ let b1: number
395
+ let b2: number
396
+ let a0: number
397
+ let a1: number
398
+ let a2: number
399
+
400
+ let coefficients: FilterCoefficients
401
+
402
+ if (freqRatio > 0 && freqRatio < 1) {
403
+ if (q > 0) {
404
+ const w0 = Math.PI * freqRatio
405
+ const alpha = Math.sin(w0) / (2 * q)
406
+ const k = Math.cos(w0)
407
+
408
+ b0 = 1 - alpha
409
+ b1 = -2 * k
410
+ b2 = 1 + alpha
411
+ a0 = 1 + alpha
412
+ a1 = -2 * k
413
+ a2 = 1 - alpha
414
+
415
+ coefficients = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2)
416
+ } else {
417
+ // q = 0
418
+ coefficients = { b0: -1, b1: 0, b2: 0, a1: 0, a2: 0 }
419
+ }
420
+ } else {
421
+ coefficients = { b0: 1, b1: 0, b2: 0, a1: 0, a2: 0 }
422
+ }
423
+
424
+ return coefficients
425
+ }
426
+
427
+ function normalizeFilterCoefficients(b0: number, b1: number, b2: number, a0: number, a1: number, a2: number): FilterCoefficients {
428
+ const scale = 1 / a0
429
+
430
+ return {
431
+ b0: b0 * scale,
432
+ b1: b1 * scale,
433
+ b2: b2 * scale,
434
+ a1: a1 * scale,
435
+ a2: a2 * scale
436
+ }
437
+ }
438
+
439
+ export function bandwidthToQFactor(bandwidth: number) {
440
+ const twoToBandwidth = 2 ** bandwidth
441
+ const q = Math.sqrt(twoToBandwidth) / (twoToBandwidth - 1)
442
+
443
+ return q
444
+ }
445
+
446
+ export function qFactorToBandwidth(q: number) {
447
+ const bandwidth = (2 / Math.log(2)) * Math.asinh(1 / (2 * q))
448
+
449
+ return bandwidth
450
+ }
451
+
452
+ export function clamp(num: number, min: number, max: number) {
453
+ return Math.max(min, Math.min(max, num))
454
+ }
455
+
456
+ // Map the filter type name to a function that computes the filter coefficents
457
+ // for the given filter type.
458
+ const filterCoefficientsFunction: { [name in FilterType]: Function } = {
459
+ 'lowpass': getLowpassFilterCoefficients,
460
+ 'highpass': getHighpassFilterCoefficients,
461
+ 'bandpass': getBandpassFilterCoefficients,
462
+ 'lowshelf': getLowShelfFilterCoefficients,
463
+ 'highshelf': getHighShelfFilterCoefficients,
464
+ 'peaking': getPeakingFilterCoefficients,
465
+ 'notch': getNotchFilterCoefficients,
466
+ 'allpass': getAllpassFilterCoefficients
467
+ }
468
+
469
+ export const filterTypeName: { [name in FilterType]: string } = {
470
+ 'lowpass': 'Lowpass filter',
471
+ 'highpass': 'Highpass filter',
472
+ 'bandpass': 'Bandpass filter',
473
+ 'lowshelf': 'Lowshelf filter',
474
+ 'highshelf': 'Highshelf filter',
475
+ 'peaking': 'Peaking filter',
476
+ 'notch': 'Notch filter',
477
+ 'allpass': 'Allpass filter'
478
+ }
479
+
480
+ export type FilterType = 'lowpass' | 'highpass' | 'bandpass' | 'lowshelf' | 'highshelf' | 'peaking' | 'notch' | 'allpass'
481
+
482
+ export type FilterCoefficients = {
483
+ b0: number
484
+ b1: number
485
+ b2: number
486
+ a1: number
487
+ a2: number
488
+ }
package/src/dsp/FFT.ts ADDED
@@ -0,0 +1,187 @@
1
+ import { RawAudio } from "../audio/AudioUtilities.js"
2
+ import { concatFloat32Arrays } from "../utilities/Utilities.js"
3
+ import { WasmMemoryManager } from "../utilities/WasmMemoryManager.js"
4
+
5
+ let kissFFTInstance: any
6
+
7
+ export async function stftr(samples: Float32Array, fftOrder: number, windowSize: number, hopLength: number, windowType: WindowType) {
8
+ if (fftOrder % 2 != 0 || windowSize % 2 != 0) {
9
+ throw new Error("FFT order and window size must multiples of 2")
10
+ }
11
+
12
+ if (windowSize > fftOrder) {
13
+ throw new Error("Window size must be lesser or equal to the FFT order")
14
+ }
15
+
16
+ const halfWindowSize = windowSize / 2
17
+
18
+ const padding = new Float32Array(halfWindowSize)
19
+ samples = concatFloat32Arrays([padding, samples, padding])
20
+
21
+ const windowWeights = getWindowWeights(windowType, windowSize)
22
+
23
+ const m = await getKissFFTInstance()
24
+ const wasmMemory = new WasmMemoryManager(m)
25
+
26
+ const statePtr = m._kiss_fftr_alloc(fftOrder, 0, 0, 0)
27
+ wasmMemory.wrapPointer(statePtr)
28
+
29
+ const sampleCount = samples.length
30
+ const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
31
+ const binsBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
32
+
33
+ const frames: Float32Array[] = []
34
+
35
+ for (let offset = 0; offset < sampleCount; offset += hopLength) {
36
+ const windowSamples = samples.subarray(offset, offset + windowSize)
37
+ frameBufferRef.clear()
38
+
39
+ const frameBufferView = frameBufferRef.view
40
+
41
+ for (let i = 0; i < windowSamples.length; i++) {
42
+ frameBufferView[i] = windowSamples[i] * windowWeights[i]
43
+ }
44
+
45
+ binsBufferRef.clear()
46
+ m._kiss_fftr(statePtr, frameBufferRef.address, binsBufferRef.address)
47
+
48
+ const bins = binsBufferRef.view.slice(0, fftOrder + 2)
49
+ frames.push(bins)
50
+ }
51
+
52
+ wasmMemory.freeAll()
53
+
54
+ return frames
55
+ }
56
+
57
+ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType, expectedOutputLength: number) {
58
+ if (fftOrder % 2 != 0 || windowSize % 2 != 0) {
59
+ throw new Error("FFT order and window size must multiples of 2")
60
+ }
61
+
62
+ if (windowSize > fftOrder) {
63
+ throw new Error("Window size must be lesser or equal to the FFT order")
64
+ }
65
+
66
+ const halfWindowSize = windowSize / 2
67
+
68
+ if (hopSize != halfWindowSize) {
69
+ throw new Error("To perform inverse short-time FFT, hop size must be half the window size")
70
+ }
71
+
72
+ if (windowType != "hann") {
73
+ throw new Error("Only Hann window is currently supported for inverse short-time FFT")
74
+ }
75
+
76
+ const m = await getKissFFTInstance()
77
+ const wasmMemory = new WasmMemoryManager(m)
78
+
79
+ const statePtr = m._kiss_fftr_alloc(fftOrder, 1, 0, 0)
80
+ wasmMemory.wrapPointer(statePtr)
81
+
82
+ const outSamples = new Float32Array(binsForFrames.length * hopSize)
83
+
84
+ const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
85
+ const binsRef = wasmMemory.allocFloat32Array(fftOrder * 2)
86
+
87
+ for (let frameIndex = 0, writeOffset = 0; frameIndex < binsForFrames.length; frameIndex++, writeOffset += hopSize) {
88
+ const bins = binsForFrames[frameIndex]
89
+ binsRef.clear()
90
+ binsRef.view.set(bins)
91
+
92
+ frameBufferRef.clear()
93
+ m._kiss_fftri(statePtr, binsRef.address, frameBufferRef.address)
94
+
95
+ const frameSamples = frameBufferRef.view
96
+ for (let i = 0; i < windowSize; i++) {
97
+ outSamples[writeOffset + i] += frameSamples[i]
98
+ }
99
+ }
100
+
101
+ wasmMemory.freeAll()
102
+
103
+ return outSamples.subarray(halfWindowSize, halfWindowSize + expectedOutputLength)
104
+ }
105
+
106
+ export function getBinFrequencies(binCount: number, maxFrequency: number) {
107
+ const binFrequencies = new Float32Array(binCount)
108
+ const frequencyStep = maxFrequency / (binCount - 1)
109
+
110
+ for (let i = 0, frequency = 0; i < binFrequencies.length; i++, frequency += frequencyStep) {
111
+ binFrequencies[i] = frequency
112
+ }
113
+
114
+ return binFrequencies
115
+ }
116
+
117
+ export function fftFramesToPowerSpectogram(fftFrames: Float32Array[]) {
118
+ return fftFrames.map(fftFrame => fftFrameToPowerSpectrum(fftFrame))
119
+ }
120
+
121
+ export function fftFrameToPowerSpectrum(fftFrame: Float32Array) {
122
+ const powerSpectrum = new Float32Array(fftFrame.length / 2)
123
+
124
+ for (let i = 0; i < powerSpectrum.length; i++) {
125
+ const binOffset = i * 2
126
+ const fftCoefficientRealPart = fftFrame[binOffset]
127
+ const fftCoefficientImaginaryPart = fftFrame[binOffset + 1]
128
+ const binPower = (fftCoefficientRealPart ** 2) + (fftCoefficientImaginaryPart ** 2)
129
+
130
+ powerSpectrum[i] = binPower
131
+ }
132
+
133
+ return powerSpectrum
134
+ }
135
+
136
+ export async function getKissFFTInstance() {
137
+ if (!kissFFTInstance) {
138
+ const { default: initializer } = await import('@echogarden/kissfft-wasm')
139
+
140
+ kissFFTInstance = await initializer()
141
+ }
142
+
143
+ return kissFFTInstance
144
+ }
145
+
146
+ function getWindowWeights(windowType: WindowType, windowSize: number) {
147
+ const weights = new Float32Array(windowSize)
148
+
149
+ if (windowType == "hann") {
150
+ //const innerMultiplier = (2 * Math.PI) / (windowSize - 1)
151
+
152
+ for (let i = 0; i < windowSize; i++) {
153
+ weights[i] = 0.5 * (1 - Math.cos(2 * Math.PI * (i / (windowSize - 1))))
154
+ //weights[i] = 0.5 * (1 - Math.cos(innerMultiplier * i))
155
+ }
156
+ } else if (windowType == "hamming") {
157
+ for (let i = 0; i < windowSize; i++) {
158
+ weights[i] = 0.54 - (0.46 * Math.cos(2 * Math.PI * (i / (windowSize - 1))))
159
+ //weights[i] = 0.54 - (0.46 * Math.cos(i * innerMultiplier))
160
+ }
161
+ } else {
162
+ throw new Error(`Unsupported window function type: ${windowType}`)
163
+ }
164
+
165
+ return weights
166
+ }
167
+
168
+ export async function testFFT1(rawAudio: RawAudio) {
169
+ const { resampleAudioSpeex } = await import("./SpeexResampler.js")
170
+
171
+ const samples = (await resampleAudioSpeex(rawAudio, 16000)).audioChannels[0]
172
+
173
+ const fftOrder = 512
174
+ const windowSize = 320
175
+ const hopLength = windowSize / 2
176
+ const windowType: WindowType = "hann"
177
+
178
+ const bins = await stftr(samples, fftOrder, windowSize, hopLength, windowType)
179
+ const normalizedBins = bins.map(bin => bin.map(x => x / fftOrder))
180
+ const recoveredSamples = await stiftr(normalizedBins, fftOrder, windowSize, hopLength, windowType, samples.length)
181
+
182
+ const recoveredRawAudio: RawAudio = { audioChannels: [recoveredSamples], sampleRate: 16000 }
183
+
184
+ return recoveredRawAudio
185
+ }
186
+
187
+ export type WindowType = "hann" | "hamming"