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.
- package/data/schemas/options.json +16 -0
- package/dist/alignment/DTWMfccSequenceAlignment.js +2 -2
- package/dist/alignment/DTWMfccSequenceAlignment.js.map +1 -1
- package/dist/alignment/SpeechAlignment.d.ts +1 -1
- package/dist/alignment/SpeechAlignment.js +15 -3
- package/dist/alignment/SpeechAlignment.js.map +1 -1
- package/dist/api/Alignment.js +3 -3
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/LanguageDetection.js +1 -1
- package/dist/api/LanguageDetection.js.map +1 -1
- package/dist/api/Recognition.js +3 -3
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +7 -6
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +3 -3
- package/dist/api/Translation.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +5 -2
- package/dist/audio/AudioUtilities.js +50 -22
- package/dist/audio/AudioUtilities.js.map +1 -1
- package/dist/cli/CLI.js +2 -2
- package/dist/cli/CLI.js.map +1 -1
- package/dist/dsp/SpeexResampler.js +1 -1
- package/dist/recognition/WhisperSTT.js +2 -2
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/subtitles/Subtitles.d.ts +10 -7
- package/dist/subtitles/Subtitles.js +268 -207
- package/dist/subtitles/Subtitles.js.map +1 -1
- package/docs/Options.md +4 -2
- package/package.json +12 -11
- package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
- package/src/alignment/DTWSequenceAlignment.ts +121 -0
- package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
- package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
- package/src/alignment/SpeechAlignment.ts +488 -0
- package/src/api/API.ts +12 -0
- package/src/api/APIOptions.ts +15 -0
- package/src/api/Alignment.ts +329 -0
- package/src/api/Common.ts +16 -0
- package/src/api/Denoising.ts +120 -0
- package/src/api/LanguageDetection.ts +286 -0
- package/src/api/Recognition.ts +344 -0
- package/src/api/Synthesis.ts +1735 -0
- package/src/api/Translation.ts +143 -0
- package/src/api/Vad.ts +172 -0
- package/src/audio/AudioBufferConversion.ts +248 -0
- package/src/audio/AudioPlayer.ts +358 -0
- package/src/audio/AudioRecorder.ts +91 -0
- package/src/audio/AudioUtilities.ts +392 -0
- package/src/audio/SoxPath.ts +24 -0
- package/src/cli/CLI.ts +1360 -0
- package/src/cli/CLIConfigFile.ts +91 -0
- package/src/cli/CLILauncher.ts +26 -0
- package/src/cli/CLIOptionsSchema.ts +54 -0
- package/src/cli/CLIParser.ts +41 -0
- package/src/cli/CLIStarter.ts +40 -0
- package/src/codecs/FFMpegTranscoder.ts +214 -0
- package/src/codecs/TIMITCodec.ts +17 -0
- package/src/codecs/WaveCodec.ts +260 -0
- package/src/denoising/RNNoise.ts +95 -0
- package/src/dsp/BiquadFilter.ts +488 -0
- package/src/dsp/FFT.ts +187 -0
- package/src/dsp/MFCC.ts +227 -0
- package/src/dsp/MelSpectogram.ts +145 -0
- package/src/dsp/Rubberband.ts +249 -0
- package/src/dsp/Sonic.ts +59 -0
- package/src/dsp/SpeexResampler.ts +79 -0
- package/src/math/VectorMath.ts +812 -0
- package/src/nlp/ChineseSegmentation.ts +68 -0
- package/src/nlp/CompromiseNLP.ts +113 -0
- package/src/nlp/EspeakPhonemizer.ts +168 -0
- package/src/nlp/IPA.ts +139 -0
- package/src/nlp/JapaneseSegmentation.ts +53 -0
- package/src/nlp/Lexicon.ts +119 -0
- package/src/nlp/PhoneConversion.ts +508 -0
- package/src/nlp/Segmentation.ts +237 -0
- package/src/nlp/TextNormalizer.ts +160 -0
- package/src/recognition/AmazonTranscribeSTT.ts +112 -0
- package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
- package/src/recognition/GoogleCloudSTT.ts +92 -0
- package/src/recognition/SileroSTT.ts +173 -0
- package/src/recognition/VoskSTT.ts +112 -0
- package/src/recognition/WhisperSTT.ts +1518 -0
- package/src/server/Client.ts +297 -0
- package/src/server/Server.ts +178 -0
- package/src/server/ServerStarter.ts +12 -0
- package/src/server/Worker.ts +400 -0
- package/src/server/WorkerStarter.ts +38 -0
- package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
- package/src/subtitles/Subtitles.ts +478 -0
- package/src/synthesis/AwsPollyTTS.ts +78 -0
- package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
- package/src/synthesis/CoquiServerTTS.ts +29 -0
- package/src/synthesis/ElevenLabsTTS.ts +104 -0
- package/src/synthesis/EspeakTTS.ts +552 -0
- package/src/synthesis/FliteTTS.ts +387 -0
- package/src/synthesis/GoogleCloudTTS.ts +112 -0
- package/src/synthesis/GoogleTranslateTTS.ts +210 -0
- package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
- package/src/synthesis/SamTTS.ts +30 -0
- package/src/synthesis/SapiTTS.ts +222 -0
- package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
- package/src/synthesis/SvoxPicoTTS.ts +318 -0
- package/src/synthesis/VitsTTS.ts +734 -0
- package/src/tests/Test.ts +24 -0
- package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
- package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
- package/src/typings/Fillers.d.ts +41 -0
- package/src/utilities/BinaryArrayConversion.ts +159 -0
- package/src/utilities/Compression.ts +91 -0
- package/src/utilities/FileDownloader.ts +201 -0
- package/src/utilities/FileSystem.ts +265 -0
- package/src/utilities/Hashing.ts +230 -0
- package/src/utilities/Locale.ts +119 -0
- package/src/utilities/Logger.ts +72 -0
- package/src/utilities/NdArrayUtilities.ts +31 -0
- package/src/utilities/ObjectUtilities.ts +169 -0
- package/src/utilities/OpenPromise.ts +13 -0
- package/src/utilities/PackageManager.ts +97 -0
- package/src/utilities/Queue.ts +17 -0
- package/src/utilities/RandomGenerator.ts +237 -0
- package/src/utilities/SignalChannel.ts +22 -0
- package/src/utilities/TarballMaker.ts +68 -0
- package/src/utilities/Timeline.ts +231 -0
- package/src/utilities/Timer.ts +93 -0
- package/src/utilities/Utilities.ts +574 -0
- package/src/utilities/WasmMemoryManager.ts +516 -0
- package/src/utilities/WebReader.ts +55 -0
- package/src/utilities/WikipediaReader.ts +41 -0
- package/src/voice-activity-detection/SileroVAD.ts +86 -0
- 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"
|