diff --git a/src/app.js b/src/app.js index 33605893..ae2adaec 100644 --- a/src/app.js +++ b/src/app.js @@ -64,7 +64,13 @@ p5.Panner3D = Panner3D; import Delay from './delay'; p5.Delay = Delay; -import './reverb'; + +import { Reverb, Convolver, createConvolver } from './reverb'; +p5.Reverb = Reverb; +p5.Convolver = Convolver; +p5.prototype.createConvolver = createConvolver; +p5.prototype.registerPreloadMethod('createConvolver', p5.prototype); + import Metro from './metro'; p5.Metro = Metro; @@ -84,11 +90,13 @@ import Compressor from './compressor'; p5.Compressor = Compressor; + import peakDetect from './peakDetect'; p5.peakDetect = peakDetect; + import SoundRecorder from './soundRecorder'; p5.SoundRecorder = SoundRecorder; diff --git a/src/reverb.js b/src/reverb.js index b7e4b756..b66ab6f0 100644 --- a/src/reverb.js +++ b/src/reverb.js @@ -1,3 +1,4 @@ +import { getAudioContext } from './audiocontext'; import CustomError from './errorHandler'; import Effect from './effect'; @@ -53,156 +54,155 @@ import Effect from './effect'; * */ -p5.Reverb = function () { - Effect.call(this); +class Reverb extends Effect { + constructor() { + super(); + this._initConvolverNode(); - this._initConvolverNode(); + // otherwise, Safari distorts + this.input.gain.value = 0.5; - // otherwise, Safari distorts - this.input.gain.value = 0.5; - - // default params - this._seconds = 3; - this._decay = 2; - this._reverse = false; - - this._buildImpulse(); -}; - -p5.Reverb.prototype = Object.create(Effect.prototype); - -p5.Reverb.prototype._initConvolverNode = function () { - this.convolverNode = this.ac.createConvolver(); - this.input.connect(this.convolverNode); - this.convolverNode.connect(this.wet); -}; + // default params + this._seconds = 3; + this._decay = 2; + this._reverse = false; -p5.Reverb.prototype._teardownConvolverNode = function () { - if (this.convolverNode) { - this.convolverNode.disconnect(); - delete this.convolverNode; + this._buildImpulse(); } -}; -p5.Reverb.prototype._setBuffer = function (audioBuffer) { - this._teardownConvolverNode(); - this._initConvolverNode(); - this.convolverNode.buffer = audioBuffer; -}; -/** - * Connect a source to the reverb, and assign reverb parameters. - * - * @method process - * @for p5.Reverb - * @param {Object} src p5.sound / Web Audio object with a sound - * output. - * @param {Number} [seconds] Duration of the reverb, in seconds. - * Min: 0, Max: 10. Defaults to 3. - * @param {Number} [decayRate] Percentage of decay with each echo. - * Min: 0, Max: 100. Defaults to 2. - * @param {Boolean} [reverse] Play the reverb backwards or forwards. - */ -p5.Reverb.prototype.process = function (src, seconds, decayRate, reverse) { - src.connect(this.input); - var rebuild = false; - if (seconds) { - this._seconds = seconds; - rebuild = true; - } - if (decayRate) { - this._decay = decayRate; - } - if (reverse) { - this._reverse = reverse; - } - if (rebuild) { - this._buildImpulse(); + _initConvolverNode() { + this.convolverNode = this.ac.createConvolver(); + this.input.connect(this.convolverNode); + this.convolverNode.connect(this.wet); } -}; -/** - * Set the reverb settings. Similar to .process(), but without - * assigning a new input. - * - * @method set - * @for p5.Reverb - * @param {Number} [seconds] Duration of the reverb, in seconds. - * Min: 0, Max: 10. Defaults to 3. - * @param {Number} [decayRate] Percentage of decay with each echo. - * Min: 0, Max: 100. Defaults to 2. - * @param {Boolean} [reverse] Play the reverb backwards or forwards. - */ -p5.Reverb.prototype.set = function (seconds, decayRate, reverse) { - var rebuild = false; - if (seconds) { - this._seconds = seconds; - rebuild = true; + _teardownConvolverNode() { + if (this.convolverNode) { + this.convolverNode.disconnect(); + delete this.convolverNode; + } } - if (decayRate) { - this._decay = decayRate; + + _setBuffer(audioBuffer) { + this._teardownConvolverNode(); + this._initConvolverNode(); + this.convolverNode.buffer = audioBuffer; } - if (reverse) { - this._reverse = reverse; + /** + * Connect a source to the reverb, and assign reverb parameters. + * + * @method process + * @for p5.Reverb + * @param {Object} src p5.sound / Web Audio object with a sound + * output. + * @param {Number} [seconds] Duration of the reverb, in seconds. + * Min: 0, Max: 10. Defaults to 3. + * @param {Number} [decayRate] Percentage of decay with each echo. + * Min: 0, Max: 100. Defaults to 2. + * @param {Boolean} [reverse] Play the reverb backwards or forwards. + */ + process(src, seconds, decayRate, reverse) { + src.connect(this.input); + var rebuild = false; + if (seconds) { + this._seconds = seconds; + rebuild = true; + } + if (decayRate) { + this._decay = decayRate; + } + if (reverse) { + this._reverse = reverse; + } + if (rebuild) { + this._buildImpulse(); + } } - if (rebuild) { - this._buildImpulse(); + + /** + * Set the reverb settings. Similar to .process(), but without + * assigning a new input. + * + * @method set + * @for p5.Reverb + * @param {Number} [seconds] Duration of the reverb, in seconds. + * Min: 0, Max: 10. Defaults to 3. + * @param {Number} [decayRate] Percentage of decay with each echo. + * Min: 0, Max: 100. Defaults to 2. + * @param {Boolean} [reverse] Play the reverb backwards or forwards. + */ + set(seconds, decayRate, reverse) { + var rebuild = false; + if (seconds) { + this._seconds = seconds; + rebuild = true; + } + if (decayRate) { + this._decay = decayRate; + } + if (reverse) { + this._reverse = reverse; + } + if (rebuild) { + this._buildImpulse(); + } } -}; -// DocBlocks for methods inherited from p5.Effect -/** - * Set the output level of the reverb effect. - * - * @method amp - * @for p5.Reverb - * @param {Number} volume amplitude between 0 and 1.0 - * @param {Number} [rampTime] create a fade that lasts rampTime - * @param {Number} [timeFromNow] schedule this event to happen - * seconds from now - */ -/** - * Send output to a p5.sound or web audio object - * - * @method connect - * @for p5.Reverb - * @param {Object} unit - */ -/** - * Disconnect all output. - * - * @method disconnect - * @for p5.Reverb - */ + // DocBlocks for methods inherited from p5.Effect + /** + * Set the output level of the reverb effect. + * + * @method amp + * @for p5.Reverb + * @param {Number} volume amplitude between 0 and 1.0 + * @param {Number} [rampTime] create a fade that lasts rampTime + * @param {Number} [timeFromNow] schedule this event to happen + * seconds from now + */ + /** + * Send output to a p5.sound or web audio object + * + * @method connect + * @for p5.Reverb + * @param {Object} unit + */ + /** + * Disconnect all output. + * + * @method disconnect + * @for p5.Reverb + */ -/** - * Inspired by Simple Reverb by Jordan Santell - * https://github.com/web-audio-components/simple-reverb/blob/master/index.js - * - * Utility function for building an impulse response - * based on the module parameters. - * - * @private - */ -p5.Reverb.prototype._buildImpulse = function () { - var rate = this.ac.sampleRate; - var length = rate * this._seconds; - var decay = this._decay; - var impulse = this.ac.createBuffer(2, length, rate); - var impulseL = impulse.getChannelData(0); - var impulseR = impulse.getChannelData(1); - var n, i; - for (i = 0; i < length; i++) { - n = this._reverse ? length - i : i; - impulseL[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay); - impulseR[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay); + /** + * Inspired by Simple Reverb by Jordan Santell + * https://github.com/web-audio-components/simple-reverb/blob/master/index.js + * + * Utility function for building an impulse response + * based on the module parameters. + * + * @private + */ + _buildImpulse() { + var rate = this.ac.sampleRate; + var length = rate * this._seconds; + var decay = this._decay; + var impulse = this.ac.createBuffer(2, length, rate); + var impulseL = impulse.getChannelData(0); + var impulseR = impulse.getChannelData(1); + var n, i; + for (i = 0; i < length; i++) { + n = this._reverse ? length - i : i; + impulseL[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay); + impulseR[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / length, decay); + } + this._setBuffer(impulse); } - this._setBuffer(impulse); -}; -p5.Reverb.prototype.dispose = function () { - Effect.prototype.dispose.apply(this); - this._teardownConvolverNode(); -}; + dispose() { + super.dispose(); + this._teardownConvolverNode(); + } +} // ======================================================================= // *** p5.Convolver *** @@ -268,37 +268,280 @@ p5.Reverb.prototype.dispose = function () { * } * */ -p5.Convolver = function (path, callback, errorCallback) { - p5.Reverb.call(this); +class Convolver extends Reverb { + constructor(path, callback, errorCallback) { + super(); + /** + * Internally, the p5.Convolver uses the a + * + * Web Audio Convolver Node. + * + * @property {ConvolverNode} convolverNode + */ + this._initConvolverNode(); + + // otherwise, Safari distorts + this.input.gain.value = 0.5; + + if (path) { + this.impulses = []; + this._loadBuffer(path, callback, errorCallback); + } else { + // parameters + this._seconds = 3; + this._decay = 2; + this._reverse = false; + + this._buildImpulse(); + } + + /** + * If you load multiple impulse files using the .addImpulse method, + * they will be stored as Objects in this Array. Toggle between them + * with the toggleImpulse(id) method. + * + * @property {Array} impulses + * @for p5.Convolver + */ + this.impulses = []; + this.set = null; + } /** - * Internally, the p5.Convolver uses the a - * - * Web Audio Convolver Node. + * Private method to load a buffer as an Impulse Response, + * assign it to the convolverNode, and add to the Array of .impulses. * - * @property {ConvolverNode} convolverNode + * @param {String} path + * @param {Function} callback + * @param {Function} errorCallback + * @private */ - this._initConvolverNode(); + _loadBuffer(_path, callback, errorCallback) { + var path = p5.prototype._checkFileFormats(_path); + var self = this; + var errorTrace = new Error().stack; + var ac = getAudioContext(); + + var request = new XMLHttpRequest(); + request.open('GET', path, true); + request.responseType = 'arraybuffer'; + + request.onload = function () { + if (request.status === 200) { + // on success loading file: + ac.decodeAudioData( + request.response, + function (buff) { + var buffer = {}; + var chunks = path.split('/'); + buffer.name = chunks[chunks.length - 1]; + buffer.audioBuffer = buff; + self.impulses.push(buffer); + self._setBuffer(buffer.audioBuffer); + if (callback) { + callback(buffer); + } + }, + // 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 + ); + } + } + ); + } + // if request status != 200, it failed + else { + var err = new CustomError('loadConvolver', 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 + ); + } + } + }; - // otherwise, Safari distorts - this.input.gain.value = 0.5; + // if there is another error, aside from 404... + request.onerror = function () { + var err = new CustomError('loadConvolver', errorTrace, self.url); + var msg = + 'There was no response from the server at ' + + self.url + + '. Check the url and internet connectivity.'; - if (path) { + if (errorCallback) { + err.message = msg; + errorCallback(err); + } else { + console.error( + msg + '\n The error stack trace includes: \n' + err.stack + ); + } + }; + request.send(); + } + + /** + * Connect a source to the convolver. + * + * @method process + * @for p5.Convolver + * @param {Object} src p5.sound / Web Audio object with a sound + * output. + * @example + *
+ * let cVerb, sound; + * function preload() { + * // We have both MP3 and OGG versions of all sound assets + * soundFormats('ogg', 'mp3'); + * + * // Try replacing 'bx-spring' with other soundfiles like + * // 'concrete-tunnel' 'small-plate' 'drum' 'beatbox' + * cVerb = createConvolver('assets/bx-spring.mp3'); + * + * // Try replacing 'Damscray_DancingTiger' with + * // 'beat', 'doorbell', lucky_dragons_-_power_melody' + * sound = loadSound('assets/Damscray_DancingTiger.mp3'); + * } + * + * function setup() { + * let cnv = createCanvas(100, 100); + * cnv.mousePressed(playSound); + * background(220); + * text('tap to play', 20, 20); + * + * // disconnect from master output... + * sound.disconnect(); + * + * // ...and process with cVerb + * // so that we only hear the convolution + * cVerb.process(sound); + * } + * + * function playSound() { + * sound.play(); + * } + * + *
+ */ + process(src) { + src.connect(this.input); + } + + /** + * Load and assign a new Impulse Response to the p5.Convolver. + * The impulse is added to the .impulses array. Previous + * impulses can be accessed with the .toggleImpulse(id) + * method. + * + * @method addImpulse + * @for p5.Convolver + * @param {String} path path to a sound file + * @param {Function} callback function (optional) + * @param {Function} errorCallback function (optional) + */ + addImpulse(path, callback, errorCallback) { + // if loading locally without a server + if ( + window.location.origin.indexOf('file://') > -1 && + window.cordova === 'undefined' + ) { + alert( + 'This sketch may require a server to load external files. Please see http://bit.ly/1qcInwS' + ); + } + this._loadBuffer(path, callback, errorCallback); + } + + /** + * Similar to .addImpulse, except that the .impulses + * Array is reset to save memory. A new .impulses + * array is created with this impulse as the only item. + * + * @method resetImpulse + * @for p5.Convolver + * @param {String} path path to a sound file + * @param {Function} callback function (optional) + * @param {Function} errorCallback function (optional) + */ + resetImpulse(path, callback, errorCallback) { + // if loading locally without a server + if ( + window.location.origin.indexOf('file://') > -1 && + window.cordova === 'undefined' + ) { + alert( + 'This sketch may require a server to load external files. Please see http://bit.ly/1qcInwS' + ); + } this.impulses = []; this._loadBuffer(path, callback, errorCallback); - } else { - // parameters - this._seconds = 3; - this._decay = 2; - this._reverse = false; + } - this._buildImpulse(); + /** + * If you have used .addImpulse() to add multiple impulses + * to a p5.Convolver, then you can use this method to toggle between + * the items in the .impulses Array. Accepts a parameter + * to identify which impulse you wish to use, identified either by its + * original filename (String) or by its position in the .impulses + * Array (Number).
+ * You can access the objects in the .impulses Array directly. Each + * Object has two attributes: an .audioBuffer (type: + * Web Audio + * AudioBuffer) and a .name, a String that corresponds + * with the original filename. + * + * @method toggleImpulse + * @for p5.Convolver + * @param {String|Number} id Identify the impulse by its original filename + * (String), or by its position in the + * .impulses Array (Number). + */ + toggleImpulse(id) { + if (typeof id === 'number' && id < this.impulses.length) { + this._setBuffer(this.impulses[id].audioBuffer); + } + if (typeof id === 'string') { + for (var i = 0; i < this.impulses.length; i++) { + if (this.impulses[i].name === id) { + this._setBuffer(this.impulses[i].audioBuffer); + break; + } + } + } } -}; -p5.Convolver.prototype = Object.create(p5.Reverb.prototype); + dispose() { + super.dispose(); -p5.prototype.registerPreloadMethod('createConvolver', p5.prototype); + // remove all the Impulse Response buffers + for (var i in this.impulses) { + if (this.impulses[i]) { + this.impulses[i] = null; + } + } + } +} /** * Create a p5.Convolver. Accepts a path to a soundfile @@ -349,7 +592,7 @@ p5.prototype.registerPreloadMethod('createConvolver', p5.prototype); * } * */ -p5.prototype.createConvolver = function (path, callback, errorCallback) { +function createConvolver(path, callback, errorCallback) { // if loading locally without a server if ( window.location.origin.indexOf('file://') > -1 && @@ -360,7 +603,7 @@ p5.prototype.createConvolver = function (path, callback, errorCallback) { ); } var self = this; - var cReverb = new p5.Convolver( + var cReverb = new Convolver( path, function (buffer) { if (typeof callback === 'function') { @@ -375,249 +618,6 @@ p5.prototype.createConvolver = function (path, callback, errorCallback) { ); cReverb.impulses = []; return cReverb; -}; - -/** - * Private method to load a buffer as an Impulse Response, - * assign it to the convolverNode, and add to the Array of .impulses. - * - * @param {String} path - * @param {Function} callback - * @param {Function} errorCallback - * @private - */ -p5.Convolver.prototype._loadBuffer = function (_path, callback, errorCallback) { - var path = p5.prototype._checkFileFormats(_path); - var self = this; - var errorTrace = new Error().stack; - var ac = p5.prototype.getAudioContext(); - - var request = new XMLHttpRequest(); - request.open('GET', path, true); - request.responseType = 'arraybuffer'; - - request.onload = function () { - if (request.status === 200) { - // on success loading file: - ac.decodeAudioData( - request.response, - function (buff) { - var buffer = {}; - var chunks = path.split('/'); - buffer.name = chunks[chunks.length - 1]; - buffer.audioBuffer = buff; - self.impulses.push(buffer); - self._setBuffer(buffer.audioBuffer); - if (callback) { - callback(buffer); - } - }, - // 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 - ); - } - } - ); - } - // if request status != 200, it failed - else { - var err = new CustomError('loadConvolver', 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('loadConvolver', 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(); -}; - -p5.Convolver.prototype.set = null; - -/** - * Connect a source to the convolver. - * - * @method process - * @for p5.Convolver - * @param {Object} src p5.sound / Web Audio object with a sound - * output. - * @example - *
- * let cVerb, sound; - * function preload() { - * // We have both MP3 and OGG versions of all sound assets - * soundFormats('ogg', 'mp3'); - * - * // Try replacing 'bx-spring' with other soundfiles like - * // 'concrete-tunnel' 'small-plate' 'drum' 'beatbox' - * cVerb = createConvolver('assets/bx-spring.mp3'); - * - * // Try replacing 'Damscray_DancingTiger' with - * // 'beat', 'doorbell', lucky_dragons_-_power_melody' - * sound = loadSound('assets/Damscray_DancingTiger.mp3'); - * } - * - * function setup() { - * let cnv = createCanvas(100, 100); - * cnv.mousePressed(playSound); - * background(220); - * text('tap to play', 20, 20); - * - * // disconnect from master output... - * sound.disconnect(); - * - * // ...and process with cVerb - * // so that we only hear the convolution - * cVerb.process(sound); - * } - * - * function playSound() { - * sound.play(); - * } - * - *
- */ -p5.Convolver.prototype.process = function (src) { - src.connect(this.input); -}; - -/** - * If you load multiple impulse files using the .addImpulse method, - * they will be stored as Objects in this Array. Toggle between them - * with the toggleImpulse(id) method. - * - * @property {Array} impulses - * @for p5.Convolver - */ -p5.Convolver.prototype.impulses = []; +} -/** - * Load and assign a new Impulse Response to the p5.Convolver. - * The impulse is added to the .impulses array. Previous - * impulses can be accessed with the .toggleImpulse(id) - * method. - * - * @method addImpulse - * @for p5.Convolver - * @param {String} path path to a sound file - * @param {Function} callback function (optional) - * @param {Function} errorCallback function (optional) - */ -p5.Convolver.prototype.addImpulse = function (path, callback, errorCallback) { - // if loading locally without a server - if ( - window.location.origin.indexOf('file://') > -1 && - window.cordova === 'undefined' - ) { - alert( - 'This sketch may require a server to load external files. Please see http://bit.ly/1qcInwS' - ); - } - this._loadBuffer(path, callback, errorCallback); -}; - -/** - * Similar to .addImpulse, except that the .impulses - * Array is reset to save memory. A new .impulses - * array is created with this impulse as the only item. - * - * @method resetImpulse - * @for p5.Convolver - * @param {String} path path to a sound file - * @param {Function} callback function (optional) - * @param {Function} errorCallback function (optional) - */ -p5.Convolver.prototype.resetImpulse = function (path, callback, errorCallback) { - // if loading locally without a server - if ( - window.location.origin.indexOf('file://') > -1 && - window.cordova === 'undefined' - ) { - alert( - 'This sketch may require a server to load external files. Please see http://bit.ly/1qcInwS' - ); - } - this.impulses = []; - this._loadBuffer(path, callback, errorCallback); -}; - -/** - * If you have used .addImpulse() to add multiple impulses - * to a p5.Convolver, then you can use this method to toggle between - * the items in the .impulses Array. Accepts a parameter - * to identify which impulse you wish to use, identified either by its - * original filename (String) or by its position in the .impulses - * Array (Number).
- * You can access the objects in the .impulses Array directly. Each - * Object has two attributes: an .audioBuffer (type: - * Web Audio - * AudioBuffer) and a .name, a String that corresponds - * with the original filename. - * - * @method toggleImpulse - * @for p5.Convolver - * @param {String|Number} id Identify the impulse by its original filename - * (String), or by its position in the - * .impulses Array (Number). - */ -p5.Convolver.prototype.toggleImpulse = function (id) { - if (typeof id === 'number' && id < this.impulses.length) { - this._setBuffer(this.impulses[id].audioBuffer); - } - if (typeof id === 'string') { - for (var i = 0; i < this.impulses.length; i++) { - if (this.impulses[i].name === id) { - this._setBuffer(this.impulses[i].audioBuffer); - break; - } - } - } -}; - -p5.Convolver.prototype.dispose = function () { - p5.Reverb.prototype.dispose.apply(this); - - // remove all the Impulse Response buffers - for (var i in this.impulses) { - if (this.impulses[i]) { - this.impulses[i] = null; - } - } -}; +export { Reverb, Convolver, createConvolver };