UNPKG

ditty

Version:

Schedule a looped sequence of Web Audio events using bopper clock source.

197 lines (165 loc) 4.04 kB
module.exports = Ditty var Stream = require('stream') var inherits = require('util').inherits function Ditty(){ if (!(this instanceof Ditty)){ return new Ditty() } Stream.call(this) this.readable = true this.writable = true this._state = { loops: {}, lengths: {}, ids: [], queue: [] } } inherits(Ditty, Stream) var proto = Ditty.prototype proto.set = function(id, events, length){ var state = this._state if (events){ if (!state.loops[id]){ state.ids.push(id) } state.loops[id] = events state.lengths[id] = length || 8 } else { var index = state.ids.indexOf(id) if (~index){ state.ids.splice(index, 1) } state.loops[id] = null } if (state.loops[id]){ this.emit('change', { id: id, events: state.loops[id], length: state.lengths[id] }) } else { this.emit('change', { id: id }) } } proto.get = function(id){ return this._state.loops[id] } proto.getLength = function(id){ return this._state.lengths[id] } proto.getIds = function(){ return this._state.ids } proto.getDescriptors = function(){ var state = this._state var result = [] for (var i=0;i<state.ids.length;i++){ var id = state.ids[i] if (state.loops[id]){ result.push({ id: id, length: state.lengths[id], events: state.loops[id] }) } } return result } proto.update = function(descriptor){ this.set(descriptor.id, descriptor.events, descriptor.length) } proto.push = function(data){ this.emit('data', data) } proto.write = function(obj){ this._transform(obj) } proto._transform = function(obj){ var begin = window.performance.now() var endAt = begin + (obj.duration * 900) var state = this._state var from = obj.from var to = obj.to var time = obj.time var nextTime = obj.time + obj.duration var beatDuration = obj.beatDuration var ids = state.ids var queue = state.queue var localQueue = [] for (var i=queue.length-1;i>=0;i--){ var item = queue[i] if (to > item.position || shouldSendImmediately(item, state.loops[item.id])){ if (to > item.position){ var delta = (item.position - from) * beatDuration item.time = time + delta } else { item.time = time item.position = from } queue.splice(i, 1) this.push(item) } } for (var i=0;i<ids.length;i++){ var id = ids[i] var events = state.loops[id] var loopLength = state.lengths[id] for (var j=0;j<events.length;j++){ var event = events[j] var startPosition = getAbsolutePosition(event[0], from, loopLength) var endPosition = startPosition + event[1] if (startPosition >= from && startPosition < to){ var delta = (startPosition - from) * beatDuration var duration = event[1] * beatDuration var startTime = time + delta var endTime = startTime + duration localQueue.push({ id: id, event: 'start', position: startPosition, args: event.slice(2), time: startTime }) localQueue.push({ id: id, event: 'stop', position: endPosition, args: event.slice(2), time: endTime }) } } } // ensure events stream in time sequence localQueue.sort(compare) for (var i=0;i<localQueue.length;i++){ var item = localQueue[i] if (item.time < nextTime){ if (window.performance.now() < endAt){ this.push(item) } } else { // queue event for later queue.push(item) } } } function compare(a,b){ return a.time-b.time } function getAbsolutePosition(pos, start, length){ pos = pos % length var micro = start % length var position = start+pos-micro if (position < start){ return position + length } else { return position } } function shouldSendImmediately(message, loop){ return message.event === 'stop' && (!loop || !loop.length) }