Exemplo n.º 1
0
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
  }
}
Exemplo n.º 2
0
function MidiToParam (context, id, value) {
  var obs = ObservStruct({
    id: Observ(id)
  })

  obs._type = 'RemoteParam'
  obs.currentValue = computed([value], fromMidi)
  return obs
}
Exemplo n.º 3
0
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
      })
    ])
  ])
}
Exemplo n.º 4
0
  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
    }
  })
Exemplo n.º 5
0
 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
   })
 })
Exemplo n.º 6
0
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())
  }
}
Exemplo n.º 7
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]);
    }
  }
}
Exemplo n.º 8
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
        }
      }
    }
  }
}
Exemplo n.º 9
0
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
}
Exemplo n.º 10
0
  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
      }
    }
  }
Exemplo n.º 11
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
}
Exemplo n.º 12
0
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
  }
}
Exemplo n.º 13
0
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
}
Exemplo n.º 14
0
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()
  }
}
Exemplo n.º 15
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
}