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]); } } }
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 (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 }