├── .gitignore ├── Gruntfile.js ├── LICENSE ├── README.md ├── blip.js ├── blip.min.js ├── bower.json ├── karma.conf.js ├── package.json ├── src ├── blip.js ├── chain.js ├── clip.js ├── context-monkeypatch.js ├── context.js ├── envelope.js ├── index.js ├── intro.js ├── loop.js ├── node-collection.js ├── node.js ├── outro.js ├── random.js ├── sample-loader.js ├── time.js └── util.js └── test ├── chaining ├── test_chain_by_adding_individual_nodes.js └── test_chain_by_passing_array.js ├── test_simple_node_connections.js └── test_time_functions.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | wav/ 4 | index.html 5 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | 5 | pkg: grunt.file.readJSON('package.json'), 6 | 7 | connect: { 8 | server: { 9 | options: { 10 | port: 8080 11 | } 12 | } 13 | }, 14 | 15 | watch: { 16 | options: { 17 | livereload: true 18 | }, 19 | src: { 20 | options: { 21 | livereload: false 22 | }, 23 | files: 'src/*.js', 24 | tasks: ['smash'] 25 | }, 26 | output: { 27 | files: ['blip.js', 'index.html'] 28 | } 29 | }, 30 | 31 | smash: { 32 | together: { 33 | src: 'src/index.js', 34 | dest: './blip.js' 35 | } 36 | }, 37 | 38 | uglify: { 39 | dist: { 40 | files: { 41 | './blip.min.js': ['./blip.js'] 42 | } 43 | } 44 | } 45 | 46 | }); 47 | 48 | grunt.loadNpmTasks('grunt-contrib-connect'); 49 | grunt.loadNpmTasks('grunt-contrib-watch'); 50 | grunt.loadNpmTasks('grunt-smash'); 51 | grunt.loadNpmTasks('grunt-contrib-uglify'); 52 | 53 | grunt.registerTask('build', ['smash', 'uglify']); 54 | grunt.registerTask('preview', ['smash', 'uglify', 'connect', 'watch']); 55 | grunt.registerTask('default', ['build']); 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, John Shanley 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [blip](http://jshanley.github.io/blip/) 2 | 3 | **blip** is a lightweight JavaScript library that wraps the Web Audio API, abstracting away the AudioContext, and simplifying node creation and audio routing. It also provides some extremely powerful and flexible methods for looping and manipulating samples that allow for both temporal precision and musical expressiveness. 4 | 5 | > Visit the [**site**](http://jshanley.github.io/blip/) >> 6 | 7 | ## Getting Started 8 | 9 | > Browse the [**API Docs**](https://github.com/jshanley/blip/wiki/API-Documentation) >> 10 | 11 | ### Loading Samples 12 | 13 | *Blip helps you load samples asynchronously, and gives you a simple callback mechanism to ensure that your samples are ready to use.* 14 | 15 | ``` javascript 16 | blip.sampleLoader() 17 | .samples({ 18 | 'kick', 'path/to/your/kick_sound.wav', 19 | 'snare', 'path/to/your/snare_sound.wav', 20 | 'kazoo', 'path/to/your/kazoo_sound.wav' 21 | }) 22 | .done(callback) 23 | .load(); 24 | 25 | function callback() { 26 | // now your samples are available 27 | blip.sample('snare') // is an AudioBuffer 28 | } 29 | ``` 30 | 31 | ### Creating Clips 32 | 33 | *A clip is a wrapper for a sample, which handles creating and wiring up a BufferSource each time the sound is played.* 34 | 35 | ``` javascript 36 | var bassDrum = blip.clip() 37 | .sample('bassDrum'); 38 | 39 | // play the clip immediately 40 | bassDrum.play(0); 41 | 42 | // play the clip again in 5 seconds 43 | bassDrum.play(5); 44 | ``` 45 | 46 | ## Looping 47 | 48 | Blip enables you to create precise loops for playing samples, controlling audio parameters, or just about anything else you can think of by letting you deal directly with time, and providing a simple and elegant scheduling mechanism. 49 | 50 | A loop simply provides markers for points in time, to which you can assign arbitrary data, and fire playback events. 51 | 52 | These examples assume the variable `clip` is a blip clip. 53 | 54 | ### Basic Looping 55 | 56 | *A loop generates "ticks" at a specific tempo, and allows you to schedule events based on the time of each tick.* 57 | ``` javascript 58 | var monotonous = blip.loop() 59 | .tempo(110) 60 | .tick(function(t) { 61 | clip.play(t) 62 | }); 63 | 64 | monotonous.start(); 65 | ``` 66 | 67 | ### Better Looping 68 | 69 | *Loops can take an array of arbitrary data to loop over, and the current datum is passed as the second argument to the tick callback.* 70 | 71 | ``` javascript 72 | var rhythmic = blip.loop() 73 | .tempo(130) 74 | .data([1,0,1,1,0]) 75 | .tick(function(t,d) { 76 | if (d) { 77 | clip.play(t) 78 | } 79 | }); 80 | 81 | rhythmic.start(); 82 | ``` 83 | 84 | ### Awesome Looping 85 | 86 | *The data passed in can represent anything you want it to. In this case it is being used to set the playback rate of the clip.* 87 | 88 | ``` javascript 89 | var melodic = blip.loop() 90 | .tempo(120) 91 | .data([0.3,0.4,0.5,0.6]) 92 | .tick(function(t,d) { 93 | clip.play(t, { 'rate': d }); 94 | }) 95 | 96 | melodic.start(); 97 | ``` 98 | 99 | ### Add some randomness 100 | 101 | Blip provides helper functions to add elements of randomness and chance to your compositions. 102 | 103 | This loop has a 1/3 chance to play a clip on each tick, and assigns it a random rate between 0.2 and 1.4 104 | 105 | ``` javascript 106 | var entropic = blip.loop() 107 | .tempo(110) 108 | .tick(function(t,d) { 109 | if (blip.chance(1/3)) clip.play(t, { 'rate': blip.random(0.2, 1.4) }); 110 | }) 111 | 112 | entropic.start(); 113 | ``` 114 | 115 | Visit the [**site**](http://jshanley.github.io/blip/) for more examples. 116 | 117 | 118 | -------------------------------------------------------------------------------- /blip.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var blip = {}; 4 | 5 | blip.version = '0.3.1'; 6 | 7 | /* AudioContext-MonkeyPatch 8 | https://github.com/cwilso/AudioContext-MonkeyPatch 9 | 10 | Copyright 2013 Chris Wilson 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | */ 24 | (function (global, exports, perf) { 25 | 'use strict'; 26 | 27 | function fixSetTarget(param) { 28 | if (!param) // if NYI, just return 29 | return; 30 | if (!param.setTargetAtTime) 31 | param.setTargetAtTime = param.setTargetValueAtTime; 32 | } 33 | 34 | if (window.hasOwnProperty('webkitAudioContext') && 35 | !window.hasOwnProperty('AudioContext')) { 36 | window.AudioContext = webkitAudioContext; 37 | 38 | if (!AudioContext.prototype.hasOwnProperty('createGain')) 39 | AudioContext.prototype.createGain = AudioContext.prototype.createGainNode; 40 | if (!AudioContext.prototype.hasOwnProperty('createDelay')) 41 | AudioContext.prototype.createDelay = AudioContext.prototype.createDelayNode; 42 | if (!AudioContext.prototype.hasOwnProperty('createScriptProcessor')) 43 | AudioContext.prototype.createScriptProcessor = AudioContext.prototype.createJavaScriptNode; 44 | if (!AudioContext.prototype.hasOwnProperty('createPeriodicWave')) 45 | AudioContext.prototype.createPeriodicWave = AudioContext.prototype.createWaveTable; 46 | 47 | 48 | AudioContext.prototype.internal_createGain = AudioContext.prototype.createGain; 49 | AudioContext.prototype.createGain = function() { 50 | var node = this.internal_createGain(); 51 | fixSetTarget(node.gain); 52 | return node; 53 | }; 54 | 55 | AudioContext.prototype.internal_createDelay = AudioContext.prototype.createDelay; 56 | AudioContext.prototype.createDelay = function(maxDelayTime) { 57 | var node = maxDelayTime ? this.internal_createDelay(maxDelayTime) : this.internal_createDelay(); 58 | fixSetTarget(node.delayTime); 59 | return node; 60 | }; 61 | 62 | AudioContext.prototype.internal_createBufferSource = AudioContext.prototype.createBufferSource; 63 | AudioContext.prototype.createBufferSource = function() { 64 | var node = this.internal_createBufferSource(); 65 | if (!node.start) { 66 | node.start = function ( when, offset, duration ) { 67 | if ( offset || duration ) 68 | this.noteGrainOn( when, offset, duration ); 69 | else 70 | this.noteOn( when ); 71 | } 72 | } 73 | if (!node.stop) 74 | node.stop = node.noteOff; 75 | fixSetTarget(node.playbackRate); 76 | return node; 77 | }; 78 | 79 | AudioContext.prototype.internal_createDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor; 80 | AudioContext.prototype.createDynamicsCompressor = function() { 81 | var node = this.internal_createDynamicsCompressor(); 82 | fixSetTarget(node.threshold); 83 | fixSetTarget(node.knee); 84 | fixSetTarget(node.ratio); 85 | fixSetTarget(node.reduction); 86 | fixSetTarget(node.attack); 87 | fixSetTarget(node.release); 88 | return node; 89 | }; 90 | 91 | AudioContext.prototype.internal_createBiquadFilter = AudioContext.prototype.createBiquadFilter; 92 | AudioContext.prototype.createBiquadFilter = function() { 93 | var node = this.internal_createBiquadFilter(); 94 | fixSetTarget(node.frequency); 95 | fixSetTarget(node.detune); 96 | fixSetTarget(node.Q); 97 | fixSetTarget(node.gain); 98 | return node; 99 | }; 100 | 101 | if (AudioContext.prototype.hasOwnProperty( 'createOscillator' )) { 102 | AudioContext.prototype.internal_createOscillator = AudioContext.prototype.createOscillator; 103 | AudioContext.prototype.createOscillator = function() { 104 | var node = this.internal_createOscillator(); 105 | if (!node.start) 106 | node.start = node.noteOn; 107 | if (!node.stop) 108 | node.stop = node.noteOff; 109 | if (!node.setPeriodicWave) 110 | node.setPeriodicWave = node.setWaveTable; 111 | fixSetTarget(node.frequency); 112 | fixSetTarget(node.detune); 113 | return node; 114 | }; 115 | } 116 | } 117 | }(window)); 118 | 119 | /* END AudioContext-MonkeyPatch */ 120 | var ctx = new AudioContext(); 121 | 122 | 123 | function now() { 124 | return ctx.currentTime; 125 | } 126 | 127 | blip.time = {}; 128 | 129 | blip.time.now = function() { 130 | return now(); 131 | }; 132 | 133 | blip.time.in = function(t) { 134 | return now() + t; 135 | }; 136 | 137 | blip.time.seconds = function(t) { 138 | return t; 139 | }; 140 | blip.time.ms = function(t) { 141 | return t * 0.001; 142 | }; 143 | blip.time.samp = function(t) { 144 | return t / ctx.sampleRate; 145 | }; 146 | 147 | blip.chance = function(p) { 148 | var attempt = Math.random(); 149 | return attempt < p; 150 | }; 151 | 152 | blip.random = function(a,b) { 153 | switch(arguments.length) { 154 | case 0: 155 | return Math.random(); 156 | case 1: 157 | return Math.random() * a; 158 | case 2: 159 | return Math.random() * (b - a) + a; 160 | } 161 | }; 162 | 163 | /** 164 | * Generates a GUID string. 165 | * @returns {String} The generated GUID. 166 | * @example af8a8416-6e18-a307-bd9c-f2c947bbb3aa 167 | * @author Slavik Meltser (slavik@meltser.info). 168 | * @link http://slavik.meltser.info/?p=142 169 | */ 170 | function guid() { 171 | function _p8(s) { 172 | var p = (Math.random().toString(16)+"000000000").substr(2,8); 173 | return s ? "-" + p.substr(0,4) + "-" + p.substr(4,4) : p ; 174 | } 175 | return _p8() + _p8(true) + _p8(true) + _p8(); 176 | } 177 | 178 | // MIDI to Frequency 179 | blip.mtof = function(midi) { 180 | return Math.pow(2, (midi - 69) / 12) * 440; 181 | }; 182 | 183 | function BlipNodeCollection(nodes) { 184 | this.nodes = nodes || []; 185 | } 186 | 187 | BlipNodeCollection.prototype = { 188 | 189 | count: function() { 190 | return this.nodes.length; 191 | }, 192 | 193 | each: function(f) { 194 | for (var i = 0; i < this.nodes.length; i++) { 195 | f.call(this, this.nodes[i], i, this.nodes); 196 | } 197 | }, 198 | 199 | contains: function(node) { 200 | for (var i = 0; i < this.nodes.length; i++) { 201 | if (this.nodes[i] === node) return true; 202 | } 203 | return false; 204 | }, 205 | 206 | add: function(node) { 207 | if (this.nodes.indexOf(node) === -1) this.nodes.push(node); 208 | }, 209 | 210 | remove: function(node) { 211 | var index = this.nodes.indexOf(node); 212 | if (index !== -1) this.nodes.splice(index, 1); 213 | }, 214 | 215 | removeAll: function() { 216 | this.nodes = []; 217 | } 218 | 219 | }; 220 | 221 | 222 | // the associated functions will be used by the `createNode` function within `blip.node` 223 | var nodeTypes = { 224 | 'gain': ctx.createGain, 225 | 'delay': ctx.createDelay, 226 | 'panner': ctx.createPanner, 227 | 'convolver': ctx.createConvolver, 228 | 'analyser': ctx.createAnalyser, 229 | 'channelSplitter': ctx.createChannelSplitter, 230 | 'channelMerger': ctx.createChannelMerger, 231 | 'dynamicsCompressor': ctx.createDynamicsCompressor, 232 | 'biquadFilter': ctx.createBiquadFilter, 233 | 'waveShaper': ctx.createWaveShaper, 234 | 'oscillator': ctx.createOscillator, 235 | 'periodicWave': ctx.createPeriodicWave, 236 | 'bufferSource': ctx.createBufferSource, 237 | 'audioBufferSource': ctx.createBufferSource, // alias 238 | }; 239 | 240 | function BlipNode() { 241 | this.inputs = new BlipNodeCollection(); 242 | this.outputs = new BlipNodeCollection(); 243 | return this; 244 | }; 245 | 246 | BlipNode.prototype.connect = function(blipnode) { 247 | if (this.node().numberOfOutputs > 0 && blipnode.node().numberOfInputs > 0) { 248 | this.node().connect(blipnode.node()); 249 | this.outputs.add(blipnode); 250 | blipnode.inputs.add(this); 251 | } 252 | return this; 253 | }; 254 | 255 | BlipNode.prototype.disconnect = function(blipnode) { 256 | // disconnect all 257 | this.node().disconnect(); 258 | 259 | var me = this; 260 | 261 | if (blipnode) { 262 | this.outputs.remove(blipnode); 263 | blipnode.inputs.remove(this); 264 | 265 | // reconnect to remaining outputs 266 | this.outputs.each(function(n) { this.connect(n); }) 267 | } else { 268 | this.outputs.each(function(n) { 269 | n.inputs.remove(me); 270 | }); 271 | this.outputs.removeAll(); 272 | } 273 | 274 | return this; 275 | } 276 | 277 | BlipNode.prototype.prop = function(name, value) { 278 | if (arguments.length < 2) { 279 | if (typeof name === 'object') { 280 | for (var p in name) { 281 | this.node()[p] = name[p]; 282 | } 283 | return this; 284 | } else { 285 | return this.node()[name]; 286 | } 287 | } 288 | this.node()[name] = value; 289 | return this; 290 | }; 291 | 292 | BlipNode.prototype.param = function(name, f) { 293 | if (arguments.length < 2) return this.node()[name]; 294 | if (typeof f !== 'function') { 295 | this.node()[name].value = f; 296 | } else { 297 | f.call(this.node()[name]); 298 | } 299 | return this; 300 | }; 301 | 302 | BlipNode.prototype.start = function(t) { 303 | this.node().start.call(this.node(), t); 304 | }; 305 | 306 | BlipNode.prototype.stop = function(t) { 307 | this.node().stop.call(this.node(), t); 308 | }; 309 | 310 | BlipNode.prototype.node = function() { 311 | return this.node(); 312 | }; 313 | 314 | BlipNode.prototype.toString = function() { 315 | return '[object BlipNode]'; 316 | }; 317 | 318 | BlipNode.prototype.valueOf = function() { 319 | return this.id(); 320 | }; 321 | 322 | BlipNode.prototype.call = function(methodName) { 323 | var args = Array.prototype.slice.call(arguments, 1); 324 | var node = this.node(); 325 | if (typeof node[methodName] !== 'function') return; 326 | node[methodName].apply(node, args); 327 | }; 328 | 329 | blip.node = function(type) { 330 | 331 | var other_args = Array.prototype.slice.call(arguments, 1); 332 | 333 | var reference = createNode(type); 334 | 335 | var id = guid(); 336 | 337 | var node = new BlipNode(); 338 | 339 | function createNode(t) { 340 | return nodeTypes[t].apply(ctx, other_args); 341 | } 342 | 343 | node.node = function() { 344 | return reference; 345 | }; 346 | 347 | node.id = function() { 348 | return id; 349 | }; 350 | 351 | return node; 352 | 353 | } 354 | 355 | var specialBlipNode = function(ref) { 356 | var node = new BlipNode(); 357 | var id = guid(); 358 | node.node = function() { return ref; }; 359 | node.id = function() { return id; }; 360 | return node; 361 | } 362 | 363 | // special nodes 364 | blip.destination = specialBlipNode(ctx.destination); 365 | blip.listener = specialBlipNode(ctx.listener); 366 | blip.chain = function(nodes) { 367 | 368 | nodes = nodes || []; 369 | 370 | wire(); 371 | 372 | var chain = {}; 373 | 374 | function wire() { 375 | for (var i = 0; i < nodes.length-1; i++) { 376 | nodes[i].connect(nodes[i+1]); 377 | } 378 | } 379 | 380 | chain.node = function(blipnode) { 381 | nodes.push(blipnode); 382 | wire(); 383 | return chain; 384 | }; 385 | chain.start = function() { 386 | var a = nodes.slice(0,1); 387 | return a.length ? a[0] : null; 388 | }; 389 | chain.end = function() { 390 | var a = nodes.slice(-1); 391 | return a.length ? a[0] : null; 392 | }; 393 | chain.from = function(blipnode) { 394 | blipnode.connect(chain.start()); 395 | return chain; 396 | }; 397 | chain.to = function(blipnode) { 398 | chain.end().connect(blipnode); 399 | return chain; 400 | }; 401 | chain.wire = function() { 402 | wire(); 403 | return chain; 404 | } 405 | 406 | return chain; 407 | } 408 | 409 | 410 | /* 411 | Precise scheduling for audio events is 412 | based on the method described in this article by Chris Wilson: 413 | http://www.html5rocks.com/en/tutorials/audio/scheduling/ 414 | */ 415 | 416 | blip.loop = function() { 417 | 418 | var lookahead = 25.0, // ms 419 | scheduleAheadTime = 0.1; // s 420 | 421 | var tempo; // ticks per minute 422 | 423 | var tickInterval; // seconds per tick 424 | 425 | var data = []; 426 | 427 | var currentTick = 0, 428 | nextTickTime = 0; 429 | 430 | var tick = function(t, d, i) {}; 431 | var each = function(t, i) {}; 432 | 433 | var iterations = 0, 434 | limit = 0; 435 | 436 | var timer; 437 | 438 | function loop() {} 439 | 440 | function nextTick() { 441 | nextTickTime += tickInterval; 442 | 443 | // cycle through ticks 444 | if (++currentTick >= data.length) { 445 | currentTick = 0; 446 | iterations += 1; 447 | } 448 | 449 | } 450 | 451 | function scheduleTick(tickNum, time) { 452 | tick.call(loop, time, data[tickNum], tickNum); 453 | } 454 | 455 | function scheduleIteration(iterationNum, time) { 456 | each.call(loop, time, iterationNum); 457 | } 458 | 459 | function scheduler() { 460 | while (nextTickTime < ctx.currentTime + scheduleAheadTime) { 461 | scheduleTick(currentTick, nextTickTime); 462 | if (currentTick === 0) { 463 | scheduleIteration(iterations, nextTickTime); 464 | } 465 | nextTick(); 466 | if (limit && iterations >= limit) { 467 | loop.reset(); 468 | return; 469 | } 470 | } 471 | timer = window.setTimeout(scheduler, lookahead); 472 | } 473 | 474 | loop.tempo = function(bpm) { 475 | if (!arguments.length) return tempo; 476 | tempo = bpm; 477 | tickInterval = 60 / tempo; 478 | return loop; 479 | }; 480 | loop.tickInterval = function(s) { 481 | if (!arguments.length) return tickInterval; 482 | tickInterval = s; 483 | tempo = 60 / tickInterval; 484 | return loop; 485 | }; 486 | loop.data = function(a) { 487 | if (!arguments.length) return data; 488 | data = a; 489 | return loop; 490 | }; 491 | loop.lookahead = function(ms) { 492 | if (!arguments.length) return lookahead; 493 | lookahead = ms; 494 | return loop; 495 | }; 496 | loop.scheduleAheadTime = function(s) { 497 | if (!arguments.length) return scheduleAheadTime; 498 | scheduleAheadTime = s; 499 | return loop; 500 | }; 501 | loop.limit = function(n) { 502 | if (!arguments.length) return limit; 503 | limit = n; 504 | return loop; 505 | }; 506 | loop.tick = function(f) { 507 | if (!arguments.length) return tick; 508 | tick = f; 509 | return loop; 510 | }; 511 | loop.each = function(f) { 512 | if (!arguments.length) return each; 513 | each = f; 514 | return loop; 515 | } 516 | loop.start = function(t) { 517 | nextTickTime = t || ctx.currentTime; 518 | scheduler(); 519 | return loop; 520 | }; 521 | loop.stop = function() { 522 | window.clearTimeout(timer); 523 | return loop; 524 | }; 525 | loop.reset = function() { 526 | currentTick = 0; 527 | iterations = 0; 528 | return loop; 529 | }; 530 | 531 | return loop; 532 | 533 | }; 534 | 535 | 536 | var loadedSamples = {}; 537 | 538 | blip.sampleLoader = function() { 539 | 540 | var samples = {}; 541 | 542 | var each = function() {}, 543 | done = function() {}; 544 | 545 | function loader() { 546 | var names = Object.keys(samples); 547 | var i = 0; 548 | next(); 549 | function next() { 550 | if (i < names.length) { 551 | var name = names[i]; 552 | i++; 553 | loadSample(name, samples[name]); 554 | } else { 555 | done(); 556 | } 557 | } 558 | function loadSample(name, url) { 559 | var request = new XMLHttpRequest(); 560 | request.open('GET', url, true); 561 | request.responseType = 'arraybuffer'; 562 | request.addEventListener('load', loaded, false); 563 | request.send(); 564 | function loaded(event) { 565 | var req = event.target; 566 | var arrayBuffer = req.response; 567 | ctx.decodeAudioData(arrayBuffer, decoded); 568 | } 569 | function decoded(buffer) { 570 | loadedSamples[name] = buffer; 571 | each(name); 572 | next(); 573 | } 574 | } 575 | } 576 | 577 | loader.samples = function(o) { 578 | if (!arguments.length) return samples; 579 | samples = o; 580 | return loader; 581 | }; 582 | loader.each = function(f) { 583 | if (!arguments.length) return each; 584 | each = f; 585 | return loader; 586 | }; 587 | loader.done = function(f) { 588 | if (!arguments.length) return done; 589 | done = f; 590 | return loader; 591 | }; 592 | loader.load = function() { 593 | return loader(); 594 | }; 595 | 596 | return loader; 597 | } 598 | 599 | blip.clip = function() { 600 | 601 | var sample, 602 | rate = 1, 603 | gain = 1; 604 | 605 | var chain = null; 606 | 607 | var output_gain = blip.node('gain').connect(blip.destination); 608 | 609 | function clip() {} 610 | 611 | clip.sample = function(name) { 612 | if (!arguments.length) return sample; 613 | sample = loadedSamples[name]; 614 | return clip; 615 | }; 616 | clip.rate = function(number) { 617 | if (!arguments.length) return rate; 618 | rate = number; 619 | return clip; 620 | }; 621 | clip.gain = function(number) { 622 | if (!arguments.length) return gain; 623 | gain = number; 624 | return clip; 625 | }; 626 | clip.chain = function(c) { 627 | if (!arguments.length) return chain; 628 | chain = c; 629 | output_gain.disconnect(blip.destination); 630 | chain.from(output_gain).to(blip.destination); 631 | return clip; 632 | }; 633 | clip.play = function(time, params) { 634 | time = time || 0; 635 | var source = ctx.createBufferSource(); 636 | source.buffer = sample; 637 | 638 | if (params) { 639 | if (typeof params.gain !== 'undefined') { 640 | if (typeof params.gain === 'function') { 641 | output_gain.param('gain', params.gain) 642 | } else { 643 | output_gain.param('gain', function() { 644 | this.setValueAtTime(params.gain, time) 645 | }) 646 | } 647 | } else { 648 | output_gain.param('gain', params.gain); 649 | } 650 | if (typeof params.rate !== 'undefined') { 651 | if (typeof params.rate === 'function') { 652 | BlipNode.prototype.param.call(specialBlipNode(source), 'playbackRate', params.rate) 653 | } else { 654 | source.playbackRate.setValueAtTime(params.rate, time) 655 | } 656 | } else { 657 | BlipNode.prototype.param.call(specialBlipNode(source), 'playbackRate', rate); 658 | } 659 | } else { 660 | if (gain !== 1) output_gain.param('gain', gain); 661 | if (rate !== 1) BlipNode.prototype.param.call(specialBlipNode(source), 'playbackRate', rate); 662 | } 663 | 664 | source.connect(output_gain.node()); 665 | source.start(time); 666 | }; 667 | 668 | return clip; 669 | } 670 | 671 | 672 | blip.envelope = function() { 673 | 674 | var attack = 0, 675 | decay = 0, 676 | sustain = 0.8, 677 | release = 0; 678 | 679 | var gain = ctx.createGain(); 680 | 681 | // wrap the GainNode, giving it BlipNode methods 682 | var envelope = specialBlipNode(gain); 683 | 684 | // initialize the gain at 0 685 | envelope.param('gain', 0); 686 | 687 | // ADSR setter/getters 688 | envelope.attack = function(a) { 689 | if (!arguments.length) return attack; 690 | attack = a; 691 | return envelope; 692 | }; 693 | envelope.decay = function(d) { 694 | if (!arguments.length) return decay; 695 | decay = d; 696 | return envelope; 697 | }; 698 | envelope.sustain = function(s) { 699 | if (!arguments.length) return sustain; 700 | sustain = s; 701 | return envelope; 702 | }; 703 | envelope.release = function(r) { 704 | if (!arguments.length) return release; 705 | release = r; 706 | return envelope; 707 | }; 708 | envelope.noteOn = function(t) { 709 | t = typeof t === 'number' ? t : now(); 710 | envelope.param('gain', function() { 711 | this.cancelScheduledValues(t); 712 | this.setValueAtTime(0, t); 713 | this.linearRampToValueAtTime(1, t + attack); 714 | this.setTargetAtTime(sustain, t + attack, decay * 0.368); 715 | this.setValueAtTime(sustain, t + attack + decay); 716 | }); 717 | return envelope; 718 | }; 719 | envelope.noteOff = function(t) { 720 | t = typeof t === 'number' ? t : now(); 721 | envelope.param('gain', function() { 722 | this.cancelScheduledValues(t); 723 | //this.setValueAtTime(sustain, t); 724 | this.setTargetAtTime(0, t, release * 0.368) 725 | this.setValueAtTime(0, t + release); 726 | }); 727 | return envelope; 728 | }; 729 | envelope.play = function(t, dur) { 730 | envelope.noteOn(t); 731 | envelope.noteOff(t + dur); 732 | return envelope; 733 | }; 734 | 735 | return envelope; 736 | } 737 | 738 | blip.getContext = function() { return ctx; }; 739 | blip.getLoadedSamples = function() { return loadedSamples; }; 740 | blip.sample = function(name) { 741 | return loadedSamples[name]; 742 | } 743 | 744 | window.blip = blip; 745 | 746 | })() 747 | -------------------------------------------------------------------------------- /blip.min.js: -------------------------------------------------------------------------------- 1 | !function(){function a(){return f.currentTime}function b(){function a(a){var b=(Math.random().toString(16)+"000000000").substr(2,8);return a?"-"+b.substr(0,4)+"-"+b.substr(4,4):b}return a()+a(!0)+a(!0)+a()}function c(a){this.nodes=a||[]}function d(){return this.inputs=new c,this.outputs=new c,this}var e={};e.version="0.3.1",function(){"use strict";function a(a){a&&(a.setTargetAtTime||(a.setTargetAtTime=a.setTargetValueAtTime))}window.hasOwnProperty("webkitAudioContext")&&!window.hasOwnProperty("AudioContext")&&(window.AudioContext=webkitAudioContext,AudioContext.prototype.hasOwnProperty("createGain")||(AudioContext.prototype.createGain=AudioContext.prototype.createGainNode),AudioContext.prototype.hasOwnProperty("createDelay")||(AudioContext.prototype.createDelay=AudioContext.prototype.createDelayNode),AudioContext.prototype.hasOwnProperty("createScriptProcessor")||(AudioContext.prototype.createScriptProcessor=AudioContext.prototype.createJavaScriptNode),AudioContext.prototype.hasOwnProperty("createPeriodicWave")||(AudioContext.prototype.createPeriodicWave=AudioContext.prototype.createWaveTable),AudioContext.prototype.internal_createGain=AudioContext.prototype.createGain,AudioContext.prototype.createGain=function(){var b=this.internal_createGain();return a(b.gain),b},AudioContext.prototype.internal_createDelay=AudioContext.prototype.createDelay,AudioContext.prototype.createDelay=function(b){var c=b?this.internal_createDelay(b):this.internal_createDelay();return a(c.delayTime),c},AudioContext.prototype.internal_createBufferSource=AudioContext.prototype.createBufferSource,AudioContext.prototype.createBufferSource=function(){var b=this.internal_createBufferSource();return b.start||(b.start=function(a,b,c){b||c?this.noteGrainOn(a,b,c):this.noteOn(a)}),b.stop||(b.stop=b.noteOff),a(b.playbackRate),b},AudioContext.prototype.internal_createDynamicsCompressor=AudioContext.prototype.createDynamicsCompressor,AudioContext.prototype.createDynamicsCompressor=function(){var b=this.internal_createDynamicsCompressor();return a(b.threshold),a(b.knee),a(b.ratio),a(b.reduction),a(b.attack),a(b.release),b},AudioContext.prototype.internal_createBiquadFilter=AudioContext.prototype.createBiquadFilter,AudioContext.prototype.createBiquadFilter=function(){var b=this.internal_createBiquadFilter();return a(b.frequency),a(b.detune),a(b.Q),a(b.gain),b},AudioContext.prototype.hasOwnProperty("createOscillator")&&(AudioContext.prototype.internal_createOscillator=AudioContext.prototype.createOscillator,AudioContext.prototype.createOscillator=function(){var b=this.internal_createOscillator();return b.start||(b.start=b.noteOn),b.stop||(b.stop=b.noteOff),b.setPeriodicWave||(b.setPeriodicWave=b.setWaveTable),a(b.frequency),a(b.detune),b}))}(window);var f=new AudioContext;e.time={},e.time.now=function(){return a()},e.time.in=function(b){return a()+b},e.time.seconds=function(a){return a},e.time.ms=function(a){return.001*a},e.time.samp=function(a){return a/f.sampleRate},e.chance=function(a){var b=Math.random();return a>b},e.random=function(a,b){switch(arguments.length){case 0:return Math.random();case 1:return Math.random()*a;case 2:return Math.random()*(b-a)+a}},e.mtof=function(a){return 440*Math.pow(2,(a-69)/12)},c.prototype={count:function(){return this.nodes.length},each:function(a){for(var b=0;b0&&a.node().numberOfInputs>0&&(this.node().connect(a.node()),this.outputs.add(a),a.inputs.add(this)),this},d.prototype.disconnect=function(a){this.node().disconnect();var b=this;return a?(this.outputs.remove(a),a.inputs.remove(this),this.outputs.each(function(a){this.connect(a)})):(this.outputs.each(function(a){a.inputs.remove(b)}),this.outputs.removeAll()),this},d.prototype.prop=function(a,b){if(arguments.length<2){if("object"==typeof a){for(var c in a)this.node()[c]=a[c];return this}return this.node()[a]}return this.node()[a]=b,this},d.prototype.param=function(a,b){return arguments.length<2?this.node()[a]:("function"!=typeof b?this.node()[a].value=b:b.call(this.node()[a]),this)},d.prototype.start=function(a){this.node().start.call(this.node(),a)},d.prototype.stop=function(a){this.node().stop.call(this.node(),a)},d.prototype.node=function(){return this.node()},d.prototype.toString=function(){return"[object BlipNode]"},d.prototype.valueOf=function(){return this.id()},d.prototype.call=function(a){var b=Array.prototype.slice.call(arguments,1),c=this.node();"function"==typeof c[a]&&c[a].apply(c,b)},e.node=function(a){function c(a){return g[a].apply(f,e)}var e=Array.prototype.slice.call(arguments,1),h=c(a),i=b(),j=new d;return j.node=function(){return h},j.id=function(){return i},j};var h=function(a){var c=new d,e=b();return c.node=function(){return a},c.id=function(){return e},c};e.destination=h(f.destination),e.listener=h(f.listener),e.chain=function(a){function b(){for(var b=0;b=l.length&&(m=0,q+=1)}function c(b,c){o.call(a,c,l[b],b)}function d(b,c){p.call(a,c,b)}function e(){for(;n=r)return void a.reset();i=window.setTimeout(e,j)}var g,h,i,j=25,k=.1,l=[],m=0,n=0,o=function(){},p=function(){},q=0,r=0;return a.tempo=function(b){return arguments.length?(g=b,h=60/g,a):g},a.tickInterval=function(b){return arguments.length?(h=b,g=60/h,a):h},a.data=function(b){return arguments.length?(l=b,a):l},a.lookahead=function(b){return arguments.length?(j=b,a):j},a.scheduleAheadTime=function(b){return arguments.length?(k=b,a):k},a.limit=function(b){return arguments.length?(r=b,a):r},a.tick=function(b){return arguments.length?(o=b,a):o},a.each=function(b){return arguments.length?(p=b,a):p},a.start=function(b){return n=b||f.currentTime,e(),a},a.stop=function(){return window.clearTimeout(i),a},a.reset=function(){return m=0,q=0,a},a};var i={};e.sampleLoader=function(){function a(){function a(){if(h" 8 | ], 9 | "description": "Sweet, sugary goodness for looping and sampling with the Web Audio API", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "audio", 15 | "webaudio" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests", 24 | "src", 25 | "wav", 26 | "karma.conf.js", 27 | "Gruntfile.js" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Oct 24 2014 20:33:19 GMT-0400 (EDT) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 13 | frameworks: ['jasmine'], 14 | 15 | 16 | // list of files / patterns to load in the browser 17 | files: [ 18 | './blip.js', 19 | './blip.min.js', 20 | 'test/**/*.js' 21 | ], 22 | 23 | 24 | // list of files to exclude 25 | exclude: [ 26 | ], 27 | 28 | 29 | // preprocess matching files before serving them to the browser 30 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 31 | preprocessors: { 32 | }, 33 | 34 | 35 | // test results reporter to use 36 | // possible values: 'dots', 'progress' 37 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 38 | reporters: ['progress'], 39 | 40 | 41 | // web server port 42 | port: 9876, 43 | 44 | 45 | // enable / disable colors in the output (reporters and logs) 46 | colors: true, 47 | 48 | 49 | // level of logging 50 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 51 | logLevel: config.LOG_INFO, 52 | 53 | 54 | // enable / disable watching file and executing tests whenever any file changes 55 | autoWatch: true, 56 | 57 | 58 | // start these browsers 59 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 60 | browsers: ['Chrome', 'Firefox', 'Safari'], 61 | 62 | 63 | // Continuous Integration mode 64 | // if true, Karma captures browsers, runs the tests and exits 65 | singleRun: true 66 | 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blip", 3 | "version": "0.3.1", 4 | "description": "Sweet, sugary goodness for looping and sampling with the Web Audio API", 5 | "main": "blip.js", 6 | "scripts": { 7 | "test": "karma start" 8 | }, 9 | "author": "John Shanley", 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/jshanley/blip.git" 13 | }, 14 | "license": "ISC", 15 | "devDependencies": { 16 | "grunt": "^0.4.5", 17 | "grunt-contrib-connect": "^0.8.0", 18 | "grunt-contrib-uglify": "^0.6.0", 19 | "grunt-contrib-watch": "^0.6.1", 20 | "grunt-smash": "^0.1.0", 21 | "karma": "^0.12.24", 22 | "karma-chrome-launcher": "^0.1.5", 23 | "karma-firefox-launcher": "^0.1.3", 24 | "karma-jasmine": "^0.2.2", 25 | "karma-safari-launcher": "^0.1.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/blip.js: -------------------------------------------------------------------------------- 1 | var blip = {}; 2 | 3 | blip.version = '0.3.1'; 4 | -------------------------------------------------------------------------------- /src/chain.js: -------------------------------------------------------------------------------- 1 | blip.chain = function(nodes) { 2 | 3 | nodes = nodes || []; 4 | 5 | wire(); 6 | 7 | var chain = {}; 8 | 9 | function wire() { 10 | for (var i = 0; i < nodes.length-1; i++) { 11 | nodes[i].connect(nodes[i+1]); 12 | } 13 | } 14 | 15 | chain.node = function(blipnode) { 16 | nodes.push(blipnode); 17 | wire(); 18 | return chain; 19 | }; 20 | chain.start = function() { 21 | var a = nodes.slice(0,1); 22 | return a.length ? a[0] : null; 23 | }; 24 | chain.end = function() { 25 | var a = nodes.slice(-1); 26 | return a.length ? a[0] : null; 27 | }; 28 | chain.from = function(blipnode) { 29 | blipnode.connect(chain.start()); 30 | return chain; 31 | }; 32 | chain.to = function(blipnode) { 33 | chain.end().connect(blipnode); 34 | return chain; 35 | }; 36 | chain.wire = function() { 37 | wire(); 38 | return chain; 39 | } 40 | 41 | return chain; 42 | } 43 | -------------------------------------------------------------------------------- /src/clip.js: -------------------------------------------------------------------------------- 1 | import "context"; 2 | import "node"; 3 | 4 | blip.clip = function() { 5 | 6 | var sample, 7 | rate = 1, 8 | gain = 1; 9 | 10 | var chain = null; 11 | 12 | var output_gain = blip.node('gain').connect(blip.destination); 13 | 14 | function clip() {} 15 | 16 | clip.sample = function(name) { 17 | if (!arguments.length) return sample; 18 | sample = loadedSamples[name]; 19 | return clip; 20 | }; 21 | clip.rate = function(number) { 22 | if (!arguments.length) return rate; 23 | rate = number; 24 | return clip; 25 | }; 26 | clip.gain = function(number) { 27 | if (!arguments.length) return gain; 28 | gain = number; 29 | return clip; 30 | }; 31 | clip.chain = function(c) { 32 | if (!arguments.length) return chain; 33 | chain = c; 34 | output_gain.disconnect(blip.destination); 35 | chain.from(output_gain).to(blip.destination); 36 | return clip; 37 | }; 38 | clip.play = function(time, params) { 39 | time = time || 0; 40 | var source = ctx.createBufferSource(); 41 | source.buffer = sample; 42 | 43 | if (params) { 44 | if (typeof params.gain !== 'undefined') { 45 | if (typeof params.gain === 'function') { 46 | output_gain.param('gain', params.gain) 47 | } else { 48 | output_gain.param('gain', function() { 49 | this.setValueAtTime(params.gain, time) 50 | }) 51 | } 52 | } else { 53 | output_gain.param('gain', params.gain); 54 | } 55 | if (typeof params.rate !== 'undefined') { 56 | if (typeof params.rate === 'function') { 57 | BlipNode.prototype.param.call(specialBlipNode(source), 'playbackRate', params.rate) 58 | } else { 59 | source.playbackRate.setValueAtTime(params.rate, time) 60 | } 61 | } else { 62 | BlipNode.prototype.param.call(specialBlipNode(source), 'playbackRate', rate); 63 | } 64 | } else { 65 | if (gain !== 1) output_gain.param('gain', gain); 66 | if (rate !== 1) BlipNode.prototype.param.call(specialBlipNode(source), 'playbackRate', rate); 67 | } 68 | 69 | source.connect(output_gain.node()); 70 | source.start(time); 71 | }; 72 | 73 | return clip; 74 | } 75 | -------------------------------------------------------------------------------- /src/context-monkeypatch.js: -------------------------------------------------------------------------------- 1 | /* AudioContext-MonkeyPatch 2 | https://github.com/cwilso/AudioContext-MonkeyPatch 3 | 4 | Copyright 2013 Chris Wilson 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | */ 18 | (function (global, exports, perf) { 19 | 'use strict'; 20 | 21 | function fixSetTarget(param) { 22 | if (!param) // if NYI, just return 23 | return; 24 | if (!param.setTargetAtTime) 25 | param.setTargetAtTime = param.setTargetValueAtTime; 26 | } 27 | 28 | if (window.hasOwnProperty('webkitAudioContext') && 29 | !window.hasOwnProperty('AudioContext')) { 30 | window.AudioContext = webkitAudioContext; 31 | 32 | if (!AudioContext.prototype.hasOwnProperty('createGain')) 33 | AudioContext.prototype.createGain = AudioContext.prototype.createGainNode; 34 | if (!AudioContext.prototype.hasOwnProperty('createDelay')) 35 | AudioContext.prototype.createDelay = AudioContext.prototype.createDelayNode; 36 | if (!AudioContext.prototype.hasOwnProperty('createScriptProcessor')) 37 | AudioContext.prototype.createScriptProcessor = AudioContext.prototype.createJavaScriptNode; 38 | if (!AudioContext.prototype.hasOwnProperty('createPeriodicWave')) 39 | AudioContext.prototype.createPeriodicWave = AudioContext.prototype.createWaveTable; 40 | 41 | 42 | AudioContext.prototype.internal_createGain = AudioContext.prototype.createGain; 43 | AudioContext.prototype.createGain = function() { 44 | var node = this.internal_createGain(); 45 | fixSetTarget(node.gain); 46 | return node; 47 | }; 48 | 49 | AudioContext.prototype.internal_createDelay = AudioContext.prototype.createDelay; 50 | AudioContext.prototype.createDelay = function(maxDelayTime) { 51 | var node = maxDelayTime ? this.internal_createDelay(maxDelayTime) : this.internal_createDelay(); 52 | fixSetTarget(node.delayTime); 53 | return node; 54 | }; 55 | 56 | AudioContext.prototype.internal_createBufferSource = AudioContext.prototype.createBufferSource; 57 | AudioContext.prototype.createBufferSource = function() { 58 | var node = this.internal_createBufferSource(); 59 | if (!node.start) { 60 | node.start = function ( when, offset, duration ) { 61 | if ( offset || duration ) 62 | this.noteGrainOn( when, offset, duration ); 63 | else 64 | this.noteOn( when ); 65 | } 66 | } 67 | if (!node.stop) 68 | node.stop = node.noteOff; 69 | fixSetTarget(node.playbackRate); 70 | return node; 71 | }; 72 | 73 | AudioContext.prototype.internal_createDynamicsCompressor = AudioContext.prototype.createDynamicsCompressor; 74 | AudioContext.prototype.createDynamicsCompressor = function() { 75 | var node = this.internal_createDynamicsCompressor(); 76 | fixSetTarget(node.threshold); 77 | fixSetTarget(node.knee); 78 | fixSetTarget(node.ratio); 79 | fixSetTarget(node.reduction); 80 | fixSetTarget(node.attack); 81 | fixSetTarget(node.release); 82 | return node; 83 | }; 84 | 85 | AudioContext.prototype.internal_createBiquadFilter = AudioContext.prototype.createBiquadFilter; 86 | AudioContext.prototype.createBiquadFilter = function() { 87 | var node = this.internal_createBiquadFilter(); 88 | fixSetTarget(node.frequency); 89 | fixSetTarget(node.detune); 90 | fixSetTarget(node.Q); 91 | fixSetTarget(node.gain); 92 | return node; 93 | }; 94 | 95 | if (AudioContext.prototype.hasOwnProperty( 'createOscillator' )) { 96 | AudioContext.prototype.internal_createOscillator = AudioContext.prototype.createOscillator; 97 | AudioContext.prototype.createOscillator = function() { 98 | var node = this.internal_createOscillator(); 99 | if (!node.start) 100 | node.start = node.noteOn; 101 | if (!node.stop) 102 | node.stop = node.noteOff; 103 | if (!node.setPeriodicWave) 104 | node.setPeriodicWave = node.setWaveTable; 105 | fixSetTarget(node.frequency); 106 | fixSetTarget(node.detune); 107 | return node; 108 | }; 109 | } 110 | } 111 | }(window)); 112 | 113 | /* END AudioContext-MonkeyPatch */ 114 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | var ctx = new AudioContext(); 2 | -------------------------------------------------------------------------------- /src/envelope.js: -------------------------------------------------------------------------------- 1 | import "context"; 2 | import "node"; 3 | 4 | blip.envelope = function() { 5 | 6 | var attack = 0, 7 | decay = 0, 8 | sustain = 0.8, 9 | release = 0; 10 | 11 | var gain = ctx.createGain(); 12 | 13 | // wrap the GainNode, giving it BlipNode methods 14 | var envelope = specialBlipNode(gain); 15 | 16 | // initialize the gain at 0 17 | envelope.param('gain', 0); 18 | 19 | // ADSR setter/getters 20 | envelope.attack = function(a) { 21 | if (!arguments.length) return attack; 22 | attack = a; 23 | return envelope; 24 | }; 25 | envelope.decay = function(d) { 26 | if (!arguments.length) return decay; 27 | decay = d; 28 | return envelope; 29 | }; 30 | envelope.sustain = function(s) { 31 | if (!arguments.length) return sustain; 32 | sustain = s; 33 | return envelope; 34 | }; 35 | envelope.release = function(r) { 36 | if (!arguments.length) return release; 37 | release = r; 38 | return envelope; 39 | }; 40 | envelope.noteOn = function(t) { 41 | t = typeof t === 'number' ? t : now(); 42 | envelope.param('gain', function() { 43 | this.cancelScheduledValues(t); 44 | this.setValueAtTime(0, t); 45 | this.linearRampToValueAtTime(1, t + attack); 46 | this.setTargetAtTime(sustain, t + attack, decay * 0.368); 47 | this.setValueAtTime(sustain, t + attack + decay); 48 | }); 49 | return envelope; 50 | }; 51 | envelope.noteOff = function(t) { 52 | t = typeof t === 'number' ? t : now(); 53 | envelope.param('gain', function() { 54 | this.cancelScheduledValues(t); 55 | //this.setValueAtTime(sustain, t); 56 | this.setTargetAtTime(0, t, release * 0.368) 57 | this.setValueAtTime(0, t + release); 58 | }); 59 | return envelope; 60 | }; 61 | envelope.play = function(t, dur) { 62 | envelope.noteOn(t); 63 | envelope.noteOff(t + dur); 64 | return envelope; 65 | }; 66 | 67 | return envelope; 68 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import "intro"; 2 | 3 | import "blip"; 4 | 5 | import "context-monkeypatch"; 6 | import "context"; 7 | 8 | import "time"; 9 | import "random"; 10 | 11 | import "util"; 12 | 13 | import "node-collection"; 14 | 15 | import "node"; 16 | import "chain"; 17 | 18 | import "loop"; 19 | 20 | import "sample-loader"; 21 | import "clip"; 22 | 23 | import "envelope"; 24 | 25 | import "outro"; 26 | -------------------------------------------------------------------------------- /src/intro.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | -------------------------------------------------------------------------------- /src/loop.js: -------------------------------------------------------------------------------- 1 | import "context"; 2 | 3 | /* 4 | Precise scheduling for audio events is 5 | based on the method described in this article by Chris Wilson: 6 | http://www.html5rocks.com/en/tutorials/audio/scheduling/ 7 | */ 8 | 9 | blip.loop = function() { 10 | 11 | var lookahead = 25.0, // ms 12 | scheduleAheadTime = 0.1; // s 13 | 14 | var tempo; // ticks per minute 15 | 16 | var tickInterval; // seconds per tick 17 | 18 | var data = []; 19 | 20 | var currentTick = 0, 21 | nextTickTime = 0; 22 | 23 | var tick = function(t, d, i) {}; 24 | var each = function(t, i) {}; 25 | 26 | var iterations = 0, 27 | limit = 0; 28 | 29 | var timer; 30 | 31 | function loop() {} 32 | 33 | function nextTick() { 34 | nextTickTime += tickInterval; 35 | 36 | // cycle through ticks 37 | if (++currentTick >= data.length) { 38 | currentTick = 0; 39 | iterations += 1; 40 | } 41 | 42 | } 43 | 44 | function scheduleTick(tickNum, time) { 45 | tick.call(loop, time, data[tickNum], tickNum); 46 | } 47 | 48 | function scheduleIteration(iterationNum, time) { 49 | each.call(loop, time, iterationNum); 50 | } 51 | 52 | function scheduler() { 53 | while (nextTickTime < ctx.currentTime + scheduleAheadTime) { 54 | scheduleTick(currentTick, nextTickTime); 55 | if (currentTick === 0) { 56 | scheduleIteration(iterations, nextTickTime); 57 | } 58 | nextTick(); 59 | if (limit && iterations >= limit) { 60 | loop.reset(); 61 | return; 62 | } 63 | } 64 | timer = window.setTimeout(scheduler, lookahead); 65 | } 66 | 67 | loop.tempo = function(bpm) { 68 | if (!arguments.length) return tempo; 69 | tempo = bpm; 70 | tickInterval = 60 / tempo; 71 | return loop; 72 | }; 73 | loop.tickInterval = function(s) { 74 | if (!arguments.length) return tickInterval; 75 | tickInterval = s; 76 | tempo = 60 / tickInterval; 77 | return loop; 78 | }; 79 | loop.data = function(a) { 80 | if (!arguments.length) return data; 81 | data = a; 82 | return loop; 83 | }; 84 | loop.lookahead = function(ms) { 85 | if (!arguments.length) return lookahead; 86 | lookahead = ms; 87 | return loop; 88 | }; 89 | loop.scheduleAheadTime = function(s) { 90 | if (!arguments.length) return scheduleAheadTime; 91 | scheduleAheadTime = s; 92 | return loop; 93 | }; 94 | loop.limit = function(n) { 95 | if (!arguments.length) return limit; 96 | limit = n; 97 | return loop; 98 | }; 99 | loop.tick = function(f) { 100 | if (!arguments.length) return tick; 101 | tick = f; 102 | return loop; 103 | }; 104 | loop.each = function(f) { 105 | if (!arguments.length) return each; 106 | each = f; 107 | return loop; 108 | } 109 | loop.start = function(t) { 110 | nextTickTime = t || ctx.currentTime; 111 | scheduler(); 112 | return loop; 113 | }; 114 | loop.stop = function() { 115 | window.clearTimeout(timer); 116 | return loop; 117 | }; 118 | loop.reset = function() { 119 | currentTick = 0; 120 | iterations = 0; 121 | return loop; 122 | }; 123 | 124 | return loop; 125 | 126 | }; 127 | -------------------------------------------------------------------------------- /src/node-collection.js: -------------------------------------------------------------------------------- 1 | function BlipNodeCollection(nodes) { 2 | this.nodes = nodes || []; 3 | } 4 | 5 | BlipNodeCollection.prototype = { 6 | 7 | count: function() { 8 | return this.nodes.length; 9 | }, 10 | 11 | each: function(f) { 12 | for (var i = 0; i < this.nodes.length; i++) { 13 | f.call(this, this.nodes[i], i, this.nodes); 14 | } 15 | }, 16 | 17 | contains: function(node) { 18 | for (var i = 0; i < this.nodes.length; i++) { 19 | if (this.nodes[i] === node) return true; 20 | } 21 | return false; 22 | }, 23 | 24 | add: function(node) { 25 | if (this.nodes.indexOf(node) === -1) this.nodes.push(node); 26 | }, 27 | 28 | remove: function(node) { 29 | var index = this.nodes.indexOf(node); 30 | if (index !== -1) this.nodes.splice(index, 1); 31 | }, 32 | 33 | removeAll: function() { 34 | this.nodes = []; 35 | } 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | import "context"; 2 | import "util"; 3 | 4 | // the associated functions will be used by the `createNode` function within `blip.node` 5 | var nodeTypes = { 6 | 'gain': ctx.createGain, 7 | 'delay': ctx.createDelay, 8 | 'panner': ctx.createPanner, 9 | 'convolver': ctx.createConvolver, 10 | 'analyser': ctx.createAnalyser, 11 | 'channelSplitter': ctx.createChannelSplitter, 12 | 'channelMerger': ctx.createChannelMerger, 13 | 'dynamicsCompressor': ctx.createDynamicsCompressor, 14 | 'biquadFilter': ctx.createBiquadFilter, 15 | 'waveShaper': ctx.createWaveShaper, 16 | 'oscillator': ctx.createOscillator, 17 | 'periodicWave': ctx.createPeriodicWave, 18 | 'bufferSource': ctx.createBufferSource, 19 | 'audioBufferSource': ctx.createBufferSource, // alias 20 | }; 21 | 22 | function BlipNode() { 23 | this.inputs = new BlipNodeCollection(); 24 | this.outputs = new BlipNodeCollection(); 25 | return this; 26 | }; 27 | 28 | BlipNode.prototype.connect = function(blipnode) { 29 | if (this.node().numberOfOutputs > 0 && blipnode.node().numberOfInputs > 0) { 30 | this.node().connect(blipnode.node()); 31 | this.outputs.add(blipnode); 32 | blipnode.inputs.add(this); 33 | } 34 | return this; 35 | }; 36 | 37 | BlipNode.prototype.disconnect = function(blipnode) { 38 | // disconnect all 39 | this.node().disconnect(); 40 | 41 | var me = this; 42 | 43 | if (blipnode) { 44 | this.outputs.remove(blipnode); 45 | blipnode.inputs.remove(this); 46 | 47 | // reconnect to remaining outputs 48 | this.outputs.each(function(n) { this.connect(n); }) 49 | } else { 50 | this.outputs.each(function(n) { 51 | n.inputs.remove(me); 52 | }); 53 | this.outputs.removeAll(); 54 | } 55 | 56 | return this; 57 | } 58 | 59 | BlipNode.prototype.prop = function(name, value) { 60 | if (arguments.length < 2) { 61 | if (typeof name === 'object') { 62 | for (var p in name) { 63 | this.node()[p] = name[p]; 64 | } 65 | return this; 66 | } else { 67 | return this.node()[name]; 68 | } 69 | } 70 | this.node()[name] = value; 71 | return this; 72 | }; 73 | 74 | BlipNode.prototype.param = function(name, f) { 75 | if (arguments.length < 2) return this.node()[name]; 76 | if (typeof f !== 'function') { 77 | this.node()[name].value = f; 78 | } else { 79 | f.call(this.node()[name]); 80 | } 81 | return this; 82 | }; 83 | 84 | BlipNode.prototype.start = function(t) { 85 | this.node().start.call(this.node(), t); 86 | }; 87 | 88 | BlipNode.prototype.stop = function(t) { 89 | this.node().stop.call(this.node(), t); 90 | }; 91 | 92 | BlipNode.prototype.node = function() { 93 | return this.node(); 94 | }; 95 | 96 | BlipNode.prototype.toString = function() { 97 | return '[object BlipNode]'; 98 | }; 99 | 100 | BlipNode.prototype.valueOf = function() { 101 | return this.id(); 102 | }; 103 | 104 | BlipNode.prototype.call = function(methodName) { 105 | var args = Array.prototype.slice.call(arguments, 1); 106 | var node = this.node(); 107 | if (typeof node[methodName] !== 'function') return; 108 | node[methodName].apply(node, args); 109 | }; 110 | 111 | blip.node = function(type) { 112 | 113 | var other_args = Array.prototype.slice.call(arguments, 1); 114 | 115 | var reference = createNode(type); 116 | 117 | var id = guid(); 118 | 119 | var node = new BlipNode(); 120 | 121 | function createNode(t) { 122 | return nodeTypes[t].apply(ctx, other_args); 123 | } 124 | 125 | node.node = function() { 126 | return reference; 127 | }; 128 | 129 | node.id = function() { 130 | return id; 131 | }; 132 | 133 | return node; 134 | 135 | } 136 | 137 | var specialBlipNode = function(ref) { 138 | var node = new BlipNode(); 139 | var id = guid(); 140 | node.node = function() { return ref; }; 141 | node.id = function() { return id; }; 142 | return node; 143 | } 144 | 145 | // special nodes 146 | blip.destination = specialBlipNode(ctx.destination); 147 | blip.listener = specialBlipNode(ctx.listener); 148 | -------------------------------------------------------------------------------- /src/outro.js: -------------------------------------------------------------------------------- 1 | blip.getContext = function() { return ctx; }; 2 | blip.getLoadedSamples = function() { return loadedSamples; }; 3 | blip.sample = function(name) { 4 | return loadedSamples[name]; 5 | } 6 | 7 | window.blip = blip; 8 | 9 | })() 10 | -------------------------------------------------------------------------------- /src/random.js: -------------------------------------------------------------------------------- 1 | import "blip"; 2 | 3 | blip.chance = function(p) { 4 | var attempt = Math.random(); 5 | return attempt < p; 6 | }; 7 | 8 | blip.random = function(a,b) { 9 | switch(arguments.length) { 10 | case 0: 11 | return Math.random(); 12 | case 1: 13 | return Math.random() * a; 14 | case 2: 15 | return Math.random() * (b - a) + a; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/sample-loader.js: -------------------------------------------------------------------------------- 1 | import "context"; 2 | 3 | var loadedSamples = {}; 4 | 5 | blip.sampleLoader = function() { 6 | 7 | var samples = {}; 8 | 9 | var each = function() {}, 10 | done = function() {}; 11 | 12 | function loader() { 13 | var names = Object.keys(samples); 14 | var i = 0; 15 | next(); 16 | function next() { 17 | if (i < names.length) { 18 | var name = names[i]; 19 | i++; 20 | loadSample(name, samples[name]); 21 | } else { 22 | done(); 23 | } 24 | } 25 | function loadSample(name, url) { 26 | var request = new XMLHttpRequest(); 27 | request.open('GET', url, true); 28 | request.responseType = 'arraybuffer'; 29 | request.addEventListener('load', loaded, false); 30 | request.send(); 31 | function loaded(event) { 32 | var req = event.target; 33 | var arrayBuffer = req.response; 34 | ctx.decodeAudioData(arrayBuffer, decoded); 35 | } 36 | function decoded(buffer) { 37 | loadedSamples[name] = buffer; 38 | each(name); 39 | next(); 40 | } 41 | } 42 | } 43 | 44 | loader.samples = function(o) { 45 | if (!arguments.length) return samples; 46 | samples = o; 47 | return loader; 48 | }; 49 | loader.each = function(f) { 50 | if (!arguments.length) return each; 51 | each = f; 52 | return loader; 53 | }; 54 | loader.done = function(f) { 55 | if (!arguments.length) return done; 56 | done = f; 57 | return loader; 58 | }; 59 | loader.load = function() { 60 | return loader(); 61 | }; 62 | 63 | return loader; 64 | } 65 | -------------------------------------------------------------------------------- /src/time.js: -------------------------------------------------------------------------------- 1 | import "context"; 2 | 3 | function now() { 4 | return ctx.currentTime; 5 | } 6 | 7 | blip.time = {}; 8 | 9 | blip.time.now = function() { 10 | return now(); 11 | }; 12 | 13 | blip.time.in = function(t) { 14 | return now() + t; 15 | }; 16 | 17 | blip.time.seconds = function(t) { 18 | return t; 19 | }; 20 | blip.time.ms = function(t) { 21 | return t * 0.001; 22 | }; 23 | blip.time.samp = function(t) { 24 | return t / ctx.sampleRate; 25 | }; -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a GUID string. 3 | * @returns {String} The generated GUID. 4 | * @example af8a8416-6e18-a307-bd9c-f2c947bbb3aa 5 | * @author Slavik Meltser (slavik@meltser.info). 6 | * @link http://slavik.meltser.info/?p=142 7 | */ 8 | function guid() { 9 | function _p8(s) { 10 | var p = (Math.random().toString(16)+"000000000").substr(2,8); 11 | return s ? "-" + p.substr(0,4) + "-" + p.substr(4,4) : p ; 12 | } 13 | return _p8() + _p8(true) + _p8(true) + _p8(); 14 | } 15 | 16 | // MIDI to Frequency 17 | blip.mtof = function(midi) { 18 | return Math.pow(2, (midi - 69) / 12) * 440; 19 | }; 20 | -------------------------------------------------------------------------------- /test/chaining/test_chain_by_adding_individual_nodes.js: -------------------------------------------------------------------------------- 1 | describe('when creating a chain by adding nodes one at a time,', function() { 2 | 3 | var node1, node2, node3, chain; 4 | 5 | beforeEach(function() { 6 | node1 = blip.node('gain'); 7 | node2 = blip.node('convolver'); 8 | node3 = blip.node('delay'); 9 | chain = blip.chain() 10 | .node(node1) 11 | .node(node2) 12 | .node(node3); 13 | }); 14 | 15 | describe('the first node', function() { 16 | it('should have zero inputs', function() { 17 | expect(node1.inputs.count()).toEqual(0); 18 | }); 19 | it('should have one output', function() { 20 | expect(node1.outputs.count()).toEqual(1); 21 | }); 22 | it('should be connected to the second node', function() { 23 | expect(node1.outputs.contains(node2)).toBe(true); 24 | }); 25 | }); 26 | 27 | describe('the second node', function() { 28 | it('should have one input', function() { 29 | expect(node2.inputs.count()).toEqual(1); 30 | }); 31 | it('should have one output', function() { 32 | expect(node2.outputs.count()).toEqual(1); 33 | }); 34 | it('should be connected from the first node', function() { 35 | expect(node2.inputs.contains(node1)).toBe(true); 36 | }); 37 | it('should be connected to the third node', function() { 38 | expect(node2.outputs.contains(node3)).toBe(true); 39 | }); 40 | }); 41 | 42 | describe('the third node', function() { 43 | it('should have one input', function() { 44 | expect(node3.inputs.count()).toEqual(1); 45 | }); 46 | it('should have zero outputs', function() { 47 | expect(node3.outputs.count()).toEqual(0); 48 | }); 49 | it('should be connected from the second node', function() { 50 | expect(node3.inputs.contains(node2)).toBe(true); 51 | }); 52 | }); 53 | }); -------------------------------------------------------------------------------- /test/chaining/test_chain_by_passing_array.js: -------------------------------------------------------------------------------- 1 | describe('when creating a chain by passing an array of nodes as an argument,', function() { 2 | 3 | var node1, node2, node3, chain; 4 | 5 | beforeEach(function() { 6 | node1 = blip.node('gain'); 7 | node2 = blip.node('convolver'); 8 | node3 = blip.node('delay'); 9 | chain = blip.chain([node1, node2, node3]) 10 | }) 11 | 12 | describe('the first node', function() { 13 | it('should have zero inputs', function() { 14 | expect(node1.inputs.count()).toEqual(0); 15 | }); 16 | it('should have one output', function() { 17 | expect(node1.outputs.count()).toEqual(1); 18 | }); 19 | it('should be connected to the second node', function() { 20 | expect(node1.outputs.contains(node2)).toBe(true); 21 | }); 22 | }); 23 | 24 | describe('the second node', function() { 25 | it('should have one input', function() { 26 | expect(node2.inputs.count()).toEqual(1); 27 | }); 28 | it('should have one output', function() { 29 | expect(node2.outputs.count()).toEqual(1); 30 | }); 31 | it('should be connected from the first node', function() { 32 | expect(node2.inputs.contains(node1)).toBe(true); 33 | }); 34 | it('should be connected to the third node', function() { 35 | expect(node2.outputs.contains(node3)).toBe(true); 36 | }); 37 | }); 38 | 39 | describe('the third node', function() { 40 | it('should have one input', function() { 41 | expect(node3.inputs.count()).toEqual(1); 42 | }); 43 | it('should have zero outputs', function() { 44 | expect(node3.outputs.count()).toEqual(0); 45 | }); 46 | it('should be connected from the second node', function() { 47 | expect(node3.inputs.contains(node2)).toBe(true); 48 | }); 49 | }); 50 | }); -------------------------------------------------------------------------------- /test/test_simple_node_connections.js: -------------------------------------------------------------------------------- 1 | describe('when creating a single connection between two nodes', function() { 2 | 3 | var g1 = blip.node('gain'); 4 | var g2 = blip.node('gain'); 5 | g1.connect(g2); 6 | 7 | describe('the first node', function() { 8 | 9 | it('should have one output', function() { 10 | expect(g1.outputs.nodes.length).toEqual(1); 11 | }); 12 | it('should have zero inputs', function() { 13 | expect(g1.inputs.nodes.length).toEqual(0); 14 | }); 15 | it('should contain the second node in its outputs', function() { 16 | expect(g1.outputs.contains(g2)).toBe(true); 17 | }); 18 | }); 19 | 20 | describe('the second node', function() { 21 | 22 | it('should have one input', function() { 23 | expect(g2.inputs.nodes.length).toEqual(1); 24 | }); 25 | it('should have zero outputs', function() { 26 | expect(g2.outputs.nodes.length).toEqual(0); 27 | }); 28 | it('should contain the first node in its inputs', function() { 29 | expect(g2.inputs.contains(g1)).toBe(true); 30 | }); 31 | }); 32 | 33 | }); 34 | 35 | describe('disconnecting two connected nodes', function() { 36 | var g1 = blip.node('gain'); 37 | var g2 = blip.node('gain'); 38 | g1.connect(g2); 39 | g1.disconnect(); 40 | 41 | it('should remove outputs from the first node', function() { 42 | expect(g1.outputs.nodes.length).toEqual(0); 43 | }); 44 | it('should remove the first node from the second node\'s inputs', function() { 45 | expect(g2.inputs.nodes.length).toEqual(0); 46 | }); 47 | }); -------------------------------------------------------------------------------- /test/test_time_functions.js: -------------------------------------------------------------------------------- 1 | describe('the time module', function() { 2 | it('should exist', function() { 3 | expect(typeof blip.time).toEqual('object'); 4 | }); 5 | }); 6 | 7 | describe('when using the time functions', function() { 8 | 9 | describe('the seconds function', function() { 10 | it('should exist', function() { 11 | expect(typeof blip.time.seconds).toEqual('function'); 12 | }); 13 | it('should give back its input unchanged', function() { 14 | expect(blip.time.seconds(3)).toEqual(3); 15 | expect(blip.time.seconds(2.5)).toEqual(2.5); 16 | }); 17 | }); 18 | describe('the ms function', function() { 19 | it('should exist', function() { 20 | expect(typeof blip.time.ms).toEqual('function'); 21 | }); 22 | it('should convert from milliseconds to seconds', function() { 23 | expect(blip.time.ms(25)).toEqual(0.025); 24 | }); 25 | }); 26 | describe('the samp function', function() { 27 | it('should exist', function() { 28 | expect(typeof blip.time.samp).toEqual('function') 29 | }); 30 | it('should convert from samples to seconds', function() { 31 | expect(blip.time.samp(200)).toEqual(200 / blip.getContext().sampleRate); 32 | }); 33 | }); 34 | }) --------------------------------------------------------------------------------