├── .gitignore ├── README.md ├── example.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bopper 2 | === 3 | 4 | A streaming clock source for scheduling Web Audio events rhythmically. 5 | 6 | Use with [ditty](https://github.com/mmckegg/ditty) if you want to create loop sequences. 7 | 8 | ## Install 9 | 10 | ```bash 11 | $ npm install bopper 12 | ``` 13 | 14 | Require into your browser bundle with [browserify](http://browserify.org/). 15 | 16 | ## Example 17 | 18 | ```js 19 | var audioContext = new AudioContext() 20 | var bopper = require('bopper')(audioContext) 21 | 22 | // save a reference on the window to avoid garbage collection 23 | window.scheduler = bopper 24 | 25 | var playback = [ 26 | {position: 0, length: 0.1}, 27 | {position: 1, length: 0.1}, 28 | {position: 2, length: 0.1}, 29 | {position: 3, length: 0.1}, 30 | {position: 3.5, length: 0.1}, 31 | {position: 4, length: 0.1}, 32 | {position: 5, length: 0.1}, 33 | {position: 6, length: 0.1}, 34 | {position: 7, length: 0.1}, 35 | {position: 7+1/3, length: 0.1}, 36 | {position: 7+2/3, length: 0.1} 37 | ] 38 | 39 | // emits data roughly every 20ms 40 | 41 | bopper.on('data', function(schedule){ 42 | // schedule: from, to, time, beatDuration 43 | 44 | playback.forEach(function(note){ 45 | if (note.position >= schedule.from && note.position < schedule.to){ 46 | var delta = note.position - schedule.from 47 | var time = schedule.time + delta 48 | var duration = note.length * schedule.beatDuration 49 | play(time, duration) 50 | } 51 | }) 52 | 53 | }) 54 | 55 | function play(at, duration){ 56 | var oscillator = audioContext.createOscillator() 57 | oscillator.connect(audioContext.destination) 58 | oscillator.start(at) 59 | oscillator.stop(at+duration) 60 | } 61 | 62 | bopper.setTempo(120) 63 | bopper.start() 64 | 65 | ``` 66 | 67 | To run the example `npm install -g beefy` then `beefy example.js` and navigate to `http://localhost:9966/` 68 | 69 | ## License 70 | 71 | MIT 72 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var audioContext = new AudioContext() 2 | var bopper = require('./')(audioContext) 3 | 4 | // save a reference on the window to avoid garbage collection 5 | window.scheduler = bopper 6 | 7 | var playback = [ 8 | {position: 0, length: 0.1}, 9 | {position: 1, length: 0.1}, 10 | {position: 2, length: 0.1}, 11 | {position: 3, length: 0.1}, 12 | {position: 3.5, length: 0.1}, 13 | {position: 4, length: 0.1}, 14 | {position: 5, length: 0.1}, 15 | {position: 6, length: 0.1}, 16 | {position: 7, length: 0.1}, 17 | {position: 7+1/3, length: 0.1}, 18 | {position: 7+2/3, length: 0.1}, 19 | {position: 8, length: 0.1}, 20 | {position: 9, length: 0.1}, 21 | {position: 10, length: 0.1}, 22 | {position: 11, length: 0.1}, 23 | {position: 11.5, length: 0.1}, 24 | {position: 12, length: 0.1}, 25 | {position: 13, length: 0.1}, 26 | {position: 14, length: 0.1}, 27 | {position: 15, length: 0.1}, 28 | {position: 15+1/3, length: 0.1}, 29 | {position: 15+2/3, length: 0.1} 30 | ] 31 | 32 | // emits data roughly every 20ms 33 | 34 | bopper.on('data', function(schedule){ 35 | // schedule: from, to, time, beatDuration 36 | 37 | playback.forEach(function(note){ 38 | if (note.position >= schedule.from && note.position < schedule.to){ 39 | var delta = note.position - schedule.from 40 | var time = schedule.time + delta 41 | var duration = note.length * schedule.beatDuration 42 | play(time, duration) 43 | } 44 | }) 45 | 46 | }) 47 | 48 | function play(at, duration){ 49 | var oscillator = audioContext.createOscillator() 50 | oscillator.connect(audioContext.destination) 51 | oscillator.start(at) 52 | oscillator.stop(at+duration) 53 | } 54 | 55 | bopper.setTempo(120) 56 | 57 | setTimeout(function(){ 58 | bopper.start() 59 | }, 500) 60 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Stream = require('stream') 2 | var Event = require('geval') 3 | var workerTimer = require('worker-timer') 4 | 5 | var inherits = require('util').inherits 6 | 7 | module.exports = Bopper 8 | 9 | function Bopper(audioContext){ 10 | if (!(this instanceof Bopper)){ 11 | return new Bopper(audioContext) 12 | } 13 | 14 | var self = this 15 | 16 | Stream.call(this) 17 | this.readable = true 18 | this.writable = false 19 | 20 | this.context = audioContext 21 | 22 | var cycleLength = (1 / audioContext.sampleRate) * 1024 23 | workerTimer.setInterval(bopperTick.bind(this), cycleLength * 1000) 24 | 25 | var tempo = 120 26 | 27 | this._state = { 28 | lastTo: 0, 29 | lastEndTime: 0, 30 | playing: false, 31 | bpm: tempo, 32 | beatDuration: 60 / tempo, 33 | increment: (tempo / 60) * cycleLength, 34 | cycleLength: cycleLength, 35 | preCycle: 2 36 | } 37 | 38 | // frp version 39 | this.onSchedule = Event(function(broadcast){ 40 | self.on('data', broadcast) 41 | }) 42 | } 43 | 44 | inherits(Bopper, Stream) 45 | 46 | var proto = Bopper.prototype 47 | 48 | 49 | proto.start = function(){ 50 | this._state.playing = true 51 | this.emit('start') 52 | } 53 | 54 | proto.stop = function(){ 55 | this._state.playing = false 56 | this.emit('stop') 57 | } 58 | 59 | proto.schedule = function(duration) { 60 | var state = this._state 61 | var currentTime = this.context.currentTime 62 | 63 | var endTime = this.context.currentTime + duration 64 | var time = state.lastEndTime 65 | 66 | if (endTime >= time) { 67 | state.lastEndTime = endTime 68 | 69 | if (state.playing){ 70 | var duration = endTime - time 71 | var length = duration / state.beatDuration 72 | 73 | var from = state.lastTo 74 | var to = from + length 75 | state.lastTo = to 76 | 77 | // skip if getting behind 78 | //if ((currentTime - (state.cycleLength*3)) < time){ 79 | this._schedule(time, from, to) 80 | //} 81 | } 82 | } 83 | 84 | } 85 | 86 | proto.setTempo = function(tempo){ 87 | var bps = tempo/60 88 | var state = this._state 89 | state.beatDuration = 60/tempo 90 | state.increment = bps * state.cycleLength 91 | state.bpm = tempo 92 | this.emit('tempo', state.bpm) 93 | } 94 | 95 | proto.getTempo = function(){ 96 | return this._state.bpm 97 | } 98 | 99 | proto.isPlaying = function(){ 100 | return this._state.playing 101 | } 102 | 103 | proto.setPosition = function(position){ 104 | this._state.lastTo = parseFloat(position) 105 | } 106 | 107 | proto.setSpeed = function(multiplier){ 108 | var state = this._state 109 | 110 | multiplier = parseFloat(multiplier) || 0 111 | 112 | var tempo = state.bpm * multiplier 113 | var bps = tempo/60 114 | 115 | state.beatDuration = 60/tempo 116 | state.increment = bps * state.cycleLength 117 | } 118 | 119 | 120 | proto.getPositionAt = function(time){ 121 | var state = this._state 122 | var delta = state.lastEndTime - time 123 | return state.lastTo - (delta / state.beatDuration) 124 | } 125 | 126 | proto.getTimeAt = function(position){ 127 | var state = this._state 128 | var positionOffset = this.getCurrentPosition() - position 129 | return this.context.currentTime - (positionOffset * state.beatDuration) 130 | } 131 | 132 | proto.getCurrentPosition = function(){ 133 | return this.getPositionAt(this.context.currentTime) 134 | } 135 | 136 | proto.getNextScheduleTime = function(){ 137 | var state = this._state 138 | return state.lastEndTime 139 | } 140 | 141 | proto.getBeatDuration = function(){ 142 | var state = this._state 143 | return state.beatDuration 144 | } 145 | 146 | 147 | proto._schedule = function(time, from, to){ 148 | var state = this._state 149 | var duration = (to - from) * state.beatDuration 150 | this.emit('data', { 151 | from: from, 152 | to: to, 153 | time: time, 154 | duration: duration, 155 | beatDuration: state.beatDuration 156 | }) 157 | } 158 | 159 | function bopperTick () { 160 | var state = this._state 161 | this.schedule(state.cycleLength * state.preCycle) 162 | } 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bopper", 3 | "version": "2.11.0", 4 | "description": "Provides a streaming clock source for scheduling Web Audio events rhythmically", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/mmckegg/bopper.git" 12 | }, 13 | "keywords": [ 14 | "midi", 15 | "tempo", 16 | "transport", 17 | "clock", 18 | "rhythm", 19 | "beat", 20 | "bar", 21 | "web audio" 22 | ], 23 | "dependencies": { 24 | "geval": "^2.1.1", 25 | "worker-timer": "^1.0.0" 26 | }, 27 | "author": "Matt McKegg", 28 | "license": "MIT" 29 | } 30 | --------------------------------------------------------------------------------