Skip to content

Commit 68daaa6

Browse files
authored
fix AudioIn getSources #231 (#234)
AudioIn uses window.navigator.mediaDevices getUserMedia and enumerateDevices add AudioIn test AudioIn uses audio context sample rate (fix a bug in Firefox)
1 parent 2a74ddb commit 68daaa6

File tree

6 files changed

+137
-127
lines changed

6 files changed

+137
-127
lines changed

src/.eslintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"node": true,
55
"mocha": true,
66
"browser": true,
7-
"builtin": true
7+
"builtin": true,
8+
"es6": true
89
},
910
"globals": {
1011
"p5": true,

src/audioin.js

Lines changed: 93 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
define(function (require) {
44
var p5sound = require('master');
55

6+
// an array of input sources
7+
p5sound.inputSources = [];
8+
69
/**
710
* <p>Get audio from an input, i.e. your computer's microphone.</p>
811
*
@@ -45,8 +48,7 @@ define(function (require) {
4548

4649
this.stream = null;
4750
this.mediaStream = null;
48-
49-
this.currentSource = 0;
51+
this.currentSource = null;
5052

5153
/**
5254
* Client must allow browser to access their microphone / audioin source.
@@ -60,18 +62,8 @@ define(function (require) {
6062
this.amplitude = new p5.Amplitude();
6163
this.output.connect(this.amplitude.input);
6264

63-
// Some browsers let developer determine their input sources
64-
if (typeof window.MediaStreamTrack === 'undefined') {
65-
if (errorCallback) {
66-
errorCallback();
67-
} else {
68-
window.alert('This browser does not support AudioIn');
69-
}
70-
} else if (typeof window.MediaDevices.enumerateDevices === 'function') {
71-
// Chrome supports getSources to list inputs. Dev picks default
72-
window.MediaDevices.enumerateDevices(this._gotSources);
73-
} else {
74-
// Firefox has no getSources() but lets user choose their input
65+
if (!window.MediaStreamTrack || !window.navigator.mediaDevices || !window.navigator.mediaDevices.getUserMedia) {
66+
errorCallback ? errorCallback() : window.alert('This browser does not support MediaStreamTrack and mediaDevices');
7567
}
7668

7769
// add to soundArray so we can dispose on close
@@ -100,50 +92,39 @@ define(function (require) {
10092
p5.AudioIn.prototype.start = function(successCallback, errorCallback) {
10193
var self = this;
10294

103-
// if stream was already started...
95+
if (this.stream) {
96+
this.stop();
97+
}
10498

99+
// set the audio source
100+
var audioSource = p5sound.inputSources[self.currentSource];
101+
var constraints = {
102+
audio: {
103+
sampleRate: p5sound.audiocontext.sampleRate,
104+
echoCancellation: false
105+
}
106+
};
105107

106-
// if _gotSources() i.e. developers determine which source to use
107-
if (p5sound.inputSources[self.currentSource]) {
108-
// set the audio source
109-
var audioSource = p5sound.inputSources[self.currentSource].id;
110-
var constraints = {
111-
audio: {
112-
optional: [{sourceId: audioSource}]
113-
}};
114-
window.navigator.getUserMedia( constraints,
115-
this._onStream = function(stream) {
116-
self.stream = stream;
117-
self.enabled = true;
118-
// Wrap a MediaStreamSourceNode around the live input
119-
self.mediaStream = p5sound.audiocontext.createMediaStreamSource(stream);
120-
self.mediaStream.connect(self.output);
121-
if (successCallback) successCallback();
122-
// only send to the Amplitude reader, so we can see it but not hear it.
123-
self.amplitude.setInput(self.output);
124-
}, this._onStreamError = function(e) {
125-
if (errorCallback) errorCallback(e);
126-
else console.error(e);
127-
});
128-
} else {
129-
// if Firefox where users select their source via browser
130-
// if (typeof MediaStreamTrack.getSources === 'undefined') {
131-
// Only get the audio stream.
132-
window.navigator.getUserMedia( {'audio':true},
133-
this._onStream = function(stream) {
134-
self.stream = stream;
135-
self.enabled = true;
136-
// Wrap a MediaStreamSourceNode around the live input
137-
self.mediaStream = p5sound.audiocontext.createMediaStreamSource(stream);
138-
self.mediaStream.connect(self.output);
139-
// only send to the Amplitude reader, so we can see it but not hear it.
140-
self.amplitude.setInput(self.output);
141-
if (successCallback) successCallback();
142-
}, this._onStreamError = function(e) {
143-
if (errorCallback) errorCallback(e);
144-
else console.error(e);
145-
});
108+
// if developers determine which source to use
109+
if (p5sound.inputSources[this.currentSource]) {
110+
constraints.audio.deviceId = audioSource.deviceId;
146111
}
112+
113+
window.navigator.mediaDevices.getUserMedia( constraints )
114+
.then( function(stream) {
115+
self.stream = stream;
116+
self.enabled = true;
117+
// Wrap a MediaStreamSourceNode around the live input
118+
self.mediaStream = p5sound.audiocontext.createMediaStreamSource(stream);
119+
self.mediaStream.connect(self.output);
120+
// only send to the Amplitude reader, so we can see it but not hear it.
121+
self.amplitude.setInput(self.output);
122+
if (successCallback) successCallback();
123+
})
124+
.catch( function(err) {
125+
if (errorCallback) errorCallback(err);
126+
else console.error(err);
127+
});
147128
};
148129

149130
/**
@@ -154,8 +135,14 @@ define(function (require) {
154135
*/
155136
p5.AudioIn.prototype.stop = function() {
156137
if (this.stream) {
157-
// assume only one track
158-
this.stream.getTracks()[0].stop();
138+
this.stream.getTracks().forEach(function(track) {
139+
track.stop();
140+
});
141+
142+
this.mediaStream.disconnect();
143+
144+
delete this.mediaStream;
145+
delete this.stream;
159146
}
160147
};
161148

@@ -216,22 +203,6 @@ define(function (require) {
216203
return this.amplitude.getLevel();
217204
};
218205

219-
/**
220-
* Add input sources to the list of available sources.
221-
*
222-
* @private
223-
*/
224-
p5.AudioIn.prototype._gotSources = function(sourceInfos) {
225-
for (var i = 0; i< sourceInfos.length; i++) {
226-
var sourceInfo = sourceInfos[i];
227-
if (sourceInfo.kind === 'audio') {
228-
// add the inputs to inputSources
229-
//p5sound.inputSources.push(sourceInfo);
230-
return sourceInfo;
231-
}
232-
}
233-
};
234-
235206
/**
236207
* Set amplitude (volume) of a mic input between 0 and 1.0. <br/>
237208
*
@@ -252,31 +223,20 @@ define(function (require) {
252223
}
253224
};
254225

255-
p5.AudioIn.prototype.listSources = function() {
256-
console.log('listSources is deprecated - please use AudioIn.getSources');
257-
console.log('input sources: ');
258-
if (p5sound.inputSources.length > 0) {
259-
return p5sound.inputSources;
260-
} else {
261-
return 'This browser does not support MediaStreamTrack.getSources()';
262-
}
263-
};
264226
/**
265-
* Chrome only. Returns a list of available input sources
266-
* and allows the user to set the media source. Firefox allows
267-
* the user to choose from input sources in the permissions dialogue
268-
* instead of enumerating available sources and selecting one.
269-
* Note: in order to have descriptive media names your page must be
270-
* served over a secure (HTTPS) connection and the page should
271-
* request user media before enumerating devices. Otherwise device
272-
* ID will be a long device ID number and does not specify device
273-
* type. For example see
274-
* https://simpl.info/getusermedia/sources/index.html vs.
275-
* http://simpl.info/getusermedia/sources/index.html
227+
* Returns a list of available input sources. This is a wrapper
228+
* for <a title="MediaDevices.enumerateDevices() - Web APIs | MDN" target="_blank" href=
229+
* "https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices"
230+
* > and it returns a Promise.
276231
*
277232
* @method getSources
278-
* @param {Function} callback a callback to handle the sources
279-
* when they have been enumerated
233+
* @param {Function} [successCallback] This callback function handles the sources when they
234+
* have been enumerated. The callback function
235+
* receives the deviceList array as its only argument
236+
* @param {Function} [errorCallback] This optional callback receives the error
237+
* message as its argument.
238+
* @returns {Promise} Returns a Promise that can be used in place of the callbacks, similar
239+
* to the enumerateDevices() method
280240
* @example
281241
* <div><code>
282242
* var audiograb;
@@ -285,51 +245,62 @@ define(function (require) {
285245
* //new audioIn
286246
* audioGrab = new p5.AudioIn();
287247
*
288-
* audioGrab.getSources(function(sourceList) {
248+
* audioGrab.getSources(function(deviceList) {
289249
* //print out the array of available sources
290-
* console.log(sourceList);
291-
* //set the source to the first item in the inputSources array
250+
* console.log(deviceList);
251+
* //set the source to the first item in the deviceList array
292252
* audioGrab.setSource(0);
293253
* });
294254
* }
295255
* </code></div>
296256
*/
297-
p5.AudioIn.prototype.getSources = function (callback) {
298-
if(typeof window.MediaStreamTrack.getSources === 'function') {
299-
window.MediaStreamTrack.getSources(function (data) {
300-
for (var i = 0, max = data.length; i < max; i++) {
301-
var sourceInfo = data[i];
302-
if (sourceInfo.kind === 'audio') {
303-
// add the inputs to inputSources
304-
p5sound.inputSources.push(sourceInfo);
257+
p5.AudioIn.prototype.getSources = function (onSuccess, onError) {
258+
return new Promise( function(resolve, reject) {
259+
window.navigator.mediaDevices.enumerateDevices()
260+
.then( function(devices) {
261+
p5sound.inputSources = devices.filter(function(device) {
262+
return device.kind === 'audioinput';
263+
});
264+
resolve(p5sound.inputSources);
265+
if (onSuccess) {
266+
onSuccess(p5sound.inputSources);
305267
}
306-
}
307-
callback(p5sound.inputSources);
308-
});
309-
} else {
310-
console.log('This browser does not support MediaStreamTrack.getSources()');
311-
}
268+
})
269+
.catch( function(error) {
270+
reject(error);
271+
if (onError) {
272+
onError(error);
273+
} else {
274+
console.error('This browser does not support MediaStreamTrack.getSources()');
275+
}
276+
});
277+
});
312278
};
279+
313280
/**
314281
* Set the input source. Accepts a number representing a
315-
* position in the array returned by listSources().
282+
* position in the array returned by getSources().
316283
* This is only available in browsers that support
317-
* MediaStreamTrack.getSources(). Instead, some browsers
318-
* give users the option to set their own media source.<br/>
284+
* <a title="MediaDevices.enumerateDevices() - Web APIs | MDN" target="_blank" href=
285+
* "https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/enumerateDevices"
286+
* >navigator.mediaDevices.enumerateDevices()</a>.<br/>
319287
*
320288
* @method setSource
321289
* @param {number} num position of input source in the array
322290
*/
323291
p5.AudioIn.prototype.setSource = function(num) {
324-
// TO DO - set input by string or # (array position)
325-
var self = this;
326292
if (p5sound.inputSources.length > 0 && num < p5sound.inputSources.length) {
327293
// set the current source
328-
self.currentSource = num;
329-
console.log('set source to ' + p5sound.inputSources[self.currentSource].id);
294+
this.currentSource = num;
295+
console.log('set source to ', p5sound.inputSources[this.currentSource]);
330296
} else {
331297
console.log('unable to set input source');
332298
}
299+
300+
// restart stream if currently active
301+
if (this.stream && this.stream.active) {
302+
this.start();
303+
}
333304
};
334305

335306
// private method
@@ -339,14 +310,15 @@ define(function (require) {
339310
p5sound.soundArray.splice(index, 1);
340311

341312
this.stop();
313+
342314
if (this.output) {
343315
this.output.disconnect();
344316
}
345317
if (this.amplitude) {
346318
this.amplitude.disconnect();
347319
}
348-
this.amplitude = null;
349-
this.output = null;
320+
delete this.amplitude;
321+
delete this.output;
350322
};
351323

352324
});

src/master.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@ define(function () {
1919

2020
this.output.disconnect();
2121

22-
// an array of input sources
23-
this.inputSources = [];
24-
2522
// connect input to limiter
2623
this.input.connect(this.limiter);
2724

test/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
<div id="mocha"></div>
1616
<script>mocha.setup('bdd')</script>
1717
</body>
18-
</html>
18+
</html>

test/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require.config({
99
var allTests = ['tests/p5.SoundFile', 'tests/p5.Amplitude',
1010
'tests/p5.Oscillator', 'tests/p5.Distortion',
1111
'tests/p5.Effect','tests/p5.Filter', 'tests/p5.FFT', 'tests/p5.Compressor',
12-
'tests/p5.EQ'];
12+
'tests/p5.EQ', 'tests/p5.AudioIn'];
1313

1414
p5.prototype.masterVolume(0);
1515

test/tests/p5.AudioIn.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
define(['chai'],
2+
function(chai) {
3+
4+
var expect = chai.expect;
5+
6+
describe('p5.AudioIn', function() {
7+
it('can be created and disposed', function(){
8+
var mic = new p5.AudioIn();
9+
mic.dispose();
10+
});
11+
12+
it('can be started and stopped', function() {
13+
var mic = new p5.AudioIn();
14+
mic.start(function() {
15+
mic.stop();
16+
});
17+
});
18+
19+
it('can get sources', function(done) {
20+
var mic = new p5.AudioIn();
21+
mic.getSources(function(sources) {
22+
console.log(sources);
23+
expect(sources).to.be.an('array');
24+
done();
25+
});
26+
});
27+
28+
it('can set source', function(done) {
29+
var mic = new p5.AudioIn();
30+
expect(mic.currentSource).to.be.null;
31+
32+
return mic.getSources(function(sources) {
33+
mic.setSource(0);
34+
expect(mic.currentSource).to.be(0);
35+
done();
36+
});
37+
});
38+
});
39+
40+
});

0 commit comments

Comments
 (0)