import BaseLfo from '../../core/BaseLfo';
import initWindow from '../utils/windows';
// https://code.soundsoftware.ac.uk/projects/js-dsp-test/repository/entry/fft/nayuki-obj/fft.js
/*
* Free Fft and convolution (JavaScript)
*
* Copyright (c) 2014 Project Nayuki
* http://www.nayuki.io/page/free-small-fft-in-multiple-languages
*
* (MIT License)
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
* - The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* - The Software is provided "as is", without warranty of any kind, express or
* implied, including but not limited to the warranties of merchantability,
* fitness for a particular purpose and noninfringement. In no event shall the
* authors or copyright holders be liable for any claim, damages or other
* liability, whether in an action of contract, tort or otherwise, arising from,
* out of or in connection with the Software or the use or other dealings in the
* Software.
*
* Slightly restructured by Chris Cannam, cannam@all-day-breakfast.com
*
* @private
*/
/*
* Construct an object for calculating the discrete Fourier transform (DFT) of
* size n, where n is a power of 2.
*
* @private
*/
function FftNayuki(n) {
this.n = n;
this.levels = -1;
for (var i = 0; i < 32; i++) {
if (1 << i == n) {
this.levels = i; // Equal to log2(n)
}
}
if (this.levels == -1) {
throw "Length is not a power of 2";
}
this.cosTable = new Array(n / 2);
this.sinTable = new Array(n / 2);
for (var i = 0; i < n / 2; i++) {
this.cosTable[i] = Math.cos(2 * Math.PI * i / n);
this.sinTable[i] = Math.sin(2 * Math.PI * i / n);
}
/*
* Computes the discrete Fourier transform (DFT) of the given complex vector,
* storing the result back into the vector.
* The vector's length must be equal to the size n that was passed to the
* object constructor, and this must be a power of 2. Uses the Cooley-Tukey
* decimation-in-time radix-2 algorithm.
*
* @private
*/
this.forward = function(real, imag) {
var n = this.n;
// Bit-reversed addressing permutation
for (var i = 0; i < n; i++) {
var j = reverseBits(i, this.levels);
if (j > i) {
var temp = real[i];
real[i] = real[j];
real[j] = temp;
temp = imag[i];
imag[i] = imag[j];
imag[j] = temp;
}
}
// Cooley-Tukey decimation-in-time radix-2 Fft
for (var size = 2; size <= n; size *= 2) {
var halfsize = size / 2;
var tablestep = n / size;
for (var i = 0; i < n; i += size) {
for (var j = i, k = 0; j < i + halfsize; j++, k += tablestep) {
var tpre = real[j+halfsize] * this.cosTable[k] +
imag[j+halfsize] * this.sinTable[k];
var tpim = -real[j+halfsize] * this.sinTable[k] +
imag[j+halfsize] * this.cosTable[k];
real[j + halfsize] = real[j] - tpre;
imag[j + halfsize] = imag[j] - tpim;
real[j] += tpre;
imag[j] += tpim;
}
}
}
// Returns the integer whose value is the reverse of the lowest 'bits'
// bits of the integer 'x'.
function reverseBits(x, bits) {
var y = 0;
for (var i = 0; i < bits; i++) {
y = (y << 1) | (x & 1);
x >>>= 1;
}
return y;
}
}
/*
* Computes the inverse discrete Fourier transform (IDFT) of the given complex
* vector, storing the result back into the vector.
* The vector's length must be equal to the size n that was passed to the
* object constructor, and this must be a power of 2. This is a wrapper
* function. This transform does not perform scaling, so the inverse is not
* a true inverse.
*
* @private
*/
this.inverse = function(real, imag) {
forward(imag, real);
}
}
const sqrt = Math.sqrt;
const isPowerOfTwo = function(number) {
while ((number % 2 === 0) && number > 1)
number = number / 2;
return number === 1;
}
const definitions = {
size: {
type: 'integer',
default: 1024,
metas: { kind: 'static' },
},
window: {
type: 'enum',
list: ['none', 'hann', 'hanning', 'hamming', 'blackman', 'blackmanharris', 'sine', 'rectangle'],
default: 'none',
metas: { kind: 'static' },
},
mode: {
type: 'enum',
list: ['magnitude', 'power'], // add complex output
default: 'magnitude',
},
norm: {
type: 'enum',
default: 'auto',
list: ['auto', 'none', 'linear', 'power'],
},
}
/**
* Compute the Fast Fourier Transform of an incomming `signal`.
*
* Fft implementation by [Nayuki](https://code.soundsoftware.ac.uk/projects/js-dsp-test/repository/entry/fft/nayuki-obj/fft.js).
*
* _support `standalone` usage_
*
* @memberof module:common.operator
*
* @param {Object} options - Override default parameters.
* @param {Number} [options.size=1024] - Size of the fft, should be a power of 2.
* If the frame size of the incomming signal is lower than this value,
* it is zero padded to match the fft size.
* @param {String} [options.window='none'] - Name of the window applied on the
* incomming signal. Available windows are: 'none', 'hann', 'hanning',
* 'hamming', 'blackman', 'blackmanharris', 'sine', 'rectangle'.
* @param {String} [options.mode='magnitude'] - Type of the output (`magnitude`
* or `power`)
* @param {String} [options.norm='auto'] - Type of normalization applied on the
* output. Possible values are 'auto', 'none', 'linear', 'power'. When set to
* `auto`, a `linear` normalization is applied on the magnitude spectrum, while
* a `power` normalization is applied on the power spectrum.
*
* @example
* import * as lfo from 'waves-lfo/client';
*
* // assuming an `audioBuffer` exists
* const source = new lfo.source.AudioInBuffer({ audioBuffer });
*
* const slicer = new lfo.operator.Slicer({
* frameSize: 256,
* });
*
* const fft = new lfo.operator.Fft({
* mode: 'power',
* window: 'hann',
* norm: 'power',
* size: 256,
* });
*
* source.connect(slicer);
* slicer.connect(fft);
* source.start();
*
* // > outputs 129 bins containing the values of the power spectrum (including
* // > DC and Nyuist frequencies).
*
* @todo - check if 'rectangle' and 'none' windows are not redondant.
* @todo - check default values for all params.
*/
class Fft extends BaseLfo {
constructor(options = {}) {
super(definitions, options);
this.windowSize = null;
this.normalizeCoefs = null;
this.window = null;
this.real = null;
this.imag = null;
this.fft = null;
if (!isPowerOfTwo(this.params.get('size')))
throw new Error('fftSize must be a power of two');
}
/** @private */
processStreamParams(prevStreamParams) {
this.prepareStreamParams(prevStreamParams);
// set the output frame size
const inFrameSize = prevStreamParams.frameSize;
const fftSize = this.params.get('size');
const mode = this.params.get('mode');
const norm = this.params.get('norm');
let windowName = this.params.get('window');
// window `none` and `rectangle` are aliases
if (windowName === 'none')
windowName = 'rectangle';
this.streamParams.frameSize = fftSize / 2 + 1;
this.streamParams.frameType = 'vector';
this.streamParams.description = [];
// size of the window to apply on the input frame
this.windowSize = (inFrameSize < fftSize) ? inFrameSize : fftSize;
// references to populate in the window functions (cf. `initWindow`)
this.normalizeCoefs = { linear: 0, power: 0 };
this.window = new Float32Array(this.windowSize);
initWindow(
windowName, // name of the window
this.window, // buffer populated with the window signal
this.windowSize, // size of the window
this.normalizeCoefs // object populated with the normalization coefs
);
const { linear, power } = this.normalizeCoefs;
switch (norm) {
case 'none':
this.windowNorm = 1;
break;
case 'linear':
this.windowNorm = linear;
break;
case 'power':
this.windowNorm = power;
break;
case 'auto':
if (mode === 'magnitude')
this.windowNorm = linear;
else if (mode === 'power')
this.windowNorm = power;
break;
}
this.real = new Float32Array(fftSize);
this.imag = new Float32Array(fftSize);
this.fft = new FftNayuki(fftSize);
this.propagateStreamParams();
}
/**
* Use the `Fft` operator in `standalone` mode (i.e. outside of a graph).
*
* @param {Array} signal - Input values.
* @return {Array} - Fft of the input signal.
*
* @example
* const fft = new lfo.operator.Fft({ size: 512, window: 'hann' });
* // mandatory for use in standalone mode
* fft.initStream({ frameSize: 256, frameType: 'signal' });
* fft.inputSignal(signal);
*/
inputSignal(signal) {
const mode = this.params.get('mode');
const windowSize = this.windowSize;
const frameSize = this.streamParams.frameSize;
const fftSize = this.params.get('size');
const outData = this.frame.data;
// apply window on the input signal and reset imag buffer
for (let i = 0; i < windowSize; i++) {
this.real[i] = signal[i] * this.window[i] * this.windowNorm;
this.imag[i] = 0;
}
// if real is bigger than input signal, fill with zeros
for (let i = windowSize; i < fftSize; i++) {
this.real[i] = 0;
this.imag[i] = 0;
}
this.fft.forward(this.real, this.imag);
if (mode === 'magnitude') {
const norm = 1 / fftSize;
// DC index
const realDc = this.real[0];
const imagDc = this.imag[0];
outData[0] = sqrt(realDc * realDc + imagDc * imagDc) * norm;
// Nquyst index
const realNy = this.real[fftSize / 2];
const imagNy = this.imag[fftSize / 2];
outData[fftSize / 2] = sqrt(realNy * realNy + imagNy * imagNy) * norm;
// power spectrum
for (let i = 1, j = fftSize - 1; i < fftSize / 2; i++, j--) {
const real = 0.5 * (this.real[i] + this.real[j]);
const imag = 0.5 * (this.imag[i] - this.imag[j]);
outData[i] = 2 * sqrt(real * real + imag * imag) * norm;
}
} else if (mode === 'power') {
const norm = 1 / (fftSize * fftSize);
// DC index
const realDc = this.real[0];
const imagDc = this.imag[0];
outData[0] = (realDc * realDc + imagDc * imagDc) * norm;
// Nquyst index
const realNy = this.real[fftSize / 2];
const imagNy = this.imag[fftSize / 2];
outData[fftSize / 2] = (realNy * realNy + imagNy * imagNy) * norm;
// power spectrum
for (let i = 1, j = fftSize - 1; i < fftSize / 2; i++, j--) {
const real = 0.5 * (this.real[i] + this.real[j]);
const imag = 0.5 * (this.imag[i] - this.imag[j]);
outData[i] = 4 * (real * real + imag * imag) * norm;
}
}
return outData;
}
/** @private */
processSignal(frame) {
this.inputSignal(frame.data);
}
}
export default Fft;