VolumeTransformer.js 3.76 KB
// Based on discord.js' old volume system

const { Transform } = require('stream');

/**
 * Transforms a stream of PCM volume.
 * @memberof core
 * @extends TransformStream
 */
class VolumeTransformer extends Transform {
  /**
   * @memberof core
   * @param {Object} options Any optional TransformStream options plus some extra:
   * @param {string} options.type The type of transformer: s16le (signed 16-bit little-endian), s16be, s32le, s32be
   * @param {number} [options.volume=1] The output volume of the stream
   * @example
   * // Half the volume of a signed 16-bit little-endian PCM stream
   * input
   *  .pipe(new prism.VolumeTransformer({ type: 's16le', volume: 0.5 }))
   *  .pipe(writeStream);
   */
  constructor(options = {}) {
    super(options);
    switch (options.type) {
      case 's16le':
        this._readInt = (buffer, index) => buffer.readInt16LE(index);
        this._writeInt = (buffer, int, index) => buffer.writeInt16LE(int, index);
        this._bits = 16;
        break;
      case 's16be':
        this._readInt = (buffer, index) => buffer.readInt16BE(index);
        this._writeInt = (buffer, int, index) => buffer.writeInt16BE(int, index);
        this._bits = 16;
        break;
      case 's32le':
        this._readInt = (buffer, index) => buffer.readInt32LE(index);
        this._writeInt = (buffer, int, index) => buffer.writeInt32LE(int, index);
        this._bits = 32;
        break;
      case 's32be':
        this._readInt = (buffer, index) => buffer.readInt32BE(index);
        this._writeInt = (buffer, int, index) => buffer.writeInt32BE(int, index);
        this._bits = 32;
        break;
      default:
        throw new Error('VolumeTransformer type should be one of s16le, s16be, s32le, s32be');
    }
    this._bytes = this._bits / 8;
    this._extremum = Math.pow(2, this._bits - 1);
    this.volume = typeof options.volume === 'undefined' ? 1 : options.volume;
    this._chunk = Buffer.alloc(0);
  }

  _readInt(buffer, index) { return index; }
  _writeInt(buffer, int, index) { return index; }

  _transform(chunk, encoding, done) {
    // If the volume is 1, act like a passthrough stream
    if (this.volume === 1) {
      this.push(chunk);
      return done();
    }

    const { _bytes, _extremum } = this;

    chunk = this._chunk = Buffer.concat([this._chunk, chunk]);
    if (chunk.length < _bytes) return done();

    const complete = Math.floor(chunk.length / _bytes) * _bytes;

    for (let i = 0; i < complete; i += _bytes) {
      const int = Math.min(_extremum - 1, Math.max(-_extremum, Math.floor(this.volume * this._readInt(chunk, i))));
      this._writeInt(chunk, int, i);
    }

    this._chunk = chunk.slice(complete);
    this.push(chunk.slice(0, complete));
    return done();
  }

  _destroy(err, cb) {
    super._destroy(err, cb);
    this._chunk = null;
  }

  /**
   * Sets the volume relative to the input stream - i.e. 1 is normal, 0.5 is half, 2 is double.
   * @param {number} volume The volume that you want to set
   */
  setVolume(volume) {
    this.volume = volume;
  }

  /**
   * Sets the volume in decibels.
   * @param {number} db The decibels
   */
  setVolumeDecibels(db) {
    this.setVolume(Math.pow(10, db / 20));
  }

  /**
   * Sets the volume so that a perceived value of 0.5 is half the perceived volume etc.
   * @param {number} value The value for the volume
   */
  setVolumeLogarithmic(value) {
    this.setVolume(Math.pow(value, 1.660964));
  }

  /**
   * The current volume of the stream in decibels
   * @readonly
   * @type {number}
   */
  get volumeDecibels() {
    return Math.log10(this.volume) * 20;
  }
  /**
   * The current volume of the stream from a logarithmic scale
   * @readonly
   * @type {number}
   */
  get volumeLogarithmic() {
    return Math.pow(this.volume, 1 / 1.660964);
  }
}

module.exports = VolumeTransformer;