diff --git a/demo/package.json b/demo/package.json index 4a962ed..478c665 100644 --- a/demo/package.json +++ b/demo/package.json @@ -17,7 +17,7 @@ "author": "Picovoice Inc", "license": "Apache-2.0", "dependencies": { - "@picovoice/web-voice-processor": "^4.0.6", + "@picovoice/web-voice-processor": "^4.0.7", "http-server": "^14.0.0", "wavefile": "^11.0.0" } diff --git a/demo/yarn.lock b/demo/yarn.lock index f76e414..34bea20 100644 --- a/demo/yarn.lock +++ b/demo/yarn.lock @@ -2,10 +2,19 @@ # yarn lockfile v1 -"@picovoice/web-voice-processor@^4.0.6": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@picovoice/web-voice-processor/-/web-voice-processor-4.0.6.tgz#4769283b82f64d3625794f7290d47c6d477a3f41" - integrity sha512-Ykfy6hrWFpOklfeN7rSJb5CGim8wDu7J+l8imRYyQxWHWVV1Wu5S8FW69zkJmwiDG2Wx+M2+h0SCMS+hNM5qow== +"@picovoice/web-utils@=1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@picovoice/web-utils/-/web-utils-1.3.1.tgz#d417e98604a650b54a8e03669015ecf98c2383ec" + integrity sha512-jcDqdULtTm+yJrnHDjg64hARup+Z4wNkYuXHNx6EM8+qZkweBq9UA6XJrHAlUkPnlkso4JWjaIKhz3x8vZcd3g== + dependencies: + commander "^9.2.0" + +"@picovoice/web-voice-processor@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@picovoice/web-voice-processor/-/web-voice-processor-4.0.7.tgz#5df76c3b283a4c90ee53865cdd5dab0099acb823" + integrity sha512-LtZDNrtezi7B/1CWeda4YE7M+SAKN7ALvGf+j357TmmJRqnMnt21urAiZ90zSMUbFCoYqKIeq8rMGU7RkKaeEw== + dependencies: + "@picovoice/web-utils" "=1.3.1" ansi-styles@^4.1.0: version "4.3.0" @@ -56,6 +65,11 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +commander@^9.2.0: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + corser@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87" diff --git a/package.json b/package.json index 60d3450..4a398d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@picovoice/web-voice-processor", - "version": "4.0.6", + "version": "4.0.7", "description": "Real-time audio processing for voice, in web browsers", "entry": "src/index.ts", "module": "dist/esm/index.js", diff --git a/src/resampler.ts b/src/resampler.ts index 8ef3c0d..0054b17 100644 --- a/src/resampler.ts +++ b/src/resampler.ts @@ -65,6 +65,8 @@ class Resampler { private static _wasm: string; public static _version: string; + private _isWasmMemoryDetached: boolean = false; + private constructor(handleWasm: ResamplerWasmOutput) { Resampler._version = handleWasm.version; @@ -244,6 +246,10 @@ class Resampler { inputFrame: Int16Array | Float32Array, outputBuffer: Int16Array, ): number { + if (this._isWasmMemoryDetached) { + return 0; + } + if (inputFrame.length > this._inputBufferLength) { throw new Error(`InputFrame length '${inputFrame.length}' must be smaller than ${this._inputBufferLength}.`); } @@ -263,24 +269,29 @@ class Resampler { throw new Error(`Invalid inputFrame type: ${typeof inputFrame}. Expected Float32Array or Int16Array.`); } - this._memoryBuffer.set( - inputBuffer, - this._inputBufferAddress / Int16Array.BYTES_PER_ELEMENT, - ); + try { + this._memoryBuffer.set( + inputBuffer, + this._inputBufferAddress / Int16Array.BYTES_PER_ELEMENT, + ); - const processedSamples = this._pvResamplerProcess( - this._objectAddress, - this._inputBufferAddress, - inputFrame.length, - this._outputBufferAddress, - ); - for (let i = 0; i < processedSamples; i++) { - outputBuffer[i] = this._memoryBufferView.getInt16( - this._outputBufferAddress + i * Int16Array.BYTES_PER_ELEMENT, - true, + const processedSamples = this._pvResamplerProcess( + this._objectAddress, + this._inputBufferAddress, + inputFrame.length, + this._outputBufferAddress, ); + for (let i = 0; i < processedSamples; i++) { + outputBuffer[i] = this._memoryBufferView.getInt16( + this._outputBufferAddress + i * Int16Array.BYTES_PER_ELEMENT, + true, + ); + } + return processedSamples; + } catch (error: any) { + this._errorHandler(); + throw error; } - return processedSamples; } public reset(): void { @@ -305,17 +316,35 @@ class Resampler { } public getNumRequiredInputSamples(numSample: number): number { - return this._pvResamplerConvertNumSamplesToInputSampleRate( - this._objectAddress, - numSample, - ); + try { + return this._pvResamplerConvertNumSamplesToInputSampleRate( + this._objectAddress, + numSample, + ); + } catch (error: any) { + this._errorHandler(); + throw error; + } } public getNumRequiredOutputSamples(numSample: number): number { - return this._pvResamplerConvertNumSamplesToOutputSampleRate( - this._objectAddress, - numSample, - ); + try { + return this._pvResamplerConvertNumSamplesToOutputSampleRate( + this._objectAddress, + numSample, + ); + } catch (error: any) { + this._errorHandler(); + throw error; + } + } + + private _errorHandler(): void { + if (this._memoryBuffer.length === 0) { + this._isWasmMemoryDetached = true; + this.release(); + throw new Error("Invalid memory state: browser might have cleaned resources automatically. Re-initialize Resampler."); + } } } diff --git a/src/resampler_worker_handler.ts b/src/resampler_worker_handler.ts index 520a563..8bb0a8a 100644 --- a/src/resampler_worker_handler.ts +++ b/src/resampler_worker_handler.ts @@ -17,6 +17,8 @@ import Resampler from './resampler'; let accumulator: BufferAccumulator | null = null; let resampler: Resampler | null = null; +let isResamplerDetached = false; +let initParams: any = {}; class BufferAccumulator { private readonly _frameLength: number; @@ -80,6 +82,13 @@ onmessage = async function (event: MessageEvent): Promis event.data.frameLength, ); + initParams = { + inputSampleRate: event.data.inputSampleRate, + outputSampleRate: event.data.outputSampleRate, + filterOrder: event.data.filterOrder, + frameLength: event.data.frameLength, + }; + accumulator = new BufferAccumulator( resampler.frameLength, resampler.inputBufferLength); @@ -96,6 +105,15 @@ onmessage = async function (event: MessageEvent): Promis } break; case 'process': + if (isResamplerDetached) { + isResamplerDetached = false; + resampler = await Resampler.create( + initParams.inputSampleRate, + initParams.outputSampleRate, + initParams.filterOrder, + initParams.frameLength, + ); + } if (resampler === null) { self.postMessage({ command: 'error', @@ -107,6 +125,11 @@ onmessage = async function (event: MessageEvent): Promis const {inputFrame} = event.data; accumulator?.process(inputFrame); } catch (e: any) { + if (e.message.includes('Invalid memory state')) { + resampler.release(); + resampler = null; + isResamplerDetached = true; + } self.postMessage({ command: 'error', message: e.message,