echogarden
Version:
An easy-to-use speech toolset. Includes tools for synthesis, recognition, alignment, speech translation, language detection, source separation and more.
123 lines (88 loc) • 4.52 kB
text/typescript
import { RawAudio, cloneRawAudio } from '../audio/AudioUtilities.js'
import { concatFloat32Arrays, isWasmSimdSupported } from '../utilities/Utilities.js'
import { wrapEmscriptenModuleHeap } from 'wasm-heap-manager'
let speexResamplerInstance: any
export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: number, quality = 0): Promise<RawAudio> {
const channelCount = rawAudio.audioChannels.length
const inSampleRate = rawAudio.sampleRate
const totalSampleCount = rawAudio.audioChannels[0].length
const sampleRateRatio = outSampleRate / inSampleRate
if (inSampleRate === outSampleRate) {
return cloneRawAudio(rawAudio)
}
if (totalSampleCount === 0) {
return {
...cloneRawAudio(rawAudio),
sampleRate: outSampleRate
} as RawAudio
}
const m = await getSpeexResamplerInstance()
const wasmHeap = wrapEmscriptenModuleHeap(m)
function speexResultCodeToString(resultCode: number) {
const errorStrPtr = m._speex_resampler_strerror(resultCode)
const messageRef = wasmHeap.wrapNullTerminatedUtf8String(errorStrPtr)
const message = messageRef.value
return message
}
const initErrRef = wasmHeap.allocInt32()
const resamplerStateAddress = m._speex_resampler_init(channelCount, inSampleRate, outSampleRate, quality, initErrRef.address)
let resultCode = initErrRef.value
if (resultCode != 0) {
throw new Error(`Speex resampler failed while initializing with code ${resultCode}: ${speexResultCodeToString(resultCode)}`)
}
const inputLatency = m._speex_resampler_get_input_latency(resamplerStateAddress)
const outputLatency = m._speex_resampler_get_output_latency(resamplerStateAddress)
const maxChunkSize = 2 ** 20
const inputChunkSampleCountRef = wasmHeap.allocInt32()
const outputChunkSampleCountRef = wasmHeap.allocInt32()
const inputChunkSamplesRef = wasmHeap.allocFloat32Array(maxChunkSize * 2)
const outputChunkSamplesRef = wasmHeap.allocFloat32Array(Math.floor(maxChunkSize * sampleRateRatio) * 2)
const resampledAudioChunksForChannels: Float32Array[][] = []
for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
resampledAudioChunksForChannels.push([])
}
for (let channelIndex = 0; channelIndex < channelCount; channelIndex++) {
for (let readOffset = 0; readOffset < totalSampleCount;) {
const isLastChunk = readOffset + maxChunkSize >= totalSampleCount
const inputPaddingSize = isLastChunk ? inputLatency : 0
const maxSamplesToRead = Math.min(maxChunkSize, totalSampleCount - readOffset) + inputPaddingSize
const maxSamplesToWrite = outputChunkSamplesRef.elementCount
const inputChunkSamplesForChannel = rawAudio.audioChannels[channelIndex].slice(readOffset, readOffset + maxSamplesToRead)
inputChunkSampleCountRef.value = maxSamplesToRead
outputChunkSampleCountRef.value = maxSamplesToWrite
inputChunkSamplesRef.view.set(inputChunkSamplesForChannel)
resultCode = m._speex_resampler_process_float(resamplerStateAddress, channelIndex, inputChunkSamplesRef.address, inputChunkSampleCountRef.address, outputChunkSamplesRef.address, outputChunkSampleCountRef.address)
if (resultCode != 0) {
throw new Error(`Speex resampler failed while resampling with code ${resultCode}: ${speexResultCodeToString(resultCode)}`)
}
const samplesReadCount = inputChunkSampleCountRef.value
const samplesWrittenCount = outputChunkSampleCountRef.value
const resampledChannelAudio = outputChunkSamplesRef.view.slice(0, samplesWrittenCount)
resampledAudioChunksForChannels[channelIndex].push(resampledChannelAudio)
readOffset += samplesReadCount
}
}
m._speex_resampler_destroy(resamplerStateAddress)
wasmHeap.freeAll()
const resampledAudio: RawAudio = {
audioChannels: [],
sampleRate: outSampleRate
}
for (let i = 0; i < channelCount; i++) {
resampledAudioChunksForChannels[i][0] = resampledAudioChunksForChannels[i][0].slice(outputLatency)
resampledAudio.audioChannels.push(concatFloat32Arrays(resampledAudioChunksForChannels[i]))
}
return resampledAudio
}
export async function getSpeexResamplerInstance() {
if (!speexResamplerInstance) {
if (await isWasmSimdSupported()) {
const { default: SpeexResamplerInitializer } = await import('@echogarden/speex-resampler-wasm/simd')
speexResamplerInstance = await SpeexResamplerInitializer()
} else {
const { default: SpeexResamplerInitializer } = await import('@echogarden/speex-resampler-wasm')
speexResamplerInstance = await SpeexResamplerInitializer()
}
}
return speexResamplerInstance
}