163 lines
5.0 KiB
TypeScript
163 lines
5.0 KiB
TypeScript
/// <reference types="@types/audioworklet" />
|
|
|
|
import type { ConverterTypeValue } from '@alexanderolsen/libsamplerate-js/dist/converter-type'
|
|
|
|
import { ConverterType, create } from '@alexanderolsen/libsamplerate-js'
|
|
|
|
import { errorMessageFromValue } from '../utils/error-message'
|
|
|
|
interface ProcessorOptions {
|
|
inputSampleRate: number
|
|
outputSampleRate: number
|
|
channels: number
|
|
converterType: ConverterTypeValue
|
|
bufferSize: number
|
|
}
|
|
|
|
class ResamplingAudioWorkletProcessor extends AudioWorkletProcessor {
|
|
private converter: Awaited<ReturnType<typeof create>> | null = null
|
|
private isInitialized = false
|
|
private options: ProcessorOptions
|
|
private inputBuffer: Float32Array[] = []
|
|
private outputBuffer: Float32Array[] = []
|
|
private bufferSize: number
|
|
|
|
constructor(options: AudioWorkletNodeOptions) {
|
|
super()
|
|
|
|
this.options = {
|
|
inputSampleRate: options.processorOptions?.inputSampleRate || 44100,
|
|
outputSampleRate: options.processorOptions?.outputSampleRate || 16000,
|
|
channels: options.processorOptions?.channels || 1,
|
|
converterType: options.processorOptions?.converterType || ConverterType.SRC_SINC_MEDIUM_QUALITY,
|
|
bufferSize: options.processorOptions?.bufferSize || 4096,
|
|
}
|
|
|
|
this.bufferSize = this.options.bufferSize
|
|
|
|
// Initialize input/output buffers for each channel
|
|
for (let i = 0; i < this.options.channels; i++) {
|
|
this.inputBuffer[i] = new Float32Array(this.bufferSize)
|
|
this.outputBuffer[i] = new Float32Array(0)
|
|
}
|
|
|
|
this.initializeConverter()
|
|
|
|
// Listen for messages from main thread
|
|
this.port.onmessage = (event) => {
|
|
if (event.data.type === 'updateOptions') {
|
|
this.updateOptions(event.data.options)
|
|
}
|
|
}
|
|
}
|
|
|
|
private async initializeConverter() {
|
|
try {
|
|
this.converter = await create(
|
|
this.options.channels,
|
|
this.options.inputSampleRate,
|
|
this.options.outputSampleRate,
|
|
{
|
|
converterType: this.options.converterType,
|
|
},
|
|
)
|
|
this.isInitialized = true
|
|
this.port.postMessage({ type: 'initialized', success: true })
|
|
}
|
|
catch (error) {
|
|
console.error('Failed to initialize sample rate converter:', error)
|
|
|
|
this.port.postMessage({
|
|
type: 'initialized',
|
|
success: false,
|
|
error: errorMessageFromValue(error),
|
|
})
|
|
}
|
|
}
|
|
|
|
private async updateOptions(newOptions: Partial<ProcessorOptions>) {
|
|
const needsReinitialize
|
|
= newOptions.inputSampleRate !== this.options.inputSampleRate
|
|
|| newOptions.outputSampleRate !== this.options.outputSampleRate
|
|
|| newOptions.channels !== this.options.channels
|
|
|| newOptions.converterType !== this.options.converterType
|
|
|
|
Object.assign(this.options, newOptions)
|
|
|
|
if (needsReinitialize && this.converter) {
|
|
this.converter.destroy()
|
|
this.converter = null
|
|
this.isInitialized = false
|
|
await this.initializeConverter()
|
|
}
|
|
}
|
|
|
|
process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
|
|
const input = inputs[0]
|
|
const output = outputs[0]
|
|
|
|
if (!this.isInitialized || !this.converter || !input.length) {
|
|
// Pass through if not ready
|
|
for (let channel = 0; channel < output.length; channel++) {
|
|
if (input[channel]) {
|
|
output[channel].set(input[channel])
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
try {
|
|
// Process each channel
|
|
for (let channel = 0; channel < Math.min(input.length, this.options.channels); channel++) {
|
|
const inputData = input[channel]
|
|
|
|
if (inputData && inputData.length > 0) {
|
|
// Resample the input data
|
|
const resampledData = this.converter.simple(inputData)
|
|
|
|
// Send resampled data to main thread
|
|
this.port.postMessage({
|
|
type: 'audioData',
|
|
channel,
|
|
data: resampledData,
|
|
originalSampleRate: this.options.inputSampleRate,
|
|
outputSampleRate: this.options.outputSampleRate,
|
|
timestamp: currentTime,
|
|
})
|
|
|
|
// Copy to output (you might want to buffer this properly for different sample rates)
|
|
if (output[channel]) {
|
|
const copyLength = Math.min(resampledData.length, output[channel].length)
|
|
for (let i = 0; i < copyLength; i++) {
|
|
output[channel][i] = resampledData[i]
|
|
}
|
|
// Zero-pad remaining
|
|
for (let i = copyLength; i < output[channel].length; i++) {
|
|
output[channel][i] = 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
console.error('Resampling error in worklet:', error)
|
|
|
|
this.port.postMessage({
|
|
type: 'error',
|
|
error: errorMessageFromValue(error),
|
|
})
|
|
|
|
// Pass through original data on error
|
|
for (let channel = 0; channel < output.length; channel++) {
|
|
if (input[channel]) {
|
|
output[channel].set(input[channel])
|
|
}
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
registerProcessor('resampling-processor', ResamplingAudioWorkletProcessor)
|