diff --git a/src/audiocontext.js b/src/audiocontext.js index 6cfeb300..5eb3b6f8 100644 --- a/src/audiocontext.js +++ b/src/audiocontext.js @@ -2,6 +2,10 @@ global.TONE_SILENCE_VERSION_LOGGING = true; +const INIT_AUDIO_ID = 'p5_init_sound'; +let shouldInitSound = false; +let firstP5Context = null; + define(['startaudiocontext', 'Tone/core/Context', 'Tone/core/Tone'], function (StartAudioContext, Context, Tone) { // Create the Audio Context const audiocontext = new window.AudioContext(); @@ -48,22 +52,82 @@ define(['startaudiocontext', 'Tone/core/Context', 'Tone/core/Tone'], function (S return audiocontext; }; + const userStartAudio = function(elements, callback) { + shouldInitSound = true; + + let elt = elements; + if (elements instanceof p5.Element) { + elt = elements.elt; + } else if (elements instanceof Array && elements[0] instanceof p5.Element ) { + elt = elements.map(function(e) { return e.elt; }); + } + + // user defined an element + if (elt) { + return StartAudioContext(audiocontext, elt, callback); + } else if (firstP5Context && firstP5Context._userNode) { + // create an initSound button on the first p5 context we found + createInitSoundButton(firstP5Context); + return StartAudioContext(audiocontext, firstP5Context._userNode, callback); + } else { + // Unknown element — fallback to the page body + return StartAudioContext(audiocontext, 'body', callback); + } + }; + + p5.prototype.registerMethod('init', function() { + // if no element is specified, + // we will add the 🔊 button to the first p5 sketch we find. + if (!firstP5Context) { + firstP5Context = this; + } + + // set timeout to allow for `p5.initSound()` to be called first + setTimeout(() => { + if (!shouldInitSound) { return; } + + // ensure that a preload function exists so that p5 will wait for preloads to finish + if (!this.preload && !window.preload) { + this.preload = function() {}; + } + + this._incrementPreload(); + audiocontext.resume() + .then(() => this._decrementPreload()) + .catch(e => console.error('unable to start audio context', e)); + }, 0); + }); + + p5.prototype.userStartAudio = (elements, callback) => { + console.warn('userStartAudio() is deprecated in favor of p5.initSound()'); + return userStartAudio(elements, callback); + }; /** *
It is a good practice to give users control over starting audio playback. - * This practice is enforced by Google Chrome's autoplay policy as of r70 + * This practice is enforced by Google Chrome's autoplay policy * (info), iOS Safari, and other browsers. *
* *- * userStartAudio() starts the Audio Context on a user gesture. It utilizes * the StartAudioContext library by * Yotam Mann (MIT Licence, 2016). Read more at https://github.com/tambien/StartAudioContext. *
* - *Starting the audio context on a user gesture can be as simple as userStartAudio()
.
- * Optional parameters let you decide on a specific element that will start the audio context,
+ *
Starting the audio context on a user gesture can be as simple as p5.initSound()
+ * at the top of any sketch that uses audio/sound. By default, it will create a button that
+ * initializes the audio context when pressed.
The button element has the ID "p5_loading" and it can be stylized using CSS. + * If an HTML element with that ID already exists on the page, then that element will be used.
+ * + *When p5.initSound() runs before preload, setup, and draw, it will wait until the + * audio context has been initialized before running preload, setup or draw.
+ * + *Optional parameters let you decide on a specific element, + * or an array of elements, that will start the audio context. * and/or call a function once the audio context is started.
* @param {Element|Array} [element(s)] This argument can be an Element, * Selector String, NodeList, p5.Element, @@ -71,35 +135,60 @@ define(['startaudiocontext', 'Tone/core/Context', 'Tone/core/Tone'], function (S * @param {Function} [callback] Callback to invoke when the AudioContext has started * @return {Promise} Returns a Promise which is resolved when * the AudioContext state is 'running' - * @method userStartAudio - * @for p5 + * @method p5.initSound * @example *
- * function setup() {
- * var myDiv = createDiv('click to start audio');
- * myDiv.position(0, 0);
+ * p5.initSound();
*
+ * // Setup won't run until the context has started
+ * function setup() {
+ * background(0, 255, 0);
* var mySynth = new p5.MonoSynth();
- *
- * // This won't play until the context has started
* mySynth.play('A6');
- *
- * // Start the audio context on a click/touch event
- * userStartAudio().then(function() {
- * myDiv.remove();
- * });
* }
*
+ * function setup() {
+ * background(255, 0, 0);
+ *
+ * var myButton = createButton('click to start audio');
+ * myButton.position(0, 0);
+ *
+ * p5.initSound(myButton).then(() => {
+ * var mySynth = new p5.MonoSynth();
+ * mySynth.play('A6');
+ * background(0, 255, 0);
+ * myButton.remove();
+ * });
*/
- p5.prototype.userStartAudio = function(elements, callback) {
- var elt = elements;
- if (elements instanceof p5.Element) {
- elt = elements.elt;
- } else if (elements instanceof Array && elements[0] instanceof p5.Element ) {
- elt = elements.map(function(e) { return e.elt});
+ p5.initSound = userStartAudio;
+
+ function createInitSoundButton(p5Context) {
+ if (document.getElementById(INIT_AUDIO_ID) === null) {
+ const sndString = document.characterSet === 'UTF-8'
+ ? '🔊'
+ : 'Sound';
+ const initSoundButton = document.createElement('button');
+ initSoundButton.setAttribute('id', INIT_AUDIO_ID);
+ initSoundButton.innerText = `Init ${sndString}`;
+ initSoundButton.style.position = 'absolute';
+ initSoundButton.style.zIndex = '2';
+ initSoundButton.style.width = '100px';
+ initSoundButton.style.height = '100px';
+ initSoundButton.style.top = '0';
+ const node = p5Context._userNode || document.body;
+ node.appendChild(initSoundButton);
+
+ removeInitSoundButtonOnAudioContextStart(initSoundButton);
}
- return StartAudioContext(audiocontext, elt, callback);
- };
+ }
+
+ function removeInitSoundButtonOnAudioContextStart(child) {
+ audiocontext.resume().then(() => {
+ child.remove();
+ });
+ }
return audiocontext;
});