|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +define(function (require) { |
| 4 | + var p5sound = require('master'); |
| 5 | + var Clock = require('Tone/core/Clock'); |
| 6 | + |
| 7 | + /** |
| 8 | + * SoundLoop |
| 9 | + * |
| 10 | + * @class p5.SoundLoop |
| 11 | + * @constructor |
| 12 | + * |
| 13 | + * @param {Function} callback this function will be called on each iteration of theloop |
| 14 | + * @param {Number or String} [interval] amount of time or beats for each iteration of the loop |
| 15 | + * defaults to 1 |
| 16 | + * |
| 17 | + * @example |
| 18 | + * <div><code> |
| 19 | + * var click; |
| 20 | + * var looper1; |
| 21 | + * |
| 22 | + * function preload() { |
| 23 | + * click = loadSound('assets/drum.mp3' |
| 24 | + * } |
| 25 | + * |
| 26 | + * function setup() { |
| 27 | + * //the looper's callback is passed the timeFromNow |
| 28 | + * //this value should be used as a reference point from |
| 29 | + * //which to schedule sounds |
| 30 | + * looper1 = new p5.SoundLoop(function(timeFromNow){ |
| 31 | + * click.play(timeFromNow); |
| 32 | + * background(255 * (looper1.iterations % 2)); |
| 33 | + * }, 2); |
| 34 | + * |
| 35 | + * //stop after 10 iteratios; |
| 36 | + * looper1.maxIterations = 10; |
| 37 | + * //start the loop |
| 38 | + * looper1.start(); |
| 39 | + * } |
| 40 | + * </code></div> |
| 41 | + */ |
| 42 | + p5.SoundLoop = function(callback, interval) { |
| 43 | + this.callback = callback; |
| 44 | + /** |
| 45 | + * musicalTimeMode uses <a href = "https://github.com/Tonejs/Tone.js/wiki/Time">Tone.Time</a> convention |
| 46 | + * @property {Boolean} musicalTimeMode true if string, false if number |
| 47 | + */ |
| 48 | + this.musicalTimeMode = typeof this._interval === 'number' ? false : true; |
| 49 | + |
| 50 | + this._interval = interval || 1; |
| 51 | + |
| 52 | + /** |
| 53 | + * musicalTimeMode variables |
| 54 | + * modify these only when the interval is specified in musicalTime format as a string |
| 55 | + * @property {Number} [BPM] beats per minute (defaults to 60) |
| 56 | + * @property {Number} [timeSignature] number of quarter notes in a measure (defaults to 4) |
| 57 | + */ |
| 58 | + this._timeSignature = 4; |
| 59 | + this._bpm = 60; |
| 60 | + |
| 61 | + this.isPlaying = false; |
| 62 | + |
| 63 | + /** |
| 64 | + * Set a limit to the number of loops to play |
| 65 | + * @property {Number} [maxIterations] defaults to Infinity |
| 66 | + */ |
| 67 | + this.maxIterations = Infinity; |
| 68 | + var self = this; |
| 69 | + |
| 70 | + this.clock = new Clock({ |
| 71 | + 'callback' : function(time) { |
| 72 | + var timeFromNow = time - p5sound.audiocontext.currentTime; |
| 73 | + /** |
| 74 | + * Do not initiate the callback if timeFromNow is < 0 |
| 75 | + * This ususually occurs for a few milliseconds when the page |
| 76 | + * is not fully loaded |
| 77 | + * |
| 78 | + * The callback should only be called until maxIterations is reached |
| 79 | + */ |
| 80 | + if (timeFromNow > 0 && self.iterations <= self.maxIterations) { |
| 81 | + self.callback(timeFromNow);} |
| 82 | + }, |
| 83 | + 'frequency' : this._calcFreq() |
| 84 | + }); |
| 85 | + }; |
| 86 | + |
| 87 | + /** |
| 88 | + * Start the loop |
| 89 | + * @method start |
| 90 | + * @param {Number} [timeFromNow] schedule a starting time |
| 91 | + */ |
| 92 | + p5.SoundLoop.prototype.start = function(timeFromNow) { |
| 93 | + var t = timeFromNow || 0; |
| 94 | + var now = p5sound.audiocontext.currentTime; |
| 95 | + if (!this.isPlaying) { |
| 96 | + this.clock.start(now + t); |
| 97 | + this.isPlaying = true; |
| 98 | + } |
| 99 | + }; |
| 100 | + |
| 101 | + /** |
| 102 | + * Stop the loop |
| 103 | + * @method stop |
| 104 | + * @param {Number} [timeFromNow] schedule a stopping time |
| 105 | + */ |
| 106 | + p5.SoundLoop.prototype.stop = function(timeFromNow) { |
| 107 | + var t = timeFromNow || 0; |
| 108 | + var now = p5sound.audiocontext.currentTime; |
| 109 | + if (this.isPlaying) { |
| 110 | + this.clock.stop(now + t); |
| 111 | + this.isPlaying = false; |
| 112 | + } |
| 113 | + }; |
| 114 | + /** |
| 115 | + * Pause the loop |
| 116 | + * @method pause |
| 117 | + * @param {Number} [timeFromNow] schedule a pausing time |
| 118 | + */ |
| 119 | + p5.SoundLoop.prototype.pause = function(timeFromNow) { |
| 120 | + var t = timeFromNow || 0; |
| 121 | + if (this.isPlaying) { |
| 122 | + this.clock.pause(t); |
| 123 | + this.isPlaying = false; |
| 124 | + } |
| 125 | + }; |
| 126 | + |
| 127 | + |
| 128 | + /** |
| 129 | + * Synchronize loops. Use this method to start two more more loops in synchronization |
| 130 | + * or to start a loop in synchronization with a loop that is already playing |
| 131 | + * This method will schedule the implicit loop in sync with the explicit master loop |
| 132 | + * i.e. loopToStart.syncedStart(loopToSyncWith) |
| 133 | + * |
| 134 | + * @method syncedStart |
| 135 | + * @param {Object} otherLoop a p5.SoundLoop to sync with |
| 136 | + * @param {Number} [timeFromNow] Start the loops in sync after timeFromNow seconds |
| 137 | + */ |
| 138 | + p5.SoundLoop.prototype.syncedStart = function(otherLoop, timeFromNow) { |
| 139 | + var t = timeFromNow || 0; |
| 140 | + var now = p5sound.audiocontext.currentTime; |
| 141 | + |
| 142 | + if (!otherLoop.isPlaying) { |
| 143 | + otherLoop.clock.start(now + t); |
| 144 | + otherLoop.isPlaying = true; |
| 145 | + this.clock.start(now + t); |
| 146 | + this.isPlaying = true; |
| 147 | + } else if (otherLoop.isPlaying) { |
| 148 | + var time = otherLoop.clock._nextTick - p5sound.audiocontext.currentTime; |
| 149 | + this.clock.start(now + time); |
| 150 | + this.isPlaying = true; |
| 151 | + } |
| 152 | + }; |
| 153 | + |
| 154 | + |
| 155 | + /** |
| 156 | + * Updates frequency value, reflected in next callback |
| 157 | + * @private |
| 158 | + * @method _update |
| 159 | + */ |
| 160 | + p5.SoundLoop.prototype._update = function() { |
| 161 | + this.clock.frequency.value = this._calcFreq(); |
| 162 | + }; |
| 163 | + |
| 164 | + /** |
| 165 | + * Calculate the frequency of the clock's callback based on bpm, interval, and timesignature |
| 166 | + * @private |
| 167 | + * @method _calcFreq |
| 168 | + * @return {Number} new clock frequency value |
| 169 | + */ |
| 170 | + p5.SoundLoop.prototype._calcFreq = function() { |
| 171 | + //Seconds mode, bpm / timesignature has no effect |
| 172 | + if (typeof this._interval === 'number') { |
| 173 | + this.musicalTimeMode = false; |
| 174 | + return 1 / this._interval; |
| 175 | + } |
| 176 | + //Musical timing mode, calculate interval based bpm, interval,and time signature |
| 177 | + else if (typeof this._interval === 'string') { |
| 178 | + this.musicalTimeMode = true; |
| 179 | + return this._bpm / 60 / this._convertNotation(this._interval) * (this._timeSignature / 4); |
| 180 | + } |
| 181 | + }; |
| 182 | + |
| 183 | + /** |
| 184 | + * Convert notation from musical time format to seconds |
| 185 | + * Uses <a href = "https://github.com/Tonejs/Tone.js/wiki/Time">Tone.Time</a> convention |
| 186 | + * @private |
| 187 | + * @method _convertNotation |
| 188 | + * @param {String} value value to be converted |
| 189 | + * @return {Number} converted value in seconds |
| 190 | + */ |
| 191 | + p5.SoundLoop.prototype._convertNotation = function(value) { |
| 192 | + var type = value.slice(-1); |
| 193 | + value = Number(value.slice(0,-1)); |
| 194 | + switch (type) { |
| 195 | + case 'm': |
| 196 | + return this._measure(value); |
| 197 | + case 'n': |
| 198 | + return this._note(value); |
| 199 | + default: |
| 200 | + console.warn('Specified interval is not formatted correctly. See Tone.js '+ |
| 201 | + 'timing reference for more info: https://github.com/Tonejs/Tone.js/wiki/Time'); |
| 202 | + } |
| 203 | + }; |
| 204 | + |
| 205 | + /** |
| 206 | + * Helper conversion methods of measure and note |
| 207 | + * @private |
| 208 | + * @method _measure |
| 209 | + * @method _note |
| 210 | + */ |
| 211 | + p5.SoundLoop.prototype._measure = function(value) { |
| 212 | + return value * this._timeSignature; |
| 213 | + }; |
| 214 | + p5.SoundLoop.prototype._note = function(value) { |
| 215 | + return this._timeSignature / value ; |
| 216 | + }; |
| 217 | + |
| 218 | + |
| 219 | + /** |
| 220 | + * Getters and Setters, setting any paramter will result in a change in the clock's |
| 221 | + * frequency, that will be reflected after the next callback |
| 222 | + * @param {Number} bpm |
| 223 | + * @param {Number} timeSignature |
| 224 | + * @param {Number/String} interval length of the loops interval |
| 225 | + * @param @readOnly {Number} iteations how many times the callback has been called so far |
| 226 | + * |
| 227 | + */ |
| 228 | + Object.defineProperty(p5.SoundLoop.prototype, 'bpm', { |
| 229 | + get : function() { |
| 230 | + return this._bpm; |
| 231 | + }, |
| 232 | + set : function(bpm) { |
| 233 | + if (!this.musicalTimeMode) { |
| 234 | + console.warn('Changing the BPM in "seconds" mode has no effect. '+ |
| 235 | + 'BPM is only relevant in musicalTimeMode '+ |
| 236 | + 'when the interval is specified as a string '+ |
| 237 | + '("2n", "4n", "1m"...etc)'); |
| 238 | + } |
| 239 | + this._bpm = bpm; |
| 240 | + this._update(); |
| 241 | + } |
| 242 | + }); |
| 243 | + |
| 244 | + Object.defineProperty(p5.SoundLoop.prototype, 'timeSignature', { |
| 245 | + get : function() { |
| 246 | + return this._timeSignature; |
| 247 | + }, |
| 248 | + set : function(timeSig) { |
| 249 | + if (!this.musicalTimeMode) { |
| 250 | + console.warn('Changing the timeSignature in "seconds" mode has no effect. '+ |
| 251 | + 'BPM is only relevant in musicalTimeMode '+ |
| 252 | + 'when the interval is specified as a string '+ |
| 253 | + '("2n", "4n", "1m"...etc)'); |
| 254 | + } |
| 255 | + this._timeSignature = timeSig; |
| 256 | + this._update(); |
| 257 | + } |
| 258 | + }); |
| 259 | + |
| 260 | + Object.defineProperty(p5.SoundLoop.prototype, 'interval', { |
| 261 | + get : function() { |
| 262 | + return this._interval; |
| 263 | + }, |
| 264 | + set : function(interval) { |
| 265 | + this.musicalTimeMode = typeof interval === 'Number'? false : true; |
| 266 | + this._interval = interval; |
| 267 | + this._update(); |
| 268 | + } |
| 269 | + }); |
| 270 | + |
| 271 | + Object.defineProperty(p5.SoundLoop.prototype, 'iterations', { |
| 272 | + get : function() { |
| 273 | + return this.clock.ticks; |
| 274 | + } |
| 275 | + }); |
| 276 | + |
| 277 | + return p5.SoundLoop; |
| 278 | +}); |
0 commit comments