addon/classes/beat-track.js

import Ember from 'ember';
import Beat from './beat';
import Sampler from './sampler';

const beatBank = new WeakMap();

/**
 * Provides classes that are capable of interacting with the Web Audio API's
 * AudioContext.
 *
 * @public
 * @module Audio
 */

const {
  computed
} = Ember;

/**
 * An instance of this class has a single "sound" (comprised of one or multiple
 * audio sources) but provides methods to play that sound repeatedly, mixed with
 * "rests," in a rhythmic way. An instance of this class behaves very similarly
 * to a "lane" on a drum machine.
 *
 * @public
 * @class BeatTrack
 * @extends Sampler
 *
 * @todo need a way to stop a BeatTrack once it's started. Maybe by creating
 * the times in advance and not calling play until it's the next beat in the
 * queue?
 */
const BeatTrack = Sampler.extend({

  /**
   * Determines the number of beats in a BeatTrack instance.
   *
   * @public
   * @property numBeats
   * @type {number}
   */
  numBeats: 4,

  /**
   * If specified, Determines length of time, in milliseconds, before isPlaying
   * and currentTimeIsPlaying are automatically switched back to false after
   * having been switched to true for each beat. 100ms is used by default.
   *
   * @public
   * @property duration
   * @type {number}
   * @default 100
   */
  duration: 100,

  /**
   * Computed property. An array of Beat instances. The number of Beat instances
   * in the array is always the same as the `numBeats` property. If 'numBeats'
   * or duration changes. This property will be recomputed, but any beats that
   * previously existed are reused so that they will maintain their `active`
   * state.
   *
   * @public
   * @property beats
   * @type {array|Beat}
   */
  beats: computed('numBeats', 'duration', function() {
    let beats = [];
    let numBeats = this.get('numBeats');
    let existingBeats;

    if (beatBank.has(this)) {
      existingBeats = beatBank.get(this);
      numBeats = numBeats - existingBeats.length;
    }

    for (let i = 0; i < numBeats; i++) {
      const beat = Beat.create({
        duration: this.get('duration'),
        _parentPlayIn: this.playIn.bind(this),
        _parentPlay: this.play.bind(this)
      });

      beats.push(beat);
    }

    if (existingBeats) {
      beats = existingBeats.concat(beats);
    }

    beatBank.set(this, beats);

    return beats;
  }),

  /**
   * Calls play on all Beat instances in the beats array.
   *
   * @public
   * @method playBeats
   *
   * @param {number} bpm The tempo at which the beats should be played.
   *
   * @param noteType {number} The (rhythmic) length of each beat. Fractions
   * are suggested here so that it's easy to reason about. For example, for
   * eighth notes, pass in `1/8`.
   */
  playBeats(bpm, noteType) {
    this._callPlayMethodOnBeats('playIn', bpm, noteType);
  },

  /**
   * Calls play on `active` Beat instances in the beats array. Any beat that
   * is not marked active is effectively a "rest".
   *
   * @public
   * @method playActiveBeats
   *
   * @param {number} bpm The tempo at which the beats and rests should be played.
   *
   * @param noteType {number} The (rhythmic) length of each beat/rest. Fractions
   * are suggested here so that it's easy to reason about. For example, for
   * eighth notes, pass in `1/8`.
   */
  playActiveBeats(bpm, noteType) {
    this._callPlayMethodOnBeats('ifActivePlayIn', bpm, noteType);
  },

  /**
   * The underlying method behind playBeats and playActiveBeats.
   *
   * @private
   * @method _callPlayMethodOnBeats
   *
   * @param {string} method The method that should be called on each beat.
   *
   * @param {number} bpm The tempo that should be used to calculate the length
   * of a beat/rest.
   *
   * @param noteType {number} The (rhythmic) length of each beat/rest that should
   * be used to calculate the length of a beat/rest in seconds.
   */
  _callPlayMethodOnBeats(method, bpm, noteType=1 / 4) {
    // http://bradthemad.org/guitar/tempo_explanation.php
    const duration = (240 * noteType) / bpm;
    this.get('beats').map((beat, idx) => beat[method](idx * duration));
  }
});

export default BeatTrack;