<|start_filename|>media/scripts/mvc/views/popup_view.js<|end_filename|>
(function() {
  'use strict';
  
  bs.views.PopupView = Backbone.View.extend({
    initialize: function(options) {
      this.$trigger = options.trigger;
      this.callback = options.callback;
      this.template = this.template || options.template;

      _.bindAll(this);
      $('body').click(this.bodyClick);
    },

    className: 'popup',

    bodyClick: function(e) {
      var $target = $(e.target);
      if ($target.is(this.$trigger)) {
        e.preventDefault();
        this.toggle();
      } else if (!$target.closest(this.$el).length) {
        if (this.$el.is(':visible')) {
          this.exit();
        }
      }
    },

    exit: function() {
      this.remove();
    },
    
    toggle: function() {
      if (this.$el.is(':visible')) {
        this.exit();
      } else {
        this.render();
      }
    },

    render: function() {
      this.$el.html(this.template());
      $('body').append(this.$el);
      return this;
    }
  });

})();


<|start_filename|>media/scripts/mvc/models/loop_module.js<|end_filename|>
(function() {
  'use strict';

  bs.models.LoopModule = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.gain = this.context.createGain();
      this.buffers = new Backbone.Collection();
      this.buffers.model = bs.models.Buffer;
      this.buffers.parentModel = this;
      this.metronome = attrs.metronome;
      this.sources = [];
      this.scheduled = [];

      this.listenTo(this.metronome, 'beat', this.beat);

      _.bindAll(this);
    },

    startMicrophone: function() {
      var model = this;
      if (this.microphone) {
        model.micGain.connect(model.gain);
        model.micGain.connect(synth.oscilloscope);
        model.micOn = true;
      } else {
        navigator.webkitGetUserMedia({audio: true}, function(stream) {
          model.microphone = synth.context.createMediaStreamSource(stream);
          model.micGain = model.context.createGain();
          model.micGain.value = 20;
          model.microphone.connect(model.micGain);
          model.micGain.connect(model.gain);
          model.micGain.connect(synth.oscilloscope);
          model.micOn = true;
        });
      }

    },

    stopMicrophone: function() {
      this.micGain.disconnect();
      this.micOn = false;
    },

    beat: function(beat) {
      var model = this;
      _.each(this.scheduled, function(ev) {
        if (((beat.number % ev.numBeats) - (ev.startBeat % ev.numBeats) === 0)) {
          console.log('scheduling', ev, 'beat', beat);
          ev.callback(beat);

          if (ev.once) {
            // remove event from schedule event
            model.scheduled.splice(model.scheduled.indexOf(ev), 1);
          }
        }
      });
    },

    toggleBufferVolume: function(sourceIndex) {
      var buffer = this.buffers.at(sourceIndex);
      var gain = buffer.get('gain') ? 0: 1;
      buffer.set({'gain': gain});
    },

    startRecording: function() {
      var model = this;
      // var newBuffer = this.context.createBuffer(2, buffers[0].length, this.context.sampleRate);
      var bufferModel = new bs.models.Buffer({tempo: synth.metronome.get('tempo')}, {context: this.context});
      var drawCallback = function(chan0, chan1) {
        bufferModel.trigger('draw', [chan0, chan1]);
      }
      this.scheduled.push(
        {
          startBeat: 0,
          numBeats: 16,
          once: true,
          callback: function(beat) {
            model.recording = true;
            model.recorder = new Recorder(model.gain, drawCallback, bufferModel, {workerPath: '/media/scripts/libs/recorder_worker.js', bufferLen: 2048});
            console.log('started recording', beat.time, 'current time', model.context.currentTime);
            model.recorder.record(beat.time);
        }
      });
      this.buffers.add(bufferModel, {at: 0});
    },

    getBufferCallback: function(buffers, beat) {
      var model = this;
      var newSource = this.context.createBufferSource();
      var bufferNode = this.context.createBuffer(2, buffers[0].length, this.context.sampleRate);
      var bufferModel = this.recorder.bufferModel;
      bufferNode.getChannelData(0).set(buffers[0]);
      bufferNode.getChannelData(1).set(buffers[1]);
      newSource.buffer = bufferNode;
      bufferNode.source = newSource;


      // try and smooth end of buffers to beginning value.
      // var offset = 0;
      // var offset1 = 0;
      // var spread = 4096;
      // for (var i = buffers[0].length - spread; i <= buffers[0].length; i++) {
      //   offset += ((buffers[0][0] - buffers[0][buffers[0].length - 1]) / spread);
      //   offset1 += ((buffers[1][0] - buffers[1][buffers[1].length - 1]) / spread);
      //   buffers[0][i] = offset;
      //   buffers[1][i] = offset1;
      // }


      // newSource.connect(synth.masterGain.node);
      newSource.connect(this.context.destination);

      // newSource.loop = true;
      // newSource.loopStart = this.context.currentTime - time;
      var offset = this.context.currentTime - beat.time;
      // console.log('current time', this.context.currentTime, 'offset', offset, 'time', time);
      newSource.start(0, offset);

      // derive the length in beats based on the length of the buffer
      var numBeats = Math.round((buffers[0].length / model.context.sampleRate) / (60 / synth.metronome.get('tempo'))) * 4;

      bufferModel.set({numBeats: numBeats, startBeat: beat.number, 'source': newSource, bufferNode: bufferNode});

      var loopTrigger = {
        startBeat: beat.number,
        numBeats: numBeats,
        callback: function(beat) {
          var source = model.context.createBufferSource();
          // source.gain.value = bufferModel.get('gain');
          source.buffer = bufferModel.bufferNode;
          source.playbackRate.value = synth.metronome.get('tempo') / bufferModel.get('tempo') * source.playbackRate.value;
          bufferNode.source = source;  // bad idea?
          source.connect(model.context.destination);
          // console.log('current time', model.context.currentTime, 'time', time);
          source.start(beat.time);
        }
      }
      bufferModel.trigger('doneRecording');
      this.scheduled.push(loopTrigger);
      loopTrigger.id = bufferModel.cid;
      model.recorder.exportWAV(function(wav) {
        bufferModel.set({'wav': wav});
      });
    },

    stopRecording: function() {
      var model = this;
      this.scheduled.push({
        startBeat: 0,
        numBeats: 16,
        once: true,
        callback: function(beat) {
          // console.log('stop recording', time, 'current time', model.context.currentTime);
          model.recorder.stop(beat.time);
          // console.log(time, model.context.currentTime, time - model.context.currentTime, (time - model.context.currentTime)*1000);
          setTimeout(function() {
            model.recorder.getBuffer(function(buffers) {
              model.getBufferCallback(buffers, beat);
            });
            model.recording = false;
          }, (beat.time - model.context.currentTime) * 1000);
        }
      });
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/metronome.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Metronome = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.gainNode = this.context.createGain();

      this.nextNoteTime = 0.0;        // when the next note is due.
      this.lookAhead = 0.25;          // How frequently to call scheduling function
      this.notesInQueue = [];         // notes to play
      this.timeOutId = 0;             // setTimeout Id
      this.noteResolution = 0;	      // 0 == 16th, 1 == 8th, 2 == quarter note
      this.noteLength = 0.01          // length of beep
      this.scheduleAheadTime = 0.1;	  // How far ahead to schedule audio (sec)
      this.current16thNote = 1;

      this.gainNode.gain.value = this.get('state') ? 1 : 0;

      this.on('change:state', this.stateChange);
      this.on('beat', this.beat);

      this.start();

    },

    defaults: {
      state: false, // false = off, true = on
      gain: 1,
      tempo: 90,
    },

    beat: function(beat) {
      // console.log(beat);
    },

    stateChange: function(model, state) {
      this.gainNode.gain.value = state ? 1 : 0;
    },

    start: function() {
      this.scheduler();
    },

    scheduler: function() {
      if (!this.context) {
        console.log('hlarr');
      }
      while (this.nextNoteTime < this.context.currentTime + this.scheduleAheadTime) {
        this.scheduleNote(this.current16thNote, this.nextNoteTime);
		this.nextNote();
      }

      this.timerId = setTimeout(_.bind(this.scheduler, this), this.lookAhead);
    },

    scheduleNote: function(beatNumber, time) {
      var model = this;
      // push the note on the queue, even if we're not playing.
      this.notesInQueue.push({ note: beatNumber, time: time });

      if ( (this.noteResolution==1) && (beatNumber%2))
          return;	// we're not playing non-8th 16th notes
      if ( (this.noteResolution==2) && (beatNumber%4))
          return;	// we're not playing non-quarter 8th notes

      // create an oscillator
      var osc = this.context.createOscillator();
      osc.connect(this.gainNode);
      if (! (beatNumber % 16) )	// beat 0 == low pitch
          osc.frequency.value = 220.0;
      else if (beatNumber % 4)	// quarter notes = medium pitch
          osc.frequency.value = 440.0;
      else						// other 16th notes = high pitch
          osc.frequency.value = 880.0;

      // TODO: Once start()/stop() deploys on Safari and iOS, these should be changed.
      osc.start(time);
      model.trigger('beat', {number: beatNumber, time: time});
      osc.stop( time + this.noteLength );
    },

    nextNote: function() {
      // Advance current note and time by a 16th note...
      var secondsPerBeat = 60.0 / this.get('tempo');	// Notice this picks up the CURRENT
                                          // tempo value to calculate beat length.
      this.nextNoteTime += 0.25 * secondsPerBeat;	// Add beat length to last beat time

      this.current16thNote++;	// Advance the beat number, wrap to zero

      if (this.current16thNote == 64) {
        this.current16thNote = 0;
      }
    },

    tempoChange: function(model, tempo) {
      this.squareWave.frequency.value = frequency;
    },

    connect: function(node) {
      this.gainNode.connect(node);
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/compressor.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Compressor = Backbone.Model.extend({
    initialize: function (attrs, options) {
      this.context = options.context;
      this.compressor = this.context.createDynamicsCompressor();
    },
    
    connect: function(node) {
      this.compressor.connect(node);
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/loop.js<|end_filename|>
(function() {
  'use strict';
  
  bs.models.Loop = Backbone.Model.extend({
      initialize: function(attrs, options) {
        _.bindAll(this);
        
      },

      defaults: {
          duration: 0,
          timestamp: 0,
          user: null,
          waveform: null,
          path: null,
          bpm: null,
          key: null,
          gain: 1,
          muted: false,
          soloMuted: false,
          children: [],
          type: 'loop'
      }
  });

})();


<|start_filename|>media/scripts/mvc/views/delay_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.DelayView = Backbone.View.extend({
    initialize: function() {
      this.timeKnob = this.model.timeKnob = new bs.models.Knob({min: 0, max: 1, value: this.model.get('time'), decimalPlace: 100, startDegree: -140});
      this.gainKnob = this.model.gainKnob = new bs.models.Knob({min: 0, max: 1, value: this.model.get('gain'), decimalPlace: 100, startDegree: -140});

      this.listenTo(this.model, 'change:time', this.timeChange);
      this.listenTo(this.model, 'change:gain', this.gainChange);
      this.listenTo(this.timeKnob, 'change:value', this.handleTimeInput);
      this.listenTo(this.gainKnob, 'change:value', this.handleGainInput);
    },

    template: _.template($('.delay-template').html()),

    handleTimeInput: function(knob, time) {
      this.model.set({time: time});
    },

    handleGainInput: function(knob, gain) {
      this.model.set({gain: gain});
    },

    timeChange: function(delay, time) {
      this.timeKnob.set({value: time});
      this.$timeReading.text(time);
    },

    gainChange: function(delay, gain) {
      this.gainKnob.set({value: gain});
      this.$gainReading.text(gain);
    },

    renderKnobs: function() {
      this.timeKnobView = new bs.views.KnobView({model: this.timeKnob});
      this.$time.html(this.timeKnobView.render().el);
      this.gainKnobView = new bs.views.KnobView({model: this.gainKnob});
      this.$gain.html(this.gainKnobView.render().el);
    },
    
    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$time = this.$('.time');
      this.$timeReading = this.$('.time-reading');
      this.$gain = this.$('.gain');
      this.$gainReading = this.$('.gain-reading');

      this.renderKnobs();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/lfo_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.LfoView = Backbone.View.extend({
    initialize: function() {
      this.frequencyKnob = this.model.frequencyKnob = new bs.models.Knob({min: 0, max: 10, decimalPlace: 10, value: this.model.get('frequency'), startDegree: -140});
      this.gainKnob = this.model.gainKnob = new bs.models.Knob({min: 0, max: this.model.get('maxGain'), value: this.model.get('gain'), decimalPlace: 100, startDegree: -140});
      this.waveformKnob = this.model.waveformKnob = new bs.models.TypeKnob({min: 0, max: 280, type: this.model.get('type'), startDegree: -140});
      
      this.listenTo(this.model, 'change:type', this.waveformChange);
      this.listenTo(this.model, 'change:frequency', this.frequencyChange);
      this.listenTo(this.model, 'change:gain', this.gainChange);
      this.listenTo(this.model, 'change:maxGain', this.maxGainChange);
      this.listenTo(this.frequencyKnob, 'change:value', this.handleFrequencyInput);
      this.listenTo(this.gainKnob, 'change:value', this.handleGainInput);
      this.listenTo(this.waveformKnob, 'change:type', this.handleWaveformInput);
    },
    
    template: _.template($('.lfo-template').html()),

    events: {
      'change .tempo-sync': 'handleTempoSyncInput'
    },

    handleWaveformInput: function(knob, waveform) {
      this.model.set({type: waveform});
    },

    handleFrequencyInput: function(knob, frequency) {
      this.model.set({frequency: frequency});
    },

    handleGainInput: function(knob, gain) {
      this.model.set({gain: gain});
    },

    handleTempoSyncInput: function(e) {
      if (e.target.checked) {
        this.model.set({frequency: synth.metronome.get('tempo') / 60});
        this.startListeningToTempo();
      } else {
        this.stopListening(synth.metronome);
      }
    },

    waveformChange: function(model, waveform) {
      this.waveformKnob.set({'type': waveform});
    },

    frequencyChange: function(model, frequency) {
      this.frequencyKnob.set({value: frequency});
      this.$frequencyReading.text(frequency);
    },

    gainChange: function(model, gain) {
      this.gainKnob.set({value: gain});
      this.$gainReading.text(gain);
    },

    maxGainChange: function(model, maxGain) {
      this.gainKnob.set({'max': maxGain});
      this.$gain[0].max = maxGain;
    },

    startListeningToTempo: function() {
      var view = this;
      this.listenTo(synth.metronome, 'change:tempo', function(model, tempo) {
        view.model.set({frequency: tempo / 60});
      });
    },

    renderKnobs: function() {
      this.frequencyKnobView = new bs.views.KnobView({model: this.frequencyKnob});
      this.gainKnobView = new bs.views.KnobView({model: this.gainKnob});
      this.waveformKnobView = new bs.views.TypeKnobView({model: this.waveformKnob, className: 'knob-wrapper waveform-knob-wrapper'});
      this.$frequency.html(this.frequencyKnobView.render().el);
      this.$gain.html(this.gainKnobView.render().el);
      this.$waveform.html(this.waveformKnobView.render().el);

      // this.waveformChange(this.model, this.model.get('type'));
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$waveform = this.$('.waveform');
      this.$frequency = this.$('.frequency');
      this.$frequencyReading = this.$('.frequency-reading');
      this.$gain = this.$('.gain');
      this.$gainReading = this.$('.gain-reading');
      this.$('.type option[value='+ this.model.get('type') +']').attr({selected: true});

      this.renderKnobs();


      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/synth_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.SynthView = Backbone.View.extend({
    initialize: function() {
      this.listenTo(this.model.keyboard, 'keyDown', this.handleKeyDown);
      this.listenTo(this.model.keyboard, 'keyUp', this.handleKeyUp);
    },

    template: _.template($('.synth-template').html()),

    handleKeyDown: function(note, freq) {
      this.model.oscillatorModule.set({frequency: freq});
      this.model.ampEnvelope.triggerAttack();
      this.model.filterEnvelope.triggerAttack();
    },

    handleKeyUp: function(note, freq) {
      if (!this.model.keyboard.pressed) {
        this.model.ampEnvelope.triggerRelease();
        this.model.filterEnvelope.triggerRelease();
      }
    },

    renderPatches: function() {
      this.patchesView = new bs.views.PatchesView({collection: this.model.patches});
      this.$('.patches').append(this.patchesView.render().el);
    },

    renderFilter: function() {
      this.filterView = new bs.views.FilterView({model: this.model.filter});
      this.$('.filter').append(this.filterView.render().el);
    },

    renderOscillatorModule: function() {
      this.oscillatorView = new bs.views.OscillatorModuleView({model: this.model.oscillatorModule});
      this.$('.oscillator-module').append(this.oscillatorView.render().el);
    },

    renderMixer: function() {
      this.mixerView = new bs.views.MixerView({model: this.model.mixer});
      this.$('.mixer').append(this.mixerView.render().el);
    },

    renderKeyboard: function() {
      this.keyboardView = new bs.views.KeyboardView({model: this.model.keyboard});
      this.$el.append(this.keyboardView.render().el);
    },

    renderOscilloscope: function() {
      this.model.oscilloscope = new WavyJones(this.model.context, 'oscilloscope');
      this.model.masterGain.connect(this.model.oscilloscope);
    },

    renderAmpEnvelope: function() {
      this.envelopeView = new bs.views.EnvelopeView({model: this.model.ampEnvelope});
      this.$('.amp-envelope').append(this.envelopeView.render().el);
    },

    renderFilterEnvelope: function() {
      this.envelopeView = new bs.views.EnvelopeView({model: this.model.filterEnvelope, template: _.template($('.envelope-template').html())});
      this.$('.filter-envelope').append(this.envelopeView.render().el);
    },

    renderDelay: function() {
      this.delayView = new bs.views.DelayView({model: this.model.delay});
      this.$('.delay').append(this.delayView.render().el);
    },

    renderLfo: function() {
      this.oscillatorView = new bs.views.LfoView({model: this.model.lfo});
      this.$('.lfo').append(this.oscillatorView.render().el);
    },

    renderLoopModule: function() {
      this.loopModuleView = new bs.views.LoopModuleView({model: this.model.loopModule});
      this.$('.loop-module').append(this.loopModuleView.render().el);
    },

    renderMetronome: function() {
      this.metronomeView = new bs.views.MetronomeView({model: this.model.metronome});
      this.$('.metronome').html(this.metronomeView.render().el);
    },

    renderCVPatches: function() {
      this.cvPatchModuleView = new bs.views.CVPatchModuleView({model: this.model.cvPatchModule});
      this.$('.cv-patches').html(this.cvPatchModuleView.render().el);
    },

    initMidi: function() {
      var view = this;
      var midi = null;   // m = MIDIAccess object for you to make calls on
      navigator.requestMIDIAccess( onsuccesscallback, function(err) {console.log('err', err)} );
      var startVal;
      var moving;
      var movingKeyCode;

      function onsuccesscallback( access ) {
          midi = access;

          // hax to hide jazz plugin, what a pain
          $('object').css({'opacity': 0, position: 'absolute', left: -1000});

          // Things you can do with the MIDIAccess object:
          var inputs = midi.getInputs();   // inputs = array of MIDIPorts
          var outputs = midi.getOutputs(); // outputs = array of MIDIPorts
          var i = midi.getInput( inputs[0] );    // grab first input device.  You can also just call getInput( index );
          i.onmessage = function(e) {
            console.log(e.data);
            var keyCode = e.data[1];
            var value = e.data[2];

            if (moving && movingKeyCode == keyCode) {
              if (this.moveTimeout) {
                clearInterval(this.moveTimeout);
              }
              this.moveTimeout = setTimeout(function() {
                moving = false;
              }, 2000);
            } else {
              startVal = 0;
              moving = true;
              movingKeyCode = keyCode;
            }


            // if a key is pressed send it to the keyboard
            if (keyCode >= 48 && keyCode <= 72) {
              view.model.keyboard.keyboard.midiIn(e);
            }

            else if (keyCode == 119) { // rec
              var loopModule = view.model.loopModule
              if (loopModule.recording) {
                loopModule.stopRecording();
              } else {
                loopModule.startRecording();
              }
            }

            // control knobs
            // Bank A

            // oscillator waveforms
            else if (keyCode == 22) { // K1
              var knob = synth.oscillatorModule.osc1.waveformKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            else if (keyCode == 23) { // K2
              var knob = synth.oscillatorModule.osc2.waveformKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            else if (keyCode == 24) { // K3
              var knob = synth.oscillatorModule.osc3.waveformKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // oscillator detune
            else if (keyCode == 26) { // K5
              var knob = synth.oscillatorModule.osc1.detuneKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            else if (keyCode == 27) { // K6
              var knob = synth.oscillatorModule.osc2.detuneKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            else if (keyCode == 28) { // K7
              var knob = synth.oscillatorModule.osc3.detuneKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // oscillator gain
            else if (keyCode == 12) { // K9
              var knob = synth.oscillatorModule.gain1Knob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            else if (keyCode == 13) { // K10
              var knob = synth.oscillatorModule.gain2Knob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            else if (keyCode == 14) { // K11
              var knob = synth.oscillatorModule.gain3Knob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // delay time
            else if (keyCode == 25) { // K4
              var knob = synth.delay.timeKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // filter cutoff
            else if (keyCode == 29) { // K8
              var knob = synth.filter.frequencyKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            else if (keyCode == 15) { // K12
              var knob = synth.lfo.frequencyKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }


            // Bank B

            // delay time
            else if (keyCode == 30) { // K4
              var knob = synth.delay.timeKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // delay gain
            else if (keyCode == 31) { // K4
              var knob = synth.delay.gainKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // filter cutoff
            else if (keyCode == 32) { // K4
              var knob = synth.filter.frequencyKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // filter resonance
            else if (keyCode == 33) { // K4
              var knob = synth.filter.resonanceKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // lfo waveform gain
            else if (keyCode == 34) { // K4
              var knob = synth.lfo.waveformKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // lfo frequency
            else if (keyCode == 35) { // K4
              var knob = synth.lfo.frequencyKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // lfo gain
            else if (keyCode == 36) { // K4
              var knob = synth.lfo.gainKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }

            // tempo
            else if (keyCode == 16) { // K4
              var knob = synth.metronome.tempoKnob;
              var value = mapKnobValue(knob, value);
              knob.set({'value': value});
            }
          } // onmessage( event ), event.data & event.timestamp are populated

          var mapKnobValue = function(knob, value) {
            var valDiff = Math.abs(startVal - value);

            // minute adjust if shift key is held
            var valRange = 127;

            var min = knob.get('min');
            var max = knob.get('max');
            var decimal = knob.get('decimalPlace');

            var range = Math.abs(max - min);

            var value = (valDiff / valRange) * range;
            value += min;
            console.log('valdiff', valDiff, 'valRange', valRange, 'range', range, 'value', value, 'startVal', startVal);
            value += new Number(startVal);

            if (value >= max) {
              return max;
            } else if (value <= min) {
              return min
            } else {
              return Math.round(value * decimal) / decimal;
            }

          }
      }
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));

      this.renderPatches();
      this.renderFilter();
      this.renderOscillatorModule();
      this.renderMixer();
      this.renderKeyboard();
      this.renderOscilloscope();
      this.renderAmpEnvelope();
      this.renderFilterEnvelope();
      this.renderDelay();
      this.renderLfo();
      this.renderLoopModule();
      this.renderMetronome();
      this.renderCVPatches();

      // this.initMidi();

      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/patches_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.PatchesView = Backbone.View.extend({
    initialize: function() {
      _.bindAll(this);
      this.listenTo(this.collection, 'add', this.insertPatch);
    },
    
    template: _.template($('.patches-template').html()),

    patchTemplate: _.template($('.patch-template').html()),

    events: {
      'change .patches': 'loadPatch'
    },

    insertPatch: function(patch) {
      this.$patches.append(this.patchTemplate(patch.toJSON()));
      
      if (patch.get('name') == synth.get('patch')) {
        this.$patches.val(patch.id);
      }
    },

    savePatchCallback: function(name) {
      this.savePatchPopupView.exit();
      this.savePatch(name);
    },

    savePatch: function(name) {
      synth.set({patch: name});
      synth.savePatch(name);
    },

    loadPatch: function(e) {
      this.$patches.blur();
      var patch = this.collection.get(e.target.value);
      synth.loadPatch(patch.get('parameters'));
      router.navigate('/' + patch.get('name').replace(/ /g, '-'));
    },

    initSavePatchPopup: function() {
      this.savePatchPopupView = new bs.views.SavePatchPopupView({trigger: this.$('.save-patch'), callback: this.savePatchCallback});
    },

    render: function() {
      var view = this;
      this.$el.html(this.template());
      this.$patches = this.$('.patches');

      this.collection.each(function(patch) {
        view.insertPatch(patch);
      });

      this.initSavePatchPopup();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/filter_envelope.js<|end_filename|>
(function() {
  'use strict';

  bs.models.FilterEnvelope = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.filterNode = options.filter.preNode;
      // this.filterNode = this.context.createBiquadFilter();
    },

    defaults: {
      'attack': .01,
      'decay': 0.2,
      'sustain': 20000,
      'release': 0.2
    },

    triggerAttack: function() {
      var now = this.context.currentTime;

      this.filterNode.frequency.cancelScheduledValues(now);
      this.filterNode.frequency.setTargetAtTime(20000, now, this.get('attack'));
        
      now += this.get('attack');
      this.filterNode.frequency.setTargetAtTime(this.get('sustain'), now, this.get('decay'));
    },

    triggerRelease: function() {
      var now = this.context.currentTime;
      
      // this.gainNode.gain.setValueAtTime(this.gainNode.gain.value, now);
      this.filterNode.frequency.cancelScheduledValues(now);
      this.filterNode.frequency.setTargetAtTime(0, now, this.get('release'));
      // this.filterNode.frequency.linearRampToValueAtTime(0, now + this.get('release'));
    },

    connect: function(node) {
      this.filterNode.connect(node);
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/type_knob.js<|end_filename|>
(function() {
  'use strict';

  bs.models.TypeKnob = Backbone.Model.extend({
    initialize: function() {
    },

    defaults: {
      types: ['sine', 'triangle', 'square', 'sawtooth'],
      type: 'sawtooth',
      max: 20000,
      min: 0,
      step: 1,
      value: 0,
      decimalPlace: 1,
      startDegree: 0
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/knob_view.js<|end_filename|>
(function() {
  'use strict';
  
  bs.views.KnobView = Backbone.View.extend({
    initialize: function() {
      _.bindAll(this);

      this.listenTo(this.model, 'change:value', this.valueChange);
      this.listenTo(this.model, 'change:max', this.maxChange);
      this.degreeRange = 280;
      this.pixelRange = 280;
    },
    
    template: _.template($('.knob-template').html()),

    className: 'knob-wrapper',

    events: {
      'mousedown': 'handleMouseDown',
      'mouseup': 'handleMouseUp'
    },

    valueChange: function(model, value) {
      var value = this.model.get('value');
      var degrees = ((value / this.range) * this.degreeRange);
      this.rotateEl(this.$knob, degrees);
    },

    handleMouseDown: function(e) {
      this.startDrag(e);
      this.listenToMouseUp();
    },

    handleMouseUp: function(e) {
      this.stopDrag();
    },

    startDrag: function(e) {
      $(document).bind('mousemove', this.dragCallback);
      $('body').addClass('unselectable');
      this.startY = e.pageY;
      this.startVal = this.model.get('value');
    },

    stopDrag: function() {
      $(document).unbind('mousemove', this.dragCallback);
      $('body').removeClass('unselectable');
    },

    listenToMouseUp: function() {
      $(document).bind('mouseup', this.handleMouseUp);
    },

    unlistenToMouseUp: function() {
      $(document).unbind('mouseup', this.handleMouseUp)
    },

    dragCallback: function(e) {
      
      var pixelDiff = this.startY - e.pageY;

      // minute adjust if shift key is held
      var pixelRange = e.shiftKey ? this.range : this.pixelRange
      
      var value = (pixelDiff / pixelRange) * this.range;
      value += new Number(this.startVal);
      
      if (value >= this.model.get('max')) {
        this.model.set({value: this.model.get('max')});
      } else if (value <= this.model.get('min')) {
        this.model.set({value: this.model.get('min')});
      } else {
        this.model.set({value: (value * this.model.get('decimalPlace')) / this.model.get('decimalPlace')});
      }
    },

    maxChange: function(model, max) {
      this.range = max - this.model.get('min');
    },

    rotateEl: function($el, degrees) {
      $el.css({
        '-webkit-transform' : 'rotate('+degrees+'deg)',
           '-moz-transform' : 'rotate('+degrees+'deg)',  
            '-ms-transform' : 'rotate('+degrees+'deg)',  
             '-o-transform' : 'rotate('+degrees+'deg)',  
                'transform' : 'rotate('+degrees+'deg)',  
                     'zoom' : 1
      });
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$knob = this.$('.knob');
      this.$indicator = this.$('.indicator');

      this.rotateEl(this.$indicator, this.model.get('startDegree'));
      this.range = this.model.get('max') - this.model.get('min');
      var degrees = (this.model.get('value') / this.range) * this.degreeRange;
      this.rotateEl(this.$knob, degrees);
      return this;
    }
  });
  
})();


<|start_filename|>media/scripts/mvc/views/save_patch_popup_view.js<|end_filename|>
(function() {
  'use strict';
  
  bs.views.SavePatchPopupView = bs.views.PopupView.extend({
    template: _.template($('.save-patch-popup-template').html()),

    events: {
      'click .save': 'savePatch',
      'keydown .patch-name': 'handleKeypress'
    },

    handleKeypress: function(e) {
      e.stopPropagation();
      if (e.keyCode == 13) { // enter
        this.savePatch(e);
      } 

      if (e.keyCode == 27) { // escape
        this.exit();
      }
    },

    savePatch: function(e) {
      e.preventDefault();
      var name = this.$name.val();
      if (name) {
        this.callback(name);
      } else {
        this.renderError('Name required');
      }
    },

    renderError: function(err) {
      this.$error.text(err);
    },

    render: function() {
      this.$el.html(this.template());
      $('body').append(this.$el);
      this.$name = this.$('.patch-name');
      this.$error = this.$('.error');
      this.$name.focus();
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/metronome_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.MetronomeView = Backbone.View.extend({
    initialize: function(options) {
      this.tempoKnob = this.model.tempoKnob = new bs.models.Knob({min: 20, max: 300, value: this.model.get('tempo'), startDegree: -140});
      
      this.listenTo(this.model, 'change:tempo', this.tempoChange);
      this.listenTo(this.model, 'change:state', this.stateChange);
      this.listenTo(this.model, 'beat', this.beat);

      this.listenTo(this.tempoKnob, 'change:value', this.handleTempoInput);
    },
    
    template: _.template($('.metronome-template').html()),

    events: {
      'change .state': 'handleStateInput',
      'change .tempo': 'handleTempoInput'
    },

    handleStateInput: function(e) {
      this.model.set({'state': e.target.checked});
    },

    handleTempoInput: function(knob, tempo) {
      this.model.set({'tempo': tempo});
    },
    
    tempoChange: function(model, tempo) {
      this.$tempo.val(tempo);
      this.$tempoReading.text(tempo);
    },

    stateChange: function(model, state) {
      this.$state.attr('checked', state)
    },

    beat: function(beat) {
      if (beat.number % 16 === 0) {
        this.$beatIndicator.css({'background-color': 'red'});
      } else if (beat.number % 4 === 0) {
        this.$beatIndicator.css({'background-color': 'maroon'});
      } else {
        this.$beatIndicator.css({'background-color': 'black'});
      }
    },

    renderKnobs: function() {
      this.tempoKnobView = new bs.views.KnobView({model: this.tempoKnob});
      this.$tempo.html(this.tempoKnobView.render().el);
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$tempo = this.$('.tempo');
      this.$state = this.$('.state');
      this.$tempoReading = this.$('.tempo-reading');
      this.$beatIndicator = this.$('.beat-indicator');

      this.renderKnobs();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/config.js<|end_filename|>
(function() {
  Backbone.emulateHTTP = true;
  Backbone.emulateJSON = true;
  Backbone.Model.prototype.idAttribute = '_id';

  bs = { // Backbone Synth
    models: {},
    views: {},
    collections: {},
    routers: {}
  }
})();


<|start_filename|>media/scripts/mvc/models/cv_patch.js<|end_filename|>
(function() {
  'use strict';

  bs.models.CVPatch = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = this.collection.context;
      this.sources = this.collection.patchSources;
      this.destinations = this.collection.patchDestinations;
      this.gainNode = this.context.createGain();
      this.destination = this.destinations[this.get('destination')].node;

      this.on('change:source', this.sourceChange);
      this.on('change:destination', this.destinationChange);
      this.on('change:gain', this.gainChange);

      this.listenTo(this.collection, 'reset', this.exit);

      this.setSource(this.get('source'));
      this.setDestination(this.get('destination'));

      this.gainNode.gain.value = this.get('gain');
    },

    defaults: {
      'gain': 0,
      'destination': 'none',
      'source': 'none',
      'outputIndex': 0
    },

    exit: function() {
      // this is hax and leaks memory
      // put a break poing here and switch presets.  You will see cv patch nodes pile up.
      // If I could figure a better way to disconnect nodes...
      this.gainNode.disconnect();
    },

    sourceChange: function(model, source) {
      this.setSource(source);
    },

    setSource: function(source) {
      var prevSource = this.sources[this.previousAttributes().source];
      var source = this.sources[source].node;
      this.destination = this.destinations[this.get('destination')].node;
      this.gainNode.disconnect();
      this.gainNode = this.context.createGain();

      if (source) {
        source.connect(this.gainNode);
      }

      if (this.destination) {
        this.set({gain: this.get('gain') || (this.calcMaxGainValue() / 2)});
        this.gainNode.connect(this.destination);
      }
    },

    destinationChange: function(model, destination) {
      this.setDestination(destination);
    },

    setDestination: function(destination) {
      var prevDestination = this.destinations[this.previousAttributes().destination];
      this.destination = this.destinations[destination].node;

      if (prevDestination) {
        this.gainNode.disconnect();
      }

      if (this.destination) {
        // set to max of 20000 or maxValue
        // for some reason maxValue on oscillator.frequency is 100000.  this is to combat that.
        this.set({gain: this.get('gain') || (this.calcMaxGainValue() / 2)});
        this.gainNode.connect(this.destination);
      }
    },

    calcMaxGainValue: function() {
      var maxValue = 20000;

      if (this.destination) {
        maxValue = (this.destination.maxValue > 20000 ? 20000 : this.destination.maxValue);
      }

      return maxValue;
    },

    gainChange: function(model, gain) {
      console.log(gain);
      this.gainNode.gain.value = gain;
    }
  });
})();


<|start_filename|>media/scripts/mvc/collections/cv_patches.js<|end_filename|>
(function() {
  'use strict';

  bs.collections.CVPatches = Backbone.Collection.extend({
    initialize: function(models, options) {
      this.context = options.context;
      this.patchSources = options.patchSources;
      this.patchDestinations = options.patchDestinations;
    },
    
    model: bs.models.CVPatch
  });
})();


<|start_filename|>media/scripts/mvc/collections/patches.js<|end_filename|>
(function() {
  'use strict';

  bs.collections.Patches = Backbone.Collection.extend({
    // url: 'http://soundkeep.com/synthpatches',
    localStorage: new Backbone.LocalStorage('patches'),
    model: bs.models.Patch
  });
})();


<|start_filename|>media/scripts/init.js<|end_filename|>
(function() {
  window.router = new bs.routers.Main();
  Backbone.history.start({pushState: true});
})();


<|start_filename|>media/scripts/mvc/views/gain_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.GainView = Backbone.View.extend({
    initialize: function() {
      this.gainKnob = new bs.models.Knob({min: 0, max: 1, startDegree: -140, decimalPlace: 100, value: this.model.get('gain')});
      
      this.listenTo(this.model, 'change:gain', this.gainChange);
      this.listenTo(this.gainKnob, 'change:value', this.handleGainInput);
    },
    
    template: _.template($('.gain-node-template').html()),

    handleGainInput: function(knob, value) {
      this.model.set({gain: value});
    },

    gainChange: function(model, gain) {
      this.gainKnob.set({value: gain});
      this.$gainReading.text(gain);
    },

    renderKnob: function() {
      this.gainKnobView = new bs.views.KnobView({model: this.gainKnob});
      this.$gain.html(this.gainKnobView.render().el);
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$gain = this.$('.gain');
      this.$gainReading = this.$('.gain-reading');

      this.renderKnob();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/patch.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Patch = Backbone.Model.extend({
  });
})();


<|start_filename|>media/scripts/mvc/models/lfo.js<|end_filename|>
(function() {
  'use strict';

  bs.models.LFO = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context || new webkitCreateAudioContext();
      this.oscillatorNode = this.context.createOscillator();
      this.gainNode = this.context.createGain();

      this.oscillatorNode.connect(this.gainNode);

      this.on('change:type', this.typeChange);
      this.on('change:frequency', this.frequencyChange);
      this.on('change:detune', this.detuneChange);
      this.on('change:gain', this.gainChange);

      this.oscillatorNode.type = this.get('type');
      this.oscillatorNode.frequency.value = this.get('frequency');
      this.oscillatorNode.detune.value = this.get('detune');
      this.gainNode.gain.value = this.get('gain');
    },

    defaults: {
      'type': 'sawtooth',
      'frequency': 440,
      'detune': 0,
      'gain': .5,
      'maxGain': 1
    },

    frequencyChange: function(model, freq) {
      this.oscillatorNode.frequency.value = freq;
    },

    typeChange: function(model, waveform) {
      this.oscillatorNode.type = waveform;
    },

    detuneChange: function(model, detune) {
      this.oscillatorNode.detune.value = detune;
    },

    gainChange: function(model, gain) {
      this.gainNode.gain.value = gain;
      console.log('lfo gain', gain);
    },

    start: function(time) {
      this.oscillatorNode.start(time);
    },

    connect: function(node, index) {
      this.gainNode.connect(node, index);
    },

    disconnect: function(index) {
      this.gainNode.disconnect(index);
    }
  });

})();


<|start_filename|>media/scripts/mvc/views/buffer_view.js<|end_filename|>
(function() {
  // 'use strict';

  bs.views.BufferView = Backbone.View.extend({
    initialize: function() {
      _.bindAll(this);
      // this.listenTo(synth.metronome, 'beat', this.scrollWaveform);
      this.listenTo(this.model, 'change:gain', this.gainChange);
      this.listenTo(this.model, 'draw', this.draw);
      this.listenTo(this.model, 'doneRecording', this.startScrolling);

      this.firstDraw = true;
      this.readyToDraw = false;
    },
    
    events: {
      'click .delete': 'clear',
      'mousedown': 'toggleGain',
    },

    className: 'buffer',
    
    template: _.template($('.buffer-template').html()),

    uploadBuffer: function(name) {
      this.model.uploadBuffer(name);
      this.saveLoopPopupView.exit();
    },

    clear: function(e) {
      e.preventDefault();
      var loopModule = this.model.collection.parentModel;
      loopModule.recording = false;
      this.model.collection.remove(this.model);
      var source = this.model.get('source');
      if (source) {
        source.stop(0);
      }
      this.model = false;
      this.remove();
    },

    draw: function(channels) {
      if (!this.readyToDraw) return false;
      
      var pcm = channels[0];
      
      var height = this.$waveform.height();
      var yCenter = height / 2;
      // start the path string with the cursor at x, yCenter
      // // adjust the sample rate based on the width of the element
      var inc = 64;

      if (this.firstDraw) {
        this.$waveform.width(0);
        this.paper = Raphael(this.$waveform[0], '100%', '100%');
        this.x = 0;
        // console.log('x', this.x);
        this.y = yCenter;
        this.firstDraw = false;
      }
      
      var pathString = 'M' + this.x + ',' + this.y;

      // sample pcm and build path string
      for (var i = 0; i < pcm.length - 1; i += inc) {
        this.y = yCenter - Math.round(pcm[i] * 20);
        pathString += 'L'+ this.x + ',' + this.y + 'M' + this.x + ',' + this.y; 
        this.x++;
      }
      pathString += 'Z';


      // console.log(pathString);
      // feed to path
      this.path = this.paper.path(pathString);
      var pixels = pcm.length / inc;
      this.$waveform.css({left: '-=' + pixels, width: '+=' + pixels});
      // for the pretty
      this.path.attr({'stroke': 'rgb(7,108,240)'});
      this.path.glow({'color': 'rgb(7,108,240)', 'width': 1, opacity: .3});
    },

    startScrolling: function() {
      var $svgClone = $(this.$('svg')[0].cloneNode(true));
      var $svgClone1 = $(this.$('svg')[0].cloneNode(true));
      $svgClone.css({'left':this.$waveform.width(), position: 'absolute'});
      $svgClone1.css({'left': -(this.$waveform.width()), position: 'absolute'});
      this.$waveform.append($svgClone);
      this.$waveform.append($svgClone1);

      this.listenTo(synth.metronome, 'beat', this.scrollWaveform);
    },

    scrollWaveform: function(beat) {
      var width = this.$waveform.width();
      
      if (((beat.number % this.model.get('numBeats')) - (this.model.get('startBeat') % this.model.get('numBeats')) === 0)) {
        this.$waveform.css({'left': this.$el.width() / 2});
      } else {
        this.$waveform.css({'left': '-=' + width / this.model.get('numBeats')});
      }
    },

    toggleGain: function(e) {
      var gain = this.model.get('gain');
      var gain = gain ? 0 : 1;
      this.model.set({gain: gain});
    },

    gainChange: function(model, gain) {
      if (!gain) {
        this.$waveform.css({opacity: .5});
        // this.path.attr({stroke: '#eee'});
        // this.path.glow({color: '#eee'});
      } else {
        this.$waveform.css({opacity: 1});
        // this.path.attr({stroke: 'blue'});
        // this.path.glow({color: 'blue'});
      }
    },

    drawWaveform: function() {
      var pcm = this.model.bufferNode.getChannelData(0);
      var width = this.$el.width();
      var height = this.$el.height();
      this.$waveform = this.$('.waveform');

      // get dat paper
      this.paper = Raphael(this.$waveform[0], width, height);

      // clone svg
      // this.$waveform.append(this.$('svg').clone(true));
      
      var x = 0;
      var yCenter = height / 2;

      // start the path string with the cursor at x, yCenter
      var pathString = 'M' + x + ',' + yCenter;

      // adjust the sample rate based on the width of the element
      var inc = Math.round(pcm.length / width);
      
      // sample pcm and build path string
      for (i = 0; i < pcm.length - 1; i += inc) {
        var y = yCenter - Math.round(pcm[i] * 20);
        pathString += 'L'+ x + ',' + y + 'M' + x + ',' + y; 
        x++;
      }

      // feed to path
      this.path = this.paper.path(pathString);

      // for the pretty
      this.path.attr({'stroke': 'rgb(7,108,240)'});
      this.path.glow({'color': 'rgb(7,108,240)', 'width': 1, opacity: .3});

      this.$waveform.css({width: width * 4});
    },

    initSaveLoopPopup: function() {
      this.saveLoopPopupView = new bs.views.SaveLoopPopupView({trigger: this.$('.upload'), callback: this.uploadBuffer});
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$waveform = this.$('.waveform');

      this.initSaveLoopPopup();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/cv_patch_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.CVPatchView = Backbone.View.extend({
    initialize: function() {
      this.gainKnob = this.model.gainKnob = new bs.models.Knob({
        min: 0,
        max: this.model.calcMaxGainValue(),
        value: this.model.get('gain'),
        startDegree: -140
      });

      this.listenTo(this.gainKnob, 'change:value', this.handleGainInput);
      
      this.listenTo(this.model, 'change:gain', this.gainChange);
    },
    
    events: {
      'change .sources': 'handleSourceInput',
      'change .destinations': 'handleDestinationInput'
    },

    tagName: 'li',

    className: 'cv-patch',
    
    template: _.template($('.cv-patch-template').html()),
    
    handleSourceInput: function(e) {
      this.$sources.blur();
      this.model.set({source: e.target.value});
    },

    handleDestinationInput: function(e) {
      this.$destinations.blur();
      this.model.set({destination: e.target.value});
    },
    
    handleGainInput: function(knob, gain) {
      this.model.set({gain: gain});
    },

    gainChange: function(model, gain) {
      this.gainKnob.set({value: gain});
    },

    renderSources: function() {
      var sourceKey;
      for (sourceKey in this.model.sources) {
        if (this.model.sources.hasOwnProperty(sourceKey)) {
          var source = this.model.sources[sourceKey];
          this.$sources.append('<option value="' + sourceKey + '">' + source.title + '</option>');
        }
      }
    },

    renderDestinations: function() {
      var destKey;
      for (destKey in this.model.destinations) {
        if (this.model.destinations.hasOwnProperty(destKey)) {
          var destination = this.model.destinations[destKey];
          this.$destinations.append('<option value="' + destKey + '">' + destination.title + '</option>');
        }
      }
    },
    
    renderKnob: function() {
      this.gainKnobView = new bs.views.KnobView({model: this.gainKnob});
      this.$gain.html(this.gainKnobView.render().el);
    },
    
    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$sources = this.$('.sources');
      this.$destinations = this.$('.destinations');
      this.$gain = this.$('.gain');

      this.renderSources();
      this.renderDestinations();
      this.renderKnob();

      this.$sources.val(this.model.get('source'));
      this.$destinations.val(this.model.get('destination'));
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/save_loop_popup_view.js<|end_filename|>
(function() {
  'use strict';
  
  bs.views.SaveLoopPopupView = bs.views.PopupView.extend({
    template: _.template($('.save-loop-popup-template').html()),

    events: {
      'click .save': 'saveLoop',
      'keydown .loop-name': 'handleKeypress'
    },

    handleKeypress: function(e) {
      if (e.keyCode == 13) { // enter
        this.saveLoop(e);
      } 

      if (e.keyCode == 27) { // escape
        this.exit();
      }
    },

    exit: function() {
      var view = this;
      this.$el.html('<h3>Thanks for sharing!</h3>');
      this.$el.fadeOut(1500, function() {
        view.remove();
      });
    },

    saveLoop: function(e) {
      e.preventDefault();
      var name = this.$name.val();
      this.callback(name);
    },

    render: function() {
      this.$el.html(this.template());
      this.$el.show();
      this.delegateEvents();
      $('body').append(this.$el);
      this.$name = this.$('.loop-name');
      this.$name.focus();
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/mixer.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Mixer = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.gain1 = new bs.models.Gain({gain: this.get('gain')}, {context: this.context});
      this.gain2 = new bs.models.Gain({gain: this.get('gain')}, {context: this.context});
      this.gain3 = new bs.models.Gain({gain: this.get('gain')}, {context: this.context});

      this.whiteNoise = new bs.models.WhiteNoise({}, {context: this.context});
      this.noiseGain = new bs.models.Gain({gain: 0}, {context: this.context});

      this.gate = new bs.models.Gain({}, {context: this.context});

      this.gain1.connect(this.gate.node);
      this.gain2.connect(this.gate.node);
      this.gain3.connect(this.gate.node);
      this.whiteNoise.connect(this.noiseGain.node);
      this.noiseGain.connect(this.gate.node);
      
      this.on('change:gain1', this.setGain1);
      this.on('change:gain2', this.setGain2);
      this.on('change:gain3', this.setGain3);
      this.on('change:noiseGain', this.setNoiseGain);
      
      this.listenTo(this.gain1, 'change', this.syncWithGain1);
      this.listenTo(this.gain2, 'change', this.syncWithGain2);
      this.listenTo(this.gain3, 'change', this.syncWithGain3);
      this.listenTo(this.noiseGain, 'change', this.syncWithNoiseGain);
    },
  
    defaults: {
      gain: .5
    },
    
    setGain1: function(model, attrs) {
      this.gain1.set(attrs);
    },

    syncWithGain1: function(model) {
      this.set({'gain1': model.changed});
    },

    setGain2: function(model, attrs) {
      this.gain2.set(attrs);
    },

    syncWithGain2: function(model) {
      this.set({'gain2': model.changed});
    },

    setGain3: function(model, attrs) {
      this.gain3.set(attrs);
    },

    syncWithGain3: function(model) {
      this.set({'gain3': model.changed});
    },

    setNoiseGain: function(model, attrs) {
      this.noiseGain.set(attrs);
    },

    syncWithNoiseGain: function(model) {
      this.set('noiseGain', model.changed);
    },

    toJSON: function() {
      this.set({'gain1': this.gain1.toJSON()});
      this.set({'gain2': this.gain2.toJSON()});
      this.set({'gain3': this.gain3.toJSON()});
      this.set({noiseGain: this.noiseGain.toJSON()});
      return this.attributes;
    },

    connect: function(node) {
      this.gate.node.connect(node);
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/filter_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.FilterView = Backbone.View.extend({
    initialize: function() {
      this.frequencyKnob = this.model.frequencyKnob = new bs.models.Knob({min: 0, max: 20000, value: 20000, startDegree: -140});
      this.resonanceKnob =  this.model.resonanceKnob = new bs.models.Knob({min: 0, max: 40, startDegree: -140});
      
      this.listenTo(this.model, 'change:frequency', this.frequencyChange);
      this.listenTo(this.model, 'change:resonance', this.resonanceChange);

      this.listenTo(this.frequencyKnob, 'change:value', this.handleFrequencyInput);
      this.listenTo(this.resonanceKnob, 'change:value', this.handleResonanceInput);
    },
    
    template: _.template($('.filter-template').html()),

    handleFrequencyInput: function(knob, frequency) {
      this.model.set({frequency: frequency});
    },
    
    handleResonanceInput: function(knob, resonance) {
      this.model.set({resonance: resonance});
    },

    frequencyChange: function(filter, frequency) {
      this.frequencyKnob.set({value: frequency});
      this.$frequencyReading.text(frequency);
    },

    resonanceChange: function(filter, resonance) {
      this.resonanceKnob.set({value: resonance});
      this.$resonanceReading.text(resonance);
    },

    renderKnobs: function() {
      this.frequencyKnobView = new bs.views.KnobView({model: this.frequencyKnob});
      this.resonanceKnobView = new bs.views.KnobView({model: this.resonanceKnob});
      this.$frequency.html(this.frequencyKnobView.render().el);
      this.$resonance.html(this.resonanceKnobView.render().el);
    },
    
    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$frequency = this.$('.frequency');
      this.$frequencyReading = this.$('.frequency-reading');
      this.$resonance = this.$('.resonance');
      this.$resonanceReading = this.$('.resonance-reading');

      this.renderKnobs();
      
      return this;
    }
  });
  
})();


<|start_filename|>media/scripts/mvc/views/mixer_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.MixerView = Backbone.View.extend({
    template: _.template($('.mixer-template').html()),
    
    renderGainNodes: function() {
      this.gain1View = new bs.views.GainView({model: this.model.gain1});
      this.gain2View = new bs.views.GainView({model: this.model.gain2});
      this.gain3View = new bs.views.GainView({model: this.model.gain3});
      this.noiseGainView = new bs.views.GainView({model: this.model.noiseGain});

      this.model.gain1Knob = this.gain1View.gainKnob;
      this.model.gain2Knob = this.gain2View.gainKnob;
      this.model.gain3Knob = this.gain3View.gainKnob;
      this.model.gain3Knob = this.noiseGainView.gainKnob;

      this.$('.gain-1').html(this.gain1View.render().el);
      this.$('.gain-2').html(this.gain2View.render().el);
      this.$('.gain-3').html(this.gain3View.render().el);
      this.$('.noise-gain').html(this.noiseGainView.render().el);
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.renderGainNodes();
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/oscillator_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.OscillatorView = Backbone.View.extend({
    initialize: function() {
      this.waveformKnob = this.model.waveformKnob = new bs.models.TypeKnob({min: 0, max: 280, type: this.model.get('type'), startDegree: -140});
      this.detuneKnob = this.model.detuneKnob = new bs.models.Knob({min: -2400, max: 2400});

      this.listenTo(this.detuneKnob, 'change:value', this.handleDetuneInput);
      this.listenTo(this.waveformKnob, 'change:type', this.handleWaveformInput);
      
      this.listenTo(this.model, 'change:type', this.waveformChange);
      this.listenTo(this.model, 'change:detune', this.detuneChange);
    },
    
    template: _.template($('.oscillator-template').html()),

    events: {
      'change .type': 'handleTypeInput',
    },

    handleWaveformInput: function(knob, waveform) {
      this.model.set({type: waveform});
    },

    handleDetuneInput: function(knob, detune) {
      this.model.set({detune: detune});
    },
    
    waveformChange: function(oscillator, waveform) {
      this.waveformKnob.set({type: waveform})
    },

    detuneChange: function(oscillator, detune) {
      this.$detuneReading.text(detune);
      this.detuneKnob.set({value: detune});
    },

    renderKnobs: function() {
      this.waveformKnobView = new bs.views.TypeKnobView({model: this.waveformKnob, className: 'knob-wrapper waveform-knob-wrapper'});
      this.detuneKnobView = new bs.views.KnobView({model: this.detuneKnob});
      this.$waveform.html(this.waveformKnobView.render().el);
      this.$detune.html(this.detuneKnobView.render().el);
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$waveform = this.$('.waveform');
      this.$detuneReading = this.$('.detune-reading');
      this.$detune = this.$('.detune');

      this.renderKnobs();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/oscillator_module_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.OscillatorModuleView = Backbone.View.extend({
    initialize: function() {
      
    },
    
    template: _.template($('.oscillator-module-template').html()),

    renderOscillators: function() {
      this.osc1View = new bs.views.OscillatorView({model: this.model.osc1});
      this.osc2View = new bs.views.OscillatorView({model: this.model.osc2});
      this.osc3View = new bs.views.OscillatorView({model: this.model.osc3});

      this.$('.osc-1').html(this.osc1View.render().el);
      this.$('.osc-2').html(this.osc2View.render().el);
      this.$('.osc-3').html(this.osc3View.render().el);
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));

      this.renderOscillators();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/oscillator_module.js<|end_filename|>
(function() {
  'use strict';

  bs.models.OscillatorModule = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      
      this.osc1 = new bs.models.Oscillator({type: 'sawtooth'}, {context: this.context});
      this.osc2 = new bs.models.Oscillator({type: 'sine'}, {context: this.context});
      this.osc3 = new bs.models.Oscillator({type: 'triangle'}, {context: this.context});

      this.osc1.start(0);
      this.osc2.start(0);
      this.osc3.start(0);

      this.on('change:osc1', this.setOsc1);
      this.on('change:osc2', this.setOsc2);
      this.on('change:osc3', this.setOsc3);
      this.on('change:frequency', this.frequencyChange);

      this.listenTo(this.osc1, 'change', this.syncWithOsc1);
      this.listenTo(this.osc2, 'change', this.syncWithOsc2);
      this.listenTo(this.osc3, 'change', this.syncWithOsc3);
    },

    defaults: {
      frequency: 440,
    },

    frequencyChange: function(model, freq) {
      this.osc1.set({frequency: freq});
      this.osc2.set({frequency: freq});
      this.osc3.set({frequency: freq});
    },

    setOsc1: function(model, attrs) {
      this.osc1.set(attrs);
    },

    syncWithOsc1: function(model) {
      this.set({'osc1': _.extend(this.get('osc1'), model.changed)});
    },

    setOsc2: function(model, attrs) {
      this.osc2.set(attrs);
    },

    syncWithOsc2: function(model) {
      this.set({'osc2': _.extend(this.get('osc2'), model.changed)});
    },

    setOsc3: function(model, attrs) {
      this.osc3.set(attrs);
    },

    syncWithOsc3: function(model) {
      this.set({'osc3': _.extend(this.get('osc3'), model.changed)});
    },

    connect: function(node) {
      this.osc1.connect(node.gain1.node);
      this.osc2.connect(node.gain2.node);
      this.osc3.connect(node.gain3.node);
    },

    toJSON: function() {
      this.set({'osc1': this.osc1.toJSON()});
      this.set({'osc2': this.osc2.toJSON()});
      this.set({'osc3': this.osc3.toJSON()});
      return this.attributes;
    }

  });
})();


<|start_filename|>media/scripts/mvc/views/keyboard_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.KeyboardView = Backbone.View.extend({
    initialize: function() {
      this.model.pressed = 0;
    },

    template: _.template($('.keyboard-template').html()),

    initKeyboard: function() {
      var view = this;
      this.model.keyboard = qwertyHancock({id: 'keyboard', startNote: 'C3'});
      
      this.model.keyboard.keyDown(function(note, freq) {
        view.model.pressed += 1;
        view.model.trigger('keyDown', note, freq);
      });
      
      this.model.keyboard.keyUp(function(note, freq) {
        view.model.pressed -= 1;
        view.model.trigger('keyUp', note, freq);
      });
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      
      this.initKeyboard();
      
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/synth.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Synth = Backbone.Model.extend({
    initialize: function() {
      var synth = this;
      this.context = new webkitAudioContext();
      this.patches = new bs.collections.Patches();
      this.filter = new bs.models.Filter({type: 'lowpass'}, {context: this.context});
      this.oscillatorModule = new bs.models.OscillatorModule({}, {context: this.context});
      this.mixer = new bs.models.Mixer({}, {context: this.context});
      this.ampEnvelope = new bs.models.Envelope({}, {context: this.context});
      this.filterEnvelope = new bs.models.Envelope({maxValue: 20000}, {context: this.context});
      this.delay = new bs.models.Delay({}, {context: this.context});
      this.keyboard = new bs.models.Keyboard();
      this.compressor = new bs.models.Compressor({}, {context: this.context});
      this.metronome = new bs.models.Metronome({}, {context: this.context});
      this.loopModule = new bs.models.LoopModule({metronome: this.metronome}, {context: this.context});
      this.masterGain = new bs.models.Gain({gain: 0}, {context: this.context});
      this.lfo = new bs.models.LFO({type: 'triangle', frequency: 5}, {context: this.context});

      // setup cv patches
      this.cvPatchSources = {
        'none': {title: '-- Source --', node: null},
        'lfo': {title: 'LFO', node: this.lfo},
        'osc1': {title: 'Osc1', node: this.oscillatorModule.gain1},
        'osc2': {title: 'Osc2', node: this.oscillatorModule.gain2},
        'osc3': {title: 'Osc3', node: this.oscillatorModule.gain3},
        'noise': {title: 'Noise', node: this.mixer.whiteNoise.sourceNode},
        'aenv': {title: 'Amp Envelope', node: this.ampEnvelope},
        'fenv': {title: 'Filter Envelope', node: this.filterEnvelope}
      };

      this.cvPatchDestinations = {
        'none': {title: '-- Destination --', node: null},
        'flt': {title: 'Filter', node: this.filter.postNode.frequency},
        'fltres': {title: 'Filter Resonance', node: this.filter.postNode.Q},
        'dlt': {title: 'Delay Time', node: this.delay.node.delayTime},
        'dlg': {title: 'Delay Gain', node: this.delay.gainNode.gain},
        'osc1f': {title: 'Osc1 Frequency', node: this.oscillatorModule.osc1.node.frequency},
        'osc2f': {title: 'Osc2 Frequency', node: this.oscillatorModule.osc2.node.frequency},
        'osc3f': {title: 'Osc3 Frequency', node: this.oscillatorModule.osc3.node.frequency},
        'osc1d': {title: 'Osc1 Detune', node: this.oscillatorModule.osc1.node.detune},
        'osc2d': {title: 'Osc2 Detune', node: this.oscillatorModule.osc2.node.detune},
        'osc3d': {title: 'Osc3 Detune', node: this.oscillatorModule.osc3.node.detune},
        'osc1g': {title: 'Osc1 Gain', node: this.mixer.gain1.node.gain},
        'osc2g': {title: 'Osc2 Gain', node: this.mixer.gain2.node.gain},
        'osc3g': {title: 'Osc3 Gain', node: this.mixer.gain3.node.gain},
        'noise': {title: 'Noise', node: this.mixer.noiseGain.node.gain},
        'lfo': {title: 'LFO', node: this.lfo.oscillatorNode.frequency},
        'mstrg': {title: 'Master Gain', node: this.masterGain.node.gain}
      };

      this.cvPatchModule = new bs.models.CVPatchModule({}, {context: this.context, patchSources: this.cvPatchSources, patchDestinations: this.cvPatchDestinations});


      // route node path
      this.oscillatorModule.connect(this.mixer);
      this.mixer.connect(this.filter.preNode);
      this.filterEnvelope.connect(this.filter.postNode.frequency);
      this.ampEnvelope.connect(this.masterGain.node.gain);
      this.filter.connect(this.delay.node);
      this.filter.connect(this.compressor.compressor);
      this.delay.connect(this.compressor.compressor);
      this.compressor.connect(this.masterGain.node);
      this.metronome.connect(this.context.destination);
      this.masterGain.node.connect(this.context.destination);
      this.compressor.connect(this.loopModule.gain);

      this.lfo.start(0);
      this.patches.fetch({add: true,
        success: function(collection, res) {
          var patch = collection.findWhere({name: synth.get('patch')});
          if (patch) {
            synth.loadPatch(patch.get('parameters'));
          }
        }
      });

    },

    defaults: {
      patch: 'warm'
    },

    savePatch: function(name) {
      var jsonStr = JSON.stringify({
        filter: this.filter.toJSON(),
        oscillatorModule: this.oscillatorModule.toJSON(),
        ampEnvelope: this.ampEnvelope.toJSON(),
        filterEnvelope: this.filterEnvelope.toJSON(),
        delay: this.delay.toJSON(),
        keyboard: this.keyboard.toJSON(),
        compressor: this.compressor.toJSON(),
        metronome: this.metronome.toJSON(),
        loopModule: this.loopModule.toJSON(),
        masterGain: this.masterGain.toJSON(),
        lfo: this.lfo.toJSON(),
        cvPatchModule: this.cvPatchModule.toJSON()
      });

      this.patches.create({name: name, parameters: JSON.parse(jsonStr)}, {
        wait: true,
        success: function(patch) {
          router.navigate(patch.get('name').replace(' ', '-'));
        }
      });
    },

    loadPatch: function(parameters) {
      var key;
      for (key in parameters) {
        if (parameters.hasOwnProperty(key)) {
          this[key].set(parameters[key]);
        }
      }
    }
  });

})();


<|start_filename|>media/scripts/mvc/views/patch_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.PatchView = Backbone.View.extend({
    template: _.template($('.patch-template').html()),

    tagName: 'option',

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$el.attr({value: this.model._id});
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/reverb.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Reverb = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.reverb = this.context.createConvolver();
    },

    connect: function(node) {
      this.reverb.connect(node);
    }
  });
})();


<|start_filename|>media/scripts/mvc/routers/main.js<|end_filename|>
(function() {
  'use strict';

  bs.routers.Main = Backbone.Router.extend({
    routes: {
      '': 'patch',
      ':patch': 'patch'
    },
    
    patch: function(patch) {
      var patch = patch ? patch.replace('-', ' ') : null;
      window.synth = new bs.models.Synth({patch: patch});
      window.synthView = new bs.views.SynthView({model: window.synth, el: $('.main')});
      window.synthView.render();
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/delay.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Delay = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.node = this.context.createDelay();
      this.delay2Node = this.context.createDelay();
      this.gainNode = this.context.createGain();
      this.gain2Node = this.context.createGain();

      this.gainNode.gain.value = this.get('gain');
      this.gain2Node.gain.value = this.get('gain');

      this.node.connect(this.gainNode);
      this.gainNode.connect(this.delay2Node);
      this.delay2Node.connect(this.gain2Node);
      this.gain2Node.connect(this.node);

      this.on('change:time', this.timeChange);
      this.on('change:gain', this.gainChange);
    },

    defaults: {
      'time': 0,
      'gain': 0.75
    },

    timeChange: function(model, time) {
      this.node.delayTime.value = time;
      this.delay2Node.delayTime.value = time;
    },

    gainChange: function(model, gain) {
      this.gainNode.gain.value = gain;
    },

    connect: function(node) {
      this.gainNode.connect(node);
      this.gain2Node.connect(node);
    }
  });
})();


<|start_filename|>server.js<|end_filename|>
var http = require('http'),
    fs = require('fs')
    mime = require('mime');

var server = http.createServer(function(req, res) {
  // send all urls to render index
  var path = 'index.html';

  // make exception for static assets
  if (req.url.match(/^\/media/)) {
    path = req.url;
  }

  var absPath = __dirname + '/' + path;
  var mimeType = mime.lookup(absPath);

  // load file
  fs.readFile(absPath, function(error, content) {
    if (error) {
      res.writeHead(500);
      res.end();
    } else {
      res.setHeader('Content-Type', mimeType);
      res.writeHead(200);
      res.end(content, 'utf-8');
    }
  });
});

server.listen(8000);
console.log('Listening on port: 8000');



<|start_filename|>media/scripts/mvc/views/envelope_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.EnvelopeView = Backbone.View.extend({
    initialize: function(attrs) {
      this.template = attrs.template || this.template;
      this.listenTo(this.model, 'change:frequency', this.frequencyChange);

      this.listenTo(this.model, 'change:attack', this.attackChange);
      this.listenTo(this.model, 'change:decay', this.decayChange);
      this.listenTo(this.model, 'change:sustain', this.sustainChange);
      this.listenTo(this.model, 'change:release', this.releaseChange);
    },
    
    template: _.template($('.envelope-template').html()),

    events: {
      'change .attack': 'handleAttackInput',
      'change .decay': 'handleDecayInput',
      'change .sustain': 'handleSustainInput',
      'change .release': 'handleReleaseInput'
    },

    handleAttackInput: function(e) {
      this.model.set({'attack': parseFloat(e.target.value)});
    },

    handleDecayInput: function(e) {
      this.model.set({'decay': parseFloat(e.target.value)});
    },

    handleSustainInput: function(e) {
      this.model.set({'sustain': parseFloat(e.target.value)});
    },

    handleReleaseInput: function(e) {
      this.model.set({'release': parseFloat(e.target.value)});
    },

    attackChange: function(envelope, attack) {
      this.$attack.val(attack);
      this.$attackReading.text(attack);
    },

    decayChange: function(envelope, decay) {
      this.$decay.val(decay);
      this.$decayReading.text(decay);
    },

    sustainChange: function(envelope, sustain) {
      this.$sustain.val(sustain);
      this.$sustainReading.text(sustain);
    },

    releaseChange: function(envelope, release) {
      this.$release.val(release);
      this.$releaseReading.text(release);
    },

    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$attack = this.$('.attack');
      this.$attackReading = this.$('.attack-reading');
      this.$decay = this.$('.decay');
      this.$decayReading = this.$('.decay-reading');
      this.$sustain = this.$('.sustain');
      this.$sustainReading = this.$('.sustain-reading');
      this.$release = this.$('.release');
      this.$releaseReading = this.$('.release-reading');
      return this;
    }
  });
  
})();


<|start_filename|>media/scripts/mvc/models/oscillator.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Oscillator = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context || new webkitCreateAudioContext();
      this.node = this.context.createOscillator();

      this.on('change:type', this.typeChange);
      this.on('change:frequency', this.frequencyChange);
      this.on('change:detune', this.detuneChange);

      this.initOscillatorNode();
    },

    defaults: {
      'type': 'sawtooth',
      'frequency': 440,
      'detune': 0,
    },

    initOscillatorNode: function() {
      this.node.type = this.get('type');
      this.node.frequency.value = this.get('frequency');
      this.node.detune.value = this.get('detune');
    },

    frequencyChange: function(model, freq) {
      this.node.frequency.value = freq;
    },

    typeChange: function(model, waveform) {
      this.node.type = waveform;
    },

    detuneChange: function(model, detune) {
      this.node.detune.value = detune;
    },

    start: function(time) {
      this.node.start(time);
    },

    connect: function(node) {
      this.node.connect(node);
    },

    disconnect: function() {
      this.node.disconnect();
    }
  });

})();


<|start_filename|>media/scripts/mvc/models/cv_patch_module.js<|end_filename|>
(function() {
  'use strict';

  bs.models.CVPatchModule = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      
      var cvPatches = attrs.cvPatches || [];
      
      this.cvPatches = new bs.collections.CVPatches(cvPatches, {context: this.context, patchSources: options.patchSources, patchDestinations: options.patchDestinations});
      this.cvPatches.add([{}]);
      
      this.on('change:cvPatches', this.cvPatchesChange);
    },

    cvPatchesChange: function(model, cvPatches) {
      this.cvPatches.reset();
      this.cvPatches.add(cvPatches);
    },

    toJSON: function() {
      this.set({cvPatches: this.cvPatches.toJSON()});
      return this.attributes;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/gain.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Gain = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.node = this.context.createGain();

      this.on('change:gain', this.gainChange);

      this.node.gain.value = this.get('gain');
    },

    defaults: {
      'gain': 1
    },

    gainChange: function(model, gain) {
      this.node.gain.value = gain;
    },

    connect: function(node) {
      this.node.connect(node);
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/envelope.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Envelope = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.gainNode = this.node = this.context.createGain();

      this.generateBuffer();
      this.createSourceNode();
      this.startSourceNode();

      this.sourceNode.connect(this.gainNode);
      this.gainNode.gain.value = this.get('gain');
    },

    defaults: {
      'maxValue': 1,
      'gain': 0,
      'attack': 0.01,
      'decay': 0.2,
      'sustain': 0.8,
      'release': 0.2
    },

    generateBuffer: function() {
      // Generate a 5 second white noise buffer.
      var lengthInSamples = 5 * this.context.sampleRate;
      this.buffer = this.context.createBuffer(1, lengthInSamples, this.context.sampleRate);
      var data = this.buffer.getChannelData(0);

      for (var i = 0; i < lengthInSamples; i++) {
        data[i] = 1;
      }
    },

    createSourceNode: function() {
      // Create a source node from the buffer.
      this.sourceNode = this.context.createBufferSource();
      this.sourceNode.buffer = this.buffer;
    },

    startSourceNode: function() {
      this.sourceNode.loop = true;
      this.sourceNode.start(0);
    },

    triggerAttack: function() {
      var now = this.context.currentTime;

      this.gainNode.gain.cancelScheduledValues(now);
      this.gainNode.gain.setTargetAtTime(this.get('maxValue'), now, this.get('attack'));

      this.gainNode.gain.setTargetAtTime(this.get('sustain'), now + this.get('attack'), this.get('decay'));
    },

    triggerRelease: function() {
      var now = this.context.currentTime;
      this.gainNode.gain.cancelScheduledValues(now);
      this.gainNode.gain.setTargetAtTime(0, now, this.get('release'));
    },

    connect: function(node) {
      this.gainNode.connect(node);
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/white_noise.js<|end_filename|>
(function() {
  'use strict';

  bs.models.WhiteNoise = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      
      this.generateWhiteNoiseBuffer();
      this.createSourceNode();
      this.startWhiteNoise();
    },

    generateWhiteNoiseBuffer: function() {
      // Generate a 5 second white noise buffer.
      var lengthInSamples = 5 * this.context.sampleRate;
      this.buffer = this.context.createBuffer(1, lengthInSamples, this.context.sampleRate);
      var data = this.buffer.getChannelData(0);

      for (var i = 0; i < lengthInSamples; i++) {
        data[i] = ((Math.random() * 2) - 1);
      }
    },

    createSourceNode: function() {
      // Create a source node from the buffer.
      this.sourceNode = this.context.createBufferSource();
      this.sourceNode.buffer = this.buffer;
    },

    startWhiteNoise: function() {
      this.sourceNode.loop = true;
      this.sourceNode.start(0);
    },

    connect: function(node) {
      this.sourceNode.connect(node);
    }
  });
})();


<|start_filename|>package.json<|end_filename|>
{
  "name": "synth",
  "version": "0.0.0",
  "description": "webaudio synthesizer",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "repository": {
    "type": "git",
    "url": "git://github.com/joshontheweb/synth.git"
  },
  "author": "joshontheweb",
  "license": "BSD",
  "dependencies": {
    "mime": "~1.2.9"
  }
}


<|start_filename|>media/styles/synth.css<|end_filename|>
body {
  font-family: helvetica, sans-serif;
}

.cf { /* from: http://nicolasgallagher.com/micro-clearfix-hack/ */
    zoom: 1;
}
.cf:before, .cf:after { content: ""; display: table; }
.cf:after { clear: both; }

a { 
  text-decoration: none; 
  background: #000;
  color: #fff;
  padding: 5px 10px;
  font-variant: small-caps; 
  text-transform: lowercase; 
  font-size: 15px;
  line-height: 14px;
}

h3 { font-variant: small-caps; text-transform: lowercase; font-size: 20px; line-height: 20px; }
h5 { margin: 0; text-transform: lowercase; font-variant: small-caps }

.error {
  color: red;
}

.popup {
  position: absolute;
  top: 300px;
  left: 770px;
  margin-left: -300px;
  border: 1px solid #000;
  padding: 15px;
  background: #fff;
  border-radius: 5px;
}

.popup .error {
  font-size: 12px;
}

.popup h5 {
  margin-bottom: 10px;
}

.patch-name {
  padding: 5px;
}

.patches select.patches {
  width: 100%;
}

.dashboard-top { overflow: hidden; clear: both; padding: 15px 15px 0 15px; }
.dashboard { position: relative; overflow: hidden; padding: 0 15px 15px 15px; }
.bottom { padding: 0 15px 15px 15px; position: relative; }

.column { height: 100%; float: left; margin-right: 15px; }
.row .block { float: left !important;}


.group {
  position: relative;
  border: 3px solid #000;
  border-radius: 15px;
  padding: 20px;
  margin-top: 25px;
}

.group h3:first-child {
  position: absolute;
  top: -34px;
  left: 15px;
  background: #fff;
  padding-left: 5px;
  padding-right: 5px;
}

.dashboard-top .group { height: 80px; margin-top: 0; }
.dashboard .group { height: 265px }

.block {
  float: left;
  text-align: center;
}

.oscillator-module .block {
  float: none;
}

.mixer .block { float: none; width: 70px; }
.mixer .noise-block { 
  position: relative;
  top: 45px;
  margin-left: -15px;
}

.reading {
  font-size: 10px;
  display: none;
}

.tempo-reading {
  display: block;
  font-size: 13px;
}

#oscilloscope {
  width: 515px;
  height: 150px;
  background: #222;
  float: left;
  margin-left: 5px;
}

#keyboard {
  float: left;
}

.buffer {
  height: 50px;
  width: 100%;
  overflow: hidden;
  position: relative;
  background: #222;
}

.buffer:before {
    content:"";
    position: absolute;
    z-index: -1;
    top: 0;
    bottom: 0;
    left: 50%;
    border-left: 1px solid #fff; /*change these values to suit your liking*/
    z-index: 100;
}

.buffer .upload {
  display: none;
  position: absolute;
  bottom: 5px;
  left: 5px;
  height: 16px;
  width: 16px;
  background: url('images/share.png');
  padding: 0;
  z-index: 100;
}

.buffer .delete {
  display: none;
  position: absolute;
  top: 0;
  left: 0;
  z-index: 100;
}

.buffer:hover .upload {
  display: block;
}
.buffer:hover .delete {
  display: block;
}

.metronome .beat-indicator {
  width: 15px;
  height: 15px;
}

.buffer .waveform { position: absolute; left: 50%; height: 50px; }
.buffer .waveform svg { position: absolute; left: 0; top: 0; }

.save-patch {
  display: block;
  margin-top: 18px;
}

.cv-patches-list {
  list-style-type: none;
  padding: 0;
  margin: 0;
}

.add-cv-patch {
  margin-top: 15px;
  float: left;
}

.cv-patch .gain {
  display: inline-block;
  position: relative;
  top: 3px;
}

.cv-patch .knob-wrapper {
  background: none;
  height: 16px;
  width: 16px;
  padding: 0;
}

.cv-patch .knob-wrapper .knob {
  height: 15px;
  width: 15px;
}

.knob {
  width: 30px;
  height: 30px;
  border: 1px solid #000;
  border-radius: 50%;
  background: #000;
}

.mini-knob .knob {
  height: 15px;
  width: 15px;
}

.knob .indicator {
  width: 1px;
  height: 20px;
  margin: 0 auto;
  border-top: 10px solid #fff;
}

.mini-knob .indicator {
  height: 5px;
  height: 11px;
  border-top-width: 5px;
}

.unselectable {
  -moz-user-select: none;
  -khtml-user-select: none;
  -webkit-user-select: none;
  user-select: none;
}

/* in order to proctect the keyboard from unwanted mouseup events */
.unselectable .bottom:after {
  content: "";
/*  background: yellow;*/
  position: absolute;
  height: 150px;
  width: 600px;
  
}

.frequency-block {
  margin-right: 10px;
}

.filter-frequency-block {
  margin-right: 25px;
}

.metronome .beat-indicator {
  float: right;
  margin-bottom: 20px;
}

.metronome .block {
  margin-right: 20px;
}

.metronome .block:last-child {
  margin-right: 0;
}


.metronome .on-off {
  clear: both;
}

.metronome .on-off .state {
  margin-right: 0;
}

.delay .block:first-child {
  margin-right: 20px;
}

.filter-envelope input, .amp-envelope input {
  margin-top: 15px;
  margin-bottom: 30px;
}

.knob-wrapper {
  height: 32px;
  width: 32px;
  padding: 20px;
  background: url('images/notches.png') -1px 1px no-repeat
}

.mini-knob .knob-wrapper {
  background: none;
  height: 16px;
  width: 16px;
  padding: 0;
}

.waveform-knob-wrapper {
  background: url('images/waveforms.png') no-repeat;
}

input[type=range] {
    -webkit-appearance: none;
    background-color: #555;
    height: 3px;
    border: 1px solid black;
}

input[type="range"]::-webkit-slider-thumb {
     -webkit-appearance: none;
    background-color: #fff;
    border: 5px solid #000;
    border-top: 1px solid #000;
    border-bottom: 1px solid #000;
    width: 11px;
    height: 26px;
}

select {
  -webkit-appearance: none;
  border: 1px solid black;
  background: #fff url('images/downarrow.png') right 6px top 5px no-repeat;
  padding: 5px 25px 5px 5px;
  font-variant: small-caps; 
  text-transform: lowercase; 
  font-size: 14px;
  line-height: 14px;
  margin-left: 0;
}

.loop-module {
  height: 100%;
  overflow: scroll;
}

.loop-module .microphone {
  position: absolute;
  height: 16px;
  width: 16px;
  top: 5px;
  right: 5px;
  background: url('images/mic.png');
  opacity: .5;
  padding: 0;
}

.loop-module .record {
  background: red;
  position: absolute;
  left: 25px;
  bottom: 25px;
}

.loop-module .microphone.active {
  opacity: 1;
}

.loop-module-group {
  width: 224px;
}



<|start_filename|>media/scripts/mvc/views/loop_module_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.LoopModuleView = Backbone.View.extend({
    initialize: function() {
      var view = this;
      $(window).on('keydown', function(e) {
        view.handleKeydown(e);
      });

      this.listenTo(this.model.buffers, 'add', this.insertBuffer);
      this.listenTo(this.model.buffers, 'remove', this.unscheduleEvent);
    },

    events: {
      'click .microphone': 'toggleMicrophone',
      'click .record': 'toggleRecord'
    },

    template: _.template($('.loop-module-template').html()),

    toggleRecord: function(e) {
      e.preventDefault();
      if (this.model.recording) {
        this.model.stopRecording();
        this.$record.text('Record Loop');
      } else {
        this.model.startRecording();
        this.$record.text('Stop Recording');
      }
    },

    handleKeydown: function(e) {
      if (e.keyCode == 192) { // ~
        this.toggleRecord(e);
      }

      if (e.keyCode >= 49 && e.keyCode <= 58) {
        switch (e.keyCode) {
          case 49:  // 1
            this.model.toggleBufferVolume(0);
            break;
          case 50:  // 2
            this.model.toggleBufferVolume(1);
            break;
          case 51:  // 3
            this.model.toggleBufferVolume(2);
            break;
          case 52:  // 4
            this.model.toggleBufferVolume(3);
            break;
          case 53:  // 5
            this.model.toggleBufferVolume(4);
            break;
          case 54:  // 6
            this.model.toggleBufferVolume(5);
            break;
          case 55:  // 7
            this.model.toggleBufferVolume(6);
            break;
          case 56:  // 8
            this.model.toggleBufferVolume(7);
            break;
          case 57:  // 9
            this.model.toggleBufferVolume(8);
            break;
          case 58:  // 0
            this.model.toggleBufferVolume(9);
            break;
        }
      } 
    },

    toggleMicrophone: function() {
      if (this.model.micOn) {
        this.model.stopMicrophone();
        this.$microphone.removeClass('active');
      } else {
        this.model.startMicrophone();    
        this.$microphone.addClass('active');
      }
    },

    insertBuffer: function(buffer) {
      this.$info.empty();
      var bufferView = new bs.views.BufferView({model: buffer});
      this.$el.prepend(bufferView.render().el);
      bufferView.readyToDraw = true;
    },

    unscheduleEvent: function(model) {
      var unscheduled = _.find(this.model.scheduled, function(ev) {
        return ev.id === model.cid;
      });
      if (unscheduled) {
        this.model.scheduled.splice(this.model.scheduled.indexOf(unscheduled), 1);
      }
    },
    
    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      this.$info = this.$('.info');
      this.$record = this.$('.record');
      this.$microphone = this.$('.microphone');
      return this;
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/filter.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Filter = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.preNode = this.context.createBiquadFilter();
      this.preNode.type = this.filterMap[attrs.type];
      this.preNode.frequency.value = this.get('frequency');
      this.preNode.frequency.q = this.get('quality');

      this.postNode = this.context.createBiquadFilter();
      this.postNode.type = this.filterMap[attrs.type];
      this.postNode.frequency.value = 0;
      this.postNode.frequency.q = this.get('quality');

      this.preNode.connect(this.postNode);

      this.on('change:frequency', this.frequencyChange);
      this.on('change:resonance', this.resonanceChange);
    },

    filterMap: {
      'lowpass': 0
    },
    
    defaults: {
      min: 20,
      max: 20000,
      frequency: 20000,
      resonance: 1
    },

    frequencyChange: function(filter, freq) {
      this.preNode.frequency.value = freq;
    },
    
    resonanceChange: function(filter, resonance) {
      this.preNode.Q.value = resonance;
      this.postNode.Q.value = resonance;
    },

    connect: function(node) {
      this.postNode.connect(node);
    }
    
  });
  
})();


<|start_filename|>media/scripts/libs/wavyjones.js<|end_filename|>
var WavyJones = function (context, elem) {
	var analyser = context.createAnalyser();

	analyser.width = document.getElementById(elem).offsetWidth;
	analyser.height = document.getElementById(elem).offsetHeight;
	analyser.lineColor = '#70db49';
	analyser.lineThickness = 5;

    var paper = Raphael(elem, analyser.width, analyser.height),
        oscLine = paper.path([['M', 0, analyser.height/2], ['L', analyser.width, analyser.height/2], 'Z']),
        noDataPoints = 10,
		freqData = new Uint8Array(analyser.frequencyBinCount);

    oscLine.attr({stroke: analyser.lineColor, 'stroke-width': analyser.lineThickness});

    var drawLine = function () {
        analyser.getByteTimeDomainData(freqData);

        var graphPoints = [],
            graphStr = '';

        graphPoints.push('M0, ' + (analyser.height/2));

        for (var i = 0; i < freqData.length; i++) {
            if (i % noDataPoints) {
                var point = (freqData[i] / 128) * (analyser.height / 2);
                graphPoints.push('L' + i + ', ' + point); 
            }
        }

        for (i = 0; i < graphPoints.length; i++) {
            graphStr += graphPoints[i];
        }

        oscLine.attr('stroke', analyser.lineColor);
        oscLine.attr('stroke-width', analyser.lineThickness);
        oscLine.attr('path', graphStr);

        setTimeout(drawLine, 30);
    };

    drawLine();

    return analyser;
};


<|start_filename|>media/scripts/mvc/models/buffer.js<|end_filename|>
(function() {
  'use strict';

  bs.models.Buffer = Backbone.Model.extend({
    initialize: function(attrs, options) {
      this.context = options.context;
      this.bufferNode = attrs.bufferNode || this.context.createBuffer(2, 4096, this.context.sampleRate);
      this.gain = this.context.createGain();
      this.on('change:gain', this.gainChange);
      this.on('change:bufferNode', this.bufferNodeChange);

      this.listenTo(synth.metronome, 'change:tempo', this.updateSourcePlaybackRate);
    },

    defaults: {
      'gain': 1
    },

    uploadBuffer: function(name) {
      var formData = new FormData();

      formData.append('apiKey', <KEY>');
      formData.append('projectID', null);
      formData.append('track', JSON.stringify(new bs.models.Loop({bpm: this.get('tempo')}).toJSON()));
      formData.append( 'audio', this.get('wav'), name + '.wav');

      var xhr = new XMLHttpRequest();
      xhr.open('POST', 'http://soundkeep.com/upload', true);
      xhr.withCredentials = true;
      xhr.onload = function(e) {
        console.log('hooray', e);
      };

      // Listen to the upload progress.
      // var progressBar = document.querySelector('progress');
      xhr.upload.onprogress = function(e) {
        if (e.lengthComputable) {
          console.log('progress', e);
          // progressBar.value = (e.loaded / e.total) * 100;
          // progressBar.textContent = progressBar.value; // Fallback for unsupported browsers.
        }
      }

      xhr.send(formData);
    },

    bufferNodeChange: function(model, bufferNode) {
      this.bufferNode = bufferNode;
    },

    gainChange: function(model, gain) {
      this.gain.value = gain;
    },

    updateSourcePlaybackRate: function() {
      var source = this.bufferNode.source;
      console.log(source);
      console.log('tempo', synth.metronome.get('tempo'), 'buffer tempo', this.get('tempo'))
      source.playbackRate.value = synth.metronome.get('tempo') / this.get('tempo');
    }
  });
})();


<|start_filename|>media/scripts/mvc/models/knob.js<|end_filename|>
(function() {
  bs.models.Knob = Backbone.Model.extend({
    defaults: {
      max: 20000,
      min: 0,
      step: 1,
      value: 0,
      decimalPlace: 1,
      startDegree: 0
    }
  });
})();


<|start_filename|>media/scripts/mvc/views/cv_patch_module_view.js<|end_filename|>
(function() {
  'use strict';

  bs.views.CVPatchModuleView = Backbone.View.extend({
    initialize: function() {
      this.listenTo(this.model.cvPatches, 'add', this.insertPatch);
      this.listenTo(this.model.cvPatches, 'reset', this.render);
    },

    template: _.template($('.cv-patches-template').html()),

    events: {
      'click .add-cv-patch': 'handleAddPatchClick'
    },

    handleAddPatchClick: function(e) {
      e.preventDefault();
      this.model.cvPatches.add({});
    },

    insertPatch: function(patch) {
      var patchView = new bs.views.CVPatchView({model: patch});
      this.$patchesList.append(patchView.render().el);
    },

    render: function() {
      var view = this;
      this.$el.html(this.template());
      this.$patchesList = this.$('.cv-patches-list')

      this.model.cvPatches.each(function(patch) {
        view.insertPatch(patch);
      });
      
      return this;
    }
  });
})();
