@euirim/microsoft-cognitiveservices-speech-sdk
Version:
Microsoft Cognitive Services Speech SDK for JavaScript
1 lines • 14.7 kB
Source Map (JSON)
{"version":3,"sources":["src/common.browser/MicAudioSource.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,iBAAiB,EAEpB,MAAM,uCAAuC,CAAC;AAC/C,OAAO,EAEH,wBAAwB,EAE3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAEH,gBAAgB,EAYhB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAEhB,OAAO,EAIV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AASxC,eAAO,MAAM,iCAAiC,gCAAgC,CAAC;AAE/E,qBAAa,cAAe,YAAW,YAAY;IAqB3C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAG7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAtB9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAA6F;IAEhI,OAAO,CAAC,WAAW,CAA8C;IAEjE,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,UAAU,CAAgC;IAElD,OAAO,CAAC,sBAAsB,CAAoB;IAElD,OAAO,CAAC,eAAe,CAAc;IAErC,OAAO,CAAC,WAAW,CAAe;IAElC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,mBAAmB,CAAS;gBAGf,YAAY,EAAE,SAAS,EACxC,eAAe,EAAE,MAAM,EACvB,aAAa,CAAC,EAAE,MAAM,EACL,QAAQ,CAAC,EAAE,MAAM;aAO3B,MAAM,EAAI,iBAAiB;IAI/B,MAAM,yBAmEZ;IAEM,EAAE,eAER;IAEM,MAAM,qDAqBZ;IAEM,MAAM,gCAMZ;IAEM,OAAO,yBAgBb;aAEU,MAAM,EAAI,WAAW,CAAC,gBAAgB,CAAC;aAIvC,UAAU,EAAI,OAAO,CAAC,wBAAwB,CAAC;IAcnD,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQrD,OAAO,CAAC,kBAAkB;IAyC1B,OAAO,CAAC,MAAM,CAeb;IAED,OAAO,CAAC,OAAO,CAGd;IAED,OAAO,CAAC,kBAAkB,CAezB;IAED,OAAO,CAAC,mBAAmB,CA2B1B;CACJ","file":"MicAudioSource.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\n// Licensed under the MIT license.\n\nimport {\n AudioStreamFormat,\n AudioStreamFormatImpl,\n} from \"../../src/sdk/Audio/AudioStreamFormat\";\nimport {\n connectivity,\n ISpeechConfigAudioDevice,\n type\n} from \"../common.speech/Exports\";\nimport {\n AudioSourceErrorEvent,\n AudioSourceEvent,\n AudioSourceInitializingEvent,\n AudioSourceOffEvent,\n AudioSourceReadyEvent,\n AudioStreamNodeAttachedEvent,\n AudioStreamNodeAttachingEvent,\n AudioStreamNodeDetachedEvent,\n AudioStreamNodeErrorEvent,\n ChunkedArrayBufferStream,\n createNoDashGuid,\n Deferred,\n Events,\n EventSource,\n IAudioSource,\n IAudioStreamNode,\n IStringDictionary,\n Promise,\n PromiseHelper,\n Stream,\n StreamReader,\n} from \"../common/Exports\";\nimport { IRecorder } from \"./IRecorder\";\n\n// Extending the default definition with browser specific definitions for backward compatibility\ninterface INavigatorUserMedia extends NavigatorUserMedia {\n webkitGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\n mozGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\n msGetUserMedia?: (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback) => void;\n}\n\nexport const AudioWorkletSourceURLPropertyName = \"MICROPHONE-WorkletSourceUrl\";\n\nexport class MicAudioSource implements IAudioSource {\n\n private static readonly AUDIOFORMAT: AudioStreamFormatImpl = AudioStreamFormat.getDefaultInputFormat() as AudioStreamFormatImpl;\n\n private privStreams: IStringDictionary<Stream<ArrayBuffer>> = {};\n\n private privId: string;\n\n private privEvents: EventSource<AudioSourceEvent>;\n\n private privInitializeDeferral: Deferred<boolean>;\n\n private privMediaStream: MediaStream;\n\n private privContext: AudioContext;\n\n private privMicrophoneLabel: string;\n\n private privOutputChunkSize: number;\n\n public constructor(\n private readonly privRecorder: IRecorder,\n outputChunkSize: number,\n audioSourceId?: string,\n private readonly deviceId?: string) {\n\n this.privOutputChunkSize = outputChunkSize;\n this.privId = audioSourceId ? audioSourceId : createNoDashGuid();\n this.privEvents = new EventSource<AudioSourceEvent>();\n }\n\n public get format(): AudioStreamFormat {\n return MicAudioSource.AUDIOFORMAT;\n }\n\n public turnOn = (): Promise<boolean> => {\n if (this.privInitializeDeferral) {\n return this.privInitializeDeferral.promise();\n }\n\n this.privInitializeDeferral = new Deferred<boolean>();\n\n this.createAudioContext();\n\n const nav = window.navigator as INavigatorUserMedia;\n\n let getUserMedia = (\n nav.getUserMedia ||\n nav.webkitGetUserMedia ||\n nav.mozGetUserMedia ||\n nav.msGetUserMedia\n );\n\n if (!!nav.mediaDevices) {\n getUserMedia = (constraints: MediaStreamConstraints, successCallback: NavigatorUserMediaSuccessCallback, errorCallback: NavigatorUserMediaErrorCallback): void => {\n nav.mediaDevices\n .getUserMedia(constraints)\n .then(successCallback)\n .catch(errorCallback);\n };\n }\n\n if (!getUserMedia) {\n const errorMsg = \"Browser does not support getUserMedia.\";\n this.privInitializeDeferral.reject(errorMsg);\n this.onEvent(new AudioSourceErrorEvent(errorMsg, \"\")); // mic initialized error - no streamid at this point\n } else {\n const next = () => {\n this.onEvent(new AudioSourceInitializingEvent(this.privId)); // no stream id\n getUserMedia(\n { audio: this.deviceId ? { deviceId: this.deviceId } : true, video: false },\n (mediaStream: MediaStream) => {\n this.privMediaStream = mediaStream;\n this.onEvent(new AudioSourceReadyEvent(this.privId));\n this.privInitializeDeferral.resolve(true);\n }, (error: MediaStreamError) => {\n const errorMsg = `Error occurred during microphone initialization: ${error}`;\n const tmp = this.privInitializeDeferral;\n // HACK: this should be handled through onError callbacks of all promises up the stack.\n // Unfortunately, the current implementation does not provide an easy way to reject promises\n // without a lot of code replication.\n // TODO: fix promise implementation, allow for a graceful reject chaining.\n this.privInitializeDeferral = null;\n tmp.reject(errorMsg); // this will bubble up through the whole chain of promises,\n // with each new level adding extra \"Unhandled callback error\" prefix to the error message.\n // The following line is not guaranteed to be executed.\n this.onEvent(new AudioSourceErrorEvent(this.privId, errorMsg));\n });\n };\n\n if (this.privContext.state === \"suspended\") {\n // NOTE: On iOS, the Web Audio API requires sounds to be triggered from an explicit user action.\n // https://github.com/WebAudio/web-audio-api/issues/790\n this.privContext.resume().then(next, (reason: any) => {\n this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason}`);\n });\n } else {\n next();\n }\n }\n\n return this.privInitializeDeferral.promise();\n }\n\n public id = (): string => {\n return this.privId;\n }\n\n public attach = (audioNodeId: string): Promise<IAudioStreamNode> => {\n this.onEvent(new AudioStreamNodeAttachingEvent(this.privId, audioNodeId));\n\n return this.listen(audioNodeId).onSuccessContinueWith<IAudioStreamNode>(\n (streamReader: StreamReader<ArrayBuffer>) => {\n this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId));\n return {\n detach: () => {\n streamReader.close();\n delete this.privStreams[audioNodeId];\n this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\n this.turnOff();\n },\n id: () => {\n return audioNodeId;\n },\n read: () => {\n return streamReader.read();\n },\n };\n });\n }\n\n public detach = (audioNodeId: string): void => {\n if (audioNodeId && this.privStreams[audioNodeId]) {\n this.privStreams[audioNodeId].close();\n delete this.privStreams[audioNodeId];\n this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\n }\n }\n\n public turnOff = (): Promise<boolean> => {\n for (const streamId in this.privStreams) {\n if (streamId) {\n const stream = this.privStreams[streamId];\n if (stream) {\n stream.close();\n }\n }\n }\n\n this.onEvent(new AudioSourceOffEvent(this.privId)); // no stream now\n this.privInitializeDeferral = null;\n\n this.destroyAudioContext();\n\n return PromiseHelper.fromResult(true);\n }\n\n public get events(): EventSource<AudioSourceEvent> {\n return this.privEvents;\n }\n\n public get deviceInfo(): Promise<ISpeechConfigAudioDevice> {\n return this.getMicrophoneLabel().onSuccessContinueWith((label: string) => {\n return {\n bitspersample: MicAudioSource.AUDIOFORMAT.bitsPerSample,\n channelcount: MicAudioSource.AUDIOFORMAT.channels,\n connectivity: connectivity.Unknown,\n manufacturer: \"Speech SDK\",\n model: label,\n samplerate: MicAudioSource.AUDIOFORMAT.samplesPerSec,\n type: type.Microphones,\n };\n });\n }\n\n public setProperty(name: string, value: string): void {\n if (name === AudioWorkletSourceURLPropertyName) {\n this.privRecorder.setWorkletUrl(value);\n } else {\n throw new Error(\"Property '\" + name + \"' is not supported on Microphone.\");\n }\n }\n\n private getMicrophoneLabel(): Promise<string> {\n const defaultMicrophoneName: string = \"microphone\";\n\n // If we did this already, return the value.\n if (this.privMicrophoneLabel !== undefined) {\n return PromiseHelper.fromResult(this.privMicrophoneLabel);\n }\n\n // If the stream isn't currently running, we can't query devices because security.\n if (this.privMediaStream === undefined || !this.privMediaStream.active) {\n return PromiseHelper.fromResult(defaultMicrophoneName);\n }\n\n // Setup a default\n this.privMicrophoneLabel = defaultMicrophoneName;\n\n // Get the id of the device running the audio track.\n const microphoneDeviceId: string = this.privMediaStream.getTracks()[0].getSettings().deviceId;\n\n // If the browser doesn't support getting the device ID, set a default and return.\n if (undefined === microphoneDeviceId) {\n return PromiseHelper.fromResult(this.privMicrophoneLabel);\n }\n\n const deferred: Deferred<string> = new Deferred<string>();\n\n // Enumerate the media devices.\n navigator.mediaDevices.enumerateDevices().then((devices: MediaDeviceInfo[]) => {\n for (const device of devices) {\n if (device.deviceId === microphoneDeviceId) {\n // Found the device\n this.privMicrophoneLabel = device.label;\n break;\n }\n }\n deferred.resolve(this.privMicrophoneLabel);\n }, () => deferred.resolve(this.privMicrophoneLabel));\n\n return deferred.promise();\n }\n\n private listen = (audioNodeId: string): Promise<StreamReader<ArrayBuffer>> => {\n return this.turnOn()\n .onSuccessContinueWith<StreamReader<ArrayBuffer>>((_: boolean) => {\n const stream = new ChunkedArrayBufferStream(this.privOutputChunkSize, audioNodeId);\n this.privStreams[audioNodeId] = stream;\n\n try {\n this.privRecorder.record(this.privContext, this.privMediaStream, stream);\n } catch (error) {\n this.onEvent(new AudioStreamNodeErrorEvent(this.privId, audioNodeId, error));\n throw error;\n }\n\n return stream.getReader();\n });\n }\n\n private onEvent = (event: AudioSourceEvent): void => {\n this.privEvents.onEvent(event);\n Events.instance.onEvent(event);\n }\n\n private createAudioContext = (): void => {\n if (!!this.privContext) {\n return;\n }\n\n // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext\n const AudioContext = ((window as any).AudioContext)\n || ((window as any).webkitAudioContext)\n || false;\n\n if (!AudioContext) {\n throw new Error(\"Browser does not support Web Audio API (AudioContext is not available).\");\n }\n\n this.privContext = new AudioContext();\n }\n\n private destroyAudioContext = (): void => {\n if (!this.privContext) {\n return;\n }\n\n this.privRecorder.releaseMediaResources(this.privContext);\n\n // This pattern brought to you by a bug in the TypeScript compiler where it\n // confuses the (\"close\" in this.privContext) with this.privContext always being null as the alternate.\n // https://github.com/Microsoft/TypeScript/issues/11498\n let hasClose: boolean = false;\n if (\"close\" in this.privContext) {\n hasClose = true;\n }\n\n if (hasClose) {\n this.privContext.close();\n this.privContext = null;\n } else if (null !== this.privContext && this.privContext.state === \"running\") {\n // Suspend actually takes a callback, but analogous to the\n // resume method, it'll be only fired if suspend is called\n // in a direct response to a user action. The later is not always\n // the case, as TurnOff is also called, when we receive an\n // end-of-speech message from the service. So, doing a best effort\n // fire-and-forget here.\n this.privContext.suspend();\n }\n }\n}\n"]}