@euirim/microsoft-cognitiveservices-speech-sdk
Version:
Microsoft Cognitive Services Speech SDK for JavaScript
239 lines (237 loc) • 11.4 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
import { AudioStreamFormat, } from "../../src/sdk/Audio/AudioStreamFormat";
import { connectivity, type } from "../common.speech/Exports";
import { AudioSourceErrorEvent, AudioSourceInitializingEvent, AudioSourceOffEvent, AudioSourceReadyEvent, AudioStreamNodeAttachedEvent, AudioStreamNodeAttachingEvent, AudioStreamNodeDetachedEvent, AudioStreamNodeErrorEvent, ChunkedArrayBufferStream, createNoDashGuid, Deferred, Events, EventSource, PromiseHelper, } from "../common/Exports";
export const AudioWorkletSourceURLPropertyName = "MICROPHONE-WorkletSourceUrl";
export class MicAudioSource {
constructor(privRecorder, outputChunkSize, audioSourceId, deviceId) {
this.privRecorder = privRecorder;
this.deviceId = deviceId;
this.privStreams = {};
this.turnOn = () => {
if (this.privInitializeDeferral) {
return this.privInitializeDeferral.promise();
}
this.privInitializeDeferral = new Deferred();
this.createAudioContext();
const nav = window.navigator;
let getUserMedia = (nav.getUserMedia ||
nav.webkitGetUserMedia ||
nav.mozGetUserMedia ||
nav.msGetUserMedia);
if (!!nav.mediaDevices) {
getUserMedia = (constraints, successCallback, errorCallback) => {
nav.mediaDevices
.getUserMedia(constraints)
.then(successCallback)
.catch(errorCallback);
};
}
if (!getUserMedia) {
const errorMsg = "Browser does not support getUserMedia.";
this.privInitializeDeferral.reject(errorMsg);
this.onEvent(new AudioSourceErrorEvent(errorMsg, "")); // mic initialized error - no streamid at this point
}
else {
const next = () => {
this.onEvent(new AudioSourceInitializingEvent(this.privId)); // no stream id
getUserMedia({ audio: this.deviceId ? { deviceId: this.deviceId } : true, video: false }, (mediaStream) => {
this.privMediaStream = mediaStream;
this.onEvent(new AudioSourceReadyEvent(this.privId));
this.privInitializeDeferral.resolve(true);
}, (error) => {
const errorMsg = `Error occurred during microphone initialization: ${error}`;
const tmp = this.privInitializeDeferral;
// HACK: this should be handled through onError callbacks of all promises up the stack.
// Unfortunately, the current implementation does not provide an easy way to reject promises
// without a lot of code replication.
// TODO: fix promise implementation, allow for a graceful reject chaining.
this.privInitializeDeferral = null;
tmp.reject(errorMsg); // this will bubble up through the whole chain of promises,
// with each new level adding extra "Unhandled callback error" prefix to the error message.
// The following line is not guaranteed to be executed.
this.onEvent(new AudioSourceErrorEvent(this.privId, errorMsg));
});
};
if (this.privContext.state === "suspended") {
// NOTE: On iOS, the Web Audio API requires sounds to be triggered from an explicit user action.
// https://github.com/WebAudio/web-audio-api/issues/790
this.privContext.resume().then(next, (reason) => {
this.privInitializeDeferral.reject(`Failed to initialize audio context: ${reason}`);
});
}
else {
next();
}
}
return this.privInitializeDeferral.promise();
};
this.id = () => {
return this.privId;
};
this.attach = (audioNodeId) => {
this.onEvent(new AudioStreamNodeAttachingEvent(this.privId, audioNodeId));
return this.listen(audioNodeId).onSuccessContinueWith((streamReader) => {
this.onEvent(new AudioStreamNodeAttachedEvent(this.privId, audioNodeId));
return {
detach: () => {
streamReader.close();
delete this.privStreams[audioNodeId];
this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));
this.turnOff();
},
id: () => {
return audioNodeId;
},
read: () => {
return streamReader.read();
},
};
});
};
this.detach = (audioNodeId) => {
if (audioNodeId && this.privStreams[audioNodeId]) {
this.privStreams[audioNodeId].close();
delete this.privStreams[audioNodeId];
this.onEvent(new AudioStreamNodeDetachedEvent(this.privId, audioNodeId));
}
};
this.turnOff = () => {
for (const streamId in this.privStreams) {
if (streamId) {
const stream = this.privStreams[streamId];
if (stream) {
stream.close();
}
}
}
this.onEvent(new AudioSourceOffEvent(this.privId)); // no stream now
this.privInitializeDeferral = null;
this.destroyAudioContext();
return PromiseHelper.fromResult(true);
};
this.listen = (audioNodeId) => {
return this.turnOn()
.onSuccessContinueWith((_) => {
const stream = new ChunkedArrayBufferStream(this.privOutputChunkSize, audioNodeId);
this.privStreams[audioNodeId] = stream;
try {
this.privRecorder.record(this.privContext, this.privMediaStream, stream);
}
catch (error) {
this.onEvent(new AudioStreamNodeErrorEvent(this.privId, audioNodeId, error));
throw error;
}
return stream.getReader();
});
};
this.onEvent = (event) => {
this.privEvents.onEvent(event);
Events.instance.onEvent(event);
};
this.createAudioContext = () => {
if (!!this.privContext) {
return;
}
// https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
const AudioContext = (window.AudioContext)
|| (window.webkitAudioContext)
|| false;
if (!AudioContext) {
throw new Error("Browser does not support Web Audio API (AudioContext is not available).");
}
this.privContext = new AudioContext();
};
this.destroyAudioContext = () => {
if (!this.privContext) {
return;
}
this.privRecorder.releaseMediaResources(this.privContext);
// This pattern brought to you by a bug in the TypeScript compiler where it
// confuses the ("close" in this.privContext) with this.privContext always being null as the alternate.
// https://github.com/Microsoft/TypeScript/issues/11498
let hasClose = false;
if ("close" in this.privContext) {
hasClose = true;
}
if (hasClose) {
this.privContext.close();
this.privContext = null;
}
else if (null !== this.privContext && this.privContext.state === "running") {
// Suspend actually takes a callback, but analogous to the
// resume method, it'll be only fired if suspend is called
// in a direct response to a user action. The later is not always
// the case, as TurnOff is also called, when we receive an
// end-of-speech message from the service. So, doing a best effort
// fire-and-forget here.
this.privContext.suspend();
}
};
this.privOutputChunkSize = outputChunkSize;
this.privId = audioSourceId ? audioSourceId : createNoDashGuid();
this.privEvents = new EventSource();
}
get format() {
return MicAudioSource.AUDIOFORMAT;
}
get events() {
return this.privEvents;
}
get deviceInfo() {
return this.getMicrophoneLabel().onSuccessContinueWith((label) => {
return {
bitspersample: MicAudioSource.AUDIOFORMAT.bitsPerSample,
channelcount: MicAudioSource.AUDIOFORMAT.channels,
connectivity: connectivity.Unknown,
manufacturer: "Speech SDK",
model: label,
samplerate: MicAudioSource.AUDIOFORMAT.samplesPerSec,
type: type.Microphones,
};
});
}
setProperty(name, value) {
if (name === AudioWorkletSourceURLPropertyName) {
this.privRecorder.setWorkletUrl(value);
}
else {
throw new Error("Property '" + name + "' is not supported on Microphone.");
}
}
getMicrophoneLabel() {
const defaultMicrophoneName = "microphone";
// If we did this already, return the value.
if (this.privMicrophoneLabel !== undefined) {
return PromiseHelper.fromResult(this.privMicrophoneLabel);
}
// If the stream isn't currently running, we can't query devices because security.
if (this.privMediaStream === undefined || !this.privMediaStream.active) {
return PromiseHelper.fromResult(defaultMicrophoneName);
}
// Setup a default
this.privMicrophoneLabel = defaultMicrophoneName;
// Get the id of the device running the audio track.
const microphoneDeviceId = this.privMediaStream.getTracks()[0].getSettings().deviceId;
// If the browser doesn't support getting the device ID, set a default and return.
if (undefined === microphoneDeviceId) {
return PromiseHelper.fromResult(this.privMicrophoneLabel);
}
const deferred = new Deferred();
// Enumerate the media devices.
navigator.mediaDevices.enumerateDevices().then((devices) => {
for (const device of devices) {
if (device.deviceId === microphoneDeviceId) {
// Found the device
this.privMicrophoneLabel = device.label;
break;
}
}
deferred.resolve(this.privMicrophoneLabel);
}, () => deferred.resolve(this.privMicrophoneLabel));
return deferred.promise();
}
}
MicAudioSource.AUDIOFORMAT = AudioStreamFormat.getDefaultInputFormat();
//# sourceMappingURL=MicAudioSource.js.map