UNPKG

@hahnpro/ms-speech-sdk

Version:
1 lines 15.5 kB
{"version":3,"sources":["src/common.browser/MicAudioSource.ts"],"names":[],"mappings":"AAGA,OAAO,EAEH,wBAAwB,EAE3B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAEH,gBAAgB,EAYhB,WAAW,EACX,YAAY,EACZ,gBAAgB,EAGnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAEH,qBAAqB,EACxB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAK3C,eAAO,MAAM,iCAAiC,gCAAgC,CAAC;AAE/E,qBAAa,cAAe,YAAW,YAAY;IAuB3C,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,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,CAAiB;IAE/C,OAAO,CAAC,eAAe,CAAc;IAErC,OAAO,CAAC,WAAW,CAAe;IAElC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,OAAO,CAAC,aAAa,CAAU;gBAGV,YAAY,EAAE,SAAS,EACvB,QAAQ,CAAC,EAAE,MAAM,EAClC,aAAa,CAAC,EAAE,MAAM,EACtB,WAAW,CAAC,EAAE,WAAW;IAU7B,IAAW,MAAM,IAAI,OAAO,CAAC,qBAAqB,CAAC,CAElD;IAEM,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAqGvB,EAAE,IAAI,MAAM;IAIZ,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAmBtD,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAQ3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAuBrC,IAAW,MAAM,IAAI,WAAW,CAAC,gBAAgB,CAAC,CAEjD;IAED,IAAW,UAAU,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAYzD;IAEM,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQrD,OAAO,CAAC,kBAAkB;YAyCZ,MAAM;IAcpB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,kBAAkB;YAQZ,mBAAmB;CAiCpC","file":"MicAudioSource.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\n// Licensed under the MIT license.\n\nimport {\n connectivity,\n ISpeechConfigAudioDevice,\n type\n} from \"../common.speech/Exports.js\";\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 Stream,\n} from \"../common/Exports.js\";\nimport { IStreamChunk } from \"../common/Stream.js\";\nimport {\n AudioStreamFormat,\n AudioStreamFormatImpl,\n} from \"../sdk/Audio/AudioStreamFormat.js\";\nimport { IRecorder } from \"./IRecorder.js\";\n\ntype NavigatorUserMediaSuccessCallback = (stream: MediaStream) => void;\ntype NavigatorUserMediaErrorCallback = (error: DOMException) => void;\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<void>;\n\n private privMediaStream: MediaStream;\n\n private privContext: AudioContext;\n\n private privMicrophoneLabel: string;\n\n private privOutputChunkSize: number;\n\n private privIsClosing: boolean;\n\n public constructor(\n private readonly privRecorder: IRecorder,\n private readonly deviceId?: string,\n audioSourceId?: string,\n mediaStream?: MediaStream\n ) {\n\n this.privOutputChunkSize = MicAudioSource.AUDIOFORMAT.avgBytesPerSec / 10;\n this.privId = audioSourceId ? audioSourceId : createNoDashGuid();\n this.privEvents = new EventSource<AudioSourceEvent>();\n this.privMediaStream = mediaStream || null;\n this.privIsClosing = false;\n }\n\n public get format(): Promise<AudioStreamFormatImpl> {\n return Promise.resolve(MicAudioSource.AUDIOFORMAT);\n }\n\n public turnOn(): Promise<void> {\n if (this.privInitializeDeferral) {\n return this.privInitializeDeferral.promise;\n }\n\n this.privInitializeDeferral = new Deferred<void>();\n\n try {\n this.createAudioContext();\n } catch (error) {\n if (error instanceof Error) {\n const typedError: Error = error;\n this.privInitializeDeferral.reject(typedError.name + \": \" + typedError.message);\n } else {\n this.privInitializeDeferral.reject(error as string);\n }\n return this.privInitializeDeferral.promise;\n }\n\n const nav = window.navigator as unknown as {\n webkitGetUserMedia?: (\n constraints: MediaStreamConstraints,\n successCallback: (stream: MediaStream) => void,\n errorCallback: (error: any) => void\n ) => void;\n mozGetUserMedia?: (\n constraints: MediaStreamConstraints,\n successCallback: (stream: MediaStream) => void,\n errorCallback: (error: any) => void\n ) => void;\n msGetUserMedia?: (\n constraints: MediaStreamConstraints,\n successCallback: (stream: MediaStream) => void,\n errorCallback: (error: any) => void\n ) => void;\n getUserMedia?: (\n constraints: MediaStreamConstraints,\n successCallback: (stream: MediaStream) => void,\n errorCallback: (error: any) => void\n ) => void;\n mediaDevices?: MediaDevices;\n };\n\n let getUserMedia = (\n // eslint-disable-next-line\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 = (): void => {\n this.onEvent(new AudioSourceInitializingEvent(this.privId)); // no stream id\n if (this.privMediaStream && this.privMediaStream.active) {\n this.onEvent(new AudioSourceReadyEvent(this.privId));\n this.privInitializeDeferral.resolve();\n } else {\n getUserMedia(\n { audio: this.deviceId ? { deviceId: this.deviceId } : true, video: false },\n (mediaStream: MediaStream): void => {\n this.privMediaStream = mediaStream;\n this.onEvent(new AudioSourceReadyEvent(this.privId));\n this.privInitializeDeferral.resolve();\n }, (error: any): void => {\n const errorMsg = `Error occurred during microphone initialization: ${error as string}`;\n this.privInitializeDeferral.reject(errorMsg);\n this.onEvent(new AudioSourceErrorEvent(this.privId, errorMsg));\n });\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()\n .then(next)\n .catch((reason: any): void => {\n this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason as string}`);\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).then<IAudioStreamNode>(\n (stream: Stream<ArrayBuffer>): IAudioStreamNode => {\n this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId));\n return {\n detach: async (): Promise<void> => {\n stream.readEnded();\n delete this.privStreams[audioNodeId];\n this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));\n return this.turnOff();\n },\n id: (): string => audioNodeId,\n read: (): Promise<IStreamChunk<ArrayBuffer>> => stream.read(),\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 async turnOff(): Promise<void> {\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 if (this.privInitializeDeferral) {\n // Correctly handle when browser forces mic off before turnOn() completes\n // eslint-disable-next-line @typescript-eslint/await-thenable\n await this.privInitializeDeferral;\n this.privInitializeDeferral = null;\n }\n\n await this.destroyAudioContext();\n\n return;\n }\n\n public get events(): EventSource<AudioSourceEvent> {\n return this.privEvents;\n }\n\n public get deviceInfo(): Promise<ISpeechConfigAudioDevice> {\n return this.getMicrophoneLabel().then((label: string): ISpeechConfigAudioDevice => (\n {\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 Promise.resolve(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 Promise.resolve(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 Promise.resolve(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[]): void => {\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<string> => deferred.resolve(this.privMicrophoneLabel));\n\n return deferred.promise;\n }\n\n private async listen(audioNodeId: string): Promise<Stream<ArrayBuffer>> {\n await this.turnOn();\n const stream = new ChunkedArrayBufferStream(this.privOutputChunkSize, audioNodeId);\n this.privStreams[audioNodeId] = stream;\n try {\n this.privRecorder.record(this.privContext, this.privMediaStream, stream);\n } catch (error) {\n this.onEvent(new AudioStreamNodeErrorEvent(this.privId, audioNodeId, error as string));\n throw error;\n }\n const result: Stream<ArrayBuffer> = stream;\n return result;\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 this.privContext = AudioStreamFormatImpl.getAudioContext(MicAudioSource.AUDIOFORMAT.samplesPerSec);\n }\n\n private async destroyAudioContext(): Promise<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 if (!this.privIsClosing) {\n // The audio context close may take enough time that the close is called twice\n this.privIsClosing = true;\n await this.privContext.close();\n this.privContext = null;\n this.privIsClosing = false;\n }\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 await this.privContext.suspend();\n }\n }\n}\n"]}