Ejemplo n.º 1
0
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]);
    }
  }
}
Ejemplo n.º 2
0
function ExternalRouter (context, defaultValue, volume) {
  var obs = Dict()
  var releases = []
  obs.context = context

  var refreshing = false
  var destroyed = false

  var connections = new Map()
  var gains = new Map()

  var set = obs.set
  obs.set = function (v) {
    set(v == null ? defaultValue : v)
  }

  if (typeof volume === 'function') {
    volume(function (value) {
      gains.forEach(function (item) {
        item.gain.value = value
      })
    })
  }

  obs.refresh = function () {
    if (!refreshing) {
      refreshing = true
      setImmediate(refresh)
    }
  }

  releases.push(obs(obs.refresh))

  var destinationIds = MutantMap(toCollection(context.chunkLookup), (item, invalidateOn) => {
    if (item.value) {
      if (item.value.loaded) {
        invalidateOn(item.value.loaded)
      }
      return item.value.id
    }
  })

  if (context.chunkLookup) {
    releases.push(watch(destinationIds, obs.refresh))
  }

  obs.destroy = function () {
    destroyed = true
    Array.from(gains.keys()).forEach(function (key) {
      gains.get(key).disconnect()
      gains.delete(key)
    })

    while (releases.length) {
      releases.pop()()
    }
  }

  return obs

  // scoped

  function refresh () {
    if (destroyed) return false
    refreshing = false
    var usedGains = []
    var routes = obs() || {}
    Object.keys(routes).forEach(function (from) {
      var target = routes[from]
      var source = context.slotLookup.get(from)

      if (typeof target === 'string') {
        target = [target]
      }

      if (source && Array.isArray(target)) {
        if (!gains.has(source)) {
          var node = context.audio.createGain()
          gains.set(source, node)
          node.gain.value = typeof resolve(volume) === 'number' ? resolve(volume) : 1
          source.connect(node)
        }

        usedGains.push(gains.get(source))

        if (!connections.has(source)) {
          connections.set(source, [])
        }

        var destinations = target.map(function (to) {
          if (to && typeof to === 'string') {
            if (to === '$default') {
              return context.output
            } else {
              to = to.split('#')
              var destinationChunk = context.chunkLookup.get(to[0])
              var destinationSlot = destinationChunk && destinationChunk.getSlot(to[1])
              if (destinationSlot && destinationSlot.input) {
                return destinationSlot.input
              }
            }
          }
        }).filter(present)

        destinations.forEach(function (output) {
          if (!connections.get(source).includes(output)) {
            gains.get(source).connect(output)
            connections.get(source).push(output)
          }
        })

        connections.set(source, connections.get(source).filter(function (output) {
          if (!destinations.includes(output)) {
            gains.get(source).disconnect(output)
            return false
          } else {
            return true
          }
        }))
      }
    })

    // remove old unused routes
    Array.from(gains.keys()).forEach(function (key) {
      if (!usedGains.includes(gains.get(key))) {
        gains.get(key).disconnect()
        gains.delete(key)
      }
    })
  }
}
Ejemplo n.º 3
0
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
}
Ejemplo n.º 4
0
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
}