addon/classes/sampler.js

import Ember from 'ember';

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

const {
  on,
  Object: EmberObject
} = Ember;

/**
 * An instance of the Sampler class behaves just like a Sound, but allows
 * many {{#crossLink "AudioBuffer"}}AudioBuffers{{/crossLink}} to exist and
 * automatically alternately plays them (round-robin) each time any of the play
 * methods are called.
 *
 * @public
 * @class Sampler
 *
 * @todo humanize gain and time - should be optional and customizable
 * @todo loop
 */
const Sampler = EmberObject.extend({

  /**
   * Determines the gain applied to each sample.
   *
   * @public
   * @property gain
   * @type {number}
   * @default 1
   */
  gain: 1,

  /**
   * Determines the stereo pan position of each sample.
   *
   * @public
   * @property pan
   * @type {number}
   * @default 0
   */
  pan: 0,

  /**
   * Temporary storage for the iterable that comes from the sounds Set.
   * This iterable is meant to be replaced with a new copy every time it reaches
   * it's end, resulting in an infinite stream of Sound instances.
   *
   * @private
   * @property _soundIterator
   * @type {Iterator}
   *
   */
  _soundIterator: null,

  /**
   * Acts as a register for loaded audio sources. Audio sources can be anything
   * that uses {{#crossLink "Playable"}}{{/crossLink}}. If not set on
   * instantiation, automatically set to `new Set()` via `_initSounds`.
   *
   * @public
   * @property sounds
   * @type {set}
   */
  sounds: null,

  /**
   * Gets the next audio source and plays it immediately.
   *
   * @public
   * @method play
   */
  play() {
    this._getNextSound().play();
  },

  /**
   * Gets the next Sound and plays it after the specified offset has elapsed.
   *
   * @public
   * @method playIn
   *
   * @param {number} seconds Number of seconds from "now" that the next Sound
   * should be played.
   */
  playIn(seconds) {
    this._getNextSound().playIn(seconds);
  },

  /**
   * Gets the next Sound and plays it at the specified moment in time. A
   * "moment in time" is measured in seconds from the moment that the
   * {{#crossLink "AudioContext"}}{{/crossLink}} was instantiated.
   *
   * @param {number} time The moment in time (in seconds, relative to the
   * {{#crossLink "AudioContext"}}AudioContext's{{/crossLink}} "beginning of
   * time") when the next Sound should be played.
   *
   * @public
   * @method playAt
   */
  playAt(time) {
    this._getNextSound().playAt(time);
  },

  /**
   * Gets _soundIterator and returns it's next value. If _soundIterator has
   * reached it's end, replaces _soundIterator with a fresh copy from sounds
   * and returns the first value from that.
   *
   * @private
   * @method _getNextSound
   * @return {Sound}
   */
  _getNextSound() {
    let soundIterator = this.get('_soundIterator');
    let nextSound;

    if (!soundIterator) {
      soundIterator = this.get('sounds').values();
    }

    nextSound = soundIterator.next();

    if (nextSound.done) {
      soundIterator = this.get('sounds').values();
      nextSound = soundIterator.next();
    }

    this.set('_soundIterator', soundIterator);

    return this._setGainAndPan(nextSound.value);
  },

  /**
   * Applies the `gain` and `pan` properties from the Sampler instance to a
   * Sound instance and returns the Sound instance.
   *
   * @private
   * @method _setGainAndPan
   * @return {Sound} The input sound after having it's gain and pan set
   */
  _setGainAndPan(sound) {
    sound.changeGainTo(this.get('gain')).from('ratio');
    sound.changePanTo(this.get('pan'));

    return sound;
  },

  /**
   * Sets `sounds` to `new Set()` if null on instantiation.
   *
   * @private
   * @method _initSounds
   */
  _initSounds: on('init', function() {
    if (!this.get('sounds')) {
      this.set('sounds', new Set());
    }
  })
});

export default Sampler;