Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.0 web #176

Merged
merged 3 commits into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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