Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: p5.initSound method #402

Closed
wants to merge 3 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 113 additions & 24 deletions src/audiocontext.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -48,58 +52,143 @@ 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);
};

/**
* <p>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
* (<a href="https://goo.gl/7K7WLu">info</a>), iOS Safari, and other browsers.
* </p>
*
* <p>
* userStartAudio() starts the <a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioContext"
* p5.initSound() starts the <a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioContext"
* target="_blank" title="Audio Context @ MDN">Audio Context</a> on a user gesture. It utilizes
* the <a href="https://github.com/tambien/StartAudioContext">StartAudioContext</a> library by
* Yotam Mann (MIT Licence, 2016). Read more at https://github.com/tambien/StartAudioContext.
* </p>
*
* <p>Starting the audio context on a user gesture can be as simple as <code>userStartAudio()</code>.
* Optional parameters let you decide on a specific element that will start the audio context,
* <p>Starting the audio context on a user gesture can be as simple as <code>p5.initSound()</code>
* at the top of any sketch that uses audio/sound. By default, it will create a button that
* initializes the audio context when pressed.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*
* <p>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.</p>
* @param {Element|Array} [element(s)] This argument can be an Element,
* Selector String, NodeList, p5.Element,
* jQuery Element, or an Array of any of those.
* @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
* <div><code>
* 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();
* });
* }
* </code></div>
* @example
* <div><code>
* 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;
});