echogarden 0.11.12 → 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/api/Alignment.js +2 -2
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Recognition.js +2 -2
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +5 -4
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +2 -2
- package/dist/api/Translation.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +1 -0
- package/dist/audio/AudioUtilities.js +25 -7
- 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/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 +7 -6
- 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
package/src/dsp/MFCC.ts
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js"
|
|
2
|
+
import { Logger } from "../utilities/Logger.js"
|
|
3
|
+
import { resampleAudioSpeex } from "./SpeexResampler.js"
|
|
4
|
+
import { computeMelSpectogram } from "./MelSpectogram.js"
|
|
5
|
+
import { RawAudio, powerToDecibels } from "../audio/AudioUtilities.js"
|
|
6
|
+
import { normalizeVectors } from "../math/VectorMath.js"
|
|
7
|
+
|
|
8
|
+
export async function computeMFCCs(monoAudio: RawAudio, options: MfccOptions = {}) {
|
|
9
|
+
const logger = new Logger()
|
|
10
|
+
logger.start("Initialize options")
|
|
11
|
+
|
|
12
|
+
if (monoAudio.audioChannels.length != 1) {
|
|
13
|
+
throw new Error("Audio must be mono")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
options = extendDefaultMfccOptions(options)
|
|
17
|
+
|
|
18
|
+
const analysisSampleRate = options.analysisSampleRate!
|
|
19
|
+
const featureCount = options.featureCount!
|
|
20
|
+
|
|
21
|
+
const fftOrder = options.fftOrder!
|
|
22
|
+
|
|
23
|
+
const windowDuration = options.windowDuration!
|
|
24
|
+
const windowSize = windowDuration * analysisSampleRate
|
|
25
|
+
const hopDuration = options.hopDuration!
|
|
26
|
+
const hopLength = hopDuration * analysisSampleRate
|
|
27
|
+
|
|
28
|
+
const filterbankCount = options.filterbankCount!
|
|
29
|
+
const lowerFrequencyHz = options.lowerFreq!
|
|
30
|
+
const upperFrequencyHz = options.upperFreq!
|
|
31
|
+
|
|
32
|
+
const emphasisFactor = options.emphasisFactor!
|
|
33
|
+
const lifteringFactor = options.lifteringFactor!
|
|
34
|
+
const zeroFirstCoefficient = options.zeroFirstCoefficient!
|
|
35
|
+
|
|
36
|
+
logger.start(`Resample audio to analysis sample rate (${analysisSampleRate}Hz)`)
|
|
37
|
+
const resampledAudio = await resampleAudioSpeex(monoAudio, analysisSampleRate)
|
|
38
|
+
|
|
39
|
+
let mfccs: number[][]
|
|
40
|
+
|
|
41
|
+
if (emphasisFactor > 0) {
|
|
42
|
+
logger.start("Apply emphasis")
|
|
43
|
+
resampledAudio.audioChannels[0] = applyEmphasis(resampledAudio.audioChannels[0], emphasisFactor)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
logger.start("Compute Mel spectogram")
|
|
47
|
+
const { melSpectogram } = await computeMelSpectogram(resampledAudio, fftOrder, windowSize, hopLength, filterbankCount, lowerFrequencyHz, upperFrequencyHz)
|
|
48
|
+
|
|
49
|
+
logger.start("Extract MFCCs from Mel spectogram")
|
|
50
|
+
const mfccsFloat32 = melSpectogramToMFCCs(melSpectogram, featureCount)
|
|
51
|
+
|
|
52
|
+
mfccs = mfccsFloat32.map(mfcc => Array.from(mfcc))
|
|
53
|
+
|
|
54
|
+
if (options.normalize!) {
|
|
55
|
+
logger.start("Normalize MFCCs")
|
|
56
|
+
|
|
57
|
+
const { normalizedVectors, mean, stdDeviation } = normalizeVectors(mfccs)
|
|
58
|
+
mfccs = normalizedVectors
|
|
59
|
+
//mfccs = mfccs.map(mfcc => subtractVectors(mfcc, mean))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (lifteringFactor > 0) {
|
|
63
|
+
logger.start("Apply liftering to MFCCs")
|
|
64
|
+
mfccs = applyLiftering(mfccs, lifteringFactor)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (zeroFirstCoefficient) {
|
|
68
|
+
for (const mfcc of mfccs) {
|
|
69
|
+
mfcc[0] = 0
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.end()
|
|
74
|
+
|
|
75
|
+
return mfccs
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function melSpectogramToMFCCs(melSpectogram: Float32Array[], mfccFeatureCount: number) {
|
|
79
|
+
const melBandCount = melSpectogram[0].length
|
|
80
|
+
const dctMatrix = createDCTType2CoefficientMatrix(mfccFeatureCount, melBandCount)
|
|
81
|
+
|
|
82
|
+
const mfccs = melSpectogram.map(frame => melSpectrumToMFCC(frame, mfccFeatureCount, dctMatrix))
|
|
83
|
+
|
|
84
|
+
return mfccs
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function melSpectrumToMFCC(melSpectrum: Float32Array, mfccFeatureCount: number, dctMatrix: Float32Array[], normalization: "none" | "orthonormal" = "orthonormal") {
|
|
88
|
+
const melBandCount = melSpectrum.length
|
|
89
|
+
|
|
90
|
+
let firstFeatureNormalizationFactor: number
|
|
91
|
+
let nonfirstFeatureNormalizationFactor: number
|
|
92
|
+
|
|
93
|
+
if (normalization == "orthonormal") {
|
|
94
|
+
firstFeatureNormalizationFactor = Math.sqrt(1 / (4 * mfccFeatureCount))
|
|
95
|
+
nonfirstFeatureNormalizationFactor = Math.sqrt(1 / (2 * mfccFeatureCount))
|
|
96
|
+
} else {
|
|
97
|
+
firstFeatureNormalizationFactor = 1
|
|
98
|
+
nonfirstFeatureNormalizationFactor = 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const mfcc = new Float32Array(mfccFeatureCount)
|
|
102
|
+
|
|
103
|
+
for (let mfccFeatureIndex = 0; mfccFeatureIndex < mfccFeatureCount; mfccFeatureIndex++) {
|
|
104
|
+
const dctMatrixRow = dctMatrix[mfccFeatureIndex]
|
|
105
|
+
|
|
106
|
+
let sum = 0
|
|
107
|
+
|
|
108
|
+
for (let j = 0; j < melBandCount; j++) {
|
|
109
|
+
const dctCoefficient = dctMatrixRow[j]
|
|
110
|
+
const logMel = powerToDecibels(melSpectrum[j])
|
|
111
|
+
//const logMel = Math.log(1e-40 + melSpectrum[j])
|
|
112
|
+
|
|
113
|
+
sum += dctCoefficient * logMel
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const normalizationFactor = mfccFeatureIndex == 0 ? firstFeatureNormalizationFactor : nonfirstFeatureNormalizationFactor
|
|
117
|
+
|
|
118
|
+
//mfcc[mfccFeatureIndex] = normalizationFactor * sum
|
|
119
|
+
mfcc[mfccFeatureIndex] = normalizationFactor * 2 * sum // Sum multiplied by 2 to match with librosa
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return mfcc
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function createDCTType2CoefficientMatrix(mfccFeatureCount: number, melBandCount: number) {
|
|
126
|
+
const dctMatrix = new Array<Float32Array>(mfccFeatureCount)
|
|
127
|
+
|
|
128
|
+
for (let mfccFeatureIndex = 0; mfccFeatureIndex < mfccFeatureCount; mfccFeatureIndex++) {
|
|
129
|
+
const row = new Float32Array(melBandCount)
|
|
130
|
+
|
|
131
|
+
const innerMultiplier = Math.PI * mfccFeatureIndex / melBandCount
|
|
132
|
+
|
|
133
|
+
for (let melBandIndex = 0; melBandIndex < melBandCount; melBandIndex++) {
|
|
134
|
+
row[melBandIndex] = Math.cos(innerMultiplier * (melBandIndex + 0.5))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
dctMatrix[mfccFeatureIndex] = row
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return dctMatrix
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function mfccBufferToVectors(mfccBuffer: Float64Array, mfccFeatureCount: number) {
|
|
144
|
+
if (mfccBuffer.length % mfccFeatureCount != 0) {
|
|
145
|
+
throw new Error(`MFCC buffer length is not a multiple of the expected feature count (${mfccFeatureCount})`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const mfccVectors: number[][] = []
|
|
149
|
+
|
|
150
|
+
for (let offset = 0; offset < mfccBuffer.length; offset += mfccFeatureCount) {
|
|
151
|
+
const mfccVector = Array.from(mfccBuffer.subarray(offset, offset + mfccFeatureCount))
|
|
152
|
+
|
|
153
|
+
mfccVectors.push(mfccVector)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return mfccVectors
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function applyEmphasis(samples: Float32Array, emphasisFactor = 0.97, initialState = 0) {
|
|
160
|
+
const processedSamples = new Float32Array(samples.length)
|
|
161
|
+
|
|
162
|
+
processedSamples[0] = samples[0] - (emphasisFactor * initialState)
|
|
163
|
+
|
|
164
|
+
for (let i = 1; i < processedSamples.length; i++) {
|
|
165
|
+
processedSamples[i] = samples[i] - (emphasisFactor * samples[i - 1])
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return processedSamples
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function applyLiftering(mfccs: number[][], lifteringFactor: number) {
|
|
172
|
+
const featureCount = mfccs[0].length
|
|
173
|
+
|
|
174
|
+
const lifterMultipliers = new Float32Array(featureCount)
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < featureCount; i++) {
|
|
177
|
+
lifterMultipliers[i] = 1 + (lifteringFactor / 2) * Math.sin(Math.PI * (i + 1) / lifteringFactor)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const lifteredMfccs: number[][] = []
|
|
181
|
+
|
|
182
|
+
for (const mfcc of mfccs) {
|
|
183
|
+
const lifteredMfcc = new Array(featureCount)
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < featureCount; i++) {
|
|
186
|
+
lifteredMfcc[i] = mfcc[i] * lifterMultipliers[i]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
lifteredMfccs.push(lifteredMfcc)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return lifteredMfccs
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export type MfccOptions = {
|
|
196
|
+
filterbankCount?: number
|
|
197
|
+
featureCount?: number
|
|
198
|
+
fftOrder?: number
|
|
199
|
+
lowerFreq?: number
|
|
200
|
+
upperFreq?: number
|
|
201
|
+
windowDuration?: number
|
|
202
|
+
hopDuration?: number
|
|
203
|
+
emphasisFactor?: number
|
|
204
|
+
analysisSampleRate?: number
|
|
205
|
+
lifteringFactor?: number
|
|
206
|
+
normalize?: boolean
|
|
207
|
+
zeroFirstCoefficient?: boolean
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const defaultMfccOptions: MfccOptions = {
|
|
211
|
+
filterbankCount: 40,
|
|
212
|
+
featureCount: 13,
|
|
213
|
+
fftOrder: 512,
|
|
214
|
+
lowerFreq: 133.3333,
|
|
215
|
+
upperFreq: 6855.4976,
|
|
216
|
+
windowDuration: 0.025,
|
|
217
|
+
hopDuration: 0.010,
|
|
218
|
+
emphasisFactor: 0.97,
|
|
219
|
+
analysisSampleRate: 16000,
|
|
220
|
+
lifteringFactor: 0,
|
|
221
|
+
normalize: false,
|
|
222
|
+
zeroFirstCoefficient: false,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function extendDefaultMfccOptions(options: MfccOptions) {
|
|
226
|
+
return extendDeep(defaultMfccOptions, options)
|
|
227
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { RawAudio } from "../audio/AudioUtilities.js"
|
|
2
|
+
import { Logger } from "../utilities/Logger.js"
|
|
3
|
+
import * as FFT from "./FFT.js"
|
|
4
|
+
|
|
5
|
+
export async function computeMelSpectogram(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbankCount: number, lowerFrequencyHz: number, upperFrequencyHz: number) {
|
|
6
|
+
const logger = new Logger()
|
|
7
|
+
|
|
8
|
+
logger.start("Compute mel filterbank")
|
|
9
|
+
const binCount = (fftOrder / 2) + 2
|
|
10
|
+
const nyquistFrequency = rawAudio.sampleRate / 2
|
|
11
|
+
const binFrequencies = FFT.getBinFrequencies(binCount, nyquistFrequency)
|
|
12
|
+
|
|
13
|
+
const lowerFrequencyMel = hertzToMel(lowerFrequencyHz)
|
|
14
|
+
const upperFrequencyMel = hertzToMel(upperFrequencyHz)
|
|
15
|
+
|
|
16
|
+
const filterbanksCenterFrequencies = getMelFilterbanksCenterFrequencies(filterbankCount, lowerFrequencyMel, upperFrequencyMel)
|
|
17
|
+
const melFilterbanks = getMelFilterbanks(binFrequencies, filterbanksCenterFrequencies, lowerFrequencyMel, upperFrequencyMel)
|
|
18
|
+
|
|
19
|
+
logger.end()
|
|
20
|
+
|
|
21
|
+
return computeMelSpectogramUsingFilterbanks(rawAudio, fftOrder, windowSize, hopLength, melFilterbanks)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function computeMelSpectogramUsingFilterbanks(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbanks: Filterbank[]) {
|
|
25
|
+
const logger = new Logger()
|
|
26
|
+
|
|
27
|
+
logger.start("Compute short-time FFTs")
|
|
28
|
+
const audioSamples = rawAudio.audioChannels[0]
|
|
29
|
+
const fftFrames = await FFT.stftr(audioSamples, fftOrder, windowSize, hopLength, "hann")
|
|
30
|
+
|
|
31
|
+
logger.start("Convert FFT frames to a mel spectogram")
|
|
32
|
+
const melSpectogram = fftFramesToMelSpectogram(fftFrames, filterbanks)
|
|
33
|
+
|
|
34
|
+
logger.end()
|
|
35
|
+
|
|
36
|
+
return { melSpectogram, fftFrames }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function fftFramesToMelSpectogram(fftFrames: Float32Array[], melFilterbanks: Filterbank[]) {
|
|
40
|
+
return fftFrames.map(fftFrame => {
|
|
41
|
+
const powerSpectrum = FFT.fftFrameToPowerSpectrum(fftFrame)
|
|
42
|
+
return powerSpectrumToMelSpectrum(powerSpectrum, melFilterbanks)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function powerSpectrumToMelSpectrum(powerSpectrum: Float32Array, filterbanks: Filterbank[]) {
|
|
47
|
+
const filterbankCount = filterbanks.length
|
|
48
|
+
const melSpectrum = new Float32Array(filterbankCount)
|
|
49
|
+
|
|
50
|
+
for (let melBandIndex = 0; melBandIndex < filterbankCount; melBandIndex++) {
|
|
51
|
+
const filterbank = filterbanks[melBandIndex]
|
|
52
|
+
const filterbankStartIndex = filterbank.startIndex
|
|
53
|
+
const filterbankWeights = filterbank.weights
|
|
54
|
+
|
|
55
|
+
if (filterbankStartIndex == -1) {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let bandValue = 0
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < filterbankWeights.length; i++) {
|
|
62
|
+
bandValue += filterbankWeights[i] * powerSpectrum[filterbankStartIndex + i]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
melSpectrum[melBandIndex] = bandValue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return melSpectrum
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getMelFilterbanks(powerSpectrumFrequenciesHz: Float32Array, centerFrequenciesMel: Float32Array, lowerFrequencyMel: number, upperFrequencyMel: number) {
|
|
72
|
+
const filterbankCount = centerFrequenciesMel.length
|
|
73
|
+
const powerSpectrumFrequenciesMel = powerSpectrumFrequenciesHz.map(frequencyHz => hertzToMel(frequencyHz))
|
|
74
|
+
|
|
75
|
+
const filterbanks: Filterbank[] = []
|
|
76
|
+
|
|
77
|
+
for (let filterbankIndex = 0; filterbankIndex < filterbankCount; filterbankIndex++) {
|
|
78
|
+
const centerFrequency = centerFrequenciesMel[filterbankIndex]
|
|
79
|
+
|
|
80
|
+
const leftFrequency = filterbankIndex > 0 ? centerFrequenciesMel[filterbankIndex - 1] : lowerFrequencyMel
|
|
81
|
+
const rightFrequency = filterbankIndex < filterbankCount - 1 ? centerFrequenciesMel[filterbankIndex + 1] : upperFrequencyMel
|
|
82
|
+
|
|
83
|
+
const width = rightFrequency - leftFrequency
|
|
84
|
+
const halfWidth = width / 2
|
|
85
|
+
|
|
86
|
+
let startIndex = -1
|
|
87
|
+
let weights: number[] = []
|
|
88
|
+
|
|
89
|
+
let weightSum = 0
|
|
90
|
+
|
|
91
|
+
for (let powerSpectrumBandIndex = 0; powerSpectrumBandIndex < powerSpectrumFrequenciesMel.length; powerSpectrumBandIndex++) {
|
|
92
|
+
const powerSpectrumBandFrequencyMel = powerSpectrumFrequenciesMel[powerSpectrumBandIndex]
|
|
93
|
+
|
|
94
|
+
let weight = 0
|
|
95
|
+
|
|
96
|
+
if (powerSpectrumBandFrequencyMel >= leftFrequency && powerSpectrumBandFrequencyMel <= centerFrequency) {
|
|
97
|
+
weight = (powerSpectrumBandFrequencyMel - leftFrequency) / halfWidth
|
|
98
|
+
} else if (powerSpectrumBandFrequencyMel > centerFrequency && powerSpectrumBandFrequencyMel <= rightFrequency) {
|
|
99
|
+
weight = (rightFrequency - powerSpectrumBandFrequencyMel) / halfWidth
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (weight > 0) {
|
|
103
|
+
if (startIndex == -1) {
|
|
104
|
+
startIndex = powerSpectrumBandIndex
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
weights.push(weight)
|
|
108
|
+
weightSum += weight
|
|
109
|
+
} else if (startIndex != -1) {
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
weights = weights.map(weight => weight / weightSum)
|
|
115
|
+
|
|
116
|
+
filterbanks.push({ startIndex, weights })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return filterbanks
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getMelFilterbanksCenterFrequencies(melBandCount: number, lowerFrequencyMel: number, upperFrequencyMel: number) {
|
|
123
|
+
const stepSizeMel = (upperFrequencyMel - lowerFrequencyMel) / (melBandCount + 1)
|
|
124
|
+
|
|
125
|
+
const centerFrequencies = new Float32Array(melBandCount)
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < melBandCount; i++) {
|
|
128
|
+
centerFrequencies[i] = lowerFrequencyMel + ((i + 1) * stepSizeMel)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return centerFrequencies
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function hertzToMel(frequency: number) {
|
|
135
|
+
return 2595.0 * Math.log10(1.0 + (frequency / 700.0))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function melToHertz(mel: number) {
|
|
139
|
+
return 700.0 * (Math.pow(10.0, mel / 2595.0) - 1.0)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type Filterbank = {
|
|
143
|
+
startIndex: number
|
|
144
|
+
weights: number[]
|
|
145
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { RawAudio } from "../audio/AudioUtilities.js"
|
|
2
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js"
|
|
3
|
+
import { concatFloat32Arrays } from "../utilities/Utilities.js"
|
|
4
|
+
import { Float32ArrayRef, WasmMemoryManager } from "../utilities/WasmMemoryManager.js"
|
|
5
|
+
|
|
6
|
+
let rubberbandInstance: any
|
|
7
|
+
|
|
8
|
+
export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchScale: number, options: RubberbandOptions) {
|
|
9
|
+
options = extendDeep(defaultRubberbandOptions, options)
|
|
10
|
+
|
|
11
|
+
const channels = rawAudio.audioChannels
|
|
12
|
+
const channelCount = channels.length
|
|
13
|
+
const sampleCount = channels[0].length
|
|
14
|
+
const sampleRate = rawAudio.sampleRate
|
|
15
|
+
|
|
16
|
+
const m = await getRubberbandInstance()
|
|
17
|
+
const wasmMemory = new WasmMemoryManager(m)
|
|
18
|
+
|
|
19
|
+
const optionFlags = rubberBandOptionsToFlags(options)
|
|
20
|
+
|
|
21
|
+
const statePtr = m._rubberband_new(sampleRate, channelCount, optionFlags, 1, 1)
|
|
22
|
+
m._rubberband_set_time_ratio(statePtr, 1 / speed)
|
|
23
|
+
m._rubberband_set_pitch_scale(statePtr, pitchScale)
|
|
24
|
+
|
|
25
|
+
const samplesRequired = m._rubberband_get_samples_required(statePtr)
|
|
26
|
+
const bufferSize = Math.min(samplesRequired, sampleCount)
|
|
27
|
+
|
|
28
|
+
const bufferChannelPtrsRef = wasmMemory.allocUint32Array(bufferSize)
|
|
29
|
+
const bufferChannelRefs: Float32ArrayRef[] = []
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < channelCount; i++) {
|
|
32
|
+
const bufferChannelRef = wasmMemory.allocFloat32Array(bufferSize)
|
|
33
|
+
bufferChannelPtrsRef.view[i] = bufferChannelRef.address
|
|
34
|
+
|
|
35
|
+
bufferChannelRefs.push(bufferChannelRef)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
m._rubberband_set_expected_input_duration(statePtr, sampleCount)
|
|
39
|
+
|
|
40
|
+
//m._rubberband_set_max_process_size(statePtr, bufferSize)
|
|
41
|
+
|
|
42
|
+
for (let offset = 0; offset < sampleCount; offset += bufferSize) {
|
|
43
|
+
let writtenSize: number
|
|
44
|
+
let isFinal: 0 | 1
|
|
45
|
+
|
|
46
|
+
if (sampleCount - offset > bufferSize) {
|
|
47
|
+
writtenSize = bufferSize
|
|
48
|
+
isFinal = 0
|
|
49
|
+
} else {
|
|
50
|
+
writtenSize = sampleCount - offset
|
|
51
|
+
isFinal = 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < channelCount; i++) {
|
|
55
|
+
const samplesToWrite = channels[i].subarray(offset, offset + writtenSize)
|
|
56
|
+
bufferChannelRefs[i].view.set(samplesToWrite)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
m._rubberband_study(statePtr, bufferChannelPtrsRef.address, writtenSize, isFinal)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const outputAudioChannelChunks: Float32Array[][] = []
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < channelCount; i++) {
|
|
65
|
+
outputAudioChannelChunks.push([])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (let readOffset = 0; readOffset < sampleCount; readOffset += bufferSize) {
|
|
69
|
+
let writtenSize: number
|
|
70
|
+
let isFinal: 0 | 1
|
|
71
|
+
|
|
72
|
+
if (sampleCount - readOffset > bufferSize) {
|
|
73
|
+
writtenSize = bufferSize
|
|
74
|
+
isFinal = 0
|
|
75
|
+
} else {
|
|
76
|
+
writtenSize = sampleCount - readOffset
|
|
77
|
+
isFinal = 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < channelCount; i++) {
|
|
81
|
+
const samplesToWrite = channels[i].subarray(readOffset, readOffset + writtenSize)
|
|
82
|
+
bufferChannelRefs[i].view.set(samplesToWrite)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
m._rubberband_process(statePtr, bufferChannelPtrsRef.address, writtenSize, isFinal)
|
|
86
|
+
|
|
87
|
+
while (true) {
|
|
88
|
+
const samplesAvailable = m._rubberband_available(statePtr)
|
|
89
|
+
if (samplesAvailable <= 0) {
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sizeToRead = Math.min(samplesAvailable, bufferSize)
|
|
94
|
+
|
|
95
|
+
const readCount = m._rubberband_retrieve(statePtr, bufferChannelPtrsRef.address, sizeToRead)
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < channelCount; i++) {
|
|
98
|
+
const readSamplesForChannel = bufferChannelRefs[i].view.slice(0, readCount)
|
|
99
|
+
outputAudioChannelChunks[i].push(readSamplesForChannel)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
m._rubberband_delete(statePtr)
|
|
105
|
+
wasmMemory.freeAll()
|
|
106
|
+
|
|
107
|
+
const outputAudioChannels = outputAudioChannelChunks.map(chunks => concatFloat32Arrays(chunks))
|
|
108
|
+
const outputRawAudio: RawAudio = { audioChannels: outputAudioChannels, sampleRate }
|
|
109
|
+
|
|
110
|
+
return outputRawAudio
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function getRubberbandInstance() {
|
|
114
|
+
if (!rubberbandInstance) {
|
|
115
|
+
const { default: RubberbandInitializer } = await import('@echogarden/rubberband-wasm')
|
|
116
|
+
|
|
117
|
+
rubberbandInstance = await RubberbandInitializer()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return rubberbandInstance
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function rubberBandOptionsToFlags(options: RubberbandOptions) {
|
|
124
|
+
let flags = 0
|
|
125
|
+
|
|
126
|
+
if (options.stretch == "precise") {
|
|
127
|
+
flags += RubberBandOptionFlag.StretchPrecise
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (options.transients == "mixed") {
|
|
131
|
+
flags += RubberBandOptionFlag.TransientsMixed
|
|
132
|
+
} else if (options.transients == "smooth") {
|
|
133
|
+
flags += RubberBandOptionFlag.TransientsSmooth
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (options.detector == "percussive") {
|
|
137
|
+
flags += RubberBandOptionFlag.DetectorPercussive
|
|
138
|
+
} else if (options.detector == "soft") {
|
|
139
|
+
flags += RubberBandOptionFlag.DetectorSoft
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (options.phase == "independent") {
|
|
143
|
+
flags += RubberBandOptionFlag.PhaseIndependent
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (options.window == "short") {
|
|
147
|
+
flags += RubberBandOptionFlag.WindowShort
|
|
148
|
+
} else if (options.window == "long") {
|
|
149
|
+
flags += RubberBandOptionFlag.WindowLong
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (options.smoothing == "on") {
|
|
153
|
+
flags += RubberBandOptionFlag.SmoothingOn
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (options.formant == "preserved") {
|
|
157
|
+
flags += RubberBandOptionFlag.FormantPreserved
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options.pitch == "high-quality") {
|
|
161
|
+
flags += RubberBandOptionFlag.PitchHighQuality
|
|
162
|
+
} else if (options.pitch == "high-consistency") {
|
|
163
|
+
flags += RubberBandOptionFlag.PitchHighConsistency
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (options.channels == "together") {
|
|
167
|
+
flags += RubberBandOptionFlag.ChannelsTogether
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (options.engine == "finer") {
|
|
171
|
+
flags += RubberBandOptionFlag.EngineFiner
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return flags
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export enum RubberBandOptionFlag {
|
|
178
|
+
ProcessOffline = 0x00000000,
|
|
179
|
+
ProcessRealTime = 0x00000001,
|
|
180
|
+
|
|
181
|
+
StretchElastic = 0x00000000,
|
|
182
|
+
StretchPrecise = 0x00000010,
|
|
183
|
+
|
|
184
|
+
TransientsCrisp = 0x00000000,
|
|
185
|
+
TransientsMixed = 0x00000100,
|
|
186
|
+
TransientsSmooth = 0x00000200,
|
|
187
|
+
|
|
188
|
+
DetectorCompound = 0x00000000,
|
|
189
|
+
DetectorPercussive = 0x00000400,
|
|
190
|
+
DetectorSoft = 0x00000800,
|
|
191
|
+
|
|
192
|
+
PhaseLaminar = 0x00000000,
|
|
193
|
+
PhaseIndependent = 0x00002000,
|
|
194
|
+
|
|
195
|
+
ThreadingAuto = 0x00000000,
|
|
196
|
+
ThreadingNever = 0x00010000,
|
|
197
|
+
ThreadingAlways = 0x00020000,
|
|
198
|
+
|
|
199
|
+
WindowStandard = 0x00000000,
|
|
200
|
+
WindowShort = 0x00100000,
|
|
201
|
+
WindowLong = 0x00200000,
|
|
202
|
+
|
|
203
|
+
SmoothingOff = 0x00000000,
|
|
204
|
+
SmoothingOn = 0x00800000,
|
|
205
|
+
|
|
206
|
+
FormantShifted = 0x00000000,
|
|
207
|
+
FormantPreserved = 0x01000000,
|
|
208
|
+
|
|
209
|
+
PitchHighSpeed = 0x00000000,
|
|
210
|
+
PitchHighQuality = 0x02000000,
|
|
211
|
+
PitchHighConsistency = 0x04000000,
|
|
212
|
+
|
|
213
|
+
ChannelsApart = 0x00000000,
|
|
214
|
+
ChannelsTogether = 0x10000000,
|
|
215
|
+
|
|
216
|
+
EngineFaster = 0x00000000,
|
|
217
|
+
EngineFiner = 0x20000000
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export enum RubberBandPresetOption {
|
|
221
|
+
DefaultOptions = 0x00000000,
|
|
222
|
+
PercussiveOptions = 0x00102000,
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const defaultRubberbandOptions: RubberbandOptions = {
|
|
226
|
+
stretch: "elastic",
|
|
227
|
+
transients: "crisp",
|
|
228
|
+
detector: "compound",
|
|
229
|
+
phase: "laminar",
|
|
230
|
+
window: "standard",
|
|
231
|
+
smoothing: "off",
|
|
232
|
+
formant: "shited",
|
|
233
|
+
pitch: "high-speed",
|
|
234
|
+
channels: "apart",
|
|
235
|
+
engine: "faster"
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export type RubberbandOptions = {
|
|
239
|
+
stretch?: "elastic" | "precise"
|
|
240
|
+
transients?: "crisp" | "mixed" | "smooth"
|
|
241
|
+
detector?: "compound" | "percussive" | "soft"
|
|
242
|
+
phase?: "laminar" | "independent"
|
|
243
|
+
window?: "standard" | "long" | "short"
|
|
244
|
+
smoothing?: "off" | "on"
|
|
245
|
+
formant?: "shited" | "preserved"
|
|
246
|
+
pitch?: "high-speed" | "high-quality" | "high-consistency"
|
|
247
|
+
channels?: "apart" | "together"
|
|
248
|
+
engine?: "faster" | "finer"
|
|
249
|
+
}
|
package/src/dsp/Sonic.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RawAudio } from "../audio/AudioUtilities.js"
|
|
2
|
+
import { WasmMemoryManager } from "../utilities/WasmMemoryManager.js"
|
|
3
|
+
|
|
4
|
+
let sonicInstance: any
|
|
5
|
+
|
|
6
|
+
export async function stretchTimePitch(rawAudio: RawAudio, speed: number, pitchScale: number) {
|
|
7
|
+
const sampleRate = rawAudio.sampleRate
|
|
8
|
+
const channelCount = rawAudio.audioChannels.length
|
|
9
|
+
const inputSamples = rawAudio.audioChannels[0]
|
|
10
|
+
const inputSampleCount = rawAudio.audioChannels[0].length
|
|
11
|
+
|
|
12
|
+
const m = await getSonicInstance()
|
|
13
|
+
const wasmMemory = new WasmMemoryManager(m)
|
|
14
|
+
|
|
15
|
+
const streamPtr = m._sonicCreateStream(sampleRate, channelCount)
|
|
16
|
+
m._sonicSetSpeed(streamPtr, speed)
|
|
17
|
+
m._sonicSetPitch(streamPtr, pitchScale)
|
|
18
|
+
|
|
19
|
+
const inputSamplesRef = wasmMemory.allocFloat32Array(inputSampleCount)
|
|
20
|
+
inputSamplesRef.view.set(inputSamples)
|
|
21
|
+
|
|
22
|
+
const writeSuccess = m._sonicWriteFloatToStream(streamPtr, inputSamplesRef.address, inputSampleCount)
|
|
23
|
+
|
|
24
|
+
if (writeSuccess != 1) {
|
|
25
|
+
throw new Error("Sonic error: failed write to stream")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const flushSuccess = m._sonicFlushStream(streamPtr)
|
|
29
|
+
|
|
30
|
+
if (flushSuccess != 1) {
|
|
31
|
+
throw new Error("Sonic error: failed flushing stream")
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const samplesAvailable = m._sonicSamplesAvailable(streamPtr)
|
|
35
|
+
|
|
36
|
+
const outputSamplesRef = wasmMemory.allocFloat32Array(samplesAvailable)
|
|
37
|
+
|
|
38
|
+
const samplesRead = m._sonicReadFloatFromStream(streamPtr, outputSamplesRef.address, outputSamplesRef.length)
|
|
39
|
+
|
|
40
|
+
const outputSamples = outputSamplesRef.view.slice(0, samplesRead)
|
|
41
|
+
|
|
42
|
+
const resultAudio: RawAudio = { audioChannels: [outputSamples], sampleRate }
|
|
43
|
+
|
|
44
|
+
m._sonicDestroyStream(streamPtr)
|
|
45
|
+
|
|
46
|
+
wasmMemory.freeAll()
|
|
47
|
+
|
|
48
|
+
return resultAudio
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function getSonicInstance() {
|
|
52
|
+
if (!sonicInstance) {
|
|
53
|
+
const { default: SonicInitializer } = await import('@echogarden/sonic-wasm')
|
|
54
|
+
|
|
55
|
+
sonicInstance = await SonicInitializer()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return sonicInstance
|
|
59
|
+
}
|