Home Reference Source Repository

src/audio/Source.js

/**
 * @fileOverview Source for binaural processing.
 * @author Jean-Philippe.Lambert@ircam.fr
 * @copyright 2016 IRCAM, Paris, France
 * @license BSD-3-Clause
 */

/**
 * Single source.
 *
 * @see {@link BinauralPanner}
 */
export class Source {

  /**
   * Construct a source, with and AudioContext and an HrtfSet.
   *
   * @see {@link HrtfSet}
   *
   * @param {Object} options
   * @param {AudioContext} options.audioContext mandatory for the creation
   * of FIR audio buffers
   * @param {HrtfSet} options.hrtfSet {@link Source#hrtfSet}
   * @param {coordinate} [options.position=[0,0,0]] in 'gl' coordinate system.
   * {@link Source#position}
   * @param {Number} [options.crossfadeDuration] in seconds
   * {@link Source#crossfadeDuration}
   */
  constructor(options = {}) {
    this._audioContext = options.audioContext;
    this._hrtfSet = options.hrtfSet;

    this._convolverCurrent = this._audioContext.createConvolver();
    this._convolverCurrent.normalize = false;

    this._gainCurrent = this._audioContext.createGain();
    this._convolverCurrent.connect(this._gainCurrent);

    this._convolverNext = this._audioContext.createConvolver();
    this._convolverNext.normalize = false;

    this._gainNext = this._audioContext.createGain();
    this._convolverNext.connect(this._gainNext);

    this.crossfadeDuration = options.crossfadeDuration;

    this._crossfadeAfterTime = this._audioContext.currentTime;
    this._crossfadeTimeout = undefined;

    // set position when everything is ready
    if (typeof options.position !== 'undefined') {
      this.position = options.position;
    }
  }

  // ----------- accessors

  /**
   * Set the crossfade duration when the position changes.
   *
   * @param {Number} [duration=0.02] in seconds
   */
  set crossfadeDuration(duration = 0.02) {
    this._crossfadeDuration = duration;
  }

  /**
   * Get the crossfade duration when the position changes.
   *
   * @returns {Number} in seconds
   */
  get crossfadeDuration() {
    return this._crossfadeDuration;
  }

  /**
   * Refer an external HRTF set.
   *
   * @param {HrtfSet} hrtfSet
   */
  set hrtfSet(hrtfSet) {
    this._hrtfSet = hrtfSet;
  }

  /**
   * Get the HrtfSet.
   *
   * @returns {HrtfSet}
   */
  get hrtfSet() {
    return this._hrtfSet;
  }

  /**
   * Set the position of the source and updates.
   *
   * @param {Coordinates} positionRequest
   */
  set position(positionRequest) {
    clearTimeout(this._crossfadeTimeout);
    let now = this._audioContext.currentTime;
    if (now >= this._crossfadeAfterTime) {
      // swap
      let tmp = this._convolverCurrent;
      this._convolverCurrent = this._convolverNext;
      this._convolverNext = tmp;

      tmp = this._gainCurrent;
      this._gainCurrent = this._gainNext;
      this._gainNext = tmp;

      this._convolverNext.buffer = this._hrtfSet.nearestFir(positionRequest);

      // reschedule after setting the buffer, as it may take time
      // (currentTime updates at least on Chrome 48)
      now = this._audioContext.currentTime;
      this._crossfadeAfterTime = now + this._crossfadeDuration;

      // fade in next
      this._gainNext.gain.cancelScheduledValues(now);
      this._gainNext.gain.setValueAtTime(0, now);
      this._gainNext.gain.linearRampToValueAtTime(
        1, now + this._crossfadeDuration);

      // fade out current
      this._gainCurrent.gain.cancelScheduledValues(now);
      this._gainCurrent.gain.setValueAtTime(1, now);
      this._gainCurrent.gain.linearRampToValueAtTime(
        0, now + this._crossfadeDuration);
    } else {
      // re-schedule later
      this._crossfadeTimeout = setTimeout( () => {
        this.position = positionRequest;
      }, 0.02);
    }

  }

  // ----------- public methods

  /**
   * Connect the input of a source.
   *
   * @param {(AudioNode|Array.<AudioNode>)} nodesToConnect
   * @param {Number} [output=0] output to connect from
   * @param {Number} [input=0] input to connect to
   * @returns {this}
   */
  connectInput(nodesToConnect, output, input) {
    const nodes = (Array.isArray(nodesToConnect)
                   ? nodesToConnect
                   : [nodesToConnect] ); // make array

    nodes.forEach( (node) => {
      node.connect(this._convolverCurrent, output, input);
      node.connect(this._convolverNext, output, input);
    });

    return this;
  }

  /**
   * Disconnect the input of a source.
   *
   * @param {(AudioNode|Array.<AudioNode>)} nodesToDisconnect disconnect
   * all when undefined.
   * @returns {this}
   */
  disconnectInput(nodesToDisconnect) {
    const nodes = (Array.isArray(nodesToDisconnect)
                   ? nodesToDisconnect
                   : [nodesToDisconnect] ); // make array

    nodes.forEach( (node) => {
      node.disconnect(this._convolverCurrent);
      node.disconnect(this._convolverNext);
    });

    return this;
  }

  /**
   * Connect the output of a source.
   *
   * @param {(AudioNode|Array.<AudioNode>)} nodesToConnect
   * @param {Number} [output=0] output to connect from
   * @param {Number} [input=0] input to connect to
   * @returns {this}
   */
  connectOutput(nodesToConnect, output, input) {
    const nodes = (Array.isArray(nodesToConnect)
                   ? nodesToConnect
                   : [nodesToConnect] ); // make array

    nodes.forEach( (node) => {
      this._gainCurrent.connect(node, output, input);
      this._gainNext.connect(node, output, input);
    });

    return this;
  }

  /**
   * Disconnect the output of a source.
   *
   * @param {(AudioNode|Array.<AudioNode>)} nodesToDisconnect disconnect
   * all when undefined.
   * @returns {this}
   */
  disconnectOutput(nodesToDisconnect) {
    if (typeof nodesToDisconnect === 'undefined') {
      // disconnect all
      this._gainCurrent.disconnect();
      this._gainNext.disconnect();
    } else {
      const nodes = (Array.isArray(nodesToDisconnect)
                     ? nodesToDisconnect
                     : [nodesToDisconnect] ); // make array

      nodes.forEach( (node) => {
        this._gainCurrent.disconnect(node);
        this._gainNext.disconnect(node);
      });
    }

    return this;
  }

}

export default Source;