mirror of
https://github.com/Tonejs/Tone.js
synced 2024-12-28 12:33:12 +00:00
260 lines
7.2 KiB
TypeScript
260 lines
7.2 KiB
TypeScript
import { connect, OutputNode, ToneAudioNode, ToneAudioNodeOptions } from "../core/context/ToneAudioNode";
|
|
import { Decibels } from "../core/type/Units";
|
|
import { Volume } from "../component/channel/Volume";
|
|
import { optionsFromArguments } from "../core/util/Defaults";
|
|
import { assert } from "../core/util/Debug";
|
|
import { Param } from "../core/context/Param";
|
|
import { readOnly } from "../core/util/Interface";
|
|
import { isDefined, isNumber } from "../core/util/TypeCheck";
|
|
|
|
export interface UserMediaOptions extends ToneAudioNodeOptions {
|
|
volume: Decibels;
|
|
mute: boolean;
|
|
}
|
|
/**
|
|
* UserMedia uses MediaDevices.getUserMedia to open up and external microphone or audio input.
|
|
* Check [MediaDevices API Support](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia)
|
|
* to see which browsers are supported. Access to an external input
|
|
* is limited to secure (HTTPS) connections.
|
|
* @example
|
|
* const meter = new Tone.Meter();
|
|
* const mic = new Tone.UserMedia().connect(meter);
|
|
* mic.open().then(() => {
|
|
* // promise resolves when input is available
|
|
* console.log("mic open");
|
|
* // print the incoming mic levels in decibels
|
|
* setInterval(() => console.log(meter.getValue()), 100);
|
|
* }).catch(e => {
|
|
* // promise is rejected when the user doesn't have or allow mic access
|
|
* console.log("mic not open");
|
|
* });
|
|
* @category Source
|
|
*/
|
|
|
|
export class UserMedia extends ToneAudioNode<UserMediaOptions> {
|
|
|
|
readonly name: string = "UserMedia";
|
|
|
|
readonly input: undefined;
|
|
readonly output: OutputNode;
|
|
|
|
/**
|
|
* The MediaStreamNode
|
|
*/
|
|
private _mediaStream?: MediaStreamAudioSourceNode;
|
|
|
|
/**
|
|
* The media stream created by getUserMedia.
|
|
*/
|
|
private _stream?: MediaStream;
|
|
|
|
/**
|
|
* The open device
|
|
*/
|
|
private _device?: MediaDeviceInfo;
|
|
|
|
/**
|
|
* The output volume node
|
|
*/
|
|
private _volume: Volume;
|
|
|
|
/**
|
|
* The volume of the output in decibels.
|
|
*/
|
|
readonly volume: Param<"decibels">;
|
|
|
|
/**
|
|
* @param volume The level of the input in decibels
|
|
*/
|
|
constructor(volume?: Decibels);
|
|
constructor(options?: Partial<UserMediaOptions>);
|
|
constructor() {
|
|
|
|
super(optionsFromArguments(UserMedia.getDefaults(), arguments, ["volume"]));
|
|
const options = optionsFromArguments(UserMedia.getDefaults(), arguments, ["volume"]);
|
|
|
|
this._volume = this.output = new Volume({
|
|
context: this.context,
|
|
volume: options.volume,
|
|
});
|
|
this.volume = this._volume.volume;
|
|
readOnly(this, "volume");
|
|
this.mute = options.mute;
|
|
}
|
|
|
|
static getDefaults(): UserMediaOptions {
|
|
return Object.assign(ToneAudioNode.getDefaults(), {
|
|
mute: false,
|
|
volume: 0
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Open the media stream. If a string is passed in, it is assumed
|
|
* to be the label or id of the stream, if a number is passed in,
|
|
* it is the input number of the stream.
|
|
* @param labelOrId The label or id of the audio input media device.
|
|
* With no argument, the default stream is opened.
|
|
* @return The promise is resolved when the stream is open.
|
|
*/
|
|
async open(labelOrId?: string | number): Promise<this> {
|
|
assert(UserMedia.supported, "UserMedia is not supported");
|
|
// close the previous stream
|
|
if (this.state === "started") {
|
|
this.close();
|
|
}
|
|
const devices = await UserMedia.enumerateDevices();
|
|
if (isNumber(labelOrId)) {
|
|
this._device = devices[labelOrId];
|
|
} else {
|
|
this._device = devices.find((device) => {
|
|
return device.label === labelOrId || device.deviceId === labelOrId;
|
|
});
|
|
// didn't find a matching device
|
|
if (!this._device && devices.length > 0) {
|
|
this._device = devices[0];
|
|
}
|
|
assert(isDefined(this._device), `No matching device ${labelOrId}`);
|
|
}
|
|
// do getUserMedia
|
|
const constraints = {
|
|
audio: {
|
|
echoCancellation: false,
|
|
sampleRate: this.context.sampleRate,
|
|
noiseSuppression: false,
|
|
mozNoiseSuppression: false,
|
|
}
|
|
};
|
|
if (this._device) {
|
|
// @ts-ignore
|
|
constraints.audio.deviceId = this._device.deviceId;
|
|
}
|
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
// start a new source only if the previous one is closed
|
|
if (!this._stream) {
|
|
this._stream = stream;
|
|
// Wrap a MediaStreamSourceNode around the live input stream.
|
|
const mediaStreamNode = this.context.createMediaStreamSource(stream);
|
|
// Connect the MediaStreamSourceNode to a gate gain node
|
|
connect(mediaStreamNode, this.output);
|
|
this._mediaStream = mediaStreamNode;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Close the media stream
|
|
*/
|
|
close(): this {
|
|
if (this._stream && this._mediaStream) {
|
|
this._stream.getAudioTracks().forEach((track) => {
|
|
track.stop();
|
|
});
|
|
this._stream = undefined;
|
|
// remove the old media stream
|
|
this._mediaStream.disconnect();
|
|
this._mediaStream = undefined;
|
|
}
|
|
this._device = undefined;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Returns a promise which resolves with the list of audio input devices available.
|
|
* @return The promise that is resolved with the devices
|
|
* @example
|
|
* Tone.UserMedia.enumerateDevices().then((devices) => {
|
|
* // print the device labels
|
|
* console.log(devices.map(device => device.label));
|
|
* });
|
|
*/
|
|
static async enumerateDevices(): Promise<MediaDeviceInfo[]> {
|
|
const allDevices = await navigator.mediaDevices.enumerateDevices();
|
|
return allDevices.filter(device => {
|
|
return device.kind === "audioinput";
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the playback state of the source, "started" when the microphone is open
|
|
* and "stopped" when the mic is closed.
|
|
*/
|
|
get state() {
|
|
return this._stream && this._stream.active ? "started" : "stopped";
|
|
}
|
|
|
|
/**
|
|
* Returns an identifier for the represented device that is
|
|
* persisted across sessions. It is un-guessable by other applications and
|
|
* unique to the origin of the calling application. It is reset when the
|
|
* user clears cookies (for Private Browsing, a different identifier is
|
|
* used that is not persisted across sessions). Returns undefined when the
|
|
* device is not open.
|
|
*/
|
|
get deviceId(): string | undefined {
|
|
if (this._device) {
|
|
return this._device.deviceId;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a group identifier. Two devices have the
|
|
* same group identifier if they belong to the same physical device.
|
|
* Returns null when the device is not open.
|
|
*/
|
|
get groupId(): string | undefined {
|
|
if (this._device) {
|
|
return this._device.groupId;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a label describing this device (for example "Built-in Microphone").
|
|
* Returns undefined when the device is not open or label is not available
|
|
* because of permissions.
|
|
*/
|
|
get label(): string | undefined {
|
|
if (this._device) {
|
|
return this._device.label;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mute the output.
|
|
* @example
|
|
* const mic = new Tone.UserMedia();
|
|
* mic.open().then(() => {
|
|
* // promise resolves when input is available
|
|
* });
|
|
* // mute the output
|
|
* mic.mute = true;
|
|
*/
|
|
get mute(): boolean {
|
|
return this._volume.mute;
|
|
}
|
|
set mute(mute) {
|
|
this._volume.mute = mute;
|
|
}
|
|
|
|
dispose(): this {
|
|
super.dispose();
|
|
this.close();
|
|
this._volume.dispose();
|
|
this.volume.dispose();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* If getUserMedia is supported by the browser.
|
|
*/
|
|
static get supported(): boolean {
|
|
return isDefined(navigator.mediaDevices) &&
|
|
isDefined(navigator.mediaDevices.getUserMedia);
|
|
}
|
|
}
|