Reference Source

src/pipes/frequency/fft.js

import { FFT } from "dsp.js";
import { pipe } from "rxjs";
import { map } from "rxjs/operators";
import { zeroPad } from "../../utils/zeroPad";

import {
  FFT_BINS as defaultFftBins,
  DATA_PROP as defaultDataProp
} from "../../constants";

/**
 * Applies a Fast Fourier Transform to a stream of Epochs of EEG data and returns a stream of PSDs (Power Spectral Density). Frequency resolution will be samplingRate / bins. If input Epoch duration is not equal to bins, data will be zero-padded or sliced so that is the same length as bins.
 * @method fft
 * @example eeg$.pipe(epoch({ duration: 256, interval: 100, samplingRate: 256 }), fft({ bins: 256 }))
 * @param {Object} options - FFT options
 * @param {number} options.bins Number of FFT bins. Must be a power of 2.
 * @param {string} [options.dataProp='data] Name of the key associated with eeg data
 *
 * @returns {Observable<PSD>}
 */
export const fft = ({
  bins = defaultFftBins,
  dataProp = defaultDataProp
} = {}) => {
  const transformChannel = (channel, samplingRate) => {
    let safeSamples = channel.map(sample => {
      if (isNaN(sample) || !sample) {
        return 0;
      }
      return sample;
    });
    const fft = new FFT(bins, samplingRate);
    if (safeSamples.length != bins) {
      if (safeSamples.length < bins) {
        safeSamples = zeroPad(safeSamples, bins);
      } else {
        safeSamples = safeSamples.slice(safeSamples.length - bins);
      }
    }
    fft.forward(safeSamples);
    return Array.from(fft.spectrum);
  };
  return pipe(
    map(epoch => ({
      psd: epoch[dataProp].map(channel =>
        transformChannel(channel, epoch.info.samplingRate)
      ),
      freqs: Array.from(
        { length: bins / 2 },
        (_, index) => index * (epoch.info.samplingRate / bins)
      ),
      info: epoch.info
    }))
  );
};