tone 15.5.2 → 15.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Tone/core/util/TypeCheck.ts +9 -9
- package/Tone/source/UserMedia.test.ts +131 -112
- package/Tone/source/UserMedia.ts +37 -26
- package/Tone/source/oscillator/Oscillator.test.ts +3 -4
- package/Tone/version.ts +1 -1
- package/build/Tone.js +1 -1
- package/build/Tone.js.map +1 -1
- package/build/esm/core/util/TypeCheck.d.ts +8 -8
- package/build/esm/core/util/TypeCheck.js.map +1 -1
- package/build/esm/source/UserMedia.d.ts +3 -3
- package/build/esm/source/UserMedia.js +32 -25
- package/build/esm/source/UserMedia.js.map +1 -1
- package/build/esm/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,7 @@ import { Note } from "../type/Units.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Test if the arg is undefined
|
|
5
5
|
*/
|
|
6
|
-
export function isUndef(arg:
|
|
6
|
+
export function isUndef(arg: unknown): arg is undefined {
|
|
7
7
|
return arg === undefined;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -17,45 +17,45 @@ export function isDefined<T>(arg: T | undefined): arg is T {
|
|
|
17
17
|
/**
|
|
18
18
|
* Test if the arg is a function
|
|
19
19
|
*/
|
|
20
|
-
export function isFunction(arg:
|
|
20
|
+
export function isFunction(arg: unknown): arg is (a: any) => any {
|
|
21
21
|
return typeof arg === "function";
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Test if the argument is a number.
|
|
26
26
|
*/
|
|
27
|
-
export function isNumber(arg:
|
|
27
|
+
export function isNumber(arg: unknown): arg is number {
|
|
28
28
|
return typeof arg === "number";
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Test if the given argument is an object literal (i.e. `{}`);
|
|
33
33
|
*/
|
|
34
|
-
export function isObject(arg:
|
|
34
|
+
export function isObject(arg: unknown): arg is object {
|
|
35
35
|
return (
|
|
36
36
|
Object.prototype.toString.call(arg) === "[object Object]" &&
|
|
37
|
-
arg.constructor === Object
|
|
37
|
+
(arg as object).constructor === Object
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Test if the argument is a boolean.
|
|
43
43
|
*/
|
|
44
|
-
export function isBoolean(arg:
|
|
44
|
+
export function isBoolean(arg: unknown): arg is boolean {
|
|
45
45
|
return typeof arg === "boolean";
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Test if the argument is an Array
|
|
50
50
|
*/
|
|
51
|
-
export function isArray(arg:
|
|
51
|
+
export function isArray(arg: unknown): arg is any[] {
|
|
52
52
|
return Array.isArray(arg);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Test if the argument is a string.
|
|
57
57
|
*/
|
|
58
|
-
export function isString(arg:
|
|
58
|
+
export function isString(arg: unknown): arg is string {
|
|
59
59
|
return typeof arg === "string";
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -63,6 +63,6 @@ export function isString(arg: any): arg is string {
|
|
|
63
63
|
* Test if the argument is in the form of a note in scientific pitch notation.
|
|
64
64
|
* e.g. "C4"
|
|
65
65
|
*/
|
|
66
|
-
export function isNote(arg:
|
|
66
|
+
export function isNote(arg: unknown): arg is Note {
|
|
67
67
|
return isString(arg) && /^([a-g]{1}(?:b|#|x|bb)?)(-?[0-9]+)/i.test(arg);
|
|
68
68
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { expect } from "chai";
|
|
1
|
+
import { expect, use } from "chai";
|
|
2
|
+
import sinon from "sinon";
|
|
3
|
+
import sinonChai from "sinon-chai";
|
|
4
|
+
use(sinonChai);
|
|
2
5
|
|
|
3
6
|
import { BasicTests } from "../../test/helper/Basic.js";
|
|
4
|
-
import {
|
|
7
|
+
import { Context } from "../core/context/Context.js";
|
|
8
|
+
import { OfflineContext } from "../core/context/OfflineContext.js";
|
|
5
9
|
import { UserMedia } from "./UserMedia.js";
|
|
6
10
|
|
|
7
11
|
describe("UserMedia", () => {
|
|
@@ -37,139 +41,154 @@ describe("UserMedia", () => {
|
|
|
37
41
|
});
|
|
38
42
|
});
|
|
39
43
|
|
|
40
|
-
context("Opening and closing",
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
context("Opening and closing", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
const mockTrack = { stop: sinon.stub() };
|
|
47
|
+
const mockStream = {
|
|
48
|
+
active: true,
|
|
49
|
+
getAudioTracks: () => [mockTrack],
|
|
50
|
+
} as unknown as MediaStream;
|
|
51
|
+
const mockMediaStreamSource = {
|
|
52
|
+
connect: sinon.stub(),
|
|
53
|
+
disconnect: sinon.stub(),
|
|
54
|
+
numberOfOutputs: 1,
|
|
55
|
+
} as unknown as MediaStreamAudioSourceNode;
|
|
56
|
+
sinon.stub(UserMedia, "enumerateDevices").resolves([
|
|
57
|
+
{
|
|
58
|
+
deviceId: "default",
|
|
59
|
+
groupId: "default",
|
|
60
|
+
label: "Default Device",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
deviceId: "other",
|
|
64
|
+
groupId: "default",
|
|
65
|
+
label: "Other Device",
|
|
66
|
+
},
|
|
67
|
+
] as MediaDeviceInfo[]);
|
|
68
|
+
sinon
|
|
69
|
+
.stub(navigator.mediaDevices, "getUserMedia")
|
|
70
|
+
.resolves(mockStream);
|
|
71
|
+
sinon
|
|
72
|
+
.stub(Context.prototype, "createMediaStreamSource")
|
|
73
|
+
.returns(mockMediaStreamSource);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
sinon.restore();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("open returns a promise", async () => {
|
|
81
|
+
const extIn = new UserMedia();
|
|
82
|
+
const promise = extIn.open();
|
|
83
|
+
expect(promise).to.be.instanceOf(Promise);
|
|
84
|
+
await promise;
|
|
85
|
+
extIn.dispose();
|
|
50
86
|
});
|
|
51
87
|
|
|
52
|
-
it("open
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
expect(promise).to.have.property("then");
|
|
57
|
-
return promise.then(() => {
|
|
58
|
-
extIn.dispose();
|
|
59
|
-
});
|
|
60
|
-
}
|
|
88
|
+
it("can open an input", async () => {
|
|
89
|
+
const extIn = new UserMedia();
|
|
90
|
+
await extIn.open();
|
|
91
|
+
extIn.dispose();
|
|
61
92
|
});
|
|
62
93
|
|
|
63
|
-
it("can open an input", () => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
it("can open an input by name", async () => {
|
|
95
|
+
const extIn = new UserMedia();
|
|
96
|
+
const devices = await UserMedia.enumerateDevices();
|
|
97
|
+
const name = devices[0].deviceId;
|
|
98
|
+
|
|
99
|
+
await extIn.open(name);
|
|
100
|
+
|
|
101
|
+
expect(extIn.deviceId).to.equal(name);
|
|
102
|
+
expect(navigator.mediaDevices.getUserMedia).to.have.been.calledWith(
|
|
103
|
+
sinon.match.hasNested("audio.deviceId", name)
|
|
104
|
+
);
|
|
105
|
+
extIn.dispose();
|
|
70
106
|
});
|
|
71
107
|
|
|
72
|
-
it("can open an input by
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.then(() => {
|
|
82
|
-
expect(extIn.deviceId).to.equal(name);
|
|
83
|
-
extIn.dispose();
|
|
84
|
-
});
|
|
85
|
-
}
|
|
108
|
+
it("can open an input by index", async () => {
|
|
109
|
+
const extIn = new UserMedia();
|
|
110
|
+
|
|
111
|
+
await extIn.open(0);
|
|
112
|
+
|
|
113
|
+
expect(navigator.mediaDevices.getUserMedia).to.have.been.calledWith(
|
|
114
|
+
sinon.match.hasNested("audio.deviceId", "default")
|
|
115
|
+
);
|
|
116
|
+
extIn.dispose();
|
|
86
117
|
});
|
|
87
118
|
|
|
88
|
-
it("can
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
it("can pass in additional constraints", async () => {
|
|
120
|
+
const extIn = new UserMedia();
|
|
121
|
+
await extIn.open({
|
|
122
|
+
preferCurrentTab: true,
|
|
123
|
+
});
|
|
124
|
+
expect(navigator.mediaDevices.getUserMedia).to.have.been.calledWith(
|
|
125
|
+
sinon.match.hasNested("preferCurrentTab", true)
|
|
126
|
+
);
|
|
127
|
+
extIn.dispose();
|
|
95
128
|
});
|
|
96
129
|
|
|
97
|
-
it("throws an error if it cant find the device name", () => {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
})
|
|
105
|
-
.catch(() => {
|
|
106
|
-
extIn.dispose();
|
|
107
|
-
});
|
|
130
|
+
it("throws an error if it cant find the device name", async () => {
|
|
131
|
+
const extIn = new UserMedia();
|
|
132
|
+
try {
|
|
133
|
+
await extIn.open("doesn't exist");
|
|
134
|
+
throw new Error("shouldn't reach here");
|
|
135
|
+
} catch {
|
|
136
|
+
extIn.dispose();
|
|
108
137
|
}
|
|
109
138
|
});
|
|
110
139
|
|
|
111
|
-
it("is 'started' after media is open and 'stopped' otherwise", () => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
140
|
+
it("is 'started' after media is open and 'stopped' otherwise", async () => {
|
|
141
|
+
const extIn = new UserMedia();
|
|
142
|
+
expect(extIn.state).to.equal("stopped");
|
|
143
|
+
|
|
144
|
+
await extIn.open();
|
|
145
|
+
|
|
146
|
+
expect(extIn.state).to.equal("started");
|
|
147
|
+
extIn.dispose();
|
|
120
148
|
});
|
|
121
149
|
|
|
122
|
-
it("has a label, group and device id when open", () => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
150
|
+
it("has a label, group and device id when open", async () => {
|
|
151
|
+
const extIn = new UserMedia();
|
|
152
|
+
expect(extIn.deviceId).to.be.undefined;
|
|
153
|
+
expect(extIn.groupId).to.be.undefined;
|
|
154
|
+
expect(extIn.label).to.be.undefined;
|
|
155
|
+
|
|
156
|
+
await extIn.open();
|
|
157
|
+
|
|
158
|
+
expect(extIn.deviceId).to.be.a("string");
|
|
159
|
+
expect(extIn.groupId).to.be.a("string");
|
|
160
|
+
expect(extIn.label).to.be.a("string");
|
|
161
|
+
extIn.dispose();
|
|
132
162
|
});
|
|
133
163
|
|
|
134
|
-
it("can reopen an input", () => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return extIn.open();
|
|
141
|
-
})
|
|
142
|
-
.then(() => {
|
|
143
|
-
extIn.dispose();
|
|
144
|
-
});
|
|
145
|
-
}
|
|
164
|
+
it("can reopen an input", async () => {
|
|
165
|
+
const extIn = new UserMedia();
|
|
166
|
+
await extIn.open();
|
|
167
|
+
extIn.close();
|
|
168
|
+
await extIn.open();
|
|
169
|
+
extIn.dispose();
|
|
146
170
|
});
|
|
147
171
|
|
|
148
|
-
it("can close an input", () => {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
extIn.dispose();
|
|
154
|
-
});
|
|
155
|
-
}
|
|
172
|
+
it("can close an input", async () => {
|
|
173
|
+
const extIn = new UserMedia();
|
|
174
|
+
await extIn.open();
|
|
175
|
+
extIn.close();
|
|
176
|
+
extIn.dispose();
|
|
156
177
|
});
|
|
157
178
|
|
|
158
|
-
it("can enumerate devices", () => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
179
|
+
it("can enumerate devices", async () => {
|
|
180
|
+
const devices = await UserMedia.enumerateDevices();
|
|
181
|
+
expect(devices).to.be.instanceOf(Array);
|
|
162
182
|
});
|
|
163
183
|
|
|
164
|
-
it("doesn't work in OfflineContext", (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
extIn.open()
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
done();
|
|
184
|
+
it("doesn't work in OfflineContext", async () => {
|
|
185
|
+
const context = new OfflineContext(2, 2, 44100);
|
|
186
|
+
const extIn = new UserMedia({ context });
|
|
187
|
+
try {
|
|
188
|
+
await extIn.open();
|
|
189
|
+
throw new Error("shouldn't reach here");
|
|
190
|
+
} catch {
|
|
191
|
+
// expected to throw in OfflineContext
|
|
173
192
|
}
|
|
174
193
|
});
|
|
175
194
|
});
|
package/Tone/source/UserMedia.ts
CHANGED
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from "../core/context/ToneAudioNode.js";
|
|
9
9
|
import { Decibels } from "../core/type/Units.js";
|
|
10
10
|
import { assert } from "../core/util/Debug.js";
|
|
11
|
-
import { optionsFromArguments } from "../core/util/Defaults.js";
|
|
11
|
+
import { deepMerge, optionsFromArguments } from "../core/util/Defaults.js";
|
|
12
12
|
import { readOnly } from "../core/util/Interface.js";
|
|
13
|
-
import { isDefined, isNumber } from "../core/util/TypeCheck.js";
|
|
13
|
+
import { isDefined, isNumber, isObject } from "../core/util/TypeCheck.js";
|
|
14
14
|
|
|
15
15
|
export interface UserMediaOptions extends ToneAudioNodeOptions {
|
|
16
16
|
volume: Decibels;
|
|
@@ -100,43 +100,54 @@ export class UserMedia extends ToneAudioNode<UserMediaOptions> {
|
|
|
100
100
|
* Open the media stream. If a string is passed in, it is assumed
|
|
101
101
|
* to be the label or id of the stream, if a number is passed in,
|
|
102
102
|
* it is the input number of the stream.
|
|
103
|
-
* @param
|
|
104
|
-
*
|
|
103
|
+
* @param labelOrIdOrConstraints The label or id of the audio input media device, or a getUserMedia constraints object.
|
|
104
|
+
* With no argument, the default stream is opened.
|
|
105
105
|
* @return The promise is resolved when the stream is open.
|
|
106
106
|
*/
|
|
107
|
-
async open(
|
|
107
|
+
async open(
|
|
108
|
+
labelOrIdOrConstraints?: string | number | MediaStreamConstraints
|
|
109
|
+
): Promise<this> {
|
|
108
110
|
assert(UserMedia.supported, "UserMedia is not supported");
|
|
109
111
|
// close the previous stream
|
|
110
112
|
if (this.state === "started") {
|
|
111
113
|
this.close();
|
|
112
114
|
}
|
|
113
|
-
|
|
114
|
-
if (isNumber(labelOrId)) {
|
|
115
|
-
this._device = devices[labelOrId];
|
|
116
|
-
} else {
|
|
117
|
-
this._device = devices.find((device) => {
|
|
118
|
-
return (
|
|
119
|
-
device.label === labelOrId || device.deviceId === labelOrId
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
// didn't find a matching device
|
|
123
|
-
if (!this._device && devices.length > 0) {
|
|
124
|
-
this._device = devices[0];
|
|
125
|
-
}
|
|
126
|
-
assert(isDefined(this._device), `No matching device ${labelOrId}`);
|
|
127
|
-
}
|
|
128
|
-
// do getUserMedia
|
|
129
|
-
const constraints = {
|
|
115
|
+
let constraints: MediaStreamConstraints = {
|
|
130
116
|
audio: {
|
|
131
117
|
echoCancellation: false,
|
|
132
118
|
sampleRate: this.context.sampleRate,
|
|
133
119
|
noiseSuppression: false,
|
|
134
|
-
mozNoiseSuppression: false,
|
|
135
120
|
},
|
|
136
121
|
};
|
|
137
|
-
if (
|
|
138
|
-
//
|
|
139
|
-
constraints
|
|
122
|
+
if (isObject(labelOrIdOrConstraints)) {
|
|
123
|
+
// if the user passed in a constraints object
|
|
124
|
+
constraints = deepMerge(constraints, labelOrIdOrConstraints);
|
|
125
|
+
} else {
|
|
126
|
+
// if the user passed in a label or id
|
|
127
|
+
const devices = await UserMedia.enumerateDevices();
|
|
128
|
+
if (isNumber(labelOrIdOrConstraints)) {
|
|
129
|
+
this._device = devices[labelOrIdOrConstraints];
|
|
130
|
+
} else {
|
|
131
|
+
this._device = devices.find((device) => {
|
|
132
|
+
return (
|
|
133
|
+
device.label === labelOrIdOrConstraints ||
|
|
134
|
+
device.deviceId === labelOrIdOrConstraints
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
// didn't find a matching device
|
|
138
|
+
if (!this._device && devices.length > 0) {
|
|
139
|
+
this._device = devices[0];
|
|
140
|
+
}
|
|
141
|
+
assert(
|
|
142
|
+
isDefined(this._device),
|
|
143
|
+
`No matching device ${labelOrIdOrConstraints}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
// if there is a device, set the deviceId
|
|
147
|
+
if (this._device) {
|
|
148
|
+
// @ts-ignore
|
|
149
|
+
constraints.audio.deviceId = this._device.deviceId;
|
|
150
|
+
}
|
|
140
151
|
}
|
|
141
152
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
142
153
|
// start a new source only if the previous one is closed
|
|
@@ -19,9 +19,8 @@ describe("Oscillator", () => {
|
|
|
19
19
|
SourceTests(Oscillator);
|
|
20
20
|
OscillatorTests(Oscillator);
|
|
21
21
|
|
|
22
|
-
const sandbox = sinon.createSandbox();
|
|
23
22
|
afterEach(() => {
|
|
24
|
-
|
|
23
|
+
sinon.restore();
|
|
25
24
|
});
|
|
26
25
|
|
|
27
26
|
it("matches a file", () => {
|
|
@@ -38,8 +37,8 @@ describe("Oscillator", () => {
|
|
|
38
37
|
|
|
39
38
|
it("cleans up connections after stopping", async () => {
|
|
40
39
|
const SignalPrototype = Signal.prototype;
|
|
41
|
-
const connectSpy =
|
|
42
|
-
const disconnectSpy =
|
|
40
|
+
const connectSpy = sinon.spy(SignalPrototype, "connect");
|
|
41
|
+
const disconnectSpy = sinon.spy(SignalPrototype, "disconnect");
|
|
43
42
|
await new Promise<void>((done) => {
|
|
44
43
|
const osc = new Oscillator({
|
|
45
44
|
onstop: () => done(),
|
package/Tone/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version: string = "15.5.
|
|
1
|
+
export const version: string = "15.5.4";
|