@speechly/browser-client
Advanced tools
Comparing version 1.0.4 to 1.0.5
@@ -86,3 +86,2 @@ | ||
private readonly isWebkit; | ||
private readonly audioContext; | ||
private readonly sampleRate; | ||
@@ -98,2 +97,3 @@ private readonly nativeResamplingSupported; | ||
private authToken?; | ||
private audioContext?; | ||
private state; | ||
@@ -397,3 +397,3 @@ private stateChangeCb; | ||
*/ | ||
initialize(isWebkit: boolean, opts: MediaStreamConstraints): Promise<void>; | ||
initialize(audioContext: AudioContext, opts: MediaStreamConstraints): Promise<void>; | ||
/** | ||
@@ -616,2 +616,3 @@ * Closes the microphone, tearing down all the infrastructure. | ||
export declare enum WebsocketResponseType { | ||
Opened = "WEBSOCKET_OPEN", | ||
Started = "started", | ||
@@ -618,0 +619,0 @@ Stopped = "stopped", |
import { Microphone } from './types'; | ||
import { APIClient } from '../websocket'; | ||
export declare class BrowserMicrophone implements Microphone { | ||
private readonly audioContext; | ||
private readonly isWebkit; | ||
private readonly apiClient; | ||
private readonly resampleRatio; | ||
private readonly sampleRate; | ||
private initialized; | ||
private muted; | ||
private audioContext?; | ||
private resampleRatio?; | ||
private audioTrack?; | ||
private mediaStream?; | ||
private audioProcessor?; | ||
constructor(audioContext: AudioContext, sampleRate: number, apiClient: APIClient); | ||
initialize(isWebkit: boolean, opts: MediaStreamConstraints): Promise<void>; | ||
constructor(isWebkit: boolean, sampleRate: number, apiClient: APIClient); | ||
initialize(audioContext: AudioContext, opts: MediaStreamConstraints): Promise<void>; | ||
close(): Promise<void>; | ||
@@ -16,0 +17,0 @@ mute(): void; |
@@ -20,3 +20,3 @@ "use strict"; | ||
class BrowserMicrophone { | ||
constructor(audioContext, sampleRate, apiClient) { | ||
constructor(isWebkit, sampleRate, apiClient) { | ||
this.initialized = false; | ||
@@ -30,8 +30,7 @@ this.muted = false; | ||
}; | ||
this.audioContext = audioContext; | ||
this.isWebkit = isWebkit; | ||
this.apiClient = apiClient; | ||
this.sampleRate = sampleRate; | ||
this.resampleRatio = this.audioContext.sampleRate / this.sampleRate; | ||
} | ||
initialize(isWebkit, opts) { | ||
initialize(audioContext, opts) { | ||
var _a; | ||
@@ -42,2 +41,4 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
this.audioContext = audioContext; | ||
this.resampleRatio = this.audioContext.sampleRate / this.sampleRate; | ||
// Start audio context if we are dealing with a WebKit browser. | ||
@@ -50,3 +51,3 @@ // | ||
// but will emit empty audio buffers. | ||
if (isWebkit) { | ||
if (this.isWebkit) { | ||
yield this.audioContext.resume(); | ||
@@ -69,3 +70,3 @@ } | ||
// `audioContext.resume()` will hang indefinitely, without being resolved or rejected. | ||
if (!isWebkit) { | ||
if (!this.isWebkit) { | ||
yield this.audioContext.resume(); | ||
@@ -104,3 +105,3 @@ } | ||
// Safari, iOS Safari and Internet Explorer | ||
if (isWebkit) { | ||
if (this.isWebkit) { | ||
// Multiply base buffer size of 4 kB by the resample ratio rounded up to the next power of 2. | ||
@@ -107,0 +108,0 @@ // i.e. for 48 kHz to 16 kHz downsampling, this will be 4096 (base) * 4 = 16384. |
@@ -43,3 +43,3 @@ /** | ||
*/ | ||
initialize(isWebkit: boolean, opts: MediaStreamConstraints): Promise<void>; | ||
initialize(audioContext: AudioContext, opts: MediaStreamConstraints): Promise<void>; | ||
/** | ||
@@ -46,0 +46,0 @@ * Closes the microphone, tearing down all the infrastructure. |
{ | ||
"name": "@speechly/browser-client", | ||
"version": "1.0.4", | ||
"version": "1.0.5", | ||
"description": "Browser client for Speechly API", | ||
@@ -5,0 +5,0 @@ "private": false, |
@@ -16,3 +16,2 @@ import { ClientOptions, StateChangeCallback, SegmentChangeCallback, TentativeTranscriptCallback, TranscriptCallback, TentativeEntitiesCallback, EntityCallback, IntentCallback } from './types'; | ||
private readonly isWebkit; | ||
private readonly audioContext; | ||
private readonly sampleRate; | ||
@@ -28,2 +27,3 @@ private readonly nativeResamplingSupported; | ||
private authToken?; | ||
private audioContext?; | ||
private state; | ||
@@ -30,0 +30,0 @@ private stateChangeCb; |
@@ -143,13 +143,15 @@ "use strict"; | ||
} | ||
const language = (_b = options.language) !== null && _b !== void 0 ? _b : defaultLanguage; | ||
if (!locale_code_1.default.validate(language)) { | ||
throw Error(`[SpeechlyClient] Invalid language "${language}"`); | ||
} | ||
this.debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false; | ||
this.loginUrl = (_d = options.loginUrl) !== null && _d !== void 0 ? _d : defaultLoginUrl; | ||
this.appId = options.appId; | ||
const apiUrl = generateWsUrl((_e = options.apiUrl) !== null && _e !== void 0 ? _e : defaultApiUrl, language, (_f = options.sampleRate) !== null && _f !== void 0 ? _f : microphone_1.DefaultSampleRate); | ||
this.apiClient = (_g = options.apiClient) !== null && _g !== void 0 ? _g : new websocket_1.WebWorkerController(apiUrl); | ||
if (window.AudioContext !== undefined) { | ||
const opts = {}; | ||
if (this.nativeResamplingSupported) { | ||
opts.sampleRate = this.sampleRate; | ||
} | ||
this.audioContext = new window.AudioContext(opts); | ||
this.isWebkit = false; | ||
} | ||
else if (window.webkitAudioContext !== undefined) { | ||
// eslint-disable-next-line new-cap | ||
this.audioContext = new window.webkitAudioContext(); | ||
this.isWebkit = true; | ||
@@ -160,12 +162,3 @@ } | ||
} | ||
const language = (_b = options.language) !== null && _b !== void 0 ? _b : defaultLanguage; | ||
if (!locale_code_1.default.validate(language)) { | ||
throw Error(`[SpeechlyClient] Invalid language "${language}"`); | ||
} | ||
this.debug = (_c = options.debug) !== null && _c !== void 0 ? _c : false; | ||
this.loginUrl = (_d = options.loginUrl) !== null && _d !== void 0 ? _d : defaultLoginUrl; | ||
this.appId = options.appId; | ||
const apiUrl = generateWsUrl((_e = options.apiUrl) !== null && _e !== void 0 ? _e : defaultApiUrl, language, (_f = options.sampleRate) !== null && _f !== void 0 ? _f : microphone_1.DefaultSampleRate); | ||
this.apiClient = (_g = options.apiClient) !== null && _g !== void 0 ? _g : new websocket_1.WebWorkerController(apiUrl); | ||
this.microphone = (_h = options.microphone) !== null && _h !== void 0 ? _h : new microphone_1.BrowserMicrophone(this.audioContext, this.sampleRate, this.apiClient); | ||
this.microphone = (_h = options.microphone) !== null && _h !== void 0 ? _h : new microphone_1.BrowserMicrophone(this.isWebkit, this.sampleRate, this.apiClient); | ||
this.storage = (_j = options.storage) !== null && _j !== void 0 ? _j : new storage_1.LocalStorage(); | ||
@@ -216,2 +209,16 @@ this.apiClient.onResponse(this.handleWebsocketResponse); | ||
} | ||
// 2. Initialise the microphone stack. | ||
if (this.isWebkit) { | ||
if (window.webkitAudioContext !== undefined) { | ||
// eslint-disable-next-line new-cap | ||
this.audioContext = new window.webkitAudioContext(); | ||
} | ||
} | ||
else { | ||
const opts = {}; | ||
if (this.nativeResamplingSupported) { | ||
opts.sampleRate = this.sampleRate; | ||
} | ||
this.audioContext = new window.AudioContext(opts); | ||
} | ||
const opts = { | ||
@@ -228,6 +235,10 @@ video: false, | ||
} | ||
// 2. Initialise websocket. | ||
yield this.apiClient.initialize(this.authToken, this.audioContext.sampleRate, this.sampleRate); | ||
// 3. Initialise the microphone stack. | ||
yield this.microphone.initialize(this.isWebkit, opts); | ||
if (this.audioContext != null) { | ||
// 3. Initialise websocket. | ||
yield this.apiClient.initialize(this.authToken, this.audioContext.sampleRate, this.sampleRate); | ||
yield this.microphone.initialize(this.audioContext, opts); | ||
} | ||
else { | ||
throw microphone_1.ErrDeviceNotSupported; | ||
} | ||
} | ||
@@ -234,0 +245,0 @@ catch (err) { |
@@ -32,2 +32,3 @@ /** | ||
export declare enum WebsocketResponseType { | ||
Opened = "WEBSOCKET_OPEN", | ||
Started = "started", | ||
@@ -34,0 +35,0 @@ Stopped = "stopped", |
@@ -9,2 +9,3 @@ "use strict"; | ||
(function (WebsocketResponseType) { | ||
WebsocketResponseType["Opened"] = "WEBSOCKET_OPEN"; | ||
WebsocketResponseType["Started"] = "started"; | ||
@@ -11,0 +12,0 @@ WebsocketResponseType["Stopped"] = "stopped"; |
@@ -6,2 +6,3 @@ import { APIClient, ResponseCallback, CloseCallback } from './types'; | ||
private worker?; | ||
private resolveInitialization?; | ||
private startCbs; | ||
@@ -8,0 +9,0 @@ private stopCbs; |
@@ -26,2 +26,7 @@ "use strict"; | ||
switch (response.type) { | ||
case types_1.WebsocketResponseType.Opened: | ||
if (this.resolveInitialization != null) { | ||
this.resolveInitialization(); | ||
} | ||
break; | ||
case types_1.WebsocketResponseType.Started: | ||
@@ -80,2 +85,5 @@ this.startCbs.forEach(cb => { | ||
} | ||
return new Promise((resolve) => { | ||
this.resolveInitialization = resolve; | ||
}); | ||
}); | ||
@@ -82,0 +90,0 @@ } |
@@ -1,2 +0,2 @@ | ||
declare const _default: "\n// Indices for the Control SAB.\nconst CONTROL = {\n 'WRITE_INDEX': 0,\n 'FRAMES_AVAILABLE': 1,\n};\nlet ws = undefined\nlet state = {\n isContextStarted: false,\n sourceSampleRate: undefined,\n targetSampleRate: undefined,\n resampleRatio: 1,\n buffer: new Float32Array(0),\n filter: undefined,\n controlSAB: undefined,\n dataSAB: undefined\n}\n\nfunction initializeWebsocket(url, protocol) {\n ws = new WebSocket(url, protocol)\n\n return new Promise((resolve, reject) => {\n const errhandler = () => {\n ws.removeEventListener('close', errhandler)\n ws.removeEventListener('error', errhandler)\n ws.removeEventListener('open', openhandler)\n\n reject(Error('Connection failed'))\n }\n\n const openhandler = () => {\n ws.removeEventListener('close', errhandler)\n ws.removeEventListener('error', errhandler)\n ws.removeEventListener('open', openhandler)\n\n resolve(ws)\n }\n\n ws.addEventListener('close', errhandler)\n ws.addEventListener('error', errhandler)\n ws.addEventListener('open', openhandler)\n })\n}\n\nfunction closeWebsocket(code, message) {\n if (ws === undefined) {\n throw Error('Websocket is not open')\n }\n\n ws.removeEventListener('message', onWebsocketMessage)\n ws.removeEventListener('error', onWebsocketError)\n ws.removeEventListener('close', onWebsocketClose)\n\n ws.close(code, message)\n ws = undefined\n}\n\nfunction onWebsocketClose(event) {\n ws = undefined\n}\n\nfunction onWebsocketError(_event) {\n onWebsocketClose(1000, 'Client disconnecting due to an error')\n}\n\nfunction onWebsocketMessage(event) {\n let response\n try {\n response = JSON.parse(event.data)\n } catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e)\n return\n }\n\n self.postMessage(response)\n}\n\nfunction float32ToInt16(buffer) {\n const buf = new Int16Array(buffer.length)\n\n for (let l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff)\n }\n\n return buf\n}\n\nself.onmessage = function(e) {\n switch (e.data.type) {\n case 'INIT':\n if (ws === undefined) {\n initializeWebsocket(e.data.apiUrl, e.data.authToken).then(function() {\n ws.addEventListener('message', onWebsocketMessage)\n ws.addEventListener('error', onWebsocketError)\n ws.addEventListener('close', onWebsocketClose)\n })\n state.sourceSampleRate = e.data.sourceSampleRate\n state.targetSampleRate = e.data.targetSampleRate\n state.resampleRatio = e.data.sourceSampleRate / e.data.targetSampleRate\n if (state.resampleRatio > 1) {\n state.filter = generateFilter(e.data.sourceSampleRate, e.data.targetSampleRate, 127)\n }\n }\n break\n case 'SET_SHARED_ARRAY_BUFFERS':\n state.controlSAB = new Int32Array(e.data.controlSAB);\n state.dataSAB = new Float32Array(e.data.dataSAB);\n setInterval(sendAudioFromSAB, 4)\n break\n case 'CLOSE':\n if (ws !== undefined) {\n closeWebsocket(e.data.code, e.data.message)\n }\n break\n case 'START_CONTEXT':\n if (ws !== undefined && !state.isContextStarted) {\n state.isContextStarted = true\n const StartEventJSON = JSON.stringify({ event: 'start' })\n ws.send(StartEventJSON)\n }\n break\n case 'STOP_CONTEXT':\n if (ws !== undefined && state.isContextStarted) {\n state.isContextStarted = false\n const StopEventJSON = JSON.stringify({ event: 'stop' })\n ws.send(StopEventJSON)\n }\n break\n case 'AUDIO':\n if (ws !== undefined && state.isContextStarted) {\n if (state.resampleRatio > 1) {\n // Downsampling\n ws.send(downsample(e.data.payload))\n } else {\n ws.send(float32ToInt16(e.data.payload))\n }\n }\n break\n default:\n console.log('WORKER', e)\n }\n}\n\nfunction sendAudioFromSAB() {\n if (state.isContextStarted) {\n const data = state.dataSAB.subarray(0, state.controlSAB[CONTROL.FRAMES_AVAILABLE]);\n state.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n state.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (state.resampleRatio > 1) {\n ws.send(downsample(data))\n } else {\n ws.send(float32ToInt16(data))\n }\n \n }\n}\n\nfunction downsample(input) {\n const inputBuffer = new Float32Array(state.buffer.length + input.length)\n inputBuffer.set(state.buffer, 0)\n inputBuffer.set(input, state.buffer.length)\n\n const outputLength = Math.ceil((inputBuffer.length - state.filter.length) / state.resampleRatio)\n const outputBuffer = new Int16Array(outputLength)\n\n for (let i = 0; i < outputLength; i++) {\n const offset = Math.round(state.resampleRatio * i)\n let val = 0.0\n\n for (let j = 0; j < state.filter.length; j++) {\n val += inputBuffer[offset + j] * state.filter[j]\n }\n\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff)\n }\n\n const remainingOffset = Math.round(state.resampleRatio * outputLength)\n if (remainingOffset < inputBuffer.length) {\n state.buffer = inputBuffer.subarray(remainingOffset)\n } else {\n state.buffer = emptyBuffer\n }\n\n return outputBuffer\n}\n\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd')\n }\n\n const cutoff = targetSampleRate / 2\n const filter = new Float32Array(length)\n let sum = 0\n\n for (let i = 0; i < length; i++) {\n const x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2))\n\n sum += x\n filter[i] = x\n }\n\n for (let i = 0; i < length; i++) {\n filter[i] = filter[i] / sum\n }\n\n return filter\n}\n\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0\n }\n\n const piX = Math.PI * x\n return Math.sin(piX) / piX\n}\n"; | ||
declare const _default: "\n// Indices for the Control SAB.\nconst CONTROL = {\n 'WRITE_INDEX': 0,\n 'FRAMES_AVAILABLE': 1,\n};\nlet ws = undefined\nlet state = {\n isContextStarted: false,\n sourceSampleRate: undefined,\n targetSampleRate: undefined,\n resampleRatio: 1,\n buffer: new Float32Array(0),\n filter: undefined,\n controlSAB: undefined,\n dataSAB: undefined\n}\n\nfunction initializeWebsocket(url, protocol) {\n ws = new WebSocket(url, protocol)\n\n return new Promise((resolve, reject) => {\n const errhandler = () => {\n ws.removeEventListener('close', errhandler)\n ws.removeEventListener('error', errhandler)\n ws.removeEventListener('open', openhandler)\n\n reject(Error('Connection failed'))\n }\n\n const openhandler = () => {\n ws.removeEventListener('close', errhandler)\n ws.removeEventListener('error', errhandler)\n ws.removeEventListener('open', openhandler)\n\n resolve(ws)\n }\n\n ws.addEventListener('close', errhandler)\n ws.addEventListener('error', errhandler)\n ws.addEventListener('open', openhandler)\n })\n}\n\nfunction closeWebsocket(code, message) {\n if (ws === undefined) {\n throw Error('Websocket is not open')\n }\n\n ws.removeEventListener('message', onWebsocketMessage)\n ws.removeEventListener('error', onWebsocketError)\n ws.removeEventListener('close', onWebsocketClose)\n\n ws.close(code, message)\n ws = undefined\n}\n\nfunction onWebsocketClose(event) {\n ws = undefined\n}\n\nfunction onWebsocketError(_event) {\n onWebsocketClose(1000, 'Client disconnecting due to an error')\n}\n\nfunction onWebsocketMessage(event) {\n let response\n try {\n response = JSON.parse(event.data)\n } catch (e) {\n console.error('[SpeechlyClient] Error parsing response from the server:', e)\n return\n }\n\n self.postMessage(response)\n}\n\nfunction float32ToInt16(buffer) {\n const buf = new Int16Array(buffer.length)\n\n for (let l = 0; l < buffer.length; l++) {\n buf[l] = buffer[l] * (buffer[l] < 0 ? 0x8000 : 0x7fff)\n }\n\n return buf\n}\n\nself.onmessage = function(e) {\n switch (e.data.type) {\n case 'INIT':\n if (ws === undefined) {\n initializeWebsocket(e.data.apiUrl, e.data.authToken).then(function() {\n self.postMessage({\n type: 'WEBSOCKET_OPEN'\n })\n ws.addEventListener('message', onWebsocketMessage)\n ws.addEventListener('error', onWebsocketError)\n ws.addEventListener('close', onWebsocketClose)\n })\n state.sourceSampleRate = e.data.sourceSampleRate\n state.targetSampleRate = e.data.targetSampleRate\n state.resampleRatio = e.data.sourceSampleRate / e.data.targetSampleRate\n if (state.resampleRatio > 1) {\n state.filter = generateFilter(e.data.sourceSampleRate, e.data.targetSampleRate, 127)\n }\n }\n break\n case 'SET_SHARED_ARRAY_BUFFERS':\n state.controlSAB = new Int32Array(e.data.controlSAB);\n state.dataSAB = new Float32Array(e.data.dataSAB);\n setInterval(sendAudioFromSAB, 4)\n break\n case 'CLOSE':\n if (ws !== undefined) {\n closeWebsocket(e.data.code, e.data.message)\n }\n break\n case 'START_CONTEXT':\n if (ws !== undefined && !state.isContextStarted) {\n state.isContextStarted = true\n const StartEventJSON = JSON.stringify({ event: 'start' })\n ws.send(StartEventJSON)\n } else {\n console.log('can not start context')\n }\n break\n case 'STOP_CONTEXT':\n if (ws !== undefined && state.isContextStarted) {\n state.isContextStarted = false\n const StopEventJSON = JSON.stringify({ event: 'stop' })\n ws.send(StopEventJSON)\n }\n break\n case 'AUDIO':\n if (ws !== undefined && state.isContextStarted) {\n if (state.resampleRatio > 1) {\n // Downsampling\n ws.send(downsample(e.data.payload))\n } else {\n ws.send(float32ToInt16(e.data.payload))\n }\n }\n break\n default:\n console.log('WORKER', e)\n }\n}\n\nfunction sendAudioFromSAB() {\n if (state.isContextStarted) {\n const data = state.dataSAB.subarray(0, state.controlSAB[CONTROL.FRAMES_AVAILABLE]);\n state.controlSAB[CONTROL.FRAMES_AVAILABLE] = 0;\n state.controlSAB[CONTROL.WRITE_INDEX] = 0;\n if (state.resampleRatio > 1) {\n ws.send(downsample(data))\n } else {\n ws.send(float32ToInt16(data))\n }\n \n }\n}\n\nfunction downsample(input) {\n const inputBuffer = new Float32Array(state.buffer.length + input.length)\n inputBuffer.set(state.buffer, 0)\n inputBuffer.set(input, state.buffer.length)\n\n const outputLength = Math.ceil((inputBuffer.length - state.filter.length) / state.resampleRatio)\n const outputBuffer = new Int16Array(outputLength)\n\n for (let i = 0; i < outputLength; i++) {\n const offset = Math.round(state.resampleRatio * i)\n let val = 0.0\n\n for (let j = 0; j < state.filter.length; j++) {\n val += inputBuffer[offset + j] * state.filter[j]\n }\n\n outputBuffer[i] = val * (val < 0 ? 0x8000 : 0x7fff)\n }\n\n const remainingOffset = Math.round(state.resampleRatio * outputLength)\n if (remainingOffset < inputBuffer.length) {\n state.buffer = inputBuffer.subarray(remainingOffset)\n } else {\n state.buffer = emptyBuffer\n }\n\n return outputBuffer\n}\n\nfunction generateFilter(sourceSampleRate, targetSampleRate, length) {\n if (length % 2 === 0) {\n throw Error('Filter length must be odd')\n }\n\n const cutoff = targetSampleRate / 2\n const filter = new Float32Array(length)\n let sum = 0\n\n for (let i = 0; i < length; i++) {\n const x = sinc(((2 * cutoff) / sourceSampleRate) * (i - (length - 1) / 2))\n\n sum += x\n filter[i] = x\n }\n\n for (let i = 0; i < length; i++) {\n filter[i] = filter[i] / sum\n }\n\n return filter\n}\n\nfunction sinc(x) {\n if (x === 0.0) {\n return 1.0\n }\n\n const piX = Math.PI * x\n return Math.sin(piX) / piX\n}\n"; | ||
export default _default; |
@@ -95,2 +95,5 @@ "use strict"; | ||
initializeWebsocket(e.data.apiUrl, e.data.authToken).then(function() { | ||
self.postMessage({ | ||
type: 'WEBSOCKET_OPEN' | ||
}) | ||
ws.addEventListener('message', onWebsocketMessage) | ||
@@ -123,2 +126,4 @@ ws.addEventListener('error', onWebsocketError) | ||
ws.send(StartEventJSON) | ||
} else { | ||
console.log('can not start context') | ||
} | ||
@@ -125,0 +130,0 @@ break |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
164345
2996