function lambda (inputs) { var nodes = inputs.filter(isAudioNode) var numbers = inputs.filter(isNumber) var params = inputs.filter(ParamSource.isParam) var numberResult = getNumberResult(numbers, params, this.reduceValues) if (nodes.length > 0) { if (!this.numberResult) { this.numberResult = Value(numberResult) this.nodes = Value(nodes) this.result = this.reduceParams(nodes[0].context, this.nodes, this.numberResult) } else { if (!deepEqual(this.nodes(), nodes)) { this.nodes.set(nodes) } this.numberResult.set(numberResult) } return this.result } else { if (this.numberResult) { // clear out existing this.numberResult.set(numberResult) this.nodes.set(nodes) } return numberResult } }
function MidiToParam (context, id, value) { var obs = ObservStruct({ id: Observ(id) }) obs._type = 'RemoteParam' obs.currentValue = computed([value], fromMidi) return obs }
module.exports = function renderOscillator (node) { var shape = Value('pulse') shape(value => { if (value !== 'pulse') { node.set(extend(node(), { node: 'source/oscillator', shape: value })) } }) return h('SourceNode -oscillatorPulse', [ Header(node, h('span', [ h('strong', 'Oscillator:'), ' ', h('span', 'Pulse') ])), h('ParamList', [ Select(shape, { options: shapeChoices }), ModRange(node.amp, { title: 'amp', defaultValue: 1, format: 'dB', flex: true }), ModRange(node.detune, { title: 'detune', format: 'cents', flex: true, defaultValue: 0 }), ModRange(node.pulseWidth, { title: 'pulse width', format: 'offset1', flex: true, defaultValue: 0 }), ModRange(node.noteOffset, { title: 'pitch', format: 'semitone', defaultValue: 0, flex: true }), ModRange(node.octave, { title: 'octave', format: 'octave', defaultValue: 0, flex: true }) ]) ]) }
timeline.primary.forEach(primaryClip => { if (resolve(primaryClip.resolved.duration)) { var fileName = Path.basename(resolve(primaryClip.src), '.json') + '.wav' var name = Path.basename(fileName, Path.extname(fileName)) var warpMarkers = primaryClip.getWarpMarkers() var start = 0 var end = resolve(primaryClip.cuePoints).length / 2 primaryClips.push({ name, fileName, start, end, warpMarkers, at: currentDuration, isTempoMaster: true }) warpMarkers.forEach(marker => { if (!tempo) tempo = marker.tempo tempoEvents.push({ time: marker.beat + currentDuration, tempo: marker.tempo }) }) var p = Value(0) progressElements.push(p) toExport.push({ clip: primaryClip, outputPath: Path.join(projectPath, fileName), onProgress: p.set }) var linked = timeline.secondary.getLinkedTo(resolve(primaryClip.id)) linked.forEach(clip => { var clips = [] tracks.push({ id: lastTrackId++, clips: clips }) var fileName = Path.basename(resolve(clip.src), '.json') + '.wav' var name = Path.basename(fileName, Path.extname(fileName)) var warpMarkers = clip.getWarpMarkers(clip.startOffset()) var start = 0 var end = resolve(clip.cuePoints).length / 2 clips.push({ name, fileName, start, end, warpMarkers, at: currentDuration, isTempoMaster: false }) var p = Value(0) progressElements.push(p) toExport.push({ clip, outputPath: Path.join(projectPath, fileName), onProgress: p.set }) }) currentDuration += end - start } })
linked.forEach(clip => { var clips = [] tracks.push({ id: lastTrackId++, clips: clips }) var fileName = Path.basename(resolve(clip.src), '.json') + '.wav' var name = Path.basename(fileName, Path.extname(fileName)) var warpMarkers = clip.getWarpMarkers(clip.startOffset()) var start = 0 var end = resolve(clip.cuePoints).length / 2 clips.push({ name, fileName, start, end, warpMarkers, at: currentDuration, isTempoMaster: false }) var p = Value(0) progressElements.push(p) toExport.push({ clip, outputPath: Path.join(projectPath, fileName), onProgress: p.set }) })
function Sustained (obs, timeThreshold, checkUpdateImmediately) { var outputValue = Value(obs()) return computed(outputValue, v => v, { onListen: () => watch(obs, onChange) }) function onChange (value) { if (checkUpdateImmediately && checkUpdateImmediately(value)) { // update immediately for falsy values clearTimeout() update() } else if (value !== outputValue()) { clearTimeout() setTimeout(update, timeThreshold) } } function update () { outputValue.set(obs()) } }
module.exports = function(context){ var loopGrid = LoopGrid(context) var looper = Looper(loopGrid) var recording = computedRecording(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: Dict({}) }) obs.gridState = ObservStruct({ active: loopGrid.active, playing: loopGrid.playing, recording: recording, triggers: loopGrid.grid }) var releaseLooper = 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(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(){ recording.destroy() midiPort.destroy() display.init() output.destroy() loopGrid.destroy() looper.destroy() releaseLooper() } 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 AudioSlot (parentContext, defaultValue) { var context = Object.create(parentContext) var audioContext = context.audio var input = audioContext.createGain() var pre = audioContext.createGain() var output = audioContext.createGain() var toProcessors = audioContext.createGain() var post = audioContext.createGain() var initialized = false var releases = [] var queue = [] input.connect(pre) pre.connect(toProcessors) toProcessors.connect(post) post.connect(output) var obs = RoutableSlot(context, { id: Observ(), modulators: Slots(context), sources: Slots(context), processors: Slots(context), noteOffset: Param(context, 0), sustain: Property(true), output: Property(null), volume: Property(1) }, input, output, releases) obs.getAttackDuration = function () { var duration = 0 forEachAll([obs.sources, obs.modulators, obs.processors], function (node) { if (node && node.getAttackDuration) { var value = node.getAttackDuration() if (value && (value > duration)) { duration = value } } }) return duration || 0.0001 } obs._type = 'AudioSlot' context.noteOffset = obs.noteOffset context.slot = obs obs.sources.onAdd(function (node) { if (node.connect) { node.connect(pre) } }) obs.sources.onRemove(function (node) { if (node.disconnect) { node.disconnect(pre) } }) obs.modulators.onRemove(function (node) { if (node && node.id && node.id()) { updateParamReferences(obs, node.id(), null) } }) context.modulatorLookup = lookup(obs.modulators, 'id') context.paramLookup = merge([ parentContext.paramLookup, context.modulatorLookup ]) obs.modulators.resolveAvailable = function (id, lastValue) { return resolveAvailable(context.paramLookup(), id, lastValue) } // reconnect processors on add / update var connectedProcessors = [ toProcessors ] var updatingProcessors = false var lastTriggerOn = null var lastTriggerOff = null obs.processors.onNodeChange(function () { if (!updatingProcessors) { setImmediate(updateProcessors) } updatingProcessors = true }) obs.processors.onAdd(triggerIfOn) obs.modulators.onAdd(triggerIfOn) obs.triggerOn = function (at) { if (!initialized) { queue.push(function () { obs.triggerOn(at) }) return false } var offTime = null forEachAll([obs.sources, obs.modulators, obs.processors], function (node) { if (node && node.triggerOn) { var time = node.triggerOn(at) if (time && (!offTime || time > offTime)) { offTime = time } } }) // track off time for immediate triggering of nodes when added var onTime = at if (!lastTriggerOn || lastTriggerOn < onTime) { lastTriggerOn = onTime } if (offTime) { triggerOff(offTime) } else if (!obs.sustain()) { triggerOff(at + obs.getAttackDuration()) } } obs.triggerOff = function (at) { if (!obs.sustain()) return // ignore triggerOff if (!initialized) { queue.push(function () { obs.triggerOff(at) }) return false } triggerOff(at) } obs.choke = function (at) { obs.sources.forEach(function (source) { source.choke && source.choke(at) }) } releases.push( function () { if (isOn()) { // force trigger off on removal obs.triggerOff(context.audio.currentTime) } } ) if (defaultValue) { obs.set(defaultValue) } setImmediate(function () { initialized = true while (queue.length) { queue.shift()() } }) return obs // scoped function triggerOff (at) { var maxProcessorDuration = 0 var maxModulatorDuration = 0 var maxSourceDuration = 0 var offEvents = [] obs.modulators.forEach(function (node) { var releaseDuration = (node.getReleaseDuration && node.getReleaseDuration()) || 0 if (releaseDuration > maxModulatorDuration) { maxModulatorDuration = releaseDuration } offEvents.push([node, releaseDuration, 'modulator']) }) obs.sources.forEach(function (node) { var releaseDuration = (node.getReleaseDuration && node.getReleaseDuration()) || 0 if (releaseDuration > maxSourceDuration) { maxSourceDuration = releaseDuration } offEvents.push([node, releaseDuration, 'source']) }) obs.processors.forEach(function (node) { if (node && node.triggerOff) { var releaseDuration = (node.getReleaseDuration && node.getReleaseDuration()) || 0 offEvents.push([node, releaseDuration, 'processor']) if (releaseDuration > maxProcessorDuration) { maxProcessorDuration = releaseDuration } } }) var difference = Math.max(maxModulatorDuration, maxProcessorDuration) - maxSourceDuration var maxDuration = Math.max(maxSourceDuration, maxProcessorDuration, maxModulatorDuration) offEvents.forEach(function (event) { var target = event[0] var releaseDuration = event[1] if (event[2] === 'processor') { target.triggerOff(at + maxDuration - releaseDuration) } else if (event[2] === 'source') { target.triggerOff(at + Math.max(0, difference)) } else { target.triggerOff(at) } }) // track off time for immediate triggering of nodes when added var offTime = at + Math.max(0, difference) if (!lastTriggerOff || lastTriggerOff < offTime) { lastTriggerOff = offTime } } function triggerIfOn (node) { if (isOn() && node.triggerOn) { // immediately trigger processors if slot is already triggered node.triggerOn(context.audio.currentTime) } } function isOn () { return lastTriggerOn && (lastTriggerOn < context.audio.currentTime && (!lastTriggerOff || lastTriggerOff < lastTriggerOn)) } function updateProcessors () { if (checkProcessorsChanged()) { toProcessors.disconnect() while (connectedProcessors.length) { connectedProcessors.pop().disconnect() } var lastProcessor = toProcessors obs.processors.forEach(function (processor) { if (processor) { lastProcessor.connect(processor.input) lastProcessor = processor connectedProcessors.push(processor) } }) lastProcessor.connect(post) } updatingProcessors = false } function checkProcessorsChanged () { if (connectedProcessors.length !== obs.processors.getLength()) { return true } else { for (var i = 0; i < connectedProcessors.length; i++) { if (connectedProcessors[i] !== obs.processors.get(i)) { return true } } } } }
function LinkParam (context) { var obs = ObservStruct({ param: Value(), minValue: Param(context, 0), maxValue: Param(context, 1), mode: Prop('linear'), quantize: Prop(0) }) var releases = [] obs._type = 'LinkParam' obs.context = context var range = Sum([ obs.maxValue, Negate(obs.minValue) ]) releases.push( watch(range) // HACK: avoid regenerating transform AudioNodes ) // only relink params if the param we want changes var param = computed([context.paramLookup, obs.param], (paramLookup, paramId) => { return context.paramLookup.get(paramId) }, { passthru: true // treat nested observables as values instead of expanding }) var inverted = computed([range], range => { return getValue(range, context.audio.currentTime) < 0 }) obs.currentValue = computed([obs.mode, obs.quantize, inverted, param], function (mode, quantize, inverted, param) { if (param != null) { if (inverted) { param = Sum([1, Negate(param)]) } if (mode === 'exp') { param = Square(param) } var result = Sum([ Multiply([param, Abs(range)]), when(inverted, obs.maxValue, obs.minValue) ]) if (quantize) { result = Quantize(result, quantize) } return result } else { return obs.minValue.currentValue } }, { nextTick: true, comparer: (a, b) => { // also compare AudioNodes and ParamSources if (a instanceof global.AudioNode || ParamSource.isParam(a)) { return a === b } } }) obs.destroy = function () { Param.destroy(obs) while (releases.length) { releases.pop()() } } return obs }
init: function (sbot, config) { var dir = path.join(config.path, 'private') var version = 0 var index = Links(dir, indexes, (x, emit) => { var value = unbox(x) if (value) { emit(value) } }, version) var notify = Notify() var pending = Value(0) watchThrottle(pending, 200, (value) => { notify({pending: Math.max(0, value)}) }) index.init(function (_, since) { countChanges(since, function (err, changes) { if (err) throw err pending.set(changes) onChange(() => { pending.set(pending() + 1) }) pull( sbot.createLogStream({gt: since || 0, live: true, sync: false}), pull.through(function () { pending.set(pending() - 1) }), index.write(function (err) { if (err) throw err }) ) }) }) return { publish: valid.async(function (data, recps, cb) { var ciphertext try { ciphertext = ssbKeys.box(data, recps) } catch (e) { return cb(explain(e, 'failed to encrypt')) } sbot.publish(ciphertext, cb) }, 'string|object', 'array'), unbox: valid.sync(function (ciphertext) { var data try { data = ssbKeys.unbox(ciphertext, sbot.keys.private) } catch (e) { throw explain(e, 'failed to decrypt') } return data }, 'string'), read: function (opts) { if (opts && typeof opts === 'string') { try { opts = {query: JSON.parse(opts)} } catch (err) { return pull.error(err) } } return index.read(opts, function (ts, cb) { sbot.sublevel('log').get(ts, function (err, key) { if (err) return cb(explain(err, 'missing timestamp:'+ts)) sbot.get(key, function (err, value) { if(err) return cb(explain(err, 'missing key:'+key)) cb(null, {key: key, value: unboxValue(value), timestamp: ts}) }) }) }) }, progress: notify.listen } function countChanges (since, cb) { var result = 0 pull( sbot.createLogStream({gt: since || 0, keys: false, values: false}), pull.drain(function () { result += 1 }, function (err) { cb(err, result) }) ) } function onChange (cb) { pull( sbot.createLogStream({keys: false, values: false, old: false}), pull.drain(function () { cb() }) ) } function unbox (msg) { if (typeof msg.value.content === 'string') { var value = unboxValue(msg.value) if (value) { return { key: msg.key, value: value, timestamp: msg.timestamp } } } } function unboxValue (value) { var plaintext = null try { plaintext = ssbKeys.unbox(value.content, sbot.keys.private) } catch (ex) {} if (!plaintext) return null return { previous: value.previous, author: value.author, sequence: value.sequence, timestamp: value.timestamp, hash: value.hash, content: plaintext, private: true } } }
module.exports = function (context) { var loopGrid = LoopGrid(context) var looper = Looper(loopGrid) var recordingLoop = Observ() var recording = computedRecording(loopGrid, recordingLoop) var scheduler = context.scheduler var gridMapping = getLaunchpadGridMapping() loopGrid.shape.set(gridMapping.shape) var activatedAt = 0 var shiftHeld = false var releases = [] var midiPort = MidiPort(context, function (port, lastPort) { // turn off on switch lastPort && lastPort.write(turnOffAll) if (port) { port.write(turnOffAll) activatedAt = Date.now() } }) // extend loop-grid instance var obs = ObservStruct({ port: midiPort, loopLength: loopGrid.loopLength, chunkPositions: Dict({}) }) obs.gridState = ObservStruct({ active: loopGrid.active, playing: loopGrid.playing, recording: recording, triggers: loopGrid.grid }) obs.activeInput = computed([midiPort.stream], function (value) { return !!value }) releases.push( watch(looper, loopGrid.loops.set) ) obs.context = context obs.playback = loopGrid obs.looper = looper obs.repeatLength = Observ(2) obs.recordingLoop = recordingLoop var repeatOffbeat = Observ(false) 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 chunkColors = ChunkColors(context.chunkLookup, context.setup.selectedChunkId, loopGrid.targets, loopGrid.shape) var outputLayers = ObservGridStack([ chunkColors, // recording mapGridValue(recording, 7), // active applyColorFilter(chunkColors, { multiply: 8, saturate: 1.5, active: loopGrid.active }), // selected mapGridValue(transforms.selector, 12), // suppressing mapGridValue(transforms.suppressor, 7), // playing applyColorFilter(chunkColors, { multiply: 8, add: 10, active: loopGrid.playing }) ]) setLights(outputLayers, midiPort.stream) var controllerGrid = ObservMidi(midiPort.stream, gridMapping) 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) 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', swapTarget: '176/110', select: '176/111' }) var releaseLoopLengthLights = [] var ignoreStoreUp = false // store loop when end time is reached releases.push( context.scheduler.onSchedule(schedule => { if (Array.isArray(obs.recordingLoop()) && obs.recordingLoop()[1]) { var scheduleLength = schedule.to - schedule.from var from = obs.recordingLoop()[0] var duration = obs.recordingLoop()[1] var to = from + duration var remaining = to - schedule.to if (remaining <= scheduleLength) { looper.store(from - remaining + duration, duration) buttons.store.flash(stateLights.green) obs.recordingLoop.set(null) } } }) ) watchButtons(buttons, { store: function (value) { // ignore an up press if we are holding the store if (!value && ignoreStoreUp) { ignoreStoreUp = false return } if (!shiftHeld && Array.isArray(obs.recordingLoop()) && scheduler.getCurrentPosition() - obs.recordingLoop()[0] > 0.9) { // button up or down, when active recording // loop the duration of recording var duration = scheduler.getCurrentPosition() - obs.recordingLoop()[0] obs.loopLength.set(quantizeDuration(duration)) looper.store() this.flash(stateLights.red) // stop recording obs.recordingLoop.set(null) } else if (value) { // BUTTON DOWN ----- if (shiftHeld) { // start recording from next % 4 beats quantized (ignore the up) ignoreStoreUp = true // calculate quantized loop recorder start var currentPosition = scheduler.getCurrentPosition() var startPosition = Math.floor(currentPosition + 1.7) obs.recordingLoop.set([startPosition, obs.loopLength()]) } else { // lets assume we're recording a loop, but if the duration is too short, we'll loop the last x bars obs.recordingLoop.set([scheduler.getCurrentPosition()]) } } else { // BUTTON UP ---- // record key was pressed quickly, just loop the last `loopLength` if (obs.recordingLoop()) { looper.store(obs.recordingLoop()[0]) obs.recordingLoop.set(null) } this.flash(stateLights.green) } }, flatten: function (value) { if (value) { if (buttons.store()) { // store is held, lock in the recording ignoreStoreUp = true } else if (obs.recordingLoop()) { // cancel recording if pressed obs.recordingLoop.set(null) } else { 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(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 if (current > 1 / 8) { obs.loopLength.set(quantizeToSquare(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 if (current < 64) { obs.loopLength.set(quantizeToSquare(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.purpleLow) 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(scheduler.getCurrentPosition(), transforms.selector.selectedIndexes(), freezeSuppress(), 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.grey) var releaseRecordingLight = null watch(obs.recordingLoop, (value) => { releaseRecordingLight && releaseRecordingLight() releaseRecordingLight = null if (value != null) { releaseRecordingLight = buttons.store.light(stateLights.red) } }) var willFlatten = computed([activeIndexes, looper.transforms, buttons.store], function (indexes, transforms, storeHeld) { return !!indexes.length || !!transforms.length || !!storeHeld }) // 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 repeatButtonOutput = computed([loopGrid.loopPosition, obs.repeatLength, repeatOffbeat, obs.recordingLoop], (loopPosition, repeatLength, repeatOffbeat, recordingLoop) => { var result = {} var repeatIndex = repeatStates.indexOf(repeatLength) var currentBeat = Math.floor(loopPosition[0]) if (recordingLoop) { var pos = scheduler.getCurrentPosition() - recordingLoop[0] var duration = recordingLoop[1] ? recordingLoop[1] : 32 for (let i = 0; i < 8; i++) { if (pos < 0) { // counting down to record if (i >= Math.floor(8 + pos * 4)) { result[i] = stateLights.yellow } } else { var currentIndex = Math.floor(pos / duration * 8) if (currentIndex === i && loopPosition[0] >= currentBeat && loopPosition[0] < currentBeat + 0.1) { // flash light on beat result[i] = stateLights.red } else if ((pos > 0 && currentIndex >= i) || (pos < 0 && currentIndex <= i)) { result[i] = stateLights.redLow } else if (repeatIndex === i) { result[i] = stateLights.grey } } } } else { var beatIndex = Math.floor(loopPosition[0] / loopPosition[1] * 8) for (let i = 0; i < 8; i++) { if (beatIndex === i && loopPosition[0] >= currentBeat && loopPosition[0] < currentBeat + 0.1) { // flash light on beat result[i] = stateLights.green } else if (repeatIndex === i) { result[i] = repeatOffbeat ? stateLights.red : stateLights.grey } else if (beatIndex === i) { result[i] = stateLights.greenLow } } } return result }) var repeatButtons = ObservMidi(midiPort.stream, { 0: '144/89', 1: '144/79', 2: '144/69', 3: '144/59', 4: '144/49', 5: '144/39', 6: '144/29', 7: '144/19' }, repeatButtonOutput) // repeater mapWatchDiff(repeatStates, repeatButtons, obs.repeatLength.set) watch(obs.repeatLength, function (value) { transforms.holder.setLength(value) if (value < 2 || shiftHeld) { repeatOffbeat.set(shiftHeld) transforms.repeater.start(grabInputExcludeNoRepeat, value, shiftHeld) } else { transforms.repeater.stop() } }) // cleanup / disconnect from keyboard on destroy obs.destroy = function () { recording.destroy() midiPort.destroy() output.destroy() loopGrid.destroy() looper.destroy() while (releases.length) { releases.pop()() } } return obs }
function FileObject (parentContext) { var context = Object.create(parentContext) context.cwd = Observ() var obs = Observ({}) var set = obs.set obs.set = function (data) { if (obs.node) { obs.node.set(data) } } obs.file = null obs.context = context obs.loaded = Observ(false) obs.path = Observ() // add self to context context.fileObject = obs var loading = false var initialized = false var releaseInstance = null var releaseResolved = null var releaseRename = null var releaseClose = null var updateFile = null var currentNodeName = null obs.node = null var broadcastClose = null obs.onClose = Event(function (broadcast) { broadcastClose = broadcast }) var broadcastClosing = null obs.onClosing = Event(function (broadcast) { broadcastClosing = broadcast }) function updateNode (data) { var newNode = getNode(data) var instance = obs.node var oldInstance = instance if (currentNodeName !== newNode) { var ctor = resolveNode(context.nodes, newNode) if (instance) { releaseResolved() releaseInstance() instance.destroy && instance.destroy() releaseResolved = releaseInstance = instance = obs.node = null } if (ctor) { instance = obs.node = ctor(context) releaseResolved = instance.resolved ? instance.resolved(obs.resolved.set) : instance(obs.resolved.set) releaseInstance = instance(function (data) { set(data) // don't update the file if we are currently upading from file // also wait until idle before accepting update if (!loading && initialized) { updateFile(data) } }) broadcastNode(instance) } else if (oldInstance) { obs.resolved.set(null) broadcastNode(null) } } if (instance) { instance.set(data) } currentNodeName = getNode(data) if (data && loading) { loading = false broadcastLoaded() // hacky callback for onLoad obs.loaded.set(true) } } var broadcastLoaded = null obs.onLoad = Event(function (broadcast) { broadcastLoaded = broadcast }) var broadcastNode = null obs.onNode = Event(function (broadcast) { broadcastNode = broadcast }) obs.resolved = Observ() obs.resolvePath = function (src) { return Path.resolve(resolve(context.cwd), src) } obs.relative = function (path) { var value = Path.relative(resolve(context.cwd), path) if (/^\./.exec(value)) { return value } else { return './' + value } } obs.load = function (path) { releaseClose && releaseClose() releaseRename && releaseRename() releaseRename = null if (obs.file) { obs.file.close() obs.file = null } if (path) { loading = true initialized = false onceIdle(() => { initialized = true }) obs.file = ObservFile(path, context.fs) releaseRename = watch(obs.file.path, obs.path.set) context.cwd.set(getDirName(path)) releaseClose = obs.file.onClose(onClose) updateFile = JsonFile(obs.file, updateNode) } } obs.close = function () { if (obs.file && obs.file.close) { broadcastClosing() obs.file.close() } } function onClose () { releaseClose && releaseClose() releaseResolved && releaseResolved() releaseInstance && releaseInstance() releaseRename && releaseRename() obs.node && obs.node.destroy && obs.node.destroy() obs.node = releaseInstance = releaseResolved = null obs.file = null broadcastClose() obs.loaded.set(false) } obs.nodeName = computed(obs.resolved, r => r && r.node || null) obs.destroy = function () { releaseClose && releaseClose() releaseResolved && releaseResolved() releaseInstance && releaseInstance() releaseRename && releaseRename() if (obs.file) { obs.file.close() obs.file = null } if (obs.node && obs.node.destroy) { obs.node.destroy() } } return obs // scoped function getNode (value) { return value && value[context.nodeKey || 'node'] || null } }
function TemplateSlot (context, shape) { var obs = Value({}) // handle defaultValue var set = obs.set obs.set = function (v) { set(v == null ? {} : v) } var templateContext = Object.create(context) templateContext.template = true obs.context = context obs.node = null var releases = [] var itemReleases = new Map() var broadcastAdd = null var broadcastRemove = null obs.nodeName = computed([obs], x => (x && x.node) || false) var ids = ShapeSlots(shape) var throttledValue = throttle(obs, 40) obs.slots = MutantMap(ids, function (id, invalidateOn) { var ctor = resolveNode(context.nodes, obs.nodeName()) invalidateOn(obs.nodeName) if (ctor) { var result = ctor(context) var value = computed([throttledValue, { id: String(id), value: id, scale: '$inherit' }], obtainWithParams) var releases = [ watch(value, result.set) ] if (result.destroy) { releases.push(result.destroy) } itemReleases.set(result, releases) broadcastAdd(result) return result } }, { maxTime: 5, onRemove: function (item) { if (item != null) { if (item.destroy) { item.destroy() } if (itemReleases.has(item)) { itemReleases.get(item).forEach(fn => fn()) itemReleases.delete(item) broadcastRemove(item) } } } }) obs.slots.onAdd = Event(b => { broadcastAdd = b }) obs.slots.onRemove = Event(b => { broadcastRemove = b }) obs.slots.onNodeChange = Event(function (broadcast) { obs.slots.onAdd(broadcast) obs.slots.onRemove(broadcast) }) var releaseSlots = watch(obs.slots, function () { // hold slots open until destroy }) watch(obs.nodeName, function (nodeName) { var ctor = resolveNode(context.nodes, nodeName) // clean up last obs.node = null while (releases.length) { releases.pop()() } if (ctor) { var instance = ctor(templateContext) releases.push(doubleBind(obs, instance)) if (instance.destroy) { releases.push(instance.destroy) } obs.node = instance } }) obs.destroy = function () { releaseSlots() while (releases.length) { releases.pop()() } Array.from(itemReleases.values()).forEach(function (releases) { releases.forEach(fn => fn()) }) itemReleases.clear() } return obs }
function External (parentContext) { var context = Object.create(parentContext) context.cwd = Observ() var volume = Property(1) var overrideVolume = Property(1) context.output = context.audio.createGain() context.output.connect(parentContext.output) context.slotLookup = ProxyDict(null, { onRemove: onSlotsChanged, onAdd: onSlotsChanged }) var outputMidiPort = MidiPort(context, null, {output: true, shared: true}) var outputMidiChannel = Property(1) var outputMidiTriggerOffset = Property(10) context.outputMidiPort = outputMidiPort context.outputMidiChannel = outputMidiChannel context.outputMidiTriggerOffset = outputMidiTriggerOffset var obs = BaseChunk(context, { src: Observ(), offset: Param(parentContext, 0), routes: ExternalRouter(context, {output: '$default'}, computed([volume, overrideVolume], multiply)), paramValues: MutantMappedDict([], (key, item) => { var param = Param(context) doubleBind(item, param) param.triggerOn(context.audio.currentTime) return [key, param] }), outputMidiChannel, outputMidiPort, outputMidiTriggerOffset, volume: volume }) obs.offset.triggerOn(context.audio.currentTime) // expose shape to external chunk instances context.shape = obs.shape context.offset = obs.offset context.flags = obs.flags context.chokeAll = obs.chokeAll context.activeSlots = obs.activeSlots context.paramValues = obs.paramValues if (context.setup) { obs.selected = computed([obs.id, context.setup.selectedChunkId], function (id, selectedId) { return id === selectedId }) } var triggerOn = obs.triggerOn var triggerOff = obs.triggerOff obs.triggerOn = function (id, at) { if (obs.node && obs.node.triggerOn) { obs.node.triggerOn(id, at) } return triggerOn(id, at) } obs.triggerOff = function (id, at) { if (obs.node && obs.node.triggerOff) { obs.node.triggerOff(id, at) } return triggerOff(id, at) } watchNodesChanged(context.slotLookup, obs.routes.refresh) context.fileObject = obs obs.context = context obs.overrideVolume = overrideVolume obs._type = 'ExternalNode' var updateFile = null var setting = false var nodeReleases = [] var fileReleases = [] var initialized = false // wait for onceIdle after load before accpting data back var loading = false var currentNodeName = null obs.node = null obs.file = null obs.path = Observ() obs.resolved = Observ() obs.loaded = Observ(false) obs.nodeName = computed(obs.resolved, r => r && r.node || null, {nextTick: true}) obs.inputs = computed(obs.resolved, data => data && data.inputs || []) obs.outputs = computed(obs.resolved, data => data && data.outputs || []) obs.params = computed(obs.resolved, data => data && data.params || []) obs.params.context = context var broadcastClose = null obs.onClose = Event(function (broadcast) { broadcastClose = broadcast }) obs.resolvePath = function (src) { return Path.resolve(resolve(context.cwd), src) } obs.relative = function (path) { var value = Path.relative(resolve(context.cwd), path) if (/^\./.exec(value)) { return value } else { return './' + value } } obs.destroy = function () { if (obs.node && obs.node.destroy) { obs.node.destroy() context.slotLookup.set(null) obs.node = null } if (obs.file) { obs.file.close() obs.file = null updateFile = null } while (fileReleases.length) { fileReleases.pop()() } while (nodeReleases.length) { nodeReleases.pop()() } unwatchPath() broadcastClose() Param.destroy(obs) } obs.getPath = function () { var descriptor = obs() if (descriptor && descriptor.src) { return obs.resolvePath(descriptor.src) } } var path = computed([parentContext.cwd, obs.src], (a, b) => a && b && Path.resolve(a, b) || null) var unwatchPath = watch(path, function (path) { if (obs.file && obs.file.path() !== path) { while (fileReleases.length) { fileReleases.pop()() } obs.file.close() obs.file = null updateFile = null } if (!updateFile) { if (path) { initialized = false onceIdle(() => { initialized = true }) loading = true obs.file = ObservFile(path, context.fs) updateFile = JsonFile(obs.file, updateNode) context.cwd.set(Path.dirname(path)) fileReleases.push( watch(obs.file.path, obs.path.set) ) } } }) function updateNode (descriptor) { var ctor = descriptor && resolveNode(context.nodes, getNode(descriptor)) if (obs.node && descriptor && obs && currentNodeName && getNode(descriptor) === currentNodeName) { setting = true obs.node.set(descriptor) setting = false } else { while (nodeReleases.length) { nodeReleases.pop()() } if (obs.node && obs.node.destroy) { obs.node.destroy() } if (descriptor && ctor) { obs.node = ctor(context) obs.node.set(descriptor) nodeReleases.push( watch(obs.node.resolved || obs.node, obs.resolved.set), obs.node(function (data) { // don't update the file if we are currently upading from file // also wait until idle before accepting update if (!setting && initialized) { updateFile(data) // if the node has changed, regenerate! if (getNode(data) !== currentNodeName) { updateNode(data) } } }) ) context.slotLookup.set(obs.node.slotLookup) } else { obs.node = null context.slotLookup.set(null) } } currentNodeName = getNode(descriptor) if (loading && descriptor) { // HACKS! loading = false obs.loaded.set(true) } } extendParams(obs) return obs // scoped function getNode (value) { return value && value[context.nodeKey || 'node'] || null } function onSlotsChanged () { obs.routes.refresh() } }
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 releases = [] var params = [] var paramLoopers = [] var recordingIndexes = Dict() var playingIndexes = Dict() var recordStarts = {} for (var i = 0; i < 8; i++) { params[i] = [ Value(0), Value(0), Value(0) ] paramLoopers[i] = [ ParamLooper(context, params[i][0]), ParamLooper(context, params[i][1]), ParamLooper(context, params[i][2]) ] recordingIndexes.put(i, computed(paramLoopers[i].map(x => x.recording), (...args) => args.some(Boolean))) playingIndexes.put(i, computed(paramLoopers[i].map(x => x.playing), (...args) => args.some(Boolean))) } var bindingReleases = new Map() var bindings = MutantMap(obs.chunkIds, (id, invalidateOn) => { var item = context.chunkLookup.get(id) var index = obs.chunkIds().indexOf(id) invalidateOn(computed([context.chunkLookup, obs.chunkIds], (_, chunkIds) => { // rebind when chunk is changed return item !== context.chunkLookup.get(id) || chunkIds.indexOf(id) !== index })) if (item) { bindingReleases.set(item, item.overrideParams(paramLoopers[index])) } return item }, { onRemove: function (item) { if (bindingReleases.has(item)) { bindingReleases.get(item)() bindingReleases.delete(item) } } }) releases.push(watch(bindings)) // grab the midi for the current port obs.grabInput = function () { midiPort.grab() } obs.context = context var setup = context.setup 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(data / 128) } }) var sliderState = [] watchKnobs(midiPort.stream, mappings.sliders, function (id, data) { var chunk = setup.context.chunkLookup.get(obs.chunkIds()[id]) if (chunk && chunk.overrideVolume) { var currentPosition = Math.pow(chunk.overrideVolume(), 1 / Math.E) * 108 var newPosition = scaleInterpolate(currentPosition, data, sliderState[id] = sliderState[id] || {}) chunk.overrideVolume.set(Math.pow(newPosition / 108, Math.E)) } }, 108) var pressed = computed(MutantMap(setup.controllers, function (controller) { return controller && controller.currentlyPressed }), function (items) { return items.reduce(function (result, pressed) { if (pressed) { pressed.map(x => x && x.split('/')[0]).reduce(addIfUnique, result) } return result }, []) }) var knobLights = computed([obs.chunkIds, setup.context.chunkLookup, pressed, setup.selectedChunkId], function (chunkIds, lookup, pressed, selected) { var result = [] if (setup.context) { for (var i = 0; i < 8; i++) { var chunk = setup.context.chunkLookup.get(chunkIds[i]) if (chunk && chunk.params) { var onValue = pressed.includes(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.includes(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() } } }) var recordButtonBase = computed([recordingIndexes, playingIndexes], function (recordingIndexes, playingIndexes) { var result = [] for (var i = 0; i < 8; i++) { if (recordingIndexes[i]) { result[i] = light(3, 0) } else if (playingIndexes[i]) { result[i] = light(0, 3) } else { result[i] = 0 } } return result }) var recordButtons = ObservMidi(midiPort.stream, mappings.trackControl, recordButtonBase) recordButtons(function (values) { values.forEach(function (val, i) { paramLoopers[i].forEach(looper => looper.recording.set(!!val)) if (val) { recordStarts[i] = Date.now() } else if (Date.now() - recordStarts[i] < 400) { paramLoopers[i].forEach(looper => looper.set(0)) } }) }) // 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 () { while (releases.length) { releases.pop()() } for (var fn of bindingReleases.values()) { fn() } bindingReleases.clear() midiPort.destroy() paramLoopers.forEach(items => items.forEach(param => param.destroy())) } return obs }