UNPKG

p5

Version:

[![Build Status](https://travis-ci.org/processing/p5.js.svg?branch=master)](https://travis-ci.org/processing/p5.js) [![npm version](https://badge.fury.io/js/p5.svg)](https://www.npmjs.com/package/p5)

1,348 lines (1,344 loc) 359 kB
/*! p5.sound.js v0.3.5 2017-07-28 */ /** * p5.sound extends p5 with <a href="http://caniuse.com/audio-api" * target="_blank">Web Audio</a> functionality including audio input, * playback, analysis and synthesis. * <br/><br/> * <a href="#/p5.SoundFile"><b>p5.SoundFile</b></a>: Load and play sound files.<br/> * <a href="#/p5.Amplitude"><b>p5.Amplitude</b></a>: Get the current volume of a sound.<br/> * <a href="#/p5.AudioIn"><b>p5.AudioIn</b></a>: Get sound from an input source, typically * a computer microphone.<br/> * <a href="#/p5.FFT"><b>p5.FFT</b></a>: Analyze the frequency of sound. Returns * results from the frequency spectrum or time domain (waveform).<br/> * <a href="#/p5.Oscillator"><b>p5.Oscillator</b></a>: Generate Sine, * Triangle, Square and Sawtooth waveforms. Base class of * <a href="#/p5.Noise">p5.Noise</a> and <a href="#/p5.Pulse">p5.Pulse</a>. * <br/> * <a href="#/p5.Env"><b>p5.Env</b></a>: An Envelope is a series * of fades over time. Often used to control an object's * output gain level as an "ADSR Envelope" (Attack, Decay, * Sustain, Release). Can also modulate other parameters.<br/> * <a href="#/p5.Delay"><b>p5.Delay</b></a>: A delay effect with * parameters for feedback, delayTime, and lowpass filter.<br/> * <a href="#/p5.Filter"><b>p5.Filter</b></a>: Filter the frequency range of a * sound. * <br/> * <a href="#/p5.Reverb"><b>p5.Reverb</b></a>: Add reverb to a sound by specifying * duration and decay. <br/> * <b><a href="#/p5.Convolver">p5.Convolver</a>:</b> Extends * <a href="#/p5.Reverb">p5.Reverb</a> to simulate the sound of real * physical spaces through convolution.<br/> * <b><a href="#/p5.SoundRecorder">p5.SoundRecorder</a></b>: Record sound for playback * / save the .wav file. * <b><a href="#/p5.Phrase">p5.Phrase</a></b>, <b><a href="#/p5.Part">p5.Part</a></b> and * <b><a href="#/p5.Score">p5.Score</a></b>: Compose musical sequences. * <br/><br/> * p5.sound is on <a href="https://github.com/therewasaguy/p5.sound/">GitHub</a>. * Download the latest version * <a href="https://github.com/therewasaguy/p5.sound/blob/master/lib/p5.sound.js">here</a>. * * @module p5.sound * @submodule p5.sound * @for p5.sound * @main */ /** * p5.sound * https://p5js.org/reference/#/libraries/p5.sound * * From the Processing Foundation and contributors * https://github.com/processing/p5.js-sound/graphs/contributors * * MIT License (MIT) * https://github.com/processing/p5.js-sound/blob/master/LICENSE * * Some of the many audio libraries & resources that inspire p5.sound: * - TONE.js (c) Yotam Mann. Licensed under The MIT License (MIT). https://github.com/TONEnoTONE/Tone.js * - buzz.js (c) Jay Salvat. Licensed under The MIT License (MIT). http://buzz.jaysalvat.com/ * - Boris Smus Web Audio API book, 2013. Licensed under the Apache License http://www.apache.org/licenses/LICENSE-2.0 * - wavesurfer.js https://github.com/katspaugh/wavesurfer.js * - Web Audio Components by Jordan Santell https://github.com/web-audio-components * - Wilm Thoben's Sound library for Processing https://github.com/processing/processing/tree/master/java/libraries/sound * * Web Audio API: http://w3.org/TR/webaudio/ */ (function (root, factory) { if (typeof define === 'function' && define.amd) define('p5.sound', ['p5'], function (p5) { (factory(p5));}); else if (typeof exports === 'object') factory(require('../p5')); else factory(root['p5']); }(this, function (p5) { var sndcore; 'use strict'; sndcore = function () { /* AudioContext Monkeypatch Copyright 2013 Chris Wilson Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ (function () { function fixSetTarget(param) { if (!param) // if NYI, just return return; if (!param.setTargetAtTime) param.setTargetAtTime = param.setTargetValueAtTime; } if (window.hasOwnProperty('webkitAudioContext') && !window.hasOwnProperty('AudioContext')) { window.AudioContext = window.webkitAudioContext; if (typeof AudioContext.prototype.createGain !== 'function') AudioContext.prototype.createGain = AudioContext.prototype.createGainNode; if (typeof AudioContext.prototype.createDelay !== 'function') AudioContext.prototype.createDelay = AudioContext.prototype.createDelayNode; if (typeof AudioContext.prototype.createScriptProcessor !== 'function') AudioContext.prototype.createScriptProcessor = AudioContext.prototype.createJavaScriptNode; if (typeof AudioContext.prototype.createPeriodicWave !== 'function') AudioContext.prototype.createPeriodicWave = AudioContext.prototype.createWaveTable; AudioContext.prototype.internal_createGain = AudioContext.prototype.createGain; AudioContext.prototype.createGain = function () { var node = this.internal_createGain(); fixSetTarget(node.gain); return node; }; AudioContext.prototype.internal_createDelay = AudioContext.prototype.createDelay; AudioContext.prototype.createDelay = function (maxDelayTime) { var node = maxDelayTime ? this.internal_createDelay(maxDelayTime) : this.internal_createDelay(); fixSetTarget(node.delayTime); return node; }; AudioContext.prototype.internal_createBufferSource = AudioContext.prototype.createBufferSource; AudioContext.prototype.createBufferSource = function () { var node = this.internal_createBufferSource(); if (!node.start) { node.start = function (when, offset, duration) { if (offset || duration) this.noteGrainOn(when || 0, offset, duration); else this.noteOn(when || 0); }; } else { node.internal_start = node.start; node.start = function (when, offset, duration) { if (typeof duration !== 'undefined') node.internal_start(when || 0, offset, duration); else node.internal_start(when || 0, offset || 0); }; } if (!node.stop) { node.stop = function (when) { this.noteOff(when || 0); }; } else { node.internal_stop = node.stop; node.stop = function (when) { node.internal_stop(when || 0); }; } fixSetTarget(node.playbackRate); return node; }; AudioContext.prototype.internal_createDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor; AudioContext.prototype.createDynamicsCompressor = function () { var node = this.internal_createDynamicsCompressor(); fixSetTarget(node.threshold); fixSetTarget(node.knee); fixSetTarget(node.ratio); fixSetTarget(node.reduction); fixSetTarget(node.attack); fixSetTarget(node.release); return node; }; AudioContext.prototype.internal_createBiquadFilter = AudioContext.prototype.createBiquadFilter; AudioContext.prototype.createBiquadFilter = function () { var node = this.internal_createBiquadFilter(); fixSetTarget(node.frequency); fixSetTarget(node.detune); fixSetTarget(node.Q); fixSetTarget(node.gain); return node; }; if (typeof AudioContext.prototype.createOscillator !== 'function') { AudioContext.prototype.internal_createOscillator = AudioContext.prototype.createOscillator; AudioContext.prototype.createOscillator = function () { var node = this.internal_createOscillator(); if (!node.start) { node.start = function (when) { this.noteOn(when || 0); }; } else { node.internal_start = node.start; node.start = function (when) { node.internal_start(when || 0); }; } if (!node.stop) { node.stop = function (when) { this.noteOff(when || 0); }; } else { node.internal_stop = node.stop; node.stop = function (when) { node.internal_stop(when || 0); }; } if (!node.setPeriodicWave) node.setPeriodicWave = node.setWaveTable; fixSetTarget(node.frequency); fixSetTarget(node.detune); return node; }; } } if (window.hasOwnProperty('webkitOfflineAudioContext') && !window.hasOwnProperty('OfflineAudioContext')) { window.OfflineAudioContext = window.webkitOfflineAudioContext; } }(window)); // <-- end MonkeyPatch. // Create the Audio Context var audiocontext = new window.AudioContext(); /** * <p>Returns the Audio Context for this sketch. Useful for users * who would like to dig deeper into the <a target='_blank' href= * 'http://webaudio.github.io/web-audio-api/'>Web Audio API * </a>.</p> * * @method getAudioContext * @return {Object} AudioContext for this sketch */ p5.prototype.getAudioContext = function () { return audiocontext; }; // Polyfill for AudioIn, also handled by p5.dom createCapture navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; /** * Determine which filetypes are supported (inspired by buzz.js) * The audio element (el) will only be used to test browser support for various audio formats */ var el = document.createElement('audio'); p5.prototype.isSupported = function () { return !!el.canPlayType; }; var isOGGSupported = function () { return !!el.canPlayType && el.canPlayType('audio/ogg; codecs="vorbis"'); }; var isMP3Supported = function () { return !!el.canPlayType && el.canPlayType('audio/mpeg;'); }; var isWAVSupported = function () { return !!el.canPlayType && el.canPlayType('audio/wav; codecs="1"'); }; var isAACSupported = function () { return !!el.canPlayType && (el.canPlayType('audio/x-m4a;') || el.canPlayType('audio/aac;')); }; var isAIFSupported = function () { return !!el.canPlayType && el.canPlayType('audio/x-aiff;'); }; p5.prototype.isFileSupported = function (extension) { switch (extension.toLowerCase()) { case 'mp3': return isMP3Supported(); case 'wav': return isWAVSupported(); case 'ogg': return isOGGSupported(); case 'aac': case 'm4a': case 'mp4': return isAACSupported(); case 'aif': case 'aiff': return isAIFSupported(); default: return false; } }; // if it is iOS, we have to have a user interaction to start Web Audio // http://paulbakaus.com/tutorials/html5/web-audio-on-ios/ var iOS = navigator.userAgent.match(/(iPad|iPhone|iPod)/g) ? true : false; if (iOS) { var iosStarted = false; var startIOS = function () { if (iosStarted) return; // create empty buffer var buffer = audiocontext.createBuffer(1, 1, 22050); var source = audiocontext.createBufferSource(); source.buffer = buffer; // connect to output (your speakers) source.connect(audiocontext.destination); // play the file source.start(0); console.log('start ios!'); if (audiocontext.state === 'running') { iosStarted = true; } }; document.addEventListener('touchend', startIOS, false); document.addEventListener('touchstart', startIOS, false); } }(); var master; 'use strict'; master = function () { /** * Master contains AudioContext and the master sound output. */ var Master = function () { var audiocontext = p5.prototype.getAudioContext(); this.input = audiocontext.createGain(); this.output = audiocontext.createGain(); //put a hard limiter on the output this.limiter = audiocontext.createDynamicsCompressor(); this.limiter.threshold.value = 0; this.limiter.ratio.value = 20; this.audiocontext = audiocontext; this.output.disconnect(); // an array of input sources this.inputSources = []; // connect input to limiter this.input.connect(this.limiter); // connect limiter to output this.limiter.connect(this.output); // meter is just for global Amplitude / FFT analysis this.meter = audiocontext.createGain(); this.fftMeter = audiocontext.createGain(); this.output.connect(this.meter); this.output.connect(this.fftMeter); // connect output to destination this.output.connect(this.audiocontext.destination); // an array of all sounds in the sketch this.soundArray = []; // an array of all musical parts in the sketch this.parts = []; // file extensions to search for this.extensions = []; }; // create a single instance of the p5Sound / master output for use within this sketch var p5sound = new Master(); /** * Returns a number representing the master amplitude (volume) for sound * in this sketch. * * @method getMasterVolume * @return {Number} Master amplitude (volume) for sound in this sketch. * Should be between 0.0 (silence) and 1.0. */ p5.prototype.getMasterVolume = function () { return p5sound.output.gain.value; }; /** * <p>Scale the output of all sound in this sketch</p> * Scaled between 0.0 (silence) and 1.0 (full volume). * 1.0 is the maximum amplitude of a digital sound, so multiplying * by greater than 1.0 may cause digital distortion. To * fade, provide a <code>rampTime</code> parameter. For more * complex fades, see the Env class. * * Alternately, you can pass in a signal source such as an * oscillator to modulate the amplitude with an audio signal. * * <p><b>How This Works</b>: When you load the p5.sound module, it * creates a single instance of p5sound. All sound objects in this * module output to p5sound before reaching your computer's output. * So if you change the amplitude of p5sound, it impacts all of the * sound in this module.</p> * * <p>If no value is provided, returns a Web Audio API Gain Node</p> * * @method masterVolume * @param {Number|Object} volume Volume (amplitude) between 0.0 * and 1.0 or modulating signal/oscillator * @param {Number} [rampTime] Fade for t seconds * @param {Number} [timeFromNow] Schedule this event to happen at * t seconds in the future */ p5.prototype.masterVolume = function (vol, rampTime, tFromNow) { if (typeof vol === 'number') { var rampTime = rampTime || 0; var tFromNow = tFromNow || 0; var now = p5sound.audiocontext.currentTime; var currentVol = p5sound.output.gain.value; p5sound.output.gain.cancelScheduledValues(now + tFromNow); p5sound.output.gain.linearRampToValueAtTime(currentVol, now + tFromNow); p5sound.output.gain.linearRampToValueAtTime(vol, now + tFromNow + rampTime); } else if (vol) { vol.connect(p5sound.output.gain); } else { // return the Gain Node return p5sound.output.gain; } }; /** * `p5.soundOut` is the p5.sound master output. It sends output to * the destination of this window's web audio context. It contains * Web Audio API nodes including a dyanmicsCompressor (<code>.limiter</code>), * and Gain Nodes for <code>.input</code> and <code>.output</code>. * * @property {Object} soundOut */ p5.prototype.soundOut = p5.soundOut = p5sound; /** * a silent connection to the DesinationNode * which will ensure that anything connected to it * will not be garbage collected * * @private */ p5.soundOut._silentNode = p5sound.audiocontext.createGain(); p5.soundOut._silentNode.gain.value = 0; p5.soundOut._silentNode.connect(p5sound.audiocontext.destination); return p5sound; }(); var helpers; 'use strict'; helpers = function () { var p5sound = master; /** * @class p5 */ /** * Returns a number representing the sample rate, in samples per second, * of all sound objects in this audio context. It is determined by the * sampling rate of your operating system's sound card, and it is not * currently possile to change. * It is often 44100, or twice the range of human hearing. * * @method sampleRate * @return {Number} samplerate samples per second */ p5.prototype.sampleRate = function () { return p5sound.audiocontext.sampleRate; }; /** * Returns the closest MIDI note value for * a given frequency. * * @param {Number} frequency A freqeuncy, for example, the "A" * above Middle C is 440Hz * @return {Number} MIDI note value */ p5.prototype.freqToMidi = function (f) { var mathlog2 = Math.log(f / 440) / Math.log(2); var m = Math.round(12 * mathlog2) + 69; return m; }; /** * Returns the frequency value of a MIDI note value. * General MIDI treats notes as integers where middle C * is 60, C# is 61, D is 62 etc. Useful for generating * musical frequencies with oscillators. * * @method midiToFreq * @param {Number} midiNote The number of a MIDI note * @return {Number} Frequency value of the given MIDI note * @example * <div><code> * var notes = [60, 64, 67, 72]; * var i = 0; * * function setup() { * osc = new p5.Oscillator('Triangle'); * osc.start(); * frameRate(1); * } * * function draw() { * var freq = midiToFreq(notes[i]); * osc.freq(freq); * i++; * if (i >= notes.length){ * i = 0; * } * } * </code></div> */ p5.prototype.midiToFreq = function (m) { return 440 * Math.pow(2, (m - 69) / 12); }; /** * List the SoundFile formats that you will include. LoadSound * will search your directory for these extensions, and will pick * a format that is compatable with the client's web browser. * <a href="http://media.io/">Here</a> is a free online file * converter. * * @method soundFormats * @param {String} [...formats] i.e. 'mp3', 'wav', 'ogg' * @example * <div><code> * function preload() { * // set the global sound formats * soundFormats('mp3', 'ogg'); * * // load either beatbox.mp3, or .ogg, depending on browser * mySound = loadSound('assets/beatbox.mp3'); * } * * function setup() { * mySound.play(); * } * </code></div> */ p5.prototype.soundFormats = function () { // reset extensions array p5sound.extensions = []; // add extensions for (var i = 0; i < arguments.length; i++) { arguments[i] = arguments[i].toLowerCase(); if ([ 'mp3', 'wav', 'ogg', 'm4a', 'aac' ].indexOf(arguments[i]) > -1) { p5sound.extensions.push(arguments[i]); } else { throw arguments[i] + ' is not a valid sound format!'; } } }; p5.prototype.disposeSound = function () { for (var i = 0; i < p5sound.soundArray.length; i++) { p5sound.soundArray[i].dispose(); } }; // register removeSound to dispose of p5sound SoundFiles, Convolvers, // Oscillators etc when sketch ends p5.prototype.registerMethod('remove', p5.prototype.disposeSound); p5.prototype._checkFileFormats = function (paths) { var path; // if path is a single string, check to see if extension is provided if (typeof paths === 'string') { path = paths; // see if extension is provided var extTest = path.split('.').pop(); // if an extension is provided... if ([ 'mp3', 'wav', 'ogg', 'm4a', 'aac' ].indexOf(extTest) > -1) { if (p5.prototype.isFileSupported(extTest)) { path = path; } else { var pathSplit = path.split('.'); var pathCore = pathSplit[pathSplit.length - 1]; for (var i = 0; i < p5sound.extensions.length; i++) { var extension = p5sound.extensions[i]; var supported = p5.prototype.isFileSupported(extension); if (supported) { pathCore = ''; if (pathSplit.length === 2) { pathCore += pathSplit[0]; } for (var i = 1; i <= pathSplit.length - 2; i++) { var p = pathSplit[i]; pathCore += '.' + p; } path = pathCore += '.'; path = path += extension; break; } } } } else { for (var i = 0; i < p5sound.extensions.length; i++) { var extension = p5sound.extensions[i]; var supported = p5.prototype.isFileSupported(extension); if (supported) { path = path + '.' + extension; break; } } } } else if (typeof paths === 'object') { for (var i = 0; i < paths.length; i++) { var extension = paths[i].split('.').pop(); var supported = p5.prototype.isFileSupported(extension); if (supported) { // console.log('.'+extension + ' is ' + supported + // ' supported by your browser.'); path = paths[i]; break; } } } return path; }; /** * Used by Osc and Env to chain signal math */ p5.prototype._mathChain = function (o, math, thisChain, nextChain, type) { // if this type of math already exists in the chain, replace it for (var i in o.mathOps) { if (o.mathOps[i] instanceof type) { o.mathOps[i].dispose(); thisChain = i; if (thisChain < o.mathOps.length - 1) { nextChain = o.mathOps[i + 1]; } } } o.mathOps[thisChain - 1].disconnect(); o.mathOps[thisChain - 1].connect(math); math.connect(nextChain); o.mathOps[thisChain] = math; return o; }; return { midiToFreq: p5.prototype.midiToFreq }; }(master); var errorHandler; 'use strict'; errorHandler = function () { /* Helper function to generate an error with a custom stack trace that points to the sketch and removes other parts of the stack trace. @private @class customError @constructor @param {String} name custom error name @param {String} errorTrace custom error trace @param {String} failedPath path to the file that failed to load @property {String} name custom error name @property {String} message custom error message @property {String} stack trace the error back to a line in the user's sketch. Note: this edits out stack trace within p5.js and p5.sound. @property {String} originalStack unedited, original stack trace @property {String} failedPath path to the file that failed to load @return {Error} returns a custom Error object */ var CustomError = function (name, errorTrace, failedPath) { var err = new Error(); var tempStack, splitStack; err.name = name; err.originalStack = err.stack + errorTrace; tempStack = err.stack + errorTrace; err.failedPath = failedPath; // only print the part of the stack trace that refers to the user code: var splitStack = tempStack.split('\n'); splitStack = splitStack.filter(function (ln) { return !ln.match(/(p5.|native code|globalInit)/g); }); err.stack = splitStack.join('\n'); return err; }; return CustomError; }(); var panner; 'use strict'; panner = function () { var p5sound = master; var ac = p5sound.audiocontext; // Stereo panner // if there is a stereo panner node use it if (typeof ac.createStereoPanner !== 'undefined') { p5.Panner = function (input, output) { this.stereoPanner = this.input = ac.createStereoPanner(); input.connect(this.stereoPanner); this.stereoPanner.connect(output); }; p5.Panner.prototype.pan = function (val, tFromNow) { var time = tFromNow || 0; var t = ac.currentTime + time; this.stereoPanner.pan.linearRampToValueAtTime(val, t); }; //not implemented because stereopanner //node does not require this and will automatically //convert single channel or multichannel to stereo. //tested with single and stereo, not with (>2) multichannel p5.Panner.prototype.inputChannels = function () { }; p5.Panner.prototype.connect = function (obj) { this.stereoPanner.connect(obj); }; p5.Panner.prototype.disconnect = function () { this.stereoPanner.disconnect(); }; } else { // if there is no createStereoPanner object // such as in safari 7.1.7 at the time of writing this // use this method to create the effect p5.Panner = function (input, output, numInputChannels) { this.input = ac.createGain(); input.connect(this.input); this.left = ac.createGain(); this.right = ac.createGain(); this.left.channelInterpretation = 'discrete'; this.right.channelInterpretation = 'discrete'; // if input is stereo if (numInputChannels > 1) { this.splitter = ac.createChannelSplitter(2); this.input.connect(this.splitter); this.splitter.connect(this.left, 1); this.splitter.connect(this.right, 0); } else { this.input.connect(this.left); this.input.connect(this.right); } this.output = ac.createChannelMerger(2); this.left.connect(this.output, 0, 1); this.right.connect(this.output, 0, 0); this.output.connect(output); }; // -1 is left, +1 is right p5.Panner.prototype.pan = function (val, tFromNow) { var time = tFromNow || 0; var t = ac.currentTime + time; var v = (val + 1) / 2; var rightVal = Math.cos(v * Math.PI / 2); var leftVal = Math.sin(v * Math.PI / 2); this.left.gain.linearRampToValueAtTime(leftVal, t); this.right.gain.linearRampToValueAtTime(rightVal, t); }; p5.Panner.prototype.inputChannels = function (numChannels) { if (numChannels === 1) { this.input.disconnect(); this.input.connect(this.left); this.input.connect(this.right); } else if (numChannels === 2) { if (typeof (this.splitter === 'undefined')) { this.splitter = ac.createChannelSplitter(2); } this.input.disconnect(); this.input.connect(this.splitter); this.splitter.connect(this.left, 1); this.splitter.connect(this.right, 0); } }; p5.Panner.prototype.connect = function (obj) { this.output.connect(obj); }; p5.Panner.prototype.disconnect = function () { this.output.disconnect(); }; } // 3D panner p5.Panner3D = function (input, output) { var panner3D = ac.createPanner(); panner3D.panningModel = 'HRTF'; panner3D.distanceModel = 'linear'; panner3D.setPosition(0, 0, 0); input.connect(panner3D); panner3D.connect(output); panner3D.pan = function (xVal, yVal, zVal) { panner3D.setPosition(xVal, yVal, zVal); }; return panner3D; }; }(master); var soundfile; 'use strict'; soundfile = function () { var CustomError = errorHandler; var p5sound = master; var ac = p5sound.audiocontext; var midiToFreq = helpers.midiToFreq; /** * <p>SoundFile object with a path to a file.</p> * * <p>The p5.SoundFile may not be available immediately because * it loads the file information asynchronously.</p> * * <p>To do something with the sound as soon as it loads * pass the name of a function as the second parameter.</p> * * <p>Only one file path is required. However, audio file formats * (i.e. mp3, ogg, wav and m4a/aac) are not supported by all * web browsers. If you want to ensure compatability, instead of a single * file path, you may include an Array of filepaths, and the browser will * choose a format that works.</p> * * @class p5.SoundFile * @constructor * @param {String|Array} path path to a sound file (String). Optionally, * you may include multiple file formats in * an array. Alternately, accepts an object * from the HTML5 File API, or a p5.File. * @param {Function} [successCallback] Name of a function to call once file loads * @param {Function} [errorCallback] Name of a function to call if file fails to * load. This function will receive an error or * XMLHttpRequest object with information * about what went wrong. * @param {Function} [whileLoadingCallback] Name of a function to call while file * is loading. That function will * receive progress of the request to * load the sound file * (between 0 and 1) as its first * parameter. This progress * does not account for the additional * time needed to decode the audio data. * * @example * <div><code> * * function preload() { * soundFormats('mp3', 'ogg'); * mySound = loadSound('assets/doorbell.mp3'); * } * * function setup() { * mySound.setVolume(0.1); * mySound.play(); * } * * </code></div> */ p5.SoundFile = function (paths, onload, onerror, whileLoading) { if (typeof paths !== 'undefined') { if (typeof paths === 'string' || typeof paths[0] === 'string') { var path = p5.prototype._checkFileFormats(paths); this.url = path; } else if (typeof paths === 'object') { if (!(window.File && window.FileReader && window.FileList && window.Blob)) { // The File API isn't supported in this browser throw 'Unable to load file because the File API is not supported'; } } // if type is a p5.File...get the actual file if (paths.file) { paths = paths.file; } this.file = paths; } // private _onended callback, set by the method: onended(callback) this._onended = function () { }; this._looping = false; this._playing = false; this._paused = false; this._pauseTime = 0; // cues for scheduling events with addCue() removeCue() this._cues = []; // position of the most recently played sample this._lastPos = 0; this._counterNode = null; this._scopeNode = null; // array of sources so that they can all be stopped! this.bufferSourceNodes = []; // current source this.bufferSourceNode = null; this.buffer = null; this.playbackRate = 1; this.input = p5sound.audiocontext.createGain(); this.output = p5sound.audiocontext.createGain(); this.reversed = false; // start and end of playback / loop this.startTime = 0; this.endTime = null; this.pauseTime = 0; // "restart" would stop playback before retriggering this.mode = 'sustain'; // time that playback was started, in millis this.startMillis = null; // stereo panning this.panPosition = 0; this.panner = new p5.Panner(this.output, p5sound.input, 2); // it is possible to instantiate a soundfile with no path if (this.url || this.file) { this.load(onload, onerror); } // add this p5.SoundFile to the soundArray p5sound.soundArray.push(this); if (typeof whileLoading === 'function') { this._whileLoading = whileLoading; } else { this._whileLoading = function () { }; } }; // register preload handling of loadSound p5.prototype.registerPreloadMethod('loadSound', p5.prototype); /** * loadSound() returns a new p5.SoundFile from a specified * path. If called during preload(), the p5.SoundFile will be ready * to play in time for setup() and draw(). If called outside of * preload, the p5.SoundFile will not be ready immediately, so * loadSound accepts a callback as the second parameter. Using a * <a href="https://github.com/processing/p5.js/wiki/Local-server"> * local server</a> is recommended when loading external files. * * @method loadSound * @param {String|Array} path Path to the sound file, or an array with * paths to soundfiles in multiple formats * i.e. ['sound.ogg', 'sound.mp3']. * Alternately, accepts an object: either * from the HTML5 File API, or a p5.File. * @param {Function} [successCallback] Name of a function to call once file loads * @param {Function} [errorCallback] Name of a function to call if there is * an error loading the file. * @param {Function} [whileLoading] Name of a function to call while file is loading. * This function will receive the percentage loaded * so far, from 0.0 to 1.0. * @return {SoundFile} Returns a p5.SoundFile * @example * <div><code> * function preload() { * mySound = loadSound('assets/doorbell.mp3'); * } * * function setup() { * mySound.setVolume(0.1); * mySound.play(); * } * </code></div> */ p5.prototype.loadSound = function (path, callback, onerror, whileLoading) { // if loading locally without a server if (window.location.origin.indexOf('file://') > -1 && window.cordova === 'undefined') { window.alert('This sketch may require a server to load external files. Please see http://bit.ly/1qcInwS'); } var self = this; var s = new p5.SoundFile(path, function () { if (typeof callback === 'function') { callback.apply(self, arguments); } self._decrementPreload(); }, onerror, whileLoading); return s; }; /** * This is a helper function that the p5.SoundFile calls to load * itself. Accepts a callback (the name of another function) * as an optional parameter. * * @private * @param {Function} [successCallback] Name of a function to call once file loads * @param {Function} [errorCallback] Name of a function to call if there is an error */ p5.SoundFile.prototype.load = function (callback, errorCallback) { var self = this; var errorTrace = new Error().stack; if (this.url !== undefined && this.url !== '') { var request = new XMLHttpRequest(); request.addEventListener('progress', function (evt) { self._updateProgress(evt); }, false); request.open('GET', this.url, true); request.responseType = 'arraybuffer'; request.onload = function () { if (request.status === 200) { // on sucess loading file: ac.decodeAudioData(request.response, // success decoding buffer: function (buff) { self.buffer = buff; self.panner.inputChannels(buff.numberOfChannels); if (callback) { callback(self); } }, // error decoding buffer. "e" is undefined in Chrome 11/22/2015 function () { var err = new CustomError('decodeAudioData', errorTrace, self.url); var msg = 'AudioContext error at decodeAudioData for ' + self.url; if (errorCallback) { err.msg = msg; errorCallback(err); } else { console.error(msg + '\n The error stack trace includes: \n' + err.stack); } }); } else { var err = new CustomError('loadSound', errorTrace, self.url); var msg = 'Unable to load ' + self.url + '. The request status was: ' + request.status + ' (' + request.statusText + ')'; if (errorCallback) { err.message = msg; errorCallback(err); } else { console.error(msg + '\n The error stack trace includes: \n' + err.stack); } } }; // if there is another error, aside from 404... request.onerror = function () { var err = new CustomError('loadSound', errorTrace, self.url); var msg = 'There was no response from the server at ' + self.url + '. Check the url and internet connectivity.'; if (errorCallback) { err.message = msg; errorCallback(err); } else { console.error(msg + '\n The error stack trace includes: \n' + err.stack); } }; request.send(); } else if (this.file !== undefined) { var reader = new FileReader(); reader.onload = function () { ac.decodeAudioData(reader.result, function (buff) { self.buffer = buff; self.panner.inputChannels(buff.numberOfChannels); if (callback) { callback(self); } }); }; reader.onerror = function (e) { if (onerror) { onerror(e); } }; reader.readAsArrayBuffer(this.file); } }; // TO DO: use this method to create a loading bar that shows progress during file upload/decode. p5.SoundFile.prototype._updateProgress = function (evt) { if (evt.lengthComputable) { var percentComplete = evt.loaded / evt.total * 0.99; this._whileLoading(percentComplete, evt); } else { // Unable to compute progress information since the total size is unknown this._whileLoading('size unknown'); } }; /** * Returns true if the sound file finished loading successfully. * * @method isLoaded * @return {Boolean} */ p5.SoundFile.prototype.isLoaded = function () { if (this.buffer) { return true; } else { return false; } }; /** * Play the p5.SoundFile * * @method play * @param {Number} [startTime] (optional) schedule playback to start (in seconds from now). * @param {Number} [rate] (optional) playback rate * @param {Number} [amp] (optional) amplitude (volume) * of playback * @param {Number} [cueStart] (optional) cue start time in seconds * @param {Number} [duration] (optional) duration of playback in seconds */ p5.SoundFile.prototype.play = function (startTime, rate, amp, _cueStart, duration) { var self = this; var now = p5sound.audiocontext.currentTime; var cueStart, cueEnd; var time = startTime || 0; if (time < 0) { time = 0; } time = time + now; this.rate(rate); this.setVolume(amp); // TO DO: if already playing, create array of buffers for easy stop() if (this.buffer) { // reset the pause time (if it was paused) this._pauseTime = 0; // handle restart playmode if (this.mode === 'restart' && this.buffer && this.bufferSourceNode) { this.bufferSourceNode.stop(time); this._counterNode.stop(time); } // make a new source and counter. They are automatically assigned playbackRate and buffer this.bufferSourceNode = this._initSourceNode(); // garbage collect counterNode and create a new one if (this._counterNode) this._counterNode = undefined; this._counterNode = this._initCounterNode(); if (_cueStart) { if (_cueStart >= 0 && _cueStart < this.buffer.duration) { // this.startTime = cueStart; cueStart = _cueStart; } else { throw 'start time out of range'; } } else { cueStart = 0; } if (duration) { // if duration is greater than buffer.duration, just play entire file anyway rather than throw an error duration = duration <= this.buffer.duration - cueStart ? duration : this.buffer.duration; } else { duration = this.buffer.duration - cueStart; } this.bufferSourceNode.connect(this.output); // if it was paused, play at the pause position if (this._paused) { this.bufferSourceNode.start(time, this.pauseTime, duration); this._counterNode.start(time, this.pauseTime, duration); } else { this.bufferSourceNode.start(time, cueStart, duration); this._counterNode.start(time, cueStart, duration); } this._playing = true; this._paused = false; // add source to sources array, which is used in stopAll() this.bufferSourceNodes.push(this.bufferSourceNode); this.bufferSourceNode._arrayIndex = this.bufferSourceNodes.length - 1; // delete this.bufferSourceNode from the sources array when it is done playing: var clearOnEnd = function () { this._playing = false; this.removeEventListener('ended', clearOnEnd, false); // call the onended callback self._onended(self); self.bufferSourceNodes.forEach(function (n, i) { if (n._playing === false) { self.bufferSourceNodes.splice(i); } }); if (self.bufferSourceNodes.length === 0) { self._playing = false; } }; this.bufferSourceNode.onended = clearOnEnd; } else { throw 'not ready to play file, buffer has yet to load. Try preload()'; } // if looping, will restart at original time this.bufferSourceNode.loop = this._looping; this._counterNode.loop = this._looping; if (this._looping === true) { cueEnd = cueStart + duration; this.bufferSourceNode.loopStart = cueStart; this.bufferSourceNode.loopEnd = cueEnd; this._counterNode.loopStart = cueStart; this._counterNode.loopEnd = cueEnd; } }; /** * p5.SoundFile has two play modes: <code>restart</code> and * <code>sustain</code>. Play Mode determines what happens to a * p5.SoundFile if it is triggered while in the middle of playback. * In sustain mode, playback will continue simultaneous to the * new playback. In restart mode, play() will stop playback * and start over. Sustain is the default mode. * * @method playMode * @param {String} str 'restart' or 'sustain' * @example * <div><code> * function setup(){ * mySound = loadSound('assets/Damscray_DancingTiger.mp3'); * } * function mouseClicked() { * mySound.playMode('sustain'); * mySound.play(); * } * function keyPressed() { * mySound.playMode('restart'); * mySound.play(); * } * * </code></div> */ p5.SoundFile.prototype.playMode = function (str) { var s = str.toLowerCase(); // if restart, stop all other sounds from playing if (s === 'restart' && this.buffer && this.bufferSourceNode) { for (var i = 0; i < this.bufferSourceNodes.length - 1; i++) { var now = p5sound.audiocontext.currentTime; this.bufferSourceNodes[i].stop(now); } } // set play mode to effect future playback if (s === 'restart' || s === 'sustain') { this.mode = s; } else { throw 'Invalid play mode. Must be either "restart" or "sustain"'; } }; /** * Pauses a file that is currently playing. If the file is not * playing, then nothing will happen. * * After pausing, .play() will resume from the paused * position. * If p5.SoundFile had been set to loop before it was paused, * it will continue to loop after it is unpaused with .play(). * * @method pause * @param {Number} [startTime] (optional) schedule event to occur * seconds from now * @example * <div><code> * var soundFile; * * function preload() { * soundFormats('ogg', 'mp3'); * soundFile = loadSound('assets/Damscray_-_Dancing_Tiger_02.mp3'); * } * function setup() { * background(0, 255, 0); * soundFile.setVolume(0.1); * soundFile.loop(); * } * function keyTyped() { * if (key == 'p') { * soundFile.pause(); * background(255, 0, 0); * } * } * * function keyReleased() { * if (key == 'p') { * soundFile.play(); * background(0, 255, 0); * } * } * </code> * </div> */ p5.SoundFile.prototype.pause = function (startTime) { var now = p5sound.audiocontext.currentTime; var time = startTime || 0; var pTime = time + now; if (this.isPlaying() && this.buffer && this.bufferSourceNode) { this.pauseTime = this.currentTime(); this.bufferSourceNode.stop(pTime); this._counterNode.stop(pTime); this._paused = true; this._playing = false; this._pauseTime = this.currentTime(); } else { this._pauseTime = 0; } }; /** * Loop the p5.SoundFile. Accepts optional parameters to set the * playback rate, playback volume, loopStart, loopEnd. * * @method loop * @param {Number} [startTime] (optional) schedule event to occur * seconds from now * @param {Number} [rate] (optional) playback rate * @param {Number} [amp] (optional) playback volume * @param {Number} [cueLoopStart](optional) startTime in seconds * @param {Number} [duration] (optional) loop duration in seconds */ p5.SoundFile.prototype.loop = function (startTime, rate, amp, loopStart, duration) { this._looping = true; this.play(startTime, rate, amp, loopStart, duration); }; /** * Set a p5.SoundFile's looping flag to true or false. If the sound * is currently playing, this change will take effect when it * reaches the end of the current playback. * * @param {Boolean} Boolean set looping to true or false */ p5.SoundFile.prototype.setLoop = function (bool) { if (bool === true) { this._looping = true; } else if (bool === false) { this._looping = false; } else { throw 'Error: setLoop accepts either true or false'; } if (this.bufferSourceNode) { this.bufferSourceNode.loop = this._looping; this._counterNode.loop = this._looping; } }; /** * Returns 'true' if a p5.SoundFile is currently looping and playing, 'false' if not. * * @return {Boolean} */ p5.SoundFile.prototype.isLooping = function () { if (!this.bufferSourceNode) { return false; } if (this._looping === true && this.isPlaying() === true) { return true; } return false; }; /** * Returns true if a p5.SoundFile is playing, false if not (i.e. * paused or stopped). * * @method isPlaying * @return {Boolean} */ p5.SoundFile.prototype.isPlaying = function () { return this._playing; }; /** * Returns true if a p5.SoundFile is paused, false if not (i.e. * playing or stopped). * * @method isPaused * @return {Boolean} */ p5.SoundFile.prototype.isPaused = function () { return this._paused; }; /** * Stop soundfile playback. * * @method stop * @param {Number} [startTime] (optional) schedule event to occur * in seconds from now */ p5.SoundFile.prototype.stop = function (timeFromNow) { var time = timeFromNow || 0; if (this.mode === 'sustain') { this.stopAll(time); this._playing = false; this.pauseTime = 0; this._paused = false; } else if (this.buffer && this.bufferSourceNode) { var now = p5sound.audiocontext.currentTime; var t = time || 0; this.pauseTime = 0; this.bufferSourceNode.stop(now + t); this._counterNode.stop(now + t); this._playing = false; this._paused = false; } }; /** * Stop playback on all of this soundfile's sources. * @private */ p5.SoundFile.prototype.stopAll = function (_time) { var now = p5sound.audiocontext.currentTime; var time = _time || 0; if (this.buffer && this.bufferSourceNode) { for (var i = 0; i < this.bufferSourceNodes.length; i++) { if (typeof this.bufferSourceNodes[i] !== undefined) { try { this.bufferSourceNodes[i].onended = function () { }; this.bufferSourceNodes[i].stop(now + time); } catch (e) { } } } this._counterNode.stop(now + time); this._onended(this); } }; /** * Multiply the output volume (amplitude) of a sound file * between 0.0 (silence) and 1.0 (full volume). * 1.0 is the maximum amplitude of a digital sound, so multiplying * by greater than 1.0 may cause digital distortion. To * fade, provide a <code>rampTime</code> parameter. For more * complex fades, see the Env class. * * Alternately, you can pass in a signal source such as an * oscillator to modulate the amplitude with an audio signal. * * @method setVolume * @param {Number|Object} volume Volume (amplitude) between 0.0 * and 1.0 or modulating signal/oscillator * @param {Number} [rampTime] Fade for t seconds * @param {Number} [timeFromN