Source: common/operator/Mel.js

import BaseLfo from '../../core/BaseLfo';

const min = Math.min;
const max = Math.max;
const pow = Math.pow;
const log10 = Math.log10;

function hertzToMelHtk(freqHz) {
  return 2595 * Math.log10(1 + (freqHz / 700));
}

function melToHertzHtk(freqMel) {
  return 700 * (Math.pow(10, freqMel / 2595) - 1);
}

/**
 * Returns a description of the weights to apply on the fft bins for each
 * Mel band filter.
 * @note - adapted from imtr-tools/rta
 *
 * @param {Number} nbrBins - Number of fft bins.
 * @param {Number} nbrFilter - Number of mel filters.
 * @param {Number} sampleRate - Sample Rate of the signal.
 * @param {Number} minFreq - Minimum Frequency to be considerered.
 * @param {Number} maxFreq - Maximum frequency to consider.
 * @return {Array<Object>} - Description of the weights to apply on the bins for
 *  each mel filter. Each description has the following structure:
 *  { startIndex: binIndex, centerFreq: binCenterFrequency, weights: [] }
 *
 * @private
 */
function getMelBandWeights(nbrBins, nbrBands, sampleRate, minFreq, maxFreq, type = 'htk') {

  let hertzToMel = null;
  let melToHertz = null;
  let minMel;
  let maxMel;

  if (type === 'htk') {
    hertzToMel = hertzToMelHtk;
    melToHertz = melToHertzHtk;
    minMel = hertzToMel(minFreq);
    maxMel = hertzToMel(maxFreq);
  } else {
    throw new Error(`Invalid mel band type: "${type}"`);
  }

  const melBandDescriptions = new Array(nbrBands);
  // center frequencies of Fft bins
  const fftFreqs = new Float32Array(nbrBins);
  // center frequencies of mel bands - uniformly spaced in mel domain between
  // limits, there are 2 more frequencies than the actual number of filters in
  // order to calculate the slopes
  const filterFreqs = new Float32Array(nbrBands + 2);

  const fftSize = (nbrBins - 1) * 2;
  // compute bins center frequencies
  for (let i = 0; i < nbrBins; i++)
    fftFreqs[i] = sampleRate * i / fftSize;

  for (let i = 0; i < nbrBands + 2; i++)
    filterFreqs[i] = melToHertz(minMel + i / (nbrBands + 1) * (maxMel - minMel));

  // loop throught filters
  for (let i = 0; i < nbrBands; i++) {
    let minWeightIndexDefined = 0;

    const description = {
      startIndex: null,
      centerFreq: null,
      weights: [],
    }

    // define contribution of each bin for the filter at index (i + 1)
    // do not process the last spectrum component (Nyquist)
    for (let j = 0; j < nbrBins - 1; j++) {
      const posSlopeContrib = (fftFreqs[j] - filterFreqs[i]) /
                              (filterFreqs[i+1] - filterFreqs[i]);

      const negSlopeContrib = (filterFreqs[i+2] - fftFreqs[j]) /
                              (filterFreqs[i+2] - filterFreqs[i+1]);
      // lowerSlope and upper slope intersect at zero and with each other
      const contribution = max(0, min(posSlopeContrib, negSlopeContrib));

      if (contribution > 0) {
        if (description.startIndex === null) {
          description.startIndex = j;
          description.centerFreq = filterFreqs[i+1];
        }

        description.weights.push(contribution);
      }
    }

    // empty filter
    if (description.startIndex === null) {
      description.startIndex = 0;
      description.centerFreq = 0;
    }

    // @todo - do some scaling for Slaney-style mel
    melBandDescriptions[i] = description;
  }

  return melBandDescriptions;
}


const definitions = {
  log: {
    type: 'boolean',
    default: false,
    metas: { kind: 'static' },
  },
  nbrBands: {
    type: 'integer',
    default: 24,
    metas: { kind: 'static' },
  },
  minFreq: {
    type: 'float',
    default: 0,
    metas: { kind: 'static' },
  },
  maxFreq: {
    type: 'float',
    default: null,
    nullable: true,
    metas: { kind: 'static' },
  },
  power: {
    type: 'integer',
    default: 1,
    metas: { kind: 'dynamic' },
  },
};


/**
 * Compute the mel bands spectrum from a given spectrum (`vector` type).
 * _Implement the `htk` mel band style._
 *
 * _support `standalone` usage_
 *
 * @memberof module:common.operator
 *
 * @param {Object} options - Override default parameters.
 * @param {Boolean} [options.log=false] - Apply a logarithmic scale on the output.
 * @param {Number} [options.nbrBands=24] - Number of filters defining the mel
 *  bands.
 * @param {Number} [options.minFreq=0] - Minimum frequency to consider.
 * @param {Number} [options.maxFreq=null] - Maximum frequency to consider.
 *  If `null`, is set to Nyquist frequency.
 * @param {Number} [options.power=1] - Apply a power scaling on each mel band.
 *
 * @todo - implement Slaney style mel bands
 *
 * @example
 * import lfo from 'waves-lfo/node'
 *
 * // read a file from path (node only source)
 * const audioInFile = new lfo.source.AudioInFile({
 *   filename: 'path/to/file',
 *   frameSize: 512,
 * });
 *
 * const slicer = new lfo.operator.Slicer({
 *   frameSize: 256,
 *   hopSize: 256,
 * });
 *
 * const fft = new lfo.operator.Fft({
 *   size: 1024,
 *   window: 'hann',
 *   mode: 'power',
 *   norm: 'power',
 * });
 *
 * const mel = new lfo.operator.Mel({
 *   log: true,
 *   nbrBands: 24,
 * });
 *
 * const logger = new lfo.sink.Logger({ data: true });
 *
 * audioInFile.connect(slicer);
 * slicer.connect(fft);
 * fft.connect(mel);
 * mel.connect(logger);
 *
 * audioInFile.start();
 */
class Mel extends BaseLfo {
  constructor(options = {}) {
    super(definitions, options);
  }

  /** @private */
  processStreamParams(prevStreamParams) {
    this.prepareStreamParams(prevStreamParams);

    const nbrBins = prevStreamParams.frameSize;
    const nbrBands = this.params.get('nbrBands');
    const sampleRate = this.streamParams.sourceSampleRate;
    const minFreq = this.params.get('minFreq');
    let maxFreq = this.params.get('maxFreq');

    //
    this.streamParams.frameSize = nbrBands;
    this.streamParams.frameType = 'vector';
    this.streamParams.description = [];

    if (maxFreq === null)
      maxFreq = this.streamParams.sourceSampleRate / 2;

    this.melBandDescriptions = getMelBandWeights(nbrBins, nbrBands, sampleRate, minFreq, maxFreq);

    this.propagateStreamParams();
  }

  /**
   * Use the `Mel` operator in `standalone` mode (i.e. outside of a graph).
   *
   * @param {Array} spectrum - Fft bins.
   * @return {Array} - Mel bands.
   *
   * @example
   * const mel = new lfo.operator.Mel({ nbrBands: 24 });
   * // mandatory for use in standalone mode
   * mel.initStream({ frameSize: 256, frameType: 'vector', sourceSampleRate: 44100 });
   * mel.inputVector(fftBins);
   */
  inputVector(bins) {

    const power = this.params.get('power');
    const log = this.params.get('log');
    const melBands = this.frame.data;
    const nbrBands = this.streamParams.frameSize;
    let scale = 1;

    const minLogValue = 1e-48;
    const minLog = -480;

    if (log)
      scale *= nbrBands;

    for (let i = 0; i < nbrBands; i++) {
      const { startIndex, weights } = this.melBandDescriptions[i];
      let value = 0;

      for (let j = 0; j < weights.length; j++)
        value += weights[j] * bins[startIndex + j];

      // apply same logic as in PiPoBands
      if (scale !== 1)
        value *= scale;

      if (log) {
        if (value > minLogValue)
          value = 10 * log10(value);
        else
          value = minLog;
      }

      if (power !== 1)
        value = pow(value, power);

      melBands[i] = value;
    }

    return melBands;
  }

  /** @private */
  processVector(frame) {
    this.inputVector(frame.data);
  }
}

export default Mel;