Skip to content

Commit

Permalink
v2.0 web (#176)
Browse files Browse the repository at this point in the history
  • Loading branch information
ksyeo1010 authored Oct 25, 2023
1 parent 9573c0e commit 1f65c80
Show file tree
Hide file tree
Showing 13 changed files with 1,455 additions and 1,031 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/web-demos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 20.x]
node-version: [16.x, 18.x, 20.x]

steps:
- uses: actions/checkout@v3
Expand All @@ -35,6 +35,10 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

- name: Build Web SDK
run: yarn && yarn copywasm && yarn build
working-directory: binding/web

- name: Pre-build dependencies
run: npm install yarn

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:

strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 20.x]
node-version: [16.x, 18.x, 20.x]

steps:
- uses: actions/checkout@v3
Expand Down
4 changes: 2 additions & 2 deletions binding/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Cobra VAD engine for web browsers (via WebAssembly)",
"author": "Picovoice Inc",
"license": "Apache-2.0",
"version": "1.2.6",
"version": "2.0.0",
"keywords": [
"cobra",
"web",
Expand Down Expand Up @@ -64,6 +64,6 @@
"wasm-feature-detect": "^1.5.0"
},
"engines": {
"node": ">=14"
"node": ">=16"
}
}
160 changes: 127 additions & 33 deletions binding/web/src/cobra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {
} from '@picovoice/web-utils';

import { simd } from 'wasm-feature-detect';
import { CobraOptions } from './types';
import { CobraOptions, PvStatus } from './types';
import * as CobraErrors from "./cobra_errors";
import { pvStatusToException } from './cobra_errors';

/**
* WebAssembly function types
Expand All @@ -39,10 +41,12 @@ type pv_cobra_process_type = (
voiceProbability: number
) => Promise<number>;
type pv_cobra_delete_type = (object: number) => Promise<void>;
type pv_status_to_string_type = (status: number) => Promise<number>;
type pv_cobra_frame_length_type = () => Promise<number>;
type pv_sample_rate_type = () => Promise<number>;
type pv_cobra_version_type = () => Promise<number>;
type pv_set_sdk_type = (sdk: number) => Promise<void>;
type pv_get_error_stack_type = (messageStack: number, messageStackDepth: number) => Promise<number>;
type pv_free_error_stack_type = (messageStack: number) => Promise<void>;

/**
* JavaScript/WebAssembly Binding for the Picovoice Cobra VAD engine.
Expand All @@ -55,62 +59,70 @@ type CobraWasmOutput = {
objectAddress: number;
pvCobraDelete: pv_cobra_delete_type;
pvCobraProcess: pv_cobra_process_type;
pvStatusToString: pv_status_to_string_type;
pvGetErrorStack: pv_get_error_stack_type;
pvFreeErrorStack: pv_free_error_stack_type;
frameLength: number;
sampleRate: number;
version: string;
inputBufferAddress: number;
voiceProbabilityAddress: number;
messageStackAddressAddressAddress: number;
messageStackDepthAddress: number;
};

const PV_STATUS_SUCCESS = 10000;

export class Cobra {
private readonly _pvCobraDelete: pv_cobra_delete_type;
private readonly _pvCobraProcess: pv_cobra_process_type;
private readonly _pvStatusToString: pv_status_to_string_type;
private readonly _pvGetErrorStack: pv_get_error_stack_type;
private readonly _pvFreeErrorStack: pv_free_error_stack_type;

private _wasmMemory: WebAssembly.Memory | undefined;
private readonly _pvFree: pv_free_type;
private readonly _processMutex: Mutex;

private readonly _objectAddress: number;
private readonly _inputBufferAddress: number;
private readonly _alignedAlloc: CallableFunction;
private readonly _voiceProbabilityAddress: number;
private readonly _messageStackAddressAddressAddress: number;
private readonly _messageStackDepthAddress: number;

private static _frameLength: number;
private static _sampleRate: number;
private static _version: string;
private static _wasm: string;
private static _wasmSimd: string;
private static _sdk: string = "web";

private static _cobraMutex = new Mutex();

private readonly _voiceProbabilityCallback: (
voiceProbability: number
) => void;
private readonly _processErrorCallback?: (error: string) => void;
private readonly _processErrorCallback?: (error: CobraErrors.CobraError) => void;

private constructor(
handleWasm: CobraWasmOutput,
voiceProbabilityCallback: (voiceProbability: number) => void,
processErrorCallback?: (error: string) => void
processErrorCallback?: (error: CobraErrors.CobraError) => void
) {
Cobra._frameLength = handleWasm.frameLength;
Cobra._sampleRate = handleWasm.sampleRate;
Cobra._version = handleWasm.version;

this._pvCobraDelete = handleWasm.pvCobraDelete;
this._pvCobraProcess = handleWasm.pvCobraProcess;
this._pvStatusToString = handleWasm.pvStatusToString;
this._pvGetErrorStack = handleWasm.pvGetErrorStack;
this._pvFreeErrorStack = handleWasm.pvFreeErrorStack;

this._wasmMemory = handleWasm.memory;
this._pvFree = handleWasm.pvFree;
this._objectAddress = handleWasm.objectAddress;
this._inputBufferAddress = handleWasm.inputBufferAddress;
this._alignedAlloc = handleWasm.aligned_alloc;
this._voiceProbabilityAddress = handleWasm.voiceProbabilityAddress;
this._messageStackAddressAddressAddress = handleWasm.messageStackDepthAddress;
this._messageStackDepthAddress = handleWasm.messageStackDepthAddress;

this._processMutex = new Mutex();

Expand Down Expand Up @@ -159,6 +171,10 @@ export class Cobra {
}
}

public static setSdk(sdk: string): void {
Cobra._sdk = sdk;
}

/**
* Creates an instance of the Picovoice Cobra VAD engine.
* Behind the scenes, it requires the WebAssembly code to load and initialize before
Expand All @@ -179,7 +195,7 @@ export class Cobra {
const { processErrorCallback } = options;

if (!isAccessKeyValid(accessKey)) {
throw new Error('Invalid AccessKey');
throw new CobraErrors.CobraInvalidArgumentError('Invalid AccessKey');
}

return new Promise<Cobra>((resolve, reject) => {
Expand Down Expand Up @@ -215,11 +231,11 @@ export class Cobra {
*/
public async process(pcm: Int16Array): Promise<void> {
if (!(pcm instanceof Int16Array)) {
const error = new Error(
const error = new CobraErrors.CobraInvalidArgumentError(
"The argument 'pcm' must be provided as an Int16Array"
);
if (this._processErrorCallback) {
this._processErrorCallback(error.toString());
this._processErrorCallback(error);
} else {
// eslint-disable-next-line no-console
console.error(error);
Expand All @@ -229,7 +245,7 @@ export class Cobra {
this._processMutex
.runExclusive(async () => {
if (this._wasmMemory === undefined) {
throw new Error('Attempted to call Cobra process after release.');
throw new CobraErrors.CobraInvalidStateError('Attempted to call Cobra process after release.');
}

const memoryBuffer = new Int16Array(this._wasmMemory.buffer);
Expand All @@ -249,12 +265,22 @@ export class Cobra {
const memoryBufferView = new DataView(this._wasmMemory.buffer);

if (status !== PV_STATUS_SUCCESS) {
throw new Error(
`process failed with status ${arrayBufferToStringAtIndex(
memoryBufferUint8,
await this._pvStatusToString(status)
)}`
const messageStack = await Cobra.getMessageStack(
this._pvGetErrorStack,
this._pvFreeErrorStack,
this._messageStackAddressAddressAddress,
this._messageStackDepthAddress,
memoryBufferView,
memoryBufferUint8
);

const error = pvStatusToException(status, "Processing failed", messageStack);
if (this._processErrorCallback) {
this._processErrorCallback(error);
} else {
// eslint-disable-next-line no-console
console.error(error);
}
}

const voiceProbability = memoryBufferView.getFloat32(
Expand Down Expand Up @@ -315,26 +341,27 @@ export class Cobra {
const pv_cobra_process = exports.pv_cobra_process as pv_cobra_process_type;
const pv_cobra_delete = exports.pv_cobra_delete as pv_cobra_delete_type;
const pv_cobra_init = exports.pv_cobra_init as pv_cobra_init_type;
const pv_status_to_string =
exports.pv_status_to_string as pv_status_to_string_type;
const pv_cobra_frame_length =
exports.pv_cobra_frame_length as pv_cobra_frame_length_type;
const pv_sample_rate = exports.pv_sample_rate as pv_sample_rate_type;
const pv_set_sdk = exports.pv_set_sdk as pv_set_sdk_type;
const pv_get_error_stack = exports.pv_get_error_stack as pv_get_error_stack_type;
const pv_free_error_stack = exports.pv_free_error_stack as pv_free_error_stack_type;

const voiceProbabilityAddress = await aligned_alloc(
Float32Array.BYTES_PER_ELEMENT,
Float32Array.BYTES_PER_ELEMENT
);
if (voiceProbabilityAddress === 0) {
throw new Error('malloc failed: Cannot allocate memory');
throw new CobraErrors.CobraOutOfMemoryError('malloc failed: Cannot allocate memory');
}

const objectAddressAddress = await aligned_alloc(
Int32Array.BYTES_PER_ELEMENT,
Int32Array.BYTES_PER_ELEMENT
);
if (objectAddressAddress === 0) {
throw new Error('malloc failed: Cannot allocate memory');
throw new CobraErrors.CobraOutOfMemoryError('malloc failed: Cannot allocate memory');
}

const accessKeyAddress = await aligned_alloc(
Expand All @@ -343,27 +370,62 @@ export class Cobra {
);

if (accessKeyAddress === 0) {
throw new Error('malloc failed: Cannot allocate memory');
throw new CobraErrors.CobraOutOfMemoryError('malloc failed: Cannot allocate memory');
}

for (let i = 0; i < accessKey.length; i++) {
memoryBufferUint8[accessKeyAddress + i] = accessKey.charCodeAt(i);
}
memoryBufferUint8[accessKeyAddress + accessKey.length] = 0;

const sdkEncoded = new TextEncoder().encode(this._sdk);
const sdkAddress = await aligned_alloc(
Uint8Array.BYTES_PER_ELEMENT,
(sdkEncoded.length + 1) * Uint8Array.BYTES_PER_ELEMENT
);
if (!sdkAddress) {
throw new CobraErrors.CobraOutOfMemoryError('malloc failed: Cannot allocate memory');
}
memoryBufferUint8.set(sdkEncoded, sdkAddress);
memoryBufferUint8[sdkAddress + sdkEncoded.length] = 0;
await pv_set_sdk(sdkAddress);

const messageStackDepthAddress = await aligned_alloc(
Int32Array.BYTES_PER_ELEMENT,
Int32Array.BYTES_PER_ELEMENT
);
if (!messageStackDepthAddress) {
throw new CobraErrors.CobraOutOfMemoryError('malloc failed: Cannot allocate memory');
}

const messageStackAddressAddressAddress = await aligned_alloc(
Int32Array.BYTES_PER_ELEMENT,
Int32Array.BYTES_PER_ELEMENT
);
if (!messageStackAddressAddressAddress) {
throw new CobraErrors.CobraOutOfMemoryError('malloc failed: Cannot allocate memory');
}

const status = await pv_cobra_init(accessKeyAddress, objectAddressAddress);
if (status !== PV_STATUS_SUCCESS) {
const msg = `'pv_cobra_init' failed with status ${arrayBufferToStringAtIndex(
memoryBufferUint8,
await pv_status_to_string(status)
)}`;

throw new Error(
`${msg}\nDetails: ${pvError.getErrorString()}`
await pv_free(accessKeyAddress);
const memoryBufferView = new DataView(memory.buffer);

if (status !== PV_STATUS_SUCCESS) {
const messageStack = await Cobra.getMessageStack(
pv_get_error_stack,
pv_free_error_stack,
messageStackAddressAddressAddress,
messageStackDepthAddress,
memoryBufferView,
memoryBufferUint8
);

throw pvStatusToException(status, "Initialization failed", messageStack, pvError);
}
const memoryBufferView = new DataView(memory.buffer);

const objectAddress = memoryBufferView.getInt32(objectAddressAddress, true);
await pv_free(objectAddressAddress);

const frameLength = await pv_cobra_frame_length();
const sampleRate = await pv_sample_rate();
Expand All @@ -378,7 +440,7 @@ export class Cobra {
frameLength * Int16Array.BYTES_PER_ELEMENT
);
if (inputBufferAddress === 0) {
throw new Error('malloc failed: Cannot allocate memory');
throw new CobraErrors.CobraOutOfMemoryError('malloc failed: Cannot allocate memory');
}

return {
Expand All @@ -388,12 +450,44 @@ export class Cobra {
objectAddress: objectAddress,
pvCobraDelete: pv_cobra_delete,
pvCobraProcess: pv_cobra_process,
pvStatusToString: pv_status_to_string,
pvGetErrorStack: pv_get_error_stack,
pvFreeErrorStack: pv_free_error_stack,
frameLength: frameLength,
sampleRate: sampleRate,
version: version,
inputBufferAddress: inputBufferAddress,
voiceProbabilityAddress: voiceProbabilityAddress,
messageStackAddressAddressAddress: messageStackAddressAddressAddress,
messageStackDepthAddress: messageStackDepthAddress,
};
}

private static async getMessageStack(
pv_get_error_stack: pv_get_error_stack_type,
pv_free_error_stack: pv_free_error_stack_type,
messageStackAddressAddressAddress: number,
messageStackDepthAddress: number,
memoryBufferView: DataView,
memoryBufferUint8: Uint8Array,
): Promise<string[]> {
const status = await pv_get_error_stack(messageStackAddressAddressAddress, messageStackDepthAddress);
if (status != PvStatus.SUCCESS) {
throw pvStatusToException(status, "Unable to get Cobra error state");
}

const messageStackAddressAddress = memoryBufferView.getInt32(messageStackAddressAddressAddress, true);

const messageStackDepth = memoryBufferView.getInt32(messageStackDepthAddress, true);
const messageStack: string[] = [];
for (let i = 0; i < messageStackDepth; i++) {
const messageStackAddress = memoryBufferView.getInt32(
messageStackAddressAddress + (i * Int32Array.BYTES_PER_ELEMENT), true);
const message = arrayBufferToStringAtIndex(memoryBufferUint8, messageStackAddress);
messageStack.push(message);
}

pv_free_error_stack(messageStackAddressAddress);

return messageStack;
}
}
Loading

0 comments on commit 1f65c80

Please sign in to comment.