loop-grid
Version:
An observable collection of looped event sequences shaped to a grid.
176 lines (151 loc) • 4.62 kB
JavaScript
var ArrayGrid = require('array-grid')
var Observ = require('observ')
var ObservDefault = require('./lib/observ-default')
var ObservStruct = require('observ-struct')
var Event = require('geval')
var computed = require('observ/computed')
var setImmediate = require('setimmediate2').setImmediate
var getEvents = require('./lib/get-events')
module.exports = LoopGrid
function LoopGrid (context) {
var obs = ObservStruct({
shape: ObservDefault([8, 8]),
loops: ObservDefault([]),
targets: ObservDefault([]),
loopLength: ObservDefault(8)
})
var listen = Listener()
obs.loopPosition = Observ([0, 8])
obs.context = context
// state layers
obs.grid = computed([obs.targets, obs.shape], ArrayGrid)
obs.playing = Observ(ArrayGrid([], obs.shape()))
obs.active = computed([obs.loops, obs.shape], function (loops, shape) {
return ArrayGrid(loops.map(function (loop) {
return (loop && loop.length && Array.isArray(loop.events) && loop.events.length)
}), shape)
})
var current = {}
var currentlyPlaying = {}
var overriding = {}
var pendingPlayingUpdate = false
var lastPosition = -1
obs.onEvent = Event(function (broadcast) {
obs.triggerEvent = broadcast
obs.triggerEvent = function (event) {
if (event.id) {
if (event.event === 'start') {
overriding[event.id] = true
} else {
overriding[event.id] = false
}
if (!current[event.id]) {
broadcast(event)
}
}
}
listen(context.scheduler.onSchedule, function (schedule) {
var targets = obs.targets()
targets.forEach(function (id, index) {
var loop = obs.loops()[index]
getEvents(loop, schedule.from, schedule.to, 1).forEach(function (event) {
if (id && current[id] !== event[1] && !(!current[id] && !event[1])) {
var delta = (event[0] - schedule.from) * schedule.beatDuration
current[id] = event[1]
if (!overriding[id]) {
broadcast({
id: id,
event: event[1] ? 'start' : 'stop',
position: event[0],
time: schedule.time + delta
})
}
}
})
})
// stop any notes that are no longer targets
Object.keys(current).forEach(function (id) {
if (!~targets.indexOf(id)) {
delete current[id]
if (current[id]) {
broadcast({
id: id,
event: 'stop',
position: schedule.from,
time: schedule.time
})
}
}
})
// update playback position
if (Math.floor(schedule.from * 10) > Math.floor(lastPosition * 10)) {
var loopLength = obs.loopLength() || 8
var pos = Math.floor(schedule.from * 10) % (loopLength * 10)
obs.loopPosition.set([pos / 10, loopLength])
lastPosition = schedule.from
}
})
listen.event(context.scheduler, 'stop', function () {
Object.keys(current).forEach(function (id) {
delete current[id]
if (current[id]) {
broadcast({
id: id,
event: 'stop',
position: context.scheduler.getCurrentPosition(),
time: context.audio.currentTime
})
}
})
})
})
obs.onEvent(function (event) {
if (context.triggerEvent) {
// send events
context.triggerEvent(event)
}
if (event.event === 'start') {
currentlyPlaying[event.id] = true
} else if (event.event === 'stop') {
currentlyPlaying[event.id] = false
}
if (!pendingPlayingUpdate) {
pendingPlayingUpdate = true
setImmediate(refreshPlaying)
}
})
obs.destroy = function () {
listen.releaseAll()
}
return obs
// scoped
function refreshPlaying () {
pendingPlayingUpdate = false
var playing = []
var targets = obs.targets()
var shape = obs.shape()
var max = Array.isArray(shape) && shape[0] * shape[1] || 0
for (var i = 0; i < max; i++) {
if (currentlyPlaying[targets[i]]) {
playing[i] = true
}
}
obs.playing.set(ArrayGrid(playing, shape))
}
}
function Listener () {
var releases = []
function listen (target, listener) {
releases.push(target(listener))
}
listen.event = function (emitter, event, listener) {
emitter.on(event, listener)
releases.push(emitter.removeListener.bind(emitter, event, listener))
}
listen.releaseAll = function () {
while (releases.length) {
releases.pop()()
}
}
return listen
}