diff --git a/binding/web/src/index.ts b/binding/web/src/index.ts index 1c1f4a56..dcd7a8f0 100644 --- a/binding/web/src/index.ts +++ b/binding/web/src/index.ts @@ -1,5 +1,5 @@ -import { Orca } from './orca'; -import { OrcaWorker } from './orca_worker'; +import { Orca, OrcaStream } from './orca'; +import { OrcaWorker, OrcaStreamWorker } from './orca_worker'; import { OrcaModel, @@ -30,6 +30,7 @@ OrcaWorker.setWasmSimd(orcaWasmSimd); export { Orca, + OrcaStream, OrcaErrors, OrcaModel, OrcaSynthesizeParams, @@ -37,6 +38,7 @@ export { OrcaAlignment, OrcaSynthesizeResult, OrcaWorker, + OrcaStreamWorker, OrcaWorkerInitRequest, OrcaWorkerSynthesizeRequest, OrcaWorkerReleaseRequest, diff --git a/binding/web/src/orca.ts b/binding/web/src/orca.ts index e60dd67f..07cf52ab 100644 --- a/binding/web/src/orca.ts +++ b/binding/web/src/orca.ts @@ -94,6 +94,260 @@ type OrcaWasmOutput = { pvOrcaStreamClose: pv_orca_stream_close_type; }; +/** + * OrcaStream object that converts a stream of text to a stream of audio. + */ +class Stream { + private _wasmMemory: WebAssembly.Memory; + private readonly _alignedAlloc: CallableFunction; + private readonly _pvFree: pv_free_type; + private readonly _pvGetErrorStack: pv_get_error_stack_type; + private readonly _pvFreeErrorStack: pv_free_error_stack_type; + private readonly _messageStackAddressAddressAddress: number; + private readonly _messageStackDepthAddress: number; + + private readonly _functionMutex: Mutex; + private readonly _streamPcmAddressAddress: number; + private readonly _pvOrcaPcmDelete: pv_orca_pcm_delete_type; + private readonly _pvOrcaStreamSynthesize: pv_orca_stream_synthesize_type; + private readonly _pvOrcaStreamFlush: pv_orca_stream_flush_type; + private readonly _pvOrcaStreamClose: pv_orca_stream_close_type; + private readonly _streamAddress: number; + private readonly _getMessageStack: any; + + constructor( + wasmMemory: WebAssembly.Memory, + alignedAlloc: CallableFunction, + pvFree: pv_free_type, + pvGetErrorStack: pv_get_error_stack_type, + pvFreeErrorStack: pv_free_error_stack_type, + messageStackAddressAddressAddress: number, + messageStackDepthAddress: number, + functionMutex: Mutex, + streamPcmAddressAddress: number, + pvOrcaPcmDelete: pv_orca_pcm_delete_type, + pvOrcaStreamSynthesize: pv_orca_stream_synthesize_type, + pvOrcaStreamFlush: pv_orca_stream_flush_type, + pvOrcaStreamClose: pv_orca_stream_close_type, + streamAddress: number, + getMessageStack: any, + ) { + this._wasmMemory = wasmMemory; + this._alignedAlloc = alignedAlloc; + this._pvFree = pvFree; + this._pvGetErrorStack = pvGetErrorStack; + this._pvFreeErrorStack = pvFreeErrorStack; + this._messageStackAddressAddressAddress = messageStackAddressAddressAddress; + this._messageStackDepthAddress = messageStackDepthAddress; + this._functionMutex = functionMutex; + this._streamPcmAddressAddress = streamPcmAddressAddress; + this._pvOrcaPcmDelete = pvOrcaPcmDelete; + this._pvOrcaStreamSynthesize = pvOrcaStreamSynthesize; + this._pvOrcaStreamFlush = pvOrcaStreamFlush; + this._pvOrcaStreamClose = pvOrcaStreamClose; + this._streamAddress = streamAddress; + this._getMessageStack = getMessageStack; + } + + /** + * Adds a chunk of text to the Stream object and generates audio if enough text has been added. + * This function is expected to be called multiple times with consecutive chunks of text from a text stream. + * The incoming text is buffered as it arrives until there is enough context to convert a chunk of the + * buffered text into audio. The caller needs to use `OrcaStream.flush()` to generate the audio chunk + * for the remaining text that has not yet been synthesized. + * + * @param text A chunk of text from a text input stream, comprised of valid characters. + * Valid characters can be retrieved by calling `validCharacters`. + * Custom pronunciations can be embedded in the text via the syntax `{word|pronunciation}`. + * They need to be added in a single call to this function. + * The pronunciation is expressed in ARPAbet format, e.g.: `I {liv|L IH V} in {Sevilla|S EH V IY Y AH}`. + * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no + * audio chunk has been produced. + */ + public async synthesize(text: string): Promise { + if (typeof text !== 'string') { + throw new OrcaErrors.OrcaInvalidArgumentError( + 'The argument \'text\' must be provided as a string', + ); + } + + return new Promise((resolve, reject) => { + this._functionMutex + .runExclusive(async () => { + if (this._wasmMemory === undefined) { + throw new OrcaErrors.OrcaInvalidStateError( + 'Attempted to call Orca stream synthesize after release.', + ); + } + + const memoryBufferText = new Uint8Array(this._wasmMemory.buffer); + const encodedText = new TextEncoder().encode(text); + const textAddress = await this._alignedAlloc( + Uint8Array.BYTES_PER_ELEMENT, + (encodedText.length + 1) * Uint8Array.BYTES_PER_ELEMENT, + ); + if (textAddress === 0) { + throw new OrcaErrors.OrcaOutOfMemoryError( + 'malloc failed: Cannot allocate memory', + ); + } + memoryBufferText.set(encodedText, textAddress); + memoryBufferText[textAddress + encodedText.length] = 0; + + const numSamplesAddress = await this._alignedAlloc( + Int32Array.BYTES_PER_ELEMENT, + Int32Array.BYTES_PER_ELEMENT, + ); + if (numSamplesAddress === 0) { + throw new OrcaErrors.OrcaOutOfMemoryError('malloc failed: Cannot allocate memory'); + } + + const streamSynthesizeStatus = await this._pvOrcaStreamSynthesize( + this._streamAddress, + textAddress, + numSamplesAddress, + this._streamPcmAddressAddress, + ); + await this._pvFree(textAddress); + + const memoryBufferView = new DataView(this._wasmMemory.buffer); + const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer); + + if (streamSynthesizeStatus !== PvStatus.SUCCESS) { + const messageStack = await this._getMessageStack( + this._pvGetErrorStack, + this._pvFreeErrorStack, + this._messageStackAddressAddressAddress, + this._messageStackDepthAddress, + memoryBufferView, + memoryBufferUint8, + ); + + throw pvStatusToException(streamSynthesizeStatus, 'Stream synthesize failed', messageStack); + } + + const pcmAddress = memoryBufferView.getInt32( + this._streamPcmAddressAddress, + true, + ); + + const numSamples = memoryBufferView.getInt32( + numSamplesAddress, + true, + ); + await this._pvFree(numSamplesAddress); + + const outputMemoryBuffer = new Int16Array(this._wasmMemory.buffer); + const pcm = outputMemoryBuffer.slice( + pcmAddress / Int16Array.BYTES_PER_ELEMENT, + (pcmAddress / Int16Array.BYTES_PER_ELEMENT) + numSamples, + ); + await this._pvOrcaPcmDelete(pcmAddress); + + return pcm.length > 0 ? pcm : null; + }) + .then((result: OrcaStreamSynthesizeResult) => { + resolve(result); + }) + .catch(async (error: any) => { + reject(error); + }); + }); + } + + /** + * Generates audio for all the buffered text that was added to the OrcaStream object + * via `OrcaStream.synthesize()`. + * + * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no + * audio chunk has been produced. + */ + public async flush(): Promise { + return new Promise((resolve, reject) => { + this._functionMutex + .runExclusive(async () => { + if (this._wasmMemory === undefined) { + throw new OrcaErrors.OrcaInvalidStateError('Attempted to call OrcaStream flush after release.'); + } + + const numSamplesAddress = await this._alignedAlloc( + Int32Array.BYTES_PER_ELEMENT, + Int32Array.BYTES_PER_ELEMENT, + ); + if (numSamplesAddress === 0) { + throw new OrcaErrors.OrcaOutOfMemoryError('malloc failed: Cannot allocate memory'); + } + + const pcmAddressAddress = await this._alignedAlloc( + Int32Array.BYTES_PER_ELEMENT, + Int32Array.BYTES_PER_ELEMENT, + ); + if (pcmAddressAddress === 0) { + throw new OrcaErrors.OrcaOutOfMemoryError('malloc failed: Cannot allocate memory'); + } + + const streamFlushStatus = await this._pvOrcaStreamFlush( + this._streamAddress, + numSamplesAddress, + pcmAddressAddress, + ); + + const memoryBufferView = new DataView(this._wasmMemory.buffer); + const memoryBufferUint8 = new Uint8Array(this._wasmMemory.buffer); + + if (streamFlushStatus !== PvStatus.SUCCESS) { + const messageStack = await this._getMessageStack( + this._pvGetErrorStack, + this._pvFreeErrorStack, + this._messageStackAddressAddressAddress, + this._messageStackDepthAddress, + memoryBufferView, + memoryBufferUint8, + ); + + throw pvStatusToException(streamFlushStatus, 'Flush failed', messageStack); + } + + const pcmAddress = memoryBufferView.getInt32( + pcmAddressAddress, + true, + ); + await this._pvFree(pcmAddressAddress); + + const numSamples = memoryBufferView.getInt32( + numSamplesAddress, + true, + ); + await this._pvFree(numSamplesAddress); + + const outputMemoryBuffer = new Int16Array(this._wasmMemory.buffer); + const pcm = outputMemoryBuffer.slice( + pcmAddress / Int16Array.BYTES_PER_ELEMENT, + (pcmAddress / Int16Array.BYTES_PER_ELEMENT) + numSamples, + ); + await this._pvOrcaPcmDelete(pcmAddress); + + return pcm.length > 0 ? pcm : null; + }) + .then((result: OrcaStreamSynthesizeResult) => { + resolve(result); + }) + .catch(async (error: any) => { + reject(error); + }); + }); + } + + /** + * Releases the resources acquired by the OrcaStream object. + */ + public async close(): Promise { + await this._pvOrcaStreamClose(this._streamAddress); + } +} + +export type OrcaStream = Stream + /** * JavaScript/WebAssembly Binding for Orca */ @@ -128,7 +382,6 @@ export class Orca { private readonly _pvOrcaStreamClose: pv_orca_stream_close_type; private readonly _functionMutex: Mutex; - private readonly _OrcaStream: any; private static _wasm: string; private static _wasmSimd: string; private static _sdk: string = 'web'; @@ -166,224 +419,6 @@ export class Orca { this._pvOrcaStreamClose = handleWasm.pvOrcaStreamClose; this._functionMutex = new Mutex(); - - /** - * OrcaStream object that converts a stream of text to a stream of audio. - */ - this._OrcaStream = class OrcaStream { - private readonly _Orca: Orca; - private readonly _streamAddress: number; - - private constructor( - orca: Orca, - streamAddress: number, - ) { - this._Orca = orca; - this._streamAddress = streamAddress; - } - - /** - * Adds a chunk of text to the Stream object and generates audio if enough text has been added. - * This function is expected to be called multiple times with consecutive chunks of text from a text stream. - * The incoming text is buffered as it arrives until there is enough context to convert a chunk of the - * buffered text into audio. The caller needs to use `OrcaStream.flush()` to generate the audio chunk - * for the remaining text that has not yet been synthesized. - * - * @param text A chunk of text from a text input stream, comprised of valid characters. - * Valid characters can be retrieved by calling `validCharacters`. - * Custom pronunciations can be embedded in the text via the syntax `{word|pronunciation}`. - * They need to be added in a single call to this function. - * The pronunciation is expressed in ARPAbet format, e.g.: `I {liv|L IH V} in {Sevilla|S EH V IY Y AH}`. - * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no - * audio chunk has been produced. - */ - public async synthesize(text: string): Promise { - if (typeof text !== 'string') { - throw new OrcaErrors.OrcaInvalidArgumentError( - 'The argument \'text\' must be provided as a string', - ); - } - - if (text.trim().length > Orca._maxCharacterLimit) { - throw new OrcaErrors.OrcaInvalidArgumentError( - `'text' length must be smaller than ${Orca._maxCharacterLimit}`, - ); - } - - return new Promise((resolve, reject) => { - this._Orca._functionMutex - .runExclusive(async () => { - if (this._Orca._wasmMemory === undefined) { - throw new OrcaErrors.OrcaInvalidStateError( - 'Attempted to call Orca stream synthesize after release.', - ); - } - - const memoryBufferText = new Uint8Array(this._Orca._wasmMemory.buffer); - const encodedText = new TextEncoder().encode(text); - const textAddress = await this._Orca._alignedAlloc( - Uint8Array.BYTES_PER_ELEMENT, - (encodedText.length + 1) * Uint8Array.BYTES_PER_ELEMENT, - ); - if (textAddress === 0) { - throw new OrcaErrors.OrcaOutOfMemoryError( - 'malloc failed: Cannot allocate memory', - ); - } - memoryBufferText.set(encodedText, textAddress); - memoryBufferText[textAddress + encodedText.length] = 0; - - const numSamplesAddress = await this._Orca._alignedAlloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT, - ); - if (numSamplesAddress === 0) { - throw new OrcaErrors.OrcaOutOfMemoryError('malloc failed: Cannot allocate memory'); - } - - const streamSynthesizeStatus = await this._Orca._pvOrcaStreamSynthesize( - this._streamAddress, - textAddress, - numSamplesAddress, - this._Orca._streamPcmAddressAddress, - ); - await this._Orca._pvFree(textAddress); - - const memoryBufferView = new DataView(this._Orca._wasmMemory.buffer); - const memoryBufferUint8 = new Uint8Array(this._Orca._wasmMemory.buffer); - - if (streamSynthesizeStatus !== PvStatus.SUCCESS) { - const messageStack = await Orca.getMessageStack( - this._Orca._pvGetErrorStack, - this._Orca._pvFreeErrorStack, - this._Orca._messageStackAddressAddressAddress, - this._Orca._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8, - ); - - throw pvStatusToException(streamSynthesizeStatus, 'Stream synthesize failed', messageStack); - } - - const pcmAddress = memoryBufferView.getInt32( - this._Orca._streamPcmAddressAddress, - true, - ); - - const numSamples = memoryBufferView.getInt32( - numSamplesAddress, - true, - ); - await this._Orca._pvFree(numSamplesAddress); - - const outputMemoryBuffer = new Int16Array(this._Orca._wasmMemory.buffer); - const pcm = outputMemoryBuffer.slice( - pcmAddress / Int16Array.BYTES_PER_ELEMENT, - (pcmAddress / Int16Array.BYTES_PER_ELEMENT) + numSamples, - ); - await this._Orca._pvOrcaPcmDelete(pcmAddress); - - return pcm.length > 0 ? pcm : null; - }) - .then((result: OrcaStreamSynthesizeResult) => { - resolve(result); - }) - .catch(async (error: any) => { - reject(error); - }); - }); - } - - /** - * Generates audio for all the buffered text that was added to the OrcaStream object - * via `OrcaStream.synthesize()`. - * - * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no - * audio chunk has been produced. - */ - public async flush(): Promise { - return new Promise((resolve, reject) => { - this._Orca._functionMutex - .runExclusive(async () => { - if (this._Orca._wasmMemory === undefined) { - throw new OrcaErrors.OrcaInvalidStateError('Attempted to call OrcaStream flush after release.'); - } - - const numSamplesAddress = await this._Orca._alignedAlloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT, - ); - if (numSamplesAddress === 0) { - throw new OrcaErrors.OrcaOutOfMemoryError('malloc failed: Cannot allocate memory'); - } - - const pcmAddressAddress = await this._Orca._alignedAlloc( - Int32Array.BYTES_PER_ELEMENT, - Int32Array.BYTES_PER_ELEMENT, - ); - if (pcmAddressAddress === 0) { - throw new OrcaErrors.OrcaOutOfMemoryError('malloc failed: Cannot allocate memory'); - } - - const streamFlushStatus = await this._Orca._pvOrcaStreamFlush( - this._streamAddress, - numSamplesAddress, - pcmAddressAddress, - ); - - const memoryBufferView = new DataView(this._Orca._wasmMemory.buffer); - const memoryBufferUint8 = new Uint8Array(this._Orca._wasmMemory.buffer); - - if (streamFlushStatus !== PvStatus.SUCCESS) { - const messageStack = await Orca.getMessageStack( - this._Orca._pvGetErrorStack, - this._Orca._pvFreeErrorStack, - this._Orca._messageStackAddressAddressAddress, - this._Orca._messageStackDepthAddress, - memoryBufferView, - memoryBufferUint8, - ); - - throw pvStatusToException(streamFlushStatus, 'Flush failed', messageStack); - } - - const pcmAddress = memoryBufferView.getInt32( - pcmAddressAddress, - true, - ); - await this._Orca._pvFree(pcmAddressAddress); - - const numSamples = memoryBufferView.getInt32( - numSamplesAddress, - true, - ); - await this._Orca._pvFree(numSamplesAddress); - - const outputMemoryBuffer = new Int16Array(this._Orca._wasmMemory.buffer); - const pcm = outputMemoryBuffer.slice( - pcmAddress / Int16Array.BYTES_PER_ELEMENT, - (pcmAddress / Int16Array.BYTES_PER_ELEMENT) + numSamples, - ); - await this._Orca._pvOrcaPcmDelete(pcmAddress); - - return pcm.length > 0 ? pcm : null; - }) - .then((result: OrcaStreamSynthesizeResult) => { - resolve(result); - }) - .catch(async (error: any) => { - reject(error); - }); - }); - } - - /** - * Releases the resources acquired by the OrcaStream object. - */ - public async close(): Promise { - await this._Orca._pvOrcaStreamClose(this._streamAddress); - } - }; } /** @@ -754,8 +789,8 @@ export class Orca { speechRate: 1.0, randomState: null, }, - ): Promise { - return new Promise((resolve, reject) => { + ): Promise { + return new Promise((resolve, reject) => { this._functionMutex .runExclusive(async () => { if (this._wasmMemory === undefined) { @@ -852,7 +887,23 @@ export class Orca { const streamAddress = memoryBufferView.getInt32(streamAddressAddress, true); await this._pvFree(streamAddressAddress); - return new this._OrcaStream(this, streamAddress); + return new Stream( + this._wasmMemory, + this._alignedAlloc, + this._pvFree, + this._pvGetErrorStack, + this._pvFreeErrorStack, + this._messageStackAddressAddressAddress, + this._messageStackDepthAddress, + this._functionMutex, + this._streamPcmAddressAddress, + this._pvOrcaPcmDelete, + this._pvOrcaStreamSynthesize, + this._pvOrcaStreamFlush, + this._pvOrcaStreamClose, + streamAddress, + Orca.getMessageStack, + ); }) .then(result => { resolve(result); diff --git a/binding/web/src/orca_worker.ts b/binding/web/src/orca_worker.ts index fb18ebd2..8f951a2d 100644 --- a/binding/web/src/orca_worker.ts +++ b/binding/web/src/orca_worker.ts @@ -29,6 +29,155 @@ import { loadModel } from '@picovoice/web-utils'; import { pvStatusToException } from './orca_errors'; +class StreamWorker { + readonly _worker: Worker; + + constructor(orcaWorker: Worker) { + this._worker = orcaWorker; + } + + /** + * Adds a chunk of text to the Stream object in a worker and generates audio if enough text has been added. + * This function is expected to be called multiple times with consecutive chunks of text from a text stream. + * The incoming text is buffered as it arrives until there is enough context to convert a chunk of the + * buffered text into audio. The caller needs to use `OrcaStream.flush()` to generate the audio chunk + * for the remaining text that has not yet been synthesized. + * + * @param text A chunk of text from a text input stream, comprised of valid characters. + * Valid characters can be retrieved by calling `validCharacters`. + * Custom pronunciations can be embedded in the text via the syntax `{word|pronunciation}`. + * They need to be added in a single call to this function. + * The pronunciation is expressed in ARPAbet format, e.g.: `I {liv|L IH V} in {Sevilla|S EH V IY Y AH}`. + * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no + * audio chunk has been produced. + */ + public synthesize(text: string): Promise { + const returnPromise: Promise = new Promise( + (resolve, reject) => { + this._worker.onmessage = ( + event: MessageEvent, + ): void => { + switch (event.data.command) { + case 'ok': + resolve(event.data.result); + break; + case 'failed': + case 'error': + // eslint-disable-next-line no-case-declarations + reject(pvStatusToException( + event.data.status, + event.data.shortMessage, + event.data.messageStack, + )); + break; + default: + reject(pvStatusToException( + PvStatus.RUNTIME_ERROR, + // @ts-ignore + `Unrecognized command: ${event.data.command}`, + )); + } + }; + }, + ); + + this._worker.postMessage( + { + command: 'streamSynthesize', + text: text, + }, + ); + + return returnPromise; + } + + /** + * Generates audio for all the buffered text that was added to the OrcaStream object + * via `OrcaStream.synthesize()`. + * + * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no + * audio chunk has been produced. + */ + public flush(): Promise { + const returnPromise: Promise = new Promise( + (resolve, reject) => { + this._worker.onmessage = ( + event: MessageEvent, + ): void => { + switch (event.data.command) { + case 'ok': + resolve(event.data.result); + break; + case 'failed': + case 'error': + reject(pvStatusToException( + event.data.status, + event.data.shortMessage, + event.data.messageStack, + )); + break; + default: + reject(pvStatusToException( + PvStatus.RUNTIME_ERROR, + // @ts-ignore + `Unrecognized command: ${event.data.command}`, + )); + } + }; + }, + ); + + this._worker.postMessage({ + command: 'streamFlush', + }); + + return returnPromise; + } + + /** + * Releases the resources acquired by the OrcaStream object. + */ + public close(): Promise { + const returnPromise: Promise = new Promise((resolve, reject) => { + this._worker.onmessage = ( + event: MessageEvent, + ): void => { + switch (event.data.command) { + case 'ok': + resolve(); + break; + case 'failed': + case 'error': + reject( + pvStatusToException( + event.data.status, + event.data.shortMessage, + event.data.messageStack, + ), + ); + break; + default: + reject( + pvStatusToException( + PvStatus.RUNTIME_ERROR, + // @ts-ignore + `Unrecognized command: ${event.data.command}`, + ), + ); + } + }; + }); + + this._worker.postMessage({ + command: 'streamClose', + }); + + return returnPromise; + } +} + +export type OrcaStreamWorker = StreamWorker + export class OrcaWorker { private readonly _worker: Worker; private readonly _version: string; @@ -40,8 +189,6 @@ export class OrcaWorker { private static _wasmSimd: string; private static _sdk: string = 'web'; - private readonly _OrcaStream: any; - private constructor( worker: Worker, version: string, @@ -54,153 +201,6 @@ export class OrcaWorker { this._sampleRate = sampleRate; this._maxCharacterLimit = maxCharacterLimit; this._validCharacters = validCharacters; - - this._OrcaStream = class OrcaStream { - private readonly _worker: Worker; - - private constructor(orcaWorker: Worker) { - this._worker = orcaWorker; - } - - /** - * Adds a chunk of text to the Stream object in a worker and generates audio if enough text has been added. - * This function is expected to be called multiple times with consecutive chunks of text from a text stream. - * The incoming text is buffered as it arrives until there is enough context to convert a chunk of the - * buffered text into audio. The caller needs to use `OrcaStream.flush()` to generate the audio chunk - * for the remaining text that has not yet been synthesized. - * - * @param text A chunk of text from a text input stream, comprised of valid characters. - * Valid characters can be retrieved by calling `validCharacters`. - * Custom pronunciations can be embedded in the text via the syntax `{word|pronunciation}`. - * They need to be added in a single call to this function. - * The pronunciation is expressed in ARPAbet format, e.g.: `I {liv|L IH V} in {Sevilla|S EH V IY Y AH}`. - * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no - * audio chunk has been produced. - */ - public synthesize(text: string): Promise { - const returnPromise: Promise = new Promise( - (resolve, reject) => { - this._worker.onmessage = ( - event: MessageEvent, - ): void => { - switch (event.data.command) { - case 'ok': - resolve(event.data.result); - break; - case 'failed': - case 'error': - // eslint-disable-next-line no-case-declarations - reject(pvStatusToException( - event.data.status, - event.data.shortMessage, - event.data.messageStack, - )); - break; - default: - reject(pvStatusToException( - PvStatus.RUNTIME_ERROR, - // @ts-ignore - `Unrecognized command: ${event.data.command}`, - )); - } - }; - }, - ); - - this._worker.postMessage( - { - command: 'streamSynthesize', - text: text, - }, - ); - - return returnPromise; - } - - /** - * Generates audio for all the buffered text that was added to the OrcaStream object - * via `OrcaStream.synthesize()`. - * - * @return The generated audio as a sequence of 16-bit linearly-encoded integers, `null` if no - * audio chunk has been produced. - */ - public flush(): Promise { - const returnPromise: Promise = new Promise( - (resolve, reject) => { - this._worker.onmessage = ( - event: MessageEvent, - ): void => { - switch (event.data.command) { - case 'ok': - resolve(event.data.result); - break; - case 'failed': - case 'error': - reject(pvStatusToException( - event.data.status, - event.data.shortMessage, - event.data.messageStack, - )); - break; - default: - reject(pvStatusToException( - PvStatus.RUNTIME_ERROR, - // @ts-ignore - `Unrecognized command: ${event.data.command}`, - )); - } - }; - }, - ); - - this._worker.postMessage({ - command: 'streamFlush', - }); - - return returnPromise; - } - - /** - * Releases the resources acquired by the OrcaStream object. - */ - public close(): Promise { - const returnPromise: Promise = new Promise((resolve, reject) => { - this._worker.onmessage = ( - event: MessageEvent, - ): void => { - switch (event.data.command) { - case 'ok': - resolve(); - break; - case 'failed': - case 'error': - reject( - pvStatusToException( - event.data.status, - event.data.shortMessage, - event.data.messageStack, - ), - ); - break; - default: - reject( - pvStatusToException( - PvStatus.RUNTIME_ERROR, - // @ts-ignore - `Unrecognized command: ${event.data.command}`, - ), - ); - } - }; - }); - - this._worker.postMessage({ - command: 'streamClose', - }); - - return returnPromise; - } - }; } /** @@ -449,15 +449,15 @@ export class OrcaWorker { * @param synthesizeParams.speechRate Configure the rate of speech of the synthesized speech. * @param synthesizeParams.randomState Configure the random seed for the synthesized speech. */ - public streamOpen(synthesizeParams: OrcaSynthesizeParams = {}): Promise { - const returnPromise: Promise = new Promise( + public streamOpen(synthesizeParams: OrcaSynthesizeParams = {}): Promise { + const returnPromise: Promise = new Promise( (resolve, reject) => { this._worker.onmessage = ( event: MessageEvent, ): void => { switch (event.data.command) { case 'ok': - resolve(new this._OrcaStream(this._worker)); + resolve(new StreamWorker(this._worker)); break; case 'failed': case 'error': diff --git a/binding/web/test/orca.test.ts b/binding/web/test/orca.test.ts index 602f4339..d531733e 100644 --- a/binding/web/test/orca.test.ts +++ b/binding/web/test/orca.test.ts @@ -144,23 +144,23 @@ describe('Orca Binding', function() { ); try { - const OrcaStream = await orca.streamOpen({ randomState: testData.random_state }); + const orcaStream = await orca.streamOpen({ randomState: testData.random_state }); - const streamPcm = []; + const streamPcm: number[] = []; for (const c of testData.test_sentences.text.split('')) { - const pcm = await OrcaStream.synthesize(c); + const pcm = await orcaStream.synthesize(c); if (pcm !== null) { streamPcm.push(...pcm); } } - const endPcm = await OrcaStream.flush(); + const endPcm = await orcaStream.flush(); if (endPcm !== null) { streamPcm.push(...endPcm); } compareArrays(new Int16Array(streamPcm), rawPcm, 500); - await OrcaStream.close(); + await orcaStream.close(); } catch (e) { expect(e).to.be.undefined; } @@ -397,7 +397,7 @@ describe('Orca Binding', function() { const errors: OrcaError[] = []; try { await orca.synthesize('test'); - } catch (e) { + } catch (e: any) { errors.push(e); }