adamrenklint/bap

View on GitHub
lib/Sample.js

Summary

Maintainability
A
3 hrs
Test Coverage
F
23%
var Layer = require('./Layer');
var Kit = require('./Kit');
var Params = require('./Params');
var BufferHelper = require('./BufferHelper');
var sampleParams = require('./mixins/sampleParams');
var uniqueId = require('./utils/uniqueId');
var slotIds = 'QWERTYUIOPASDFGHJKLZXCVBNM';

var buffers = {};
var loading = {};

var Sample = Layer.extend({

  type: 'sample',

  props: {
    loaded: ['boolean', true, false],
    sliceExpression: 'string',
    buffer: 'any'
  },

  constructor: function (src, params, options) {
    if (typeof src === 'string') {
      params = params || {};
      params.src = src;
    }
    else {
      options = params;
      params = src;
    }
    Layer.call(this, params, options);
  },

  initialize: function (src, options) {
    Layer.prototype.initialize.call(this, src, options);
    this.on('change:src', this._loadSample);
    this.on('change:loaded', this._configureSlice);
    this.on('change:sliceExpression', this._configureSlice);
    this.src && this._loadSample();
    this._configureSlice();
  },

  _getSlotId: function (index) {
    if (index < slotIds.length) {
      return slotIds[index];
    }
    else {
      var cycles = Math.floor(index / slotIds.length);
      index -= (cycles * slotIds.length);
      return slotIds[cycles - 1] + slotIds[index];
    }
  },

  slice: function (pieces) {
    // not clean, but I'm not sure of another way of solving this circular dependency
    var kit = new this.vent.bap.kit();
    for (var i = 0; i < pieces; i++) {
      kit.slot(this._getSlotId(i)).layer(this.with({
        sliceExpression: '' + (i + 1) + '/' + pieces
      }));
    }
    return kit;
  },

  _params: function () {
    var params = Layer.prototype._params.apply(this, arguments);

    params.length = params.length || this.buffer && (this.buffer.duration - params.offset) || 0;

    var playbackRate = params.playbackRate = this._getPlaybackRate(params);

    if (!params.hasLengthFromDuration) {
      var adjustedLength = params.length / playbackRate;
      var durationLength = params.duration && Params.lengthFromDuration(params, params.duration);

      if (durationLength && durationLength < adjustedLength) {
        params.hasLengthFromDuration = true;
        params.length = durationLength;
      }
    }

    return params;
  },

  _source: function (params) {
    if (!this.src) {
      throw new Error('Cannot start sample without source url');
    }
    if (!this.buffer) {
      throw new Error('Cannot start sample without loaded buffer');
    }

    var buffer = BufferHelper.makeBuffer(params, this.buffer, this.context);

    if (buffer) {
      var source = this.context.createBufferSource();
      source.buffer = buffer;

      source.playbackRate.value = params.playbackRate;

      if (params.loop > 0) {
        source.loop = true;
        source.loopStart = params.offset;
        source.loopEnd = params.offset + params.loop;
      }

      return source;
    }
  },

  _getStopTime: function (time, params, source) {
    var duration = params.hasLengthFromDuration ? source.buffer.duration : source.buffer.duration / params.playbackRate;
    return time + duration;
  },

  _getPlaybackRate: function (params) {
    return Math.pow(2, params.pitch / 12);
  },

  _loadSample: function (countLoading) {
    var buffer = buffers[this.src];
    this.context.resume();
    if (!buffer) {
      this.loaded = false;

      if (loading[this.src]) {
        if (countLoading !== false) {
          loading[this.src]++;
        }
        return setTimeout(this._loadSample.bind(this, false), 10);
      }

      this.vent.trigger('loadingState:add', this.src);
      loading[this.src] = 1;

      var request = new XMLHttpRequest();
      request.open('GET', this.src, true);
      request.responseType = 'arraybuffer';
      request.onload = function soundWasLoaded () {
        this.context.decodeAudioData(request.response, function (buffer) {
          buffer.uid = uniqueId('buffer');
          this.buffer = buffers[this.src] = buffer;
          loading[this.src]--;
          this.loaded = true;
          if (!loading[this.src]) {
            this.vent.trigger('loadingState:remove', this.src);
          }
          request.onload = null;
        }.bind(this));
      }.bind(this);
      request.send();
    }
    else {
      this.buffer = buffer;
      loading[this.src]--;
      this.loaded = true;
      if (!loading[this.src]) {
        this.vent.trigger('loadingState:remove', this.src);
      }
    }
  },

  _configureRelease: function (time, params, gain) {
    if (params.trimToZeroCrossingPoint) {
      time -= BufferHelper.silentTailSize / 44100;
    }
    Layer.prototype._configureRelease.call(this, time, params, gain);
  },

  _configureSlice: function () {
    if (this.buffer && this.loaded && this.sliceExpression) {
      var length = this.buffer.duration;
      var parts = this.sliceExpression.split('/');
      var index = parseInt(parts[0], 10) - 1;
      var total = parseInt(parts[1], 10);
      var sliceLength = length / total;
      var offset = index * sliceLength;

      this.length = sliceLength;
      this.offset = offset;
    }
  }
}, sampleParams);

module.exports = Sample;