function ChunkColors (chunkLookup, selected, targets, shape) { var highlighted = Observ() var timer = null highlighted.clear = function () { highlighted.set(null) } selected(function (id) { clearTimeout(timer) highlighted.set(id) timer = setTimeout(highlighted.clear, 400) }) return computed([chunkLookup, targets, shape, highlighted], function computeMapGridValues (chunkLookup, targets, shape, highlighted) { var result = ArrayGrid([], shape) for (var r = 0; r < shape[0]; r++) { for (var c = 0; c < shape[1]; c++) { var id = getChunkId(targets[result.index(r, c)]) var chunk = chunkLookup[id] if (chunk) { var color = saturate(notBlack(chunk.color, [5, 5, 5]), 2) if (highlighted === id) { color = normalize(color, 1) } else { color = normalize(color, 0.2) } result.set(r, c, color) } } } return result }) }
module.exports = function activeIndexes (obs) { return computed([obs], function (grid) { var result = [] grid.data.forEach(pushIndexIfPresent, result) return result }) }
function createInput () { const keyboard = Keyboard() const changeSlide = computed([keyboard.keyDown], (num) => { return keycode(num) || 'void' }) return changeSlide }
function DiffData (first, last) { return { first: first, last: last, patch: Computed([first, last], function (b, p) { return patcher.computePatch(b, p) }) } }
function link(model, uri, text, expected) { return ["li", [ ["a", { className: computed([model.route], function (route) { return route === expected ? "selected" : "" }), href: uri }, text] ]] }
function mapGridValues(grid, trueValue){ function mapColor(value){ if (value){ return trueValue } } return computed([grid], function computeMapGridValues(source){ if (source){ return ArrayGrid(source.data.map(mapColor), source.shape, source.stride) } }) }
function withResolved (obj, keys) { var result = computed(keys.map(function (k) { return obj[k] }).concat(obj), function (args) { var value = extend(arguments[arguments.length - 1]) keys.forEach(function (key, i) { value[key] = arguments[i] }) return value }) for (var k in obj) { if (k !== 'set') { result[k] = obj[k] } } result.node = obj return result }
function statsSection(model) { return ["footer.footer", { hidden: either(model.todosLength, false, true) }, [ ["span.todo-count", [ ["strong", model.todosLeft], computed([model.todosLength], function (len) { return len === 1 ? " item" : " items" }), " left" ]], ["ul.filters", [ link(model, "#/", "All", "all"), link(model, "#/active", "Active", "active"), link(model, "#/completed", "Completed", "completed") ]] ]] }
/* Display := ( db: SurfaceDB, viewModel: Object<String, Observable> ) => { view: DOMElement } */ function Display(database, viewModel) { var layer = database.layer("main") layer = simpleCache(layer, { deltaX: 32, deltaY: 32 }) layer = clip(layer) var canvas = CanvasRender(layer, { width: WIDTH, height: HEIGHT, blankColor: SKY_COLOR, grass: { width: CONSTANTS.SIZE, height: CONSTANTS.SIZE, color: GRASS_COLOR, outline: BLACK }, dirt: { width: CONSTANTS.SIZE, height: CONSTANTS.SIZE, color: DIRT_COLOR, outline: BLACK }, rock: { width: CONSTANTS.SIZE, height: CONSTANTS.SIZE, color: ROCK_COLOR, outline: null }, entity: { width: CONSTANTS.SIZE, height: CONSTANTS.PLAYER_HEIGHT, color: CONSTANTS.PLAYER_COLOR, outline: "blue" } }, computed([viewModel.camera], SurfaceDB.Rectangle)) return { view: canvas } }
module.exports = function (obs) { // hack for mixer params var params = ['A', 'B', 'C'] obs.params = computed([obs], function (values) { var usedParams = [] JSON.stringify(values, function (key, value) { if (value && value.node === 'linkParam') { var index = params.indexOf(value.param) if (~index) { usedParams[index] = value.param } } return value }) return usedParams }) obs.paramValues = ObservVarhash({}) obs.context.paramLookup = ParamLookup(obs.context.paramLookup, params, obs.paramValues) return obs.context.paramLookup.destroy }
function todoItem(todo, model) { var className = computed([ todo.completed, todo.editing ], function (completed, editing) { return (completed ? "completed " : "") + (editing ? "editing" : "") }) return ["li", { className: className, // when events occur from jsonml-event // you can access the nearest bound model with // `ev.meta` meta: eventMeta(todo) }, [ ["div.view", [ ["input.toggle", { type: "checkbox", checked: todo.completed, change: event(model.events.toggle) }], ["label", { dblclick: event(model.events.editing) }, todo.title], ["button.destroy", { click: event(model.events.destroy) }] ]], ["input.edit", { value: todo.title, // focus primitive, when observable is triggered // it calls .focus() on this element focus: focus(todo.editing), submit: event(model.events.edit), blur: event(model.events.edit) }] ]] }
function Highlighting (grid, selected) { var highlighted = Observ() var timer = null highlighted.clear = function () { highlighted.set(null) } selected(function (id) { clearTimeout(timer) highlighted.set(id) timer = setTimeout(highlighted.clear, 400) }) return computed([grid, highlighted], function (grid, highlighted) { return ArrayGrid(grid.data.map(function (val) { var id = getChunkId(val) if (id === highlighted) { return true } }), grid.shape) }) }
var doubles = arr.map(function (o) { return computed([o], function (o) { return o * 2 }) })
function SlicerChunk (parentContext) { var context = Object.create(parentContext) var output = context.output = context.audio.createGain() context.output.connect(parentContext.output) var queueRefreshSlices = noargs(debounce(refreshSlices, 200)) var slots = NodeArray(context) context.slotLookup = lookup(slots, 'id') var obs = BaseChunk(context, { sample: Sample(context), eq: EQ(context), sliceMode: Property('divide'), stretch: Property(false), tempo: Property(100), outputs: Property(['output']), routes: ExternalRouter(context, {output: '$default'}), volume: Property(1) }, { includedAllTriggers: true }) var releaseMixerParams = applyMixerParams(obs) obs.overrideVolume = Property(1) var volume = computed([obs.volume, obs.overrideVolume], function (a, b) { return a * b }) var watchApplied = obs.sample.resolvedBuffer(function (value) { if (value) { watchApplied() setImmediate(function () { obs.shape(queueRefreshSlices) obs.sample.resolvedBuffer(queueRefreshSlices) obs.sliceMode(queueRefreshSlices) obs.sample.mode(queueRefreshSlices) throttle(obs.sample.offset, 1000)(queueRefreshSlices) if (!obs.sample.slices()) { // ensure slices have been generated queueRefreshSlices() } }) } }) obs.sample.resolvedBuffer(function (value) { // without this everything breaks :( no idea why :( }) var computedSlots = computedNextTick([ obs.sample, obs.stretch, obs.tempo, obs.eq, volume, obs.sample.resolvedBuffer ], function (sample, stretch, tempo, eq, volume, buffer) { var result = (sample.slices || []).map(function (offset, i) { if (stretch && buffer) { var originalDuration = getOffsetDuration(buffer.duration, offset) var stretchedDuration = tempo / 60 * originalDuration return { node: 'slot', id: String(i), output: 'output', volume: volume, sources: [ extend(sample, { node: 'source/granular', mode: 'oneshot', attack: 0.1, hold: 1, release: 0.1, duration: stretchedDuration, sync: true, offset: offset }) ] } } else { return { node: 'slot', id: String(i), output: 'output', volume: volume, sources: [ extend(sample, { node: 'source/sample', mode: 'oneshot', offset: offset }) ] } } }) result.unshift({ node: 'slot', id: 'output', processors: [ extend(eq, {node: 'processor/eq'}) ] }) return result }) watch(computedSlots, slots.set) slots.onUpdate(obs.routes.refresh) obs.destroy = function(){ releaseMixerParams() destroyAll(obs) } return obs // scoped function refreshSlices (cb) { var shape = obs.shape() var buffer = obs.sample.resolvedBuffer() var sliceMode = obs.sliceMode() var triggerMode = obs.sample.mode() var offset = obs.sample.offset() var count = shape[0] * shape[1] var playToEnd = triggerMode === 'full' if (sliceMode === 'peak' || sliceMode === 'transient') { if (buffer) { detectPeaks(buffer.getChannelData(0), count, offset, function (peaks) { obs.sample.slices.set(sliceOffsets(peaks, offset, playToEnd)) cb && cb() }) } else { cb && cb() } } else if (sliceMode === 'snap') { if (buffer) { gridSlicePeaks(buffer.getChannelData(0), count, offset, function (peaks) { obs.sample.slices.set(sliceOffsets(peaks, offset, playToEnd)) cb && cb() }) } else { cb && cb() } } else { obs.sample.slices.set(divideSlices(count, offset, playToEnd)) cb && setImmediate(cb) } } }
function SynthChunk (parentContext) { var context = Object.create(parentContext) context.output = context.audio.createGain() context.output.connect(parentContext.output) var slots = NodeArray(context) context.slotLookup = lookup(slots, 'id') var obs = BaseChunk(context, { osc1: Osc(context), osc2: Osc(context, { optional: true }), osc3: Osc(context, { optional: true, allowMultiply: true }), offset: Param(context, 0), amp: Param(context, 1), filter: Filter(context), eq: EQ(context), outputs: Property(['output']), volume: Property(1), routes: ExternalRouter(context, {output: '$default'}) }) var releaseMixerParams = applyMixerParams(obs) obs.overrideVolume = Property(1) var volume = computed([obs.volume, obs.overrideVolume], function (a, b) { return a * b }) context.offset = obs.offset obs.amp.triggerable = true var scale = Property({ offset: 0, notes: [0,2,4,5,7,9,11] }) if (context.globalScale) { var releaseGlobalScale = watch(context.globalScale, scale.set) } var computedSlots = computed([ obs.shape, obs.osc1, obs.osc2, obs.osc3, obs.filter, obs.amp, obs.eq, volume, scale ], function (shape, osc1, osc2, osc3, filter, amp, eq, volume, scale) { var length = shape[0] * shape[1] var result = [{ node: 'slot', id: 'output', volume: volume, processors: [ extend(eq, {node: 'processor/eq'}), ] }] for (var i = 0; i < length; i++) { var noteOffset = { node: 'modulator/scale', value: i, scale: scale } var sources = [ extend(osc1, {node: 'source/oscillator' }) ] var processors = [ extend(filter, {node: 'processor/filter'}), { node: 'processor/gain', gain: amp } ] if (osc2.enabled) { sources.push(extend(osc2, { node: 'source/oscillator' })) } if (osc3.enabled) { if (osc3.multiply) { processors.unshift({ node: 'processor/ring-modulator', carrier: osc3 }) } else { sources.push(extend(osc3, {node: 'source/oscillator' })) } } result.push({ node: 'slot', id: String(i), output: 'output', noteOffset: noteOffset, sources: sources, processors: processors }) } return result }) computedSlots(slots.set) //throttleWatch(computedSlots, 50, function (value) { // slots.set(value) // // // HACK: bump shape to trigger update of slot mapping // obs.shape.set(obs.shape()) //}) slots.onUpdate(obs.routes.refresh) obs.destroy = function () { releaseMixerParams() destroyAll(obs) slots.destroy() releaseGlobalScale && releaseGlobalScale() releaseGlobalScale = null } return obs }
module.exports = function (context) { var midiPort = MidiPort(context, function (port, lastPort) { // turn off on switch lastPort && lastPort.write(turnOffAll) port && port.write(turnOffAll) }) var obs = ObservStruct({ port: midiPort, chunkIds: Property([]) }) var params = [] for (var i = 0; i < 8; i++) { params[i] = [ Param(context, 0), Param(context, 0), Param(context, 0) ] } var paramReleases = [] obs.chunkIds(refreshParamLinks) context.chunkLookup(refreshParamLinks) function refreshParamLinks () { while (paramReleases.length) { paramReleases.pop()() } obs.chunkIds().forEach(function (id, i) { var chunk = context.chunkLookup.get(id) if (chunk && chunk.overrideParams) { paramReleases.push(chunk.overrideParams(params[i])) } }) } // grab the midi for the current port obs.grabInput = function () { midiPort.grab() } obs.context = context var setup = context.setup var paramState = [] watchKnobs(midiPort.stream, mappings.row1.concat(mappings.row2, mappings.row3), function (id, data) { var param = params[id % 8][Math.floor(id / 8)] var chunk = setup.context.chunkLookup.get(obs.chunkIds()[id % 8]) if (chunk && chunk.overrideParams && chunk.params) { param.set(scaleInterpolate(param() * 128, data, paramState[id] = paramState[id] || {}) / 128) } }) var sliderState = [] watchKnobs(midiPort.stream, mappings.sliders, function (id, data) { var chunk = setup.chunks.lookup.get(obs.chunkIds()[id]) if (chunk) { var volume = chunk.overrideVolume || chunk.node && chunk.node.overrideVolume if (volume) { var currentPosition = Math.pow(volume(), 1 / Math.E) * 108 var newPosition = scaleInterpolate(currentPosition, data, sliderState[id] = sliderState[id] || {}) volume.set(Math.pow(newPosition / 108, Math.E)) } } }, 108) var pressed = PressedChunks(setup.controllers) var knobLights = computed([obs.chunkIds, setup.context.chunkLookup, pressed, setup.selectedChunkId], function (chunkIds, lookup, pressed, selected) { var result = [] for (var i = 0; i < 8; i++) { var chunk = setup.context.chunkLookup.get(chunkIds[i]) if (chunk && chunk.params) { var onValue = pressed[chunkIds[i]] ? light(0, 2) : selected === chunkIds[i] ? light(1, 1) : light(2, 0) result[0 + i] = chunk.params()[0] ? onValue : 0 result[8 + i] = chunk.params()[1] ? onValue : 0 result[16 + i] = chunk.params()[2] ? onValue : 0 } else { result[0 + i] = result[8 + i] = result[16 + i] = 0 } } return result }) setLights(knobLights, midiPort.stream) var buttonBase = computed([setup.selectedChunkId, obs.chunkIds, pressed], function (selected, chunkIds, pressed) { var result = [] for (var i = 0; i < 8; i++) { var chunkId = chunkIds[i] if (chunkId) { if (chunkId === selected) { result.push(light(2, 3)) } else if (pressed[chunkId]) { result.push(light(0, 1)) } else { result.push(light(1, 0)) } } else { result.push(0) } } return result }) var buttonFlash = FlashArray() setup.onTrigger(function (event) { if (event.id) { var chunkId = event.id.split('/')[0] var index = obs.chunkIds().indexOf(chunkId) if (event.event === 'start') { if (chunkId === setup.selectedChunkId()) { buttonFlash.flash(index, light(3, 3), 40) } else { buttonFlash.flash(index, light(3, 0), 40) } } } }) var buttons = ObservMidi(midiPort.stream, mappings.trackFocus, ArrayStack([ buttonBase, buttonFlash ])) buttons(function (values) { var result = null values.forEach(function (val, i) { if (val) { result = i } }) if (result != null) { var id = obs.chunkIds()[result] if (id) { setup.selectedChunkId.set(id) setup.context.actions.scrollToSelectedChunk() } } }) // CONTROL BUTTONS: var controlButtons = LightStack(midiPort.stream, { mode: mappings.device }) controlButtons.mode(function (value) { if (value) { context.project.globalControllers.forEach(function (controller) { if (controller && controller.port && controller.port() === obs.port() && controller.grabInput) { controller.grabInput() controller.port.override.set(true) } }) } }) obs.destroy = function () { midiPort.destroy() } return obs }
module.exports = function(context){ var loopGrid = LoopGrid(context) var looper = Looper(loopGrid) var scheduler = context.scheduler var gridMapping = getLaunchpadGridMapping() loopGrid.shape.set(gridMapping.shape) var shiftHeld = false var activatedAt = 0 var midiPort = MidiPort(context, function (port, lastPort) { // turn off on switch lastPort && lastPort.write([176, 0, 0]) if (port) { port.write([176, 0, 0]) activatedAt = Date.now() } }) // extend loop-grid instance var obs = ObservStruct({ port: midiPort, loopLength: loopGrid.loopLength, chunkPositions: ObservVarhash({}) }) obs.gridState = ObservStruct({ active: loopGrid.active, playing: loopGrid.playing, recording: looper.recording, triggers: loopGrid.grid }) obs.activeInput = computed([midiPort.stream], function (value) { return !!value }) watch(looper, loopGrid.loops.set) obs.context = context obs.playback = loopGrid obs.looper = looper obs.repeatLength = Observ(2) var flags = computeFlags(context.chunkLookup, obs.chunkPositions, loopGrid.shape) watch( // compute targets from chunks computeTargets(context.chunkLookup, obs.chunkPositions, loopGrid.shape), loopGrid.targets.set ) // grab the midi for the current port obs.grabInput = function () { midiPort.grab() } // loop transforms var transforms = { selector: Selector(gridMapping.shape, gridMapping.stride), holder: Holder(looper.transform), mover: Mover(looper.transform), repeater: Repeater(looper.transformTop), suppressor: Suppressor(looper.transform, gridMapping.shape, gridMapping.stride) } var outputLayers = ObservGridStack([ // recording mapGridValue(looper.recording, stateLights.redLow), // active mapGridValue(loopGrid.active, stateLights.greenLow), // flash selected chunk mapGridValue(Highlighting(loopGrid.grid, context.setup.selectedChunkId), stateLights.amberLow), // selected mapGridValue(transforms.selector, stateLights.green), // suppressing mapGridValue(transforms.suppressor, stateLights.red), // playing mapGridValue(loopGrid.playing, stateLights.amber) ]) var controllerGrid = ObservMidi(midiPort.stream, gridMapping, outputLayers) var inputGrabber = GrabGrid(controllerGrid) var noRepeat = computeIndexesWhereContains(flags, 'noRepeat') var grabInputExcludeNoRepeat = function (listener) { return inputGrabber(listener, { exclude: noRepeat }) } var inputGrid = Observ() watch(inputGrabber, inputGrid.set) var activeIndexes = computeActiveIndexes(inputGrid) // trigger notes at bottom of input stack var output = DittyGridStream(inputGrid, loopGrid.grid, context.scheduler) output.on('data', loopGrid.triggerEvent) obs.currentlyPressed = computed([controllerGrid, loopGrid.grid], function (value, grid) { return grid.data.filter(function (name, index) { if (value.data[index]) { return true } }) }) // midi button mapping var buttons = MidiButtons(midiPort.stream, { store: '176/104', flatten: '176/105', undo: '176/106', redo: '176/107', hold: '176/108', suppress: '176/109', snap2: '176/110', select: '176/111' }) var releaseLoopLengthLights = [] watchButtons(buttons, { store: function(value){ if (value){ this.flash(stateLights.green) looper.store() } }, flatten: function(value){ if (value){ var active = activeIndexes() if (looper.isTransforming() || active.length){ looper.transform(holdActive, active) looper.flatten() transforms.selector.stop() this.flash(stateLights.green, 100) } else { this.flash(stateLights.red, 100) transforms.suppressor.start(transforms.selector.selectedIndexes()) looper.flatten() transforms.suppressor.stop() transforms.selector.stop() } } }, undo: function(value){ if (value){ if (shiftHeld){ // halve loopLength var current = obs.loopLength() || 1 obs.loopLength.set(current/2) this.flash(stateLights.green, 100) } else { looper.undo() this.flash(stateLights.red, 100) buttons.store.flash(stateLights.red) } } }, redo: function(value){ if (value){ if (shiftHeld){ // double loopLength var current = obs.loopLength() || 1 obs.loopLength.set(current*2) this.flash(stateLights.green, 100) } else { looper.redo() this.flash(stateLights.red, 100) buttons.store.flash(stateLights.red) } } }, hold: function(value){ if (value){ var turnOffLight = this.light(stateLights.yellow) transforms.holder.start( scheduler.getCurrentPosition(), transforms.selector.selectedIndexes(), turnOffLight ) } else { transforms.holder.stop() } }, suppress: function(value){ if (value){ var turnOffLight = this.light(stateLights.red) transforms.suppressor.start(transforms.selector.selectedIndexes(), turnOffLight) } else { transforms.suppressor.stop() } }, swapTarget: function (value) { if (value) { getPortSiblings(obs, context.setup.controllers)[1].grabInput() } else if (Date.now() - activatedAt > 500) { getPortSiblings(obs, context.setup.controllers)[0].grabInput() } }, select: function(value){ if (value){ var turnOffLight = this.light(stateLights.green) transforms.selector.start(inputGrabber, function done(){ transforms.mover.stop() transforms.selector.clear() turnOffLight() }) } else { if (transforms.selector.selectedIndexes().length){ transforms.mover.start(inputGrabber, transforms.selector.selectedIndexes()) } else { transforms.selector.stop() } } } }) // shift button (share select button) watch(buttons.select, function(value){ if (value){ shiftHeld = true // turn on loop length lights releaseLoopLengthLights.push( buttons.undo.light(stateLights.greenLow), buttons.redo.light(stateLights.greenLow) ) } else { shiftHeld = false // turn off loop length lights while (releaseLoopLengthLights.length){ releaseLoopLengthLights.pop()() } } }) // light up undo buttons by default buttons.undo.light(stateLights.redLow) buttons.redo.light(stateLights.redLow) buttons.store.light(stateLights.amberLow) var willFlatten = computed([activeIndexes, looper.transforms], function (indexes, transforms) { return !!indexes.length || !!transforms.length }) // light up store button when transforming (flatten mode) var releaseFlattenLight = null watch(willFlatten, function(value){ if (value && !releaseFlattenLight){ releaseFlattenLight = buttons.flatten.light(stateLights.greenLow) } else if (!value && releaseFlattenLight){ releaseFlattenLight() releaseFlattenLight = null } }) var repeatButtons = MidiButtons(midiPort.stream, { 0: '144/8', 1: '144/24', 2: '144/40', 3: '144/56', 4: '144/72', 5: '144/88', 6: '144/104', 7: '144/120' }) // repeater var releaseRepeatLight = null mapWatchDiff(repeatStates, repeatButtons, obs.repeatLength.set) watch(obs.repeatLength, function(value){ var button = repeatButtons[repeatStates.indexOf(value)] if (button){ if (releaseRepeatLight) releaseRepeatLight() releaseRepeatLight = button.light(stateLights.amberLow) } transforms.holder.setLength(value) if (value < 2){ transforms.repeater.start(grabInputExcludeNoRepeat, value) } else { transforms.repeater.stop() } }) // visual metronome / loop position var releaseBeatLight = null var currentBeatLight = null var currentBeat = null watch(loopGrid.loopPosition, function(value){ var beat = Math.floor(value[0]) var index = Math.floor(value[0] / value[1] * 8) var button = repeatButtons[index] if (index != currentBeatLight){ if (button){ releaseBeatLight&&releaseBeatLight() releaseBeatLight = button.light(stateLights.greenLow, 0) } currentBeatLight = index } if (beat != currentBeat){ button.flash(stateLights.green) currentBeat = beat } }) // cleanup / disconnect from keyboard on destroy obs.destroy = function () { midiPort.destroy() output.destroy() loopGrid.destroy() } return obs }
module.exports = function applyParams (obs) { // HACK: make ring modulator effect work on output channel var triggeredSlots = [] obs.slots(function (slots) { obs.slots.forEach(function (slot) { var id = slot.id() if (!isFinite(id) && !~triggeredSlots.indexOf(id)) { triggeredSlots.push(slot.id) slot.triggerOn(obs.context.audio.currentTime) } }) }) var paramOverrideStack = ObservArray([]) obs.overrideParams = function (params) { paramOverrideStack.push(params) return function release () { var index = paramOverrideStack.indexOf(params) if (~index) { paramOverrideStack.splice(index, 1) } } } var raw = {} var paramLookup = computed([obs.params, obs.paramValues, paramOverrideStack], function (params, values, overrides) { var result = {} var rawResult = {} for (var i = 0; i < params.length; i++) { var key = params[i] var override = paramOverrideStack.get(paramOverrideStack.getLength() - 1) if (override && override[i] != null) { result[key] = typeof override[i] === 'function' ? override[i]() : override[i] || 0 rawResult[key] = override[i] } else { result[key] = values && values[key] || 0 rawResult[key] = obs.paramValues.get(key) } } raw = rawResult return result }) paramLookup.get = function(key) { return raw[key] } paramLookup.keys = function(key) { return Object.keys(raw) } obs.context.paramLookup = paramLookup obs.resolveAvailableParam = function(id){ var base = id var items = obs.params() var incr = 0 while (~items.indexOf(id)){ incr += 1 id = base + ' ' + (incr + 1) } return id } }
function LoopGrid(opts, additionalProperties){ // required options: var player = opts.player var recorder = opts.recorder // optional: var shape = opts.shape || [8,8] var scheduler = opts.scheduler || null var triggerOutput = opts.triggerOutput || null var obs = ObservStruct(xtend({ node: Observ(), // this is useful for loop-drop-setup :) chunkPositions: ObservVarhash({}) }, additionalProperties)) obs.transforms = ObservArray([]) var soundChunkLookup = {} var undos = [] var redos = [] var baseLoops = {} var currentLoops = {} var releases = obs._releases = [] releases.push( // update playback when transforms change obs.transforms(refreshCurrent) ) obs.chunkState = Observ([]) obs.flags = Observ({}) obs.triggerIds = Observ([]) obs.loopLength = Observ(8) obs.grid = computed([obs.chunkPositions, opts.chunkLookup], function(chunkPositions, chunkLookup){ var result = Grid([], shape) var flags = Grid([], shape) var triggerIds = [] var chunkState = [] if (chunkPositions){ soundChunkLookup = {} Object.keys(chunkPositions).forEach(function(chunkId){ var chunk = chunkLookup[chunkId] var origin = chunkPositions[chunkId] if (chunk && origin){ chunkState.push({ id: chunkId, origin: origin, shape: chunk.grid.shape, stride: chunk.grid.stride, node: chunk.node, color: chunk.color }) result.place(origin[0], origin[1], chunk.grid) for (var k in chunk.flags){ if (Array.isArray(chunk.flags[k]) && chunk.flags[k].length){ var index = result.data.indexOf(k) if (~index){ flags.data[index] = chunk.flags[k] } } } for (var i=0;i<chunk.grid.data.length;i++){ if (chunk.grid.data[i] != null){ triggerIds.push(chunk.grid.data[i]) soundChunkLookup[chunk.grid.data[i]] = chunk.id } } } }) } obs.chunkState.set(chunkState) obs.flags.set(flags) obs.triggerIds.set(triggerIds) return result }) if (triggerOutput){ obs.playing = computedDittyGrid(triggerOutput, obs.grid) } if (player){ obs.active = computedActiveGrid(player, obs.grid) } if (scheduler){ obs.loopPosition = computedLoopPosition(scheduler, obs.loopLength) } if (scheduler && triggerOutput){ obs.recording = computedRecording(scheduler, triggerOutput, obs.grid, obs.loopLength) } if (obs.playing && obs.active && obs.recording){ // for binding to grid visual interface obs.gridState = computed([ obs.grid, obs.playing, obs.active, obs.recording, obs.loopPosition, obs.loopLength ], function(grid, playing, active, recording, loopPosition, loopLength){ var length = grid.data.length var result = [] for (var i=0;i<length;i++){ if (grid.data[i]){ result[i] = { id: grid.data[i], isPlaying: playing.data[i], isActive: active.data[i], isRecording: recording.data[i] } } } return { loopPosition: loopPosition, loopLength: loopLength, grid: Grid(result, grid.shape, grid.stride), chunks: obs.chunkState() } }) } obs.destroy = function(){ if (obs.playing) obs.playing.destroy() if (obs.active) obs.active.destroy() if (obs.loopPosition) obs.loopPosition.destroy() if (obs.recording) obs.recording.destroy() releases.forEach(invoke) releases = [] } // grab loops from recorder for all sounds currently in grid and set loop obs.store = function(length, start){ // defaults length = length || obs.loopLength() start = start == null && opts.scheduler ? opts.scheduler.getCurrentPosition() - length : start undos.push(baseLoops) var snapshot = obs.triggerIds().reduce(function(result, id){ result[id] = { events: recorder.getLoop(id, start, length), length: length } return result }, {}) setBase(snapshot) } obs.transform = function(func, args){ // transform relative to grid var t = { func: func, args: Array.prototype.slice.call(arguments, 1) } obs.transforms.push(t) return function release(){ var index = obs.transforms.indexOf(t) if (~index){ obs.transforms.splice(index, 1) } } } obs.isTransforming = function(){ return !!obs.transforms.getLength() } obs.flatten = function(){ // flatten transforms undos.push(baseLoops) targetLoops = currentLoops obs.transforms.set([]) setBase(targetLoops) } obs.undo = function(){ var snapshot = undos.pop() if (snapshot){ redos.push(baseLoops) setBase(snapshot) } } obs.redo = function(){ var snapshot = redos.pop() if (snapshot){ undos.push(baseLoops) setBase(snapshot) } } return obs // scoped function refreshCurrent(){ currentLoops = gridTransform(baseLoops, obs.transforms()) obs.triggerIds().forEach(function(id){ var channel = currentLoops[id] if (channel && channel.events && channel.events.length){ player.set(id, channel.events, channel.length) } else { player.set(id, null) } currentLoops[id] = channel }) } function cloneBaseLoop(id){ if (baseLoops[id] && baseLoops[id].events){ return { events: baseLoops[id].events.concat(), length: baseLoops[id].length } } else { return null } return baseLoops[id] } function wrapToLookup(result, value, index){ var id = obs.grid().data[index] if (id != null){ result[id] = value } return result } function performTransform(input, f){ return f.func.apply(this, [input].concat(f.args||[])) } function gridTransform(input, transforms){ if (transforms && transforms.length){ var soundGrid = obs.grid() var data = soundGrid.data.map(cloneBaseLoop) var playbackGrid = Grid(data, soundGrid.shape, soundGrid.stride) // perform transform soundGrid = transforms.reduce(performTransform, playbackGrid) // turn back into loop lookup return soundGrid.data.reduce(wrapToLookup, {}) } else { return input } } function setBase(snapshot){ baseLoops = {} obs.triggerIds().forEach(function(id){ baseLoops[id] = snapshot[id] }) refreshCurrent() } }
module.exports = function(context){ var loopGrid = LoopGrid(context) var looper = Looper(loopGrid) var scheduler = context.scheduler var gridMapping = getPushGridMapping() loopGrid.shape.set(gridMapping.shape) var shiftHeld = false // controller midi port var midiPort = MidiPort(context, function (stream, lastStream) { if (lastStream) turnOffAllLights(lastStream) if (stream) { turnOffAllLights(stream) initDisplay() } }) // Push display var display = new PushDisplay(midiPort.stream) // extend loop-grid instance var obs = ObservStruct({ port: midiPort, loopLength: loopGrid.loopLength, chunkPositions: ObservVarhash({}) }) obs.gridState = ObservStruct({ active: loopGrid.active, playing: loopGrid.playing, recording: looper.recording, triggers: loopGrid.grid }) watch(looper, loopGrid.loops.set) obs.context = context obs.playback = loopGrid obs.looper = looper obs.repeatLength = Observ(2) var flags = computeFlags(context.chunkLookup, obs.chunkPositions, loopGrid.shape) watch( // compute targets from chunks computeTargets(context.chunkLookup, obs.chunkPositions, loopGrid.shape), loopGrid.targets.set ) // grab the midi for the current port obs.grabInput = function(){ midiPort.grab() } // loop transforms var transforms = { selector: Selector(gridMapping.shape, gridMapping.stride), holder: Holder(looper.transform), mover: Mover(looper.transform), repeater: Repeater(looper.transformTop), suppressor: Suppressor(looper.transform, gridMapping.shape, gridMapping.stride) } var outputLayers = ObservGridStack([ // recording mapGridValue(looper.recording, pushColors.pads.redLow), // active mapGridValue(loopGrid.active, pushColors.pads.greenLow), // selected mapGridValue(transforms.selector, pushColors.pads.blue), // suppressing mapGridValue(transforms.suppressor, pushColors.pads.red), // playing mapGridValue(loopGrid.playing, pushColors.pads.limeHi) ]) var controllerGrid = ObservMidi(midiPort.stream, gridMapping, outputLayers) var inputGrabber = GrabGrid(controllerGrid) var noRepeat = computeIndexesWhereContains(flags, 'noRepeat') var freezeSuppress = computeIndexesWhereContains(flags, 'freezeSuppress') var grabInputExcludeNoRepeat = function (listener) { return inputGrabber(listener, { exclude: noRepeat }) } var inputGrid = Observ() watch(inputGrabber, inputGrid.set) var activeIndexes = computeActiveIndexes(inputGrid) // trigger notes at bottom of input stack var output = DittyGridStream(inputGrid, loopGrid.grid, context.scheduler) output.on('data', loopGrid.triggerEvent) // midi command button mapping // On Push this is the row right above the pads var buttons = MidiButtons(midiPort.stream, { store: '176/102', flatten: '176/103', undo: '176/104', redo: '176/105', hold: '176/106', suppress: '176/107', snap2: '176/108', select: '176/109' }) var releaseLoopLengthLights = [] watchButtons(buttons, { store: function(value){ if (value){ this.flash(pushColors.pads.green) looper.store() } }, flatten: function(value){ if (value){ var active = activeIndexes() if (looper.isTransforming() || active.length){ looper.transform(holdActive, active) looper.flatten() transforms.selector.stop() this.flash(pushColors.pads.green, 100) } else { this.flash(pushColors.pads.red, 100) transforms.suppressor.start(scheduler.getCurrentPosition(), transforms.selector.selectedIndexes()) looper.flatten() transforms.suppressor.stop() transforms.selector.stop() } } }, undo: function(value){ if (value){ if (shiftHeld){ // halve loopLength var current = obs.loopLength() || 1 obs.loopLength.set(current/2) this.flash(pushColors.pads.green, 100) } else { looper.undo() this.flash(pushColors.pads.red, 100) buttons.store.flash(pushColors.pads.red) } } }, redo: function(value){ if (value){ if (shiftHeld){ // double loopLength var current = obs.loopLength() || 1 obs.loopLength.set(current*2) this.flash(pushColors.pads.green, 100) } else { looper.redo() this.flash(pushColors.pads.red, 100) buttons.store.flash(pushColors.pads.red) } } }, hold: function(value){ if (value){ var turnOffLight = this.light(pushColors.pads.yellow) transforms.holder.start( scheduler.getCurrentPosition(), transforms.selector.selectedIndexes(), turnOffLight ) } else { transforms.holder.stop() } }, suppress: function (value) { if (value) { var turnOffLight = this.light(pushColors.pads.red) transforms.suppressor.start(scheduler.getCurrentPosition(), transforms.selector.selectedIndexes(), freezeSuppress(), turnOffLight) } else { transforms.suppressor.stop() } }, select: function(value){ if (value){ var turnOffLight = this.light(pushColors.pads.green) transforms.selector.start(inputGrabber, function done(){ transforms.mover.stop() transforms.selector.clear() turnOffLight() }) } else { if (transforms.selector.selectedIndexes().length){ transforms.mover.start(inputGrabber, transforms.selector.selectedIndexes()) } else { transforms.selector.stop() } } } }) // shift button (share select button) watch(buttons.select, function(value){ if (value){ shiftHeld = true // turn on loop length lights releaseLoopLengthLights.push( buttons.undo.light(pushColors.pads.pink), buttons.redo.light(pushColors.pads.pink) ) // Update display display .setCell(3, 2, "Halve") .setCell(3, 3, "Double") .update(); } else { shiftHeld = false // turn off loop length lights while (releaseLoopLengthLights.length){ releaseLoopLengthLights.pop()() } // Update display display .setCell(3, 2, "Undo") .setCell(3, 3, "Redo") .update(); } }) // light up undo buttons by default buttons.undo.light(pushColors.pads.cyan) buttons.redo.light(pushColors.pads.cyan) buttons.store.light(pushColors.pads.amberLow) var willFlatten = computed([activeIndexes, looper.transforms], function (indexes, transforms) { return !!indexes.length || !!transforms.length }) // light up store button when transforming (flatten mode) var releaseFlattenLight = null watch(willFlatten, function(value){ if (value && !releaseFlattenLight){ releaseFlattenLight = buttons.flatten.light(pushColors.pads.greenLow) } else if (!value && releaseFlattenLight){ releaseFlattenLight() releaseFlattenLight = null } }) // Push side buttons - labels don't match, left here for reference // var repeatButtons = MidiButtons(duplexPort, { // 0: '176/43', // 1: '176/42', // 2: '176/41', // 3: '176/40', // 4: '176/39', // 5: '176/38', // 6: '176/37', // 7: '176/36' // }) // Push top row buttons var repeatButtons = MidiButtons(midiPort.stream, { 0: '176/20', 1: '176/21', 2: '176/22', 3: '176/23', 4: '176/24', 5: '176/25', 6: '176/26', 7: '176/27' }) // repeater var releaseRepeatLight = null mapWatchDiff(repeatStates, repeatButtons, obs.repeatLength.set) watch(obs.repeatLength, function(value){ var button = repeatButtons[repeatStates.indexOf(value)] if (button){ if (releaseRepeatLight) releaseRepeatLight() releaseRepeatLight = button.light(pushColors.buttons.amberLow) } transforms.holder.setLength(value) if (value < 2){ transforms.repeater.start(grabInputExcludeNoRepeat, value) } else { transforms.repeater.stop() } }) // visual metronome / loop position var releaseBeatLight = null var currentBeatLight = null var currentBeat = null watch(loopGrid.loopPosition, function(value){ var beat = Math.floor(value[0]) var index = Math.floor(value[0] / value[1] * 8) var button = repeatButtons[index] if (index != currentBeatLight){ if (button){ releaseBeatLight&&releaseBeatLight() releaseBeatLight = button.light(pushColors.buttons.greenLow, 0) } currentBeatLight = index } if (beat != currentBeat){ button.flash(pushColors.buttons.greenHi) currentBeat = beat } }) // cleanup / disconnect from keyboard on destroy obs.destroy = function(){ midiPort.destroy() display.init() output.destroy() loopGrid.destroy() } return obs // scoped function initDisplay() { // Clear screen display.init(); // Top line display .setCell(0, 3, " Loop") .setCell(0, 4, "Drop"); // Repeats display .setCell(2, 0, "Trigger") .setCell(2, 1, " 1") .setCell(2, 2, " 2/3") .setCell(2, 3, " 1/2") .setCell(2, 4, " 1/3") .setCell(2, 5, " 1/4") .setCell(2, 6, " 1/6") .setCell(2, 7, " 1/8"); // Buttons display .setCell(3, 0, "RecLoop") .setCell(3, 1, "Clr/Flat") .setCell(3, 2, "Undo") .setCell(3, 3, "Redo") .setCell(3, 4, "BeatHold") .setCell(3, 5, "Suppress") .setCell(3, 6, "SwapTrgt") .setCell(3, 7, "Select"); display.update(); } function turnOffAllLights (port) { var LOW_PAD = 36, // Bottom left HI_PAD = 99, // Top Right LOW_REPEAT = 10, HI_REPEAT = 27, LOW_BUTTON = 102, HI_BUTTON = 109; // Clear notes for (var pad = LOW_PAD; pad <= HI_PAD; pad++) { port.write([128, pad, 0]); } // Clear repeat buttons for (var button = LOW_REPEAT; button <= HI_REPEAT; button++) { port.write([176, button, 0]); } // Clear buttons for (var button = LOW_BUTTON; button <= HI_BUTTON; button++) { port.write([176, button, 0]); } } }
function BaseChunk (context, extraProperties, opts) { var obs = ObservStruct(extend({ id: Observ(), shape: Property([1,4]), flags: Property([]), chokeAll: Property(false), chokeGroup: Property(), color: Property([255,255,255]) }, extraProperties)) if (context.setup) { obs.selected = computed([obs.id, context.setup.selectedChunkId], function (id, selectedId) { return id === selectedId }) } obs.context = context obs.triggerOn = function(id, at){ var slot = context.slotLookup.get(id) var shape = obs.shape() var length = shape[0] * shape[1] if (obs.chokeGroup()) { var chokeGroup = resolve(obs.chokeGroup) context.setup.chunks.forEach(function (chunk) { if (chunk && chunk !== obs && resolve(chunk.chokeGroup) === chokeGroup) { chokeAll(chunk) } }) } if (obs.chokeAll()) { chokeAll(obs, at) } if (slot) { if (slot().chokeGroup) { obs.triggers().forEach(function (id) { var otherSlot = context.slotLookup.get(id) if (otherSlot && otherSlot !== slot && otherSlot().chokeGroup === slot().chokeGroup) { otherSlot.choke && otherSlot.choke(at) } }) } slot.triggerOn(at) } } obs.triggerOff = function(id, at){ var slot = context.slotLookup.get(id) if (slot){ slot.triggerOff(at) } } obs.choke = function (id, at) { var slot = context.slotLookup.get(id) if (slot && slot.choke){ slot.choke(at) } } obs.getSlot = function(id){ return context.slotLookup.get(id) } obs.triggers = computed([obs.id, obs.shape, context.slotLookup], function(id, shape){ var length = shape[0] * shape[1] var result = [] for (var i=0;i<length;i++){ if (obs.getSlot(String(i)) || (opts && opts.includedAllTriggers)) { result.push(String(i)) } else { result.push(null) } } return result }) obs.grid = computed([obs.triggers, obs.shape], ArrayGrid) obs.resolvedGrid = computed([obs.triggers, obs.shape], function(triggers, shape){ return ArrayGrid(triggers.map(getGlobalId), shape) }) return obs // scoped function getGlobalId(id){ if (id){ return obs.id() + '/' + id } } }
function Setup(parentContext){ var context = Object.create(parentContext) var audioContext = context.audio var refreshingParamCount = false var node = ObservStruct({ controllers: NodeArray(context), chunks: NodeArray(context), selectedChunkId: Observ(), volume: Property(1), globalScale: Property({ offset: 0, notes: [0,2,4,5,7,9,11] }) }) node.overrideVolume = Property(1) node.overrideLowPass = Property(0) node.overrideHighPass = Property(0) node._type = 'LoopDropSetup' context.setup = node context.globalScale = node.globalScale // main output context.output = audioContext.createGain() // mixer FX var outputLowPass = audioContext.createBiquadFilter() outputLowPass.Q.value = 0 var outputHighPass = audioContext.createBiquadFilter() outputHighPass.Q.value = 0 outputHighPass.type = 'highpass' context.output.connect(outputLowPass) outputLowPass.connect(outputHighPass) node.output = YankSilence(audioContext, outputHighPass) node.output.connect(parentContext.output) context.active = node.output.active watch(computed([node.volume, node.overrideVolume], (a, b) => a * b), function (value) { node.output.gain.value = value }) watch(node.overrideLowPass, function (value) { outputLowPass.frequency.setTargetAtTime(interpolate(value, 20000, 20, 'exp'), audioContext.currentTime, 0.01) }) watch(node.overrideHighPass, function (value) { outputHighPass.frequency.setTargetAtTime(interpolate(value, 20, 15000, 'exp'), audioContext.currentTime, 0.01) }) node.onTrigger = Event(function (b) { context.triggerEvent = b }) node.onTrigger(function (event) { if (event.id) { node.output.trigger() var split = event.id.split('/') var chunk = context.chunkLookup.get(split[0]) var slotId = split[1] if (chunk) { if (event.event === 'start' && event.time >= context.audio.currentTime - 0.001) { chunk.triggerOn(slotId, event.time) } else if (event.event === 'stop') { chunk.triggerOff(slotId, event.time) } } } }) node.chunks.resolveAvailable = function(id){ var base = id var lookup = context.chunkLookup() var incr = 0 while (lookup[id]){ incr += 1 id = base + ' ' + (incr + 1) } return id } // deprecated: use chunks.resolveAvailable node.resolveAvailableChunk = node.chunks.resolveAvailable node.destroy = function(){ destroyAll(node) context.paramLookup.destroy() } // maps and lookup node.controllers.resolved = map(node.controllers, resolve) node.chunks.resolved = map(node.chunks, resolve) node.chunks.lookup = lookup(node.chunks, function(x){ var descriptor = get(x) return descriptor && descriptor.id || undefined }) // enforce controller types node.controllers.onUpdate(function (update) { update.slice(2).forEach(function (controller) { if (controller.port) { assignAvailablePort(controller) } }) }) context.chunkLookup = lookup(node.chunks, function(x){ if (x){ var data = x.resolved ? x.resolved() : x() return data && data.id || undefined } }, resolve, resolveInner) // extend param lookup var lookups = [] if (context.paramLookup) { lookups.push(context.paramLookup) } lookups.push( lookup(node.chunks, function(x){ if (x && x.onSchedule){ return x.id() } }, resolve, resolveInner) ) context.paramLookup = merge(lookups) node.context = context node.resolved = ObservStruct({ selectedChunkId: node.selectedChunkId, controllers: node.controllers.resolved, chunks: node.chunks.resolved, paramCount: Observ(0) }) context.paramLookup(refreshParamCount) node.grabInput = function(){ var length = node.controllers.getLength() for (var i=0;i<length;i++){ var controller = node.controllers.get(i) if (controller.grabInput){ controller.grabInput() } } // now focus the selected chunk if (node.selectedChunkId){ var chunkId = node.selectedChunkId() for (var i=0;i<length;i++){ var controller = node.controllers.get(i) var chunkPositions = controller().chunkPositions || {} if (controller.grabInput && chunkPositions[chunkId]){ controller.grabInput() } } } } node.updateChunkReferences = function (oldId, newId) { node.controllers.forEach(function (controller) { if (controller.chunkPositions && controller.chunkPositions()) { var value = controller.chunkPositions()[oldId] if (value && controller.chunkPositions.put) { controller.chunkPositions.delete(oldId) if (newId) { controller.chunkPositions.put(newId, value) } } } if (controller.chunkIds && controller.chunkIds()) { controller.chunkIds.set(controller.chunkIds().map(x => x === oldId ? newId : x)) } }) updateParamReferences(node.chunks, oldId, newId) // check for routes matching chunk id node.chunks.forEach(function (chunk) { updateRouteReferences(chunk, oldId, newId) }) if (node.selectedChunkId() === oldId){ node.selectedChunkId.set(newId) } } return node // scoped function refreshParamCount () { if (!refreshingParamCount) { refreshingParamCount = true process.nextTick(refreshParamCountNow) } } function refreshParamCountNow () { refreshingParamCount = false var count = Object.keys(context.paramLookup()).length if (count !== node.resolved.paramCount()) { node.resolved.paramCount.set(count) } } }
function ModulatorChunk(parentContext){ var context = Object.create(parentContext) var obs = ObservStruct({ id: Observ(), slots: NodeArray(context), minimised: Property(false), shape: Property([1,1]), color: Property([0,0,0]), flags: Property([]) }) obs._type = 'ModulatorChunk' obs.context = context context.chunk = obs context.slotLookup = lookup(obs.slots, 'id') var broadcastSchedule = null obs.onSchedule = Event(function(b){ broadcastSchedule = b }) var currentTransform = null obs.slots.onUpdate(function(){ if (currentTransform){ currentTransform.destroy() currentTransform = null } var transforms = [0] obs.slots.forEach(function(slot){ if (slot.onSchedule){ transforms.push({param: slot, transform: operation}) } }) currentTransform = Transform(context, transforms) currentTransform.onSchedule(broadcastSchedule) }) obs.triggers = computed([obs.id, obs.shape], function(id, shape){ var length = shape[0] * shape[1] var result = [] for (var i=0;i<length;i++){ result.push(String(i)) } return result }) obs.grid = computed([obs.triggers, obs.shape], ArrayGrid) obs.triggerOn = function(id, at){ var slot = context.slotLookup.get(id) if (slot){ slot.triggerOn(at) } } obs.triggerOff = function(id, at){ var slot = context.slotLookup.get(id) if (slot){ slot.triggerOff(at) } } obs.destroy = function(){ if (currentTransform){ currentTransform.destroy() currentTransform = null } } obs.resolvedGrid = computed([obs.triggers, obs.shape], function(triggers, shape){ return ArrayGrid(triggers.map(getGlobalId), shape) }) return obs // scoped function operation(base, value){ return (parseFloat(base)||0) + (parseFloat(value)||0) } function getGlobalId(id){ if (id){ return obs.id() + '/' + id } } }
module.exports = function (context) { var activatedAt = Date.now() var unloadState = { lastSuppressId: null, lastSuppressAt: 0 } var midiPort = MidiPort(context, function (port, lastPort) { // turn off on switch lastPort && lastPort.write(turnOffAll) if (port) { activatedAt = Date.now() port.write(turnOffAll) var values = [] for (var i = 0; i < 8; i++) { if (i >= 2) { values.push(i, light(1, 1)) } else { values.push(i, light(2, 2)) } values.push(i + 8, light(1, 0)) values.push(i + 16, light(0, 1)) } port.write(setLed.concat(values, 247)) } }) var obs = ObservStruct({ port: midiPort }) // grab the midi for the current port obs.grabInput = function () { midiPort.grab() } obs.context = context var project = context.project var onTrigger = AnyTrigger(project.items) var knobs = { 3: Observ(0), 4: Observ(0), 5: Observ(0), 6: Observ(0), 7: Observ(0), 8: Observ(0) } var params = [] Object.keys(knobs).forEach(function (key) { var id = 'Launch Control XL > ' + key params.push(id) context.paramLookup.put(id, MidiParam(context, id, knobs[key])) }) var paramState = [] watchKnobs(midiPort.stream, mappings.row1.concat(mappings.row2, mappings.row3), function (id, data) { var state = paramState[id] = paramState[id] || {} var index = Math.floor(id / 8) if (index === 0) { if (id === 0) { project.tempo.set(scaleInterpolate(project.tempo() - 60, data, state) + 60) } else if (id === 1) { project.swing.set(scaleInterpolate(project.swing() * 128, data, state) / 128) } else { knobs[id + 1].set(scaleInterpolate(knobs[id + 1](), data, state)) } } else { var item = project.items.get(id % 8) if (isSetup(item)) { var setup = item.node var param = index === 1 ? setup.overrideLowPass : setup.overrideHighPass var currentValue = getValue(param()) param.set(scaleInterpolate(currentValue * 128, data, state) / 128) } } }) var sliderState = [] watchKnobs(midiPort.stream, mappings.sliders, function (id, data) { var state = sliderState[id] = sliderState[id] || {} var item = project.items.get(id) if (isSetup(item)) { var setup = item.node var volume = setup.overrideVolume var currentPosition = Math.pow(volume(), 1 / Math.E) * 108 var newPosition = scaleInterpolate(currentPosition, data, state) volume.set(Math.pow(newPosition / 108, Math.E)) } }, 108) var selectedId = 0 var buttonBase = computed([project.selected, project.items], function (selected, items) { var result = [] for (var i = 0; i < 8; i++) { var item = project.items.get(i) if (item) { if (item.path === selected) { selectedId = i result.push(light(2, 3)) } else { result.push(light(0, 1)) } } else { result.push(0) } } return result }) var stopButtonBase = Observ([light(1,0),light(1,0),light(1,0),light(1,0),light(1,0),light(1,0),light(1,0),light(1,0)]) var buttonFlash = FlashArray() onTrigger(function (index) { if (index === selectedId) { buttonFlash.flash(index, light(3, 3), 40) } else { buttonFlash.flash(index, light(0, 3), 40) } }) var buttons = ObservMidi(midiPort.stream, mappings.trackFocus, ArrayStack([ buttonBase, buttonFlash ])) buttons(function (values) { var result = null values.forEach(function (val, i) { if (val) { result = i } }) if (result != null) { var item = project.items.get(result) if (item) { project.selected.set(item.path) } } }) var stopAllButtons = ObservMidi(midiPort.stream, mappings.trackControl, ArrayStack([ stopButtonBase, buttonFlash ])) stopAllButtons(function (values) { var result = null values.forEach(function (val, i) { if (val) { result = i } }) if (result != null) { var item = project.items.get(result) if (item && item.node) { if (unloadState.lastSuppressId === result && unloadState.lastSuppressAt > Date.now() - 500) { unloadState.lastSuppressAt = 0 item.close() } else { unloadState.lastSuppressId = result unloadState.lastSuppressAt = Date.now() suppressNode(item.node, true) } } } }) // CONTROL BUTTONS: var controlButtons = LightStack(midiPort.stream, { mode: mappings.device }) controlButtons.mode(function (value) { if (value || Date.now() - activatedAt > 300) { midiPort.override.set(false) var activeItem = findItemByPath(project.items, project.selected()) if (isSetup(activeItem)) { activeItem.node.grabInput() } else { midiPort.previous() } } }) controlButtons.mode.light(light(2, 2)) obs.grabInput = function () { midiPort.grab() } obs.destroy = function () { midiPort.destroy() params.forEach(function (id) { context.paramLookup.delete(id) }) } return obs // scoped function suppressNode (node, flatten) { if (node && node.controllers) { node.controllers.forEach(function (controller) { if (controller.looper) { var release = controller.looper.transform(function (grid) { grid.data = [] return grid }) if (flatten) { controller.looper.flatten() } else { return release } } }) } } }