echogarden 1.7.0 → 1.8.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 (54) hide show
  1. package/data/schemas/options.json +22 -10
  2. package/dist/api/Alignment.js +2 -2
  3. package/dist/api/Alignment.js.map +1 -1
  4. package/dist/api/Denoising.js +5 -4
  5. package/dist/api/Denoising.js.map +1 -1
  6. package/dist/api/Recognition.js +2 -2
  7. package/dist/api/Recognition.js.map +1 -1
  8. package/dist/api/SourceSeparation.d.ts +2 -6
  9. package/dist/api/SourceSeparation.js +8 -9
  10. package/dist/api/SourceSeparation.js.map +1 -1
  11. package/dist/api/SpeechLanguageDetection.js +2 -2
  12. package/dist/api/SpeechLanguageDetection.js.map +1 -1
  13. package/dist/api/SpeechTranslation.js +2 -2
  14. package/dist/api/SpeechTranslation.js.map +1 -1
  15. package/dist/api/Synthesis.js +5 -5
  16. package/dist/api/Synthesis.js.map +1 -1
  17. package/dist/api/TranslationAlignment.js +2 -2
  18. package/dist/api/TranslationAlignment.js.map +1 -1
  19. package/dist/audio/AudioUtilities.d.ts +5 -3
  20. package/dist/audio/AudioUtilities.js +39 -25
  21. package/dist/audio/AudioUtilities.js.map +1 -1
  22. package/dist/dsp/FFT.d.ts +2 -1
  23. package/dist/dsp/FFT.js +37 -18
  24. package/dist/dsp/FFT.js.map +1 -1
  25. package/dist/nlp/IPA.d.ts +3 -3
  26. package/dist/recognition/WhisperSTT.d.ts +3 -0
  27. package/dist/recognition/WhisperSTT.js +12 -5
  28. package/dist/recognition/WhisperSTT.js.map +1 -1
  29. package/dist/source-separation/MDXNetSourceSeparation.d.ts +8 -3
  30. package/dist/source-separation/MDXNetSourceSeparation.js +35 -13
  31. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
  32. package/dist/utilities/PackageManager.js +3 -6
  33. package/dist/utilities/PackageManager.js.map +1 -1
  34. package/dist/utilities/WasmMemoryManager.d.ts +9 -1
  35. package/dist/utilities/WasmMemoryManager.js +24 -3
  36. package/dist/utilities/WasmMemoryManager.js.map +1 -1
  37. package/docs/Options.md +9 -9
  38. package/package.json +6 -5
  39. package/src/alignment/SpeechAlignment.ts +1 -1
  40. package/src/api/Alignment.ts +2 -2
  41. package/src/api/Denoising.ts +5 -4
  42. package/src/api/Recognition.ts +3 -3
  43. package/src/api/SourceSeparation.ts +12 -16
  44. package/src/api/SpeechLanguageDetection.ts +2 -2
  45. package/src/api/SpeechTranslation.ts +2 -2
  46. package/src/api/Synthesis.ts +5 -5
  47. package/src/api/TranslationAlignment.ts +2 -2
  48. package/src/audio/AudioUtilities.ts +53 -33
  49. package/src/dsp/FFT.ts +45 -19
  50. package/src/recognition/WhisperSTT.ts +18 -6
  51. package/src/source-separation/MDXNetSourceSeparation.ts +44 -12
  52. package/src/typings/Fillers.d.ts +2 -0
  53. package/src/utilities/PackageManager.ts +3 -7
  54. package/src/utilities/WasmMemoryManager.ts +38 -4
@@ -1,6 +1,6 @@
1
1
  import { deepClone, extendDeep } from '../utilities/ObjectUtilities.js'
2
2
 
3
- import { AudioSourceParam, RawAudio, ensureRawAudio, getRawAudioDuration, normalizeAudioLevel, sliceRawAudioByTime, trimAudioEnd } from '../audio/AudioUtilities.js'
3
+ import { AudioSourceParam, RawAudio, ensureRawAudio, getRawAudioDuration, normalizeAudioLevelInPlace, sliceRawAudioByTime, trimAudioEnd } from '../audio/AudioUtilities.js'
4
4
  import { Logger } from '../utilities/Logger.js'
5
5
 
6
6
  import * as API from './API.js'
@@ -28,7 +28,7 @@ export async function detectSpeechLanguage(input: AudioSourceParam, options: Spe
28
28
 
29
29
  logger.start(`Resample audio to 16kHz mono`)
30
30
  let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
31
- sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
31
+ normalizeAudioLevelInPlace(sourceRawAudio)
32
32
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
33
33
 
34
34
  if (options.crop) {
@@ -1,7 +1,7 @@
1
1
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
2
 
3
3
  import { logToStderr } from '../utilities/Utilities.js'
4
- import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevel, trimAudioEnd } from '../audio/AudioUtilities.js'
4
+ import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace, trimAudioEnd } from '../audio/AudioUtilities.js'
5
5
  import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
@@ -61,7 +61,7 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
61
61
 
62
62
  logger.start('Normalize and trim audio')
63
63
 
64
- sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
64
+ normalizeAudioLevelInPlace(sourceRawAudio)
65
65
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
66
66
 
67
67
  if (options.sourceLanguage) {
@@ -5,7 +5,7 @@ import { deepClone, extendDeep } from '../utilities/ObjectUtilities.js'
5
5
  import * as FFMpegTranscoder from '../codecs/FFMpegTranscoder.js'
6
6
 
7
7
  import { clip, sha256AsHex, stringifyAndFormatJson, logToStderr, yieldToEventLoop, runOperationWithRetries } from '../utilities/Utilities.js'
8
- import { RawAudio, attenuateIfClipping, concatAudioSegments, downmixToMono, encodeRawAudioToWave, getSamplePeakDecibels, getEmptyRawAudio, getRawAudioDuration, normalizeAudioLevel, trimAudioEnd, trimAudioStart } from '../audio/AudioUtilities.js'
8
+ import { RawAudio, concatAudioSegments, downmixToMono, encodeRawAudioToWave, getSamplePeakDecibels, getEmptyRawAudio, getRawAudioDuration, trimAudioEnd, trimAudioStart, attenuateIfClippingInPlace, normalizeAudioLevelInPlace } from '../audio/AudioUtilities.js'
9
9
  import { Logger } from '../utilities/Logger.js'
10
10
 
11
11
  import { isWordOrSymbolWord, splitToParagraphs, splitToSentences } from '../nlp/Segmentation.js'
@@ -250,9 +250,9 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
250
250
  resultRawAudio = { audioChannels: joinedAudioBuffers, sampleRate: segmentsRawAudio[0].sampleRate }
251
251
 
252
252
  if (options.postProcessing!.normalizeAudio) {
253
- resultRawAudio = normalizeAudioLevel(resultRawAudio, options.postProcessing!.targetPeak, options.postProcessing!.maxGainIncrease)
253
+ normalizeAudioLevelInPlace(resultRawAudio, options.postProcessing!.targetPeak, options.postProcessing!.maxGainIncrease)
254
254
  } else {
255
- resultRawAudio = attenuateIfClipping(resultRawAudio)
255
+ attenuateIfClippingInPlace(resultRawAudio)
256
256
  }
257
257
  } else {
258
258
  resultRawAudio = getEmptyRawAudio(1, 24000)
@@ -866,9 +866,9 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
866
866
  synthesizedAudio = downmixToMono(synthesizedAudio)
867
867
 
868
868
  if (options.postProcessing!.normalizeAudio) {
869
- synthesizedAudio = normalizeAudioLevel(synthesizedAudio, options.postProcessing!.targetPeak!, options.postProcessing!.maxGainIncrease!)
869
+ normalizeAudioLevelInPlace(synthesizedAudio, options.postProcessing!.targetPeak!, options.postProcessing!.maxGainIncrease!)
870
870
  } else {
871
- synthesizedAudio = attenuateIfClipping(synthesizedAudio)
871
+ attenuateIfClippingInPlace(synthesizedAudio)
872
872
  }
873
873
 
874
874
  const preTrimSampleCount = synthesizedAudio.audioChannels[0].length
@@ -1,7 +1,7 @@
1
1
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
2
 
3
3
  import { logToStderr } from '../utilities/Utilities.js'
4
- import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevel, trimAudioEnd } from '../audio/AudioUtilities.js'
4
+ import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace, trimAudioEnd } from '../audio/AudioUtilities.js'
5
5
  import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import * as API from './API.js'
@@ -53,7 +53,7 @@ export async function alignTranslation(input: AudioSourceParam, translatedTransc
53
53
 
54
54
  logger.start('Normalize and trim audio')
55
55
 
56
- sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
56
+ normalizeAudioLevelInPlace(sourceRawAudio)
57
57
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
58
58
 
59
59
  logger.end()
@@ -47,11 +47,13 @@ export function trimAudioEnd(audioSamples: Float32Array, targetEndSilentSampleCo
47
47
  }
48
48
 
49
49
  export function getStartingSilentSampleCount(audioSamples: Float32Array, amplitudeThresholdDecibels = defaultSilenceThresholdDecibels) {
50
+ const sampleCount = audioSamples.length
51
+
50
52
  const minSampleAmplitude = decibelsToGainFactor(amplitudeThresholdDecibels)
51
53
 
52
54
  let silentSampleCount = 0
53
55
 
54
- for (let i = 0; i < audioSamples.length - 1; i++) {
56
+ for (let i = 0; i < sampleCount - 1; i++) {
55
57
  if (Math.abs(audioSamples[i]) > minSampleAmplitude) {
56
58
  break
57
59
  }
@@ -63,11 +65,13 @@ export function getStartingSilentSampleCount(audioSamples: Float32Array, amplitu
63
65
  }
64
66
 
65
67
  export function getEndingSilentSampleCount(audioSamples: Float32Array, amplitudeThresholdDecibels = defaultSilenceThresholdDecibels) {
68
+ const sampleCount = audioSamples.length
69
+
66
70
  const minSampleAmplitude = decibelsToGainFactor(amplitudeThresholdDecibels)
67
71
 
68
72
  let silentSampleCount = 0
69
73
 
70
- for (let i = audioSamples.length - 1; i >= 0; i--) {
74
+ for (let i = sampleCount - 1; i >= 0; i--) {
71
75
  if (Math.abs(audioSamples[i]) > minSampleAmplitude) {
72
76
  break
73
77
  }
@@ -82,15 +86,27 @@ export function getEndingSilentSampleCount(audioSamples: Float32Array, amplitude
82
86
  // Gain, normalization, mixing, and channel downmixing
83
87
  ////////////////////////////////////////////////////////////////////////////////////////////////
84
88
  export function downmixToMonoAndNormalize(rawAudio: RawAudio, targetPeakDecibels = -3) {
85
- return normalizeAudioLevel(downmixToMono(rawAudio), targetPeakDecibels)
89
+ const downmixedAudio = downmixToMono(rawAudio)
90
+
91
+ normalizeAudioLevelInPlace(downmixedAudio, targetPeakDecibels)
92
+
93
+ return downmixedAudio
94
+ }
95
+
96
+ export function attenuateIfClippingInPlace(rawAudio: RawAudio, clippingThreshold = -0.1) {
97
+ normalizeAudioLevelInPlace(rawAudio, clippingThreshold, 0)
86
98
  }
87
99
 
88
- export function attenuateIfClipping(rawAudio: RawAudio) {
89
- return normalizeAudioLevel(rawAudio, -0.1, 0)
100
+ export function normalizeAudioLevel(rawAudio: RawAudio, targetPeakDecibels = -3, maxGainIncreaseDecibels = 30) {
101
+ const clonedRawAudio = cloneRawAudio(rawAudio)
102
+
103
+ normalizeAudioLevelInPlace(clonedRawAudio, targetPeakDecibels, maxGainIncreaseDecibels)
104
+
105
+ return clonedRawAudio
90
106
  }
91
107
 
92
- export function normalizeAudioLevel(rawAudio: RawAudio, targetPeakDecibels = -3, maxGainIncreaseDecibels = 30): RawAudio {
93
- //rawAudio = correctDCBias(rawAudio)
108
+ export function normalizeAudioLevelInPlace(rawAudio: RawAudio, targetPeakDecibels = -3, maxGainIncreaseDecibels = 30){
109
+ //correctDCBiasInPlace(rawAudio)
94
110
 
95
111
  const targetPeakAmplitude = decibelsToGainFactor(targetPeakDecibels)
96
112
  const maxGainFactor = decibelsToGainFactor(maxGainIncreaseDecibels)
@@ -99,12 +115,10 @@ export function normalizeAudioLevel(rawAudio: RawAudio, targetPeakDecibels = -3,
99
115
 
100
116
  const gainFactor = Math.min(targetPeakAmplitude / peakAmplitude, maxGainFactor)
101
117
 
102
- return applyGainFactor(rawAudio, gainFactor)
118
+ applyGainFactorInPlace(rawAudio, gainFactor)
103
119
  }
104
120
 
105
- export function correctDCBias(rawAudio: RawAudio): RawAudio {
106
- const outputAudioChannels: Float32Array[] = []
107
-
121
+ export function correctDCBiasInPlace(rawAudio: RawAudio) {
108
122
  for (const channelSamples of rawAudio.audioChannels) {
109
123
  const sampleCount = channelSamples.length
110
124
 
@@ -116,38 +130,36 @@ export function correctDCBias(rawAudio: RawAudio): RawAudio {
116
130
 
117
131
  const sampleAverage = sampleSum / sampleCount
118
132
 
119
- const outputChannelSamples = new Float32Array(sampleCount)
120
-
121
133
  for (let i = 0; i < sampleCount; i++) {
122
- outputChannelSamples[i] = channelSamples[i] - sampleAverage
134
+ channelSamples[i] -= sampleAverage
123
135
  }
124
-
125
- outputAudioChannels.push(outputChannelSamples)
126
136
  }
137
+ }
127
138
 
128
- return { audioChannels: outputAudioChannels, sampleRate: rawAudio.sampleRate } as RawAudio
139
+ export function applyGainDecibels(rawAudio: RawAudio, gainDecibels: number) {
140
+ const clonedRawAudio = cloneRawAudio(rawAudio)
141
+
142
+ applyGainDecibelsInPlace(clonedRawAudio, gainDecibels)
143
+
144
+ return clonedRawAudio
129
145
  }
130
146
 
131
- export function applyGainDecibels(rawAudio: RawAudio, gainDecibels: number): RawAudio {
132
- return applyGainFactor(rawAudio, decibelsToGainFactor(gainDecibels))
147
+ export function applyGainDecibelsInPlace(rawAudio: RawAudio, gainDecibels: number) {
148
+ applyGainFactorInPlace(rawAudio, decibelsToGainFactor(gainDecibels))
133
149
  }
134
150
 
135
- export function applyGainFactor(rawAudio: RawAudio, gainFactor: number): RawAudio {
136
- const outputAudioChannels: Float32Array[] = []
151
+ export function applyGainFactorInPlace(rawAudio: RawAudio, gainFactor: number) {
152
+ if (gainFactor === 1.0) {
153
+ return
154
+ }
137
155
 
138
156
  for (const channelSamples of rawAudio.audioChannels) {
139
157
  const sampleCount = channelSamples.length
140
158
 
141
- const outputChannelSamples = new Float32Array(sampleCount)
142
-
143
159
  for (let i = 0; i < sampleCount; i++) {
144
- outputChannelSamples[i] = channelSamples[i] * gainFactor
160
+ channelSamples[i] *= gainFactor
145
161
  }
146
-
147
- outputAudioChannels.push(outputChannelSamples)
148
162
  }
149
-
150
- return { audioChannels: outputAudioChannels, sampleRate: rawAudio.sampleRate } as RawAudio
151
163
  }
152
164
 
153
165
  export function downmixToMono(rawAudio: RawAudio): RawAudio {
@@ -183,8 +195,14 @@ export function getSamplePeakAmplitude(audioChannels: Float32Array[]) {
183
195
  let maxAmplitude = 0.00001
184
196
 
185
197
  for (const channelSamples of audioChannels) {
186
- for (const sample of channelSamples) {
187
- maxAmplitude = Math.max(maxAmplitude, Math.abs(sample))
198
+ const sampleCount = channelSamples.length
199
+
200
+ for (let i = 0; i < sampleCount; i++) {
201
+ const sampleAbsValue = Math.abs(channelSamples[i])
202
+
203
+ if (sampleAbsValue > maxAmplitude) {
204
+ maxAmplitude = sampleAbsValue
205
+ }
188
206
  }
189
207
  }
190
208
 
@@ -257,7 +275,7 @@ export function sliceAudioChannels(audioChannels: Float32Array[], startSampleInd
257
275
  }
258
276
 
259
277
  export function concatAudioSegments(audioSegments: Float32Array[][]) {
260
- if (audioSegments.length == 0) {
278
+ if (audioSegments.length === 0) {
261
279
  return []
262
280
  }
263
281
 
@@ -317,7 +335,9 @@ export function fadeAudioInOut(rawAudio: RawAudio, fadeTime: number): RawAudio {
317
335
  const outAudioChannels = rawAudio.audioChannels.map(channel => channel.slice())
318
336
 
319
337
  for (const channel of outAudioChannels) {
320
- if (channel.length < fadeSampleCount * 2) {
338
+ const sampleCount = channel.length
339
+
340
+ if (sampleCount < fadeSampleCount * 2) {
321
341
  continue
322
342
  }
323
343
 
@@ -331,7 +351,7 @@ export function fadeAudioInOut(rawAudio: RawAudio, fadeTime: number): RawAudio {
331
351
 
332
352
  factor = 1.0
333
353
 
334
- for (let i = channel.length - fadeSampleCount; i < channel.length; i++) {
354
+ for (let i = sampleCount - fadeSampleCount; i < sampleCount; i++) {
335
355
  channel[i] *= factor
336
356
 
337
357
  factor *= gainReductionPerFrameFactor
package/src/dsp/FFT.ts CHANGED
@@ -2,8 +2,6 @@ import { ComplexNumber } from '../math/VectorMath.js'
2
2
  import { concatFloat32Arrays } from '../utilities/Utilities.js'
3
3
  import { WasmMemoryManager } from '../utilities/WasmMemoryManager.js'
4
4
 
5
- let kissFFTInstance: any
6
-
7
5
  // Compute short-term Fourier transform (real-valued)
8
6
  export async function stftr(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
9
7
  const frames: Float32Array[] = []
@@ -36,15 +34,18 @@ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, w
36
34
 
37
35
  const windowWeights = getWindowWeights(windowType, windowSize)
38
36
 
39
- const m = await getKissFFTInstance()
40
- const wasmMemory = new WasmMemoryManager(m)
37
+ const m = await getPFFFTInstance(isPffftSimdSupportedForFFTOrder(fftOrder))
38
+ const wasmMemory = new WasmMemoryManager(m, {
39
+ wasmAlloc: m._pffft_aligned_malloc,
40
+ wasmFree: m._pffft_aligned_free
41
+ })
41
42
 
42
- const statePtr = m._kiss_fftr_alloc(fftOrder, 0, 0, 0)
43
- wasmMemory.wrapPointer(statePtr)
43
+ const statePtr = m._pffft_new_setup(fftOrder, 0)
44
44
 
45
45
  const sampleCount = samples.length
46
46
  const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
47
47
  const binsBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
48
+ const workBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
48
49
 
49
50
  for (let offset = 0; offset < sampleCount; offset += hopSize) {
50
51
  const windowSamples = samples.subarray(offset, offset + windowSize)
@@ -58,13 +59,15 @@ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, w
58
59
 
59
60
  binsBufferRef.clear()
60
61
 
61
- m._kiss_fftr(statePtr, frameBufferRef.address, binsBufferRef.address)
62
+ m._pffft_transform_ordered(statePtr, frameBufferRef.address, binsBufferRef.address, workBufferRef.address, 0)
62
63
 
63
64
  const bins = binsBufferRef.view.slice(0, fftOrder + 2)
64
65
 
65
66
  yield bins
66
67
  }
67
68
 
69
+ m._pffft_destroy_setup(statePtr)
70
+
68
71
  wasmMemory.freeAll()
69
72
  }
70
73
 
@@ -96,14 +99,18 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
96
99
 
97
100
  const outSamples = new Float32Array(outSampleCount)
98
101
 
99
- const m = await getKissFFTInstance()
100
- const wasmMemory = new WasmMemoryManager(m)
102
+ const m = await getPFFFTInstance(isPffftSimdSupportedForFFTOrder(fftOrder))
101
103
 
102
- const statePtr = m._kiss_fftr_alloc(fftOrder, 1, 0, 0)
103
- wasmMemory.wrapPointer(statePtr)
104
+ const wasmMemory = new WasmMemoryManager(m, {
105
+ wasmAlloc: m._pffft_aligned_malloc,
106
+ wasmFree: m._pffft_aligned_free
107
+ })
108
+
109
+ const statePtr = m._pffft_new_setup(fftOrder, 0)
104
110
 
105
111
  const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
106
112
  const binsRef = wasmMemory.allocFloat32Array(fftOrder * 2)
113
+ const workBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
107
114
 
108
115
  const sumOfSquaredWeightsForSample = new Float32Array(outSampleCount)
109
116
 
@@ -114,7 +121,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
114
121
 
115
122
  frameBufferRef.clear()
116
123
 
117
- m._kiss_fftri(statePtr, binsRef.address, frameBufferRef.address)
124
+ m._pffft_transform_ordered(statePtr, binsRef.address, frameBufferRef.address, workBufferRef.address, 1)
118
125
 
119
126
  const frameSamples = frameBufferRef.view
120
127
 
@@ -132,6 +139,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
132
139
  }
133
140
  }
134
141
 
142
+ m._pffft_destroy_setup(statePtr)
135
143
  wasmMemory.freeAll()
136
144
 
137
145
  // Divide each output sample by the sum of squared weights
@@ -247,15 +255,33 @@ export function getWindowWeights(windowType: WindowType, windowSize: number) {
247
255
  return weights
248
256
  }
249
257
 
250
- // Get KISS FFT instance (initialize new if not exists)
251
- export async function getKissFFTInstance() {
252
- if (!kissFFTInstance) {
253
- const { default: initializer } = await import('@echogarden/kissfft-wasm')
258
+ export function isPffftSimdSupportedForFFTOrder(fftOrder: number) {
259
+ return fftOrder % 32 === 0
260
+ }
254
261
 
255
- kissFFTInstance = await initializer()
256
- }
262
+ let pffftNonSimdInstance: any
263
+ let pffftSimdInstance: any
264
+
265
+ export async function getPFFFTInstance(enableSimd: boolean) {
266
+ return enableSimd ? getSimdPFFFTInstance() : getNonSimdPFFFTInstance()
267
+ }
268
+
269
+ // Get non-SIMD PFFFT instance (initialize new if not exists)
270
+ async function getNonSimdPFFFTInstance() {
271
+ const { default: initializer } = await import('@echogarden/pffft-wasm')
272
+
273
+ pffftNonSimdInstance = await initializer()
274
+
275
+ return pffftNonSimdInstance
276
+ }
277
+
278
+ // Get SIMD PFFFT instance (initialize new if not exists)
279
+ async function getSimdPFFFTInstance() {
280
+ const { default: initializer } = await import('@echogarden/pffft-wasm/simd')
281
+
282
+ pffftSimdInstance = await initializer()
257
283
 
258
- return kissFFTInstance
284
+ return pffftSimdInstance
259
285
  }
260
286
 
261
287
  export type WindowType = 'hann' | 'hamming' | 'povey'
@@ -459,7 +459,7 @@ export class Whisper {
459
459
  // Find alignment path
460
460
  let alignmentHeads: number[] | undefined = undefined
461
461
 
462
- if (this.modelName.startsWith('small') || this.modelName.startsWith('medium') || this.modelName.startsWith('large')) {
462
+ if (options.useOptimizedAlignmentHeads) {
463
463
  alignmentHeads = this.alignmentHeadIndexes
464
464
  }
465
465
 
@@ -566,12 +566,15 @@ export class Whisper {
566
566
  topCandidateCount: 1,
567
567
  punctuationThreshold: Infinity,
568
568
  autoPromptParts: false,
569
- maxTokensPerPart: Infinity,
569
+ maxTokensPerPart: whisperAlignmentOptions.maxTokensPerPart!,
570
570
  suppressRepetition: false,
571
571
  repetitionThreshold: Infinity,
572
572
  decodeTimestampTokens: true,
573
- endTokenThreshold: whisperAlignmentOptions!.endTokenThreshold!,
573
+ endTokenThreshold: whisperAlignmentOptions.endTokenThreshold!,
574
574
  includeEndTokenInCandidates: false,
575
+ useOptimizedAlignmentHeads: whisperAlignmentOptions.useOptimizedAlignmentHeads!,
576
+ encoderProvider: whisperAlignmentOptions.encoderProvider!,
577
+ decoderProvider: whisperAlignmentOptions.decoderProvider!,
575
578
  seed: undefined,
576
579
  }
577
580
 
@@ -1103,7 +1106,8 @@ export class Whisper {
1103
1106
  const sampleRate = rawAudio.sampleRate
1104
1107
 
1105
1108
  const fftOrder = 400
1106
- const hopLength = 160
1109
+ const fftWindowSize = 400
1110
+ const fftHopLength = 160
1107
1111
 
1108
1112
  const filterbankCount = this.filterbankCount
1109
1113
  const filterbanks = this.filterbanks
@@ -1128,7 +1132,7 @@ export class Whisper {
1128
1132
 
1129
1133
  const rawAudioPart: RawAudio = { audioChannels: [paddedAudioSamples], sampleRate }
1130
1134
 
1131
- const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftOrder, hopLength, filterbanks)
1135
+ const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftWindowSize, fftHopLength, filterbanks)
1132
1136
 
1133
1137
  await logger.startAsync('Normalize mel spectogram')
1134
1138
 
@@ -2440,6 +2444,7 @@ export interface WhisperOptions {
2440
2444
  decodeTimestampTokens?: boolean
2441
2445
  endTokenThreshold?: number
2442
2446
  includeEndTokenInCandidates?: boolean
2447
+ useOptimizedAlignmentHeads?: boolean,
2443
2448
  encoderProvider?: OnnxExecutionProvider
2444
2449
  decoderProvider?: OnnxExecutionProvider
2445
2450
  seed?: number
@@ -2458,6 +2463,7 @@ export const defaultWhisperOptions: WhisperOptions = {
2458
2463
  decodeTimestampTokens: true,
2459
2464
  endTokenThreshold: 0.9,
2460
2465
  includeEndTokenInCandidates: true,
2466
+ useOptimizedAlignmentHeads: true,
2461
2467
  encoderProvider: undefined,
2462
2468
  decoderProvider: undefined,
2463
2469
  seed: undefined,
@@ -2467,6 +2473,9 @@ export const defaultWhisperOptions: WhisperOptions = {
2467
2473
  export interface WhisperAlignmentOptions {
2468
2474
  model?: WhisperModelName
2469
2475
  endTokenThreshold?: number
2476
+ maxTokensPerPart?: number
2477
+ useOptimizedAlignmentHeads?: boolean
2478
+
2470
2479
  encoderProvider?: OnnxExecutionProvider
2471
2480
  decoderProvider?: OnnxExecutionProvider
2472
2481
  }
@@ -2474,8 +2483,11 @@ export interface WhisperAlignmentOptions {
2474
2483
  export const defaultWhisperAlignmentOptions: WhisperAlignmentOptions = {
2475
2484
  model: undefined,
2476
2485
  endTokenThreshold: 0.9,
2486
+ maxTokensPerPart: 250,
2487
+ useOptimizedAlignmentHeads: true,
2488
+
2477
2489
  encoderProvider: undefined,
2478
- decoderProvider: undefined
2490
+ decoderProvider: undefined,
2479
2491
  }
2480
2492
 
2481
2493
  // Language detection options
@@ -6,6 +6,7 @@ import { Logger } from '../utilities/Logger.js'
6
6
  import { OnnxExecutionProvider, dmlProviderAvailable, getOnnxSessionOptions } from '../utilities/OnnxUtilities.js'
7
7
  import chalk from 'chalk'
8
8
  import { WindowedList } from '../utilities/WindowedList.js'
9
+ import { logLevelGreaterOrEqualTo } from '../api/API.js'
9
10
 
10
11
  const log = logToStderr
11
12
 
@@ -13,9 +14,9 @@ export async function isolate(
13
14
  rawAudio: RawAudio,
14
15
  modelFilePath: string,
15
16
  modelProfile: MDXNetModelProfile,
16
- executionProviders: OnnxExecutionProvider[]) {
17
+ options: MDXNetOptions) {
17
18
 
18
- const model = new MDXNet(modelFilePath, modelProfile, executionProviders)
19
+ const model = new MDXNet(modelFilePath, modelProfile, options)
19
20
 
20
21
  return model.processAudio(rawAudio)
21
22
  }
@@ -27,11 +28,11 @@ export class MDXNet {
27
28
  constructor(
28
29
  public readonly modelFilePath: string,
29
30
  public readonly modelProfile: MDXNetModelProfile,
30
- public readonly executionProviders: OnnxExecutionProvider[]) {
31
+ public readonly options: MDXNetOptions) {
31
32
  }
32
33
 
33
34
  async processAudio(rawAudio: RawAudio) {
34
- if (rawAudio.audioChannels.length != 2) {
35
+ if (rawAudio.audioChannels.length !== 2) {
35
36
  throw new Error(`Input audio must be stereo`)
36
37
  }
37
38
 
@@ -43,12 +44,18 @@ export class MDXNet {
43
44
  return getEmptyRawAudio(rawAudio.audioChannels.length, rawAudio.sampleRate)
44
45
  }
45
46
 
47
+ const enableTraceLogging = logLevelGreaterOrEqualTo('trace')
48
+
46
49
  const logger = new Logger()
47
50
 
48
- await logger.startAsync(`Initialize MDXNet model`)
51
+ await logger.startAsync(`Initialize session for MDX-NET model '${this.options.model!}'`)
49
52
 
50
53
  await this.initializeSessionIfNeeded()
51
54
 
55
+ logger.end()
56
+
57
+ logger.logTitledMessage(`Using ONNX execution provider`, `${this.onnxSessionOptions!.executionProviders!.join(', ')}`)
58
+
52
59
  const Onnx = await import('onnxruntime-node')
53
60
 
54
61
  const sampleRate = this.modelProfile.sampleRate
@@ -81,7 +88,11 @@ export class MDXNet {
81
88
 
82
89
  const timePosition = segmentStartFrameOffset * (fftHopSize / sampleRate)
83
90
 
84
- await logger.startAsync(`Compute STFT of segment at time position ${timePosition.toFixed(2)}`, undefined, chalk.magentaBright)
91
+ if (enableTraceLogging) {
92
+ await logger.startAsync(`Compute STFT of segment at time position ${timePosition.toFixed(2)}`, undefined, chalk.magentaBright)
93
+ } else {
94
+ await logger.startAsync(`Process segment at time position ${timePosition.toFixed(2)}`)
95
+ }
85
96
 
86
97
  while (fftFramesLeftWindowedList.endOffset < segmentEndFrameOffset) {
87
98
  const nextLeftFrameResult = await fftFramesLeftGenerator.next()
@@ -109,7 +120,9 @@ export class MDXNet {
109
120
 
110
121
  const isLastSegment = segmentLength < segmentSize
111
122
 
112
- await logger.startAsync(`Reshape STFT frames`)
123
+ if (enableTraceLogging) {
124
+ await logger.startAsync(`Reshape STFT frames`)
125
+ }
113
126
 
114
127
  const flattenedInputTensor = new Float32Array(1 * 4 * binCount * segmentSize)
115
128
 
@@ -142,13 +155,17 @@ export class MDXNet {
142
155
  }
143
156
  }
144
157
 
145
- await logger.startAsync(`Process with MDXNet model (ONNX provider: ${this.onnxSessionOptions!.executionProviders!.join(', ')})`)
158
+ if (enableTraceLogging) {
159
+ await logger.startAsync(`Process segment with MDXNet model`)
160
+ }
146
161
 
147
162
  const inputTensor = new Onnx.Tensor('float32', flattenedInputTensor, [1, 4, binCount, segmentSize])
148
163
 
149
164
  const { output: outputTensor } = await this.session!.run({ input: inputTensor })
150
165
 
151
- await logger.startAsync('Reshape processed frames')
166
+ if (enableTraceLogging) {
167
+ await logger.startAsync('Reshape processed frames')
168
+ }
152
169
 
153
170
  const flattenedOutputTensor = outputTensor.data as Float32Array
154
171
 
@@ -191,7 +208,9 @@ export class MDXNet {
191
208
 
192
209
  const outputAudioChannels: Float32Array[] = []
193
210
 
194
- await logger.startAsync(`Compute inverse STFT of model output for segment`)
211
+ if (enableTraceLogging) {
212
+ await logger.startAsync(`Compute inverse STFT of model output for segment`)
213
+ }
195
214
 
196
215
  for (let channelIndex = 0; channelIndex < 2; channelIndex++) {
197
216
  const samples = await stiftr(
@@ -268,13 +287,16 @@ export class MDXNet {
268
287
 
269
288
  const Onnx = await import('onnxruntime-node')
270
289
 
271
- this.onnxSessionOptions = getOnnxSessionOptions({ executionProviders: this.executionProviders })
290
+ const executionProviders: OnnxExecutionProvider[] =
291
+ this.options.provider ? [this.options.provider] : getDefaultMDXNetProviders()
292
+
293
+ this.onnxSessionOptions = getOnnxSessionOptions({ executionProviders })
272
294
 
273
295
  this.session = await Onnx.InferenceSession.create(this.modelFilePath, this.onnxSessionOptions)
274
296
  }
275
297
  }
276
298
 
277
- export function getDefaultMDXNetProviders() : OnnxExecutionProvider[] {
299
+ export function getDefaultMDXNetProviders(): OnnxExecutionProvider[] {
278
300
  if (dmlProviderAvailable()) {
279
301
  return ['dml', 'cpu']
280
302
  } else {
@@ -344,3 +366,13 @@ export type MDXNetModelName =
344
366
  'UVR_MDXNET_Main' |
345
367
  'Kim_Vocal_1' |
346
368
  'Kim_Vocal_2'
369
+
370
+ export interface MDXNetOptions {
371
+ model?: MDXNetModelName
372
+ provider?: OnnxExecutionProvider
373
+ }
374
+
375
+ export const defaultMDXNetOptions: MDXNetOptions = {
376
+ model: 'UVR_MDXNET_1_9703',
377
+ provider: undefined,
378
+ }
@@ -56,6 +56,8 @@ declare module '@echogarden/rnnoise-wasm'
56
56
  declare module '@echogarden/fvad-wasm'
57
57
  declare module '@echogarden/sonic-wasm'
58
58
  declare module '@echogarden/kissfft-wasm'
59
+ declare module '@echogarden/pffft-wasm'
60
+ declare module '@echogarden/pffft-wasm/simd'
59
61
  declare module '@echogarden/speex-resampler-wasm'
60
62
  declare module '@echogarden/vosk'
61
63
 
@@ -192,13 +192,9 @@ const packageVersionTagResolutionLookup: { [packageName: string]: string } = {
192
192
  'whisper.cpp-large-v3-turbo-q5_0': '20241003',
193
193
 
194
194
  // whisper.cpp binaries
195
- 'whisper.cpp-binaries-linux-x64-cpu-1.6.2': '20241001',
196
- 'whisper.cpp-binaries-windows-x64-cpu-1.6.2': '20241001',
197
- 'whisper.cpp-binaries-windows-x64-cublas-12.4.0-1.6.2': '20241002',
198
-
199
- 'whisper.cpp-binaries-linux-x64-cpu-latest': '20241003',
200
- 'whisper.cpp-binaries-windows-x64-cpu-latest': '20241003',
201
- 'whisper.cpp-binaries-windows-x64-cublas-12.4.0-latest': '20241003',
195
+ 'whisper.cpp-binaries-linux-x64-cpu-latest': '20241010',
196
+ 'whisper.cpp-binaries-windows-x64-cpu-latest': '20241010',
197
+ 'whisper.cpp-binaries-windows-x64-cublas-12.4.0-latest': '20241010',
202
198
 
203
199
  // E5 models
204
200
  'xenova-multilingual-e5-small-q8': '20240504',