view at/ofai/music/audio/FFT.java @ 5:bcb4c9697967 tip

Add README and CITATION files
author Chris Cannam
date Tue, 03 Dec 2013 12:58:05 +0000
parents 4c3f5bc01c97
children
line wrap: on
line source
/*
	Copyright (C) 2001, 2006 by Simon Dixon

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License along
	with this program (the file gpl.txt); if not, download it from
	http://www.gnu.org/licenses/gpl.txt or write to the
	Free Software Foundation, Inc.,
	51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

package at.ofai.music.audio;

/** Class for computing a windowed fast Fourier transform.
 *  Implements some of the window functions for the STFT from
 *  Harris (1978), Proc. IEEE, 66, 1, 51-83.
 */
public class FFT {

	/** used in {@link FFT#fft(double[], double[], int)} to specify
	 *  a forward Fourier transform */
	public static final int FORWARD = -1;
	/** used in {@link FFT#fft(double[], double[], int)} to specify
	 *  an inverse Fourier transform */
	public static final int REVERSE = 1;
	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
	 *  rectangular window function */
	public static final int RECT = 0;
	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
	 *  Hamming window function */
	public static final int HAMMING = 1;
	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
	 *  61-dB 3-sample Blackman-Harris window function */
	public static final int BH3 = 2;
	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
	 *  74-dB 4-sample Blackman-Harris window function */
	public static final int BH4 = 3;
	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
	 *  minimum 3-sample Blackman-Harris window function */
	public static final int BH3MIN = 4;
	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
	 *  minimum 4-sample Blackman-Harris window function */
	public static final int BH4MIN = 5;
	/** used in {@link FFT#makeWindow(int,int,int)} to specify a
	 *  Gaussian window function */
	public static final int GAUSS = 6; 
	static final double twoPI = 2 * Math.PI;

	/** The FFT method. Calculation is inline, for complex data stored
	 *  in 2 separate arrays. Length of input data must be a power of two.
	 *  @param re        the real part of the complex input and output data
	 *  @param im        the imaginary part of the complex input and output data
	 *  @param direction the direction of the Fourier transform (FORWARD or
	 *  REVERSE)
	 *  @throws IllegalArgumentException if the length of the input data is
	 *  not a power of 2
	 */
	public static void fft(double re[], double im[], int direction) {
		int n = re.length;
		int bits = (int)Math.rint(Math.log(n) / Math.log(2));
		if (n != (1 << bits))
			throw new IllegalArgumentException("FFT data must be power of 2");
		int localN;
		int j = 0;
		for (int i = 0; i < n-1; i++) {
			if (i < j) {
				double temp = re[j];
				re[j] = re[i];
				re[i] = temp;
				temp = im[j];
				im[j] = im[i];
				im[i] = temp;
			}
			int k = n / 2;
			while ((k >= 1) &&  (k - 1 < j)) {
				j = j - k;
				k = k / 2;
			}
			j = j + k;
		}
		for(int m = 1; m <= bits; m++) {
			localN = 1 << m;
			double Wjk_r = 1;
			double Wjk_i = 0;
			double theta = twoPI / localN;
			double Wj_r = Math.cos(theta);
			double Wj_i = direction * Math.sin(theta);
			int nby2 = localN / 2;
			for (j = 0; j < nby2; j++) {
				for (int k = j; k < n; k += localN) {
					int id = k + nby2;
					double tempr = Wjk_r * re[id] - Wjk_i * im[id];
					double tempi = Wjk_r * im[id] + Wjk_i * re[id];
					re[id] = re[k] - tempr;
					im[id] = im[k] - tempi;
					re[k] += tempr;
					im[k] += tempi;
				}
				double wtemp = Wjk_r;
				Wjk_r = Wj_r * Wjk_r  - Wj_i * Wjk_i;
				Wjk_i = Wj_r * Wjk_i  + Wj_i * wtemp;
			}
		}
	} // fft()

	/** Computes the power spectrum of a real sequence (in place).
	 *  @param re the real input and output data; length must be a power of 2
	 */
	public static void powerFFT(double[] re) {
		double[] im = new double[re.length];
		fft(re, im, FORWARD);
		for (int i = 0; i < re.length; i++)
			re[i] = re[i] * re[i] + im[i] * im[i];
	} // powerFFT()	

	/** Converts a real power sequence from to magnitude representation,
	 *  by computing the square root of each value.
	 *  @param re the real input (power) and output (magnitude) data; length
	 *  must be a power of 2
	 */
	public static void toMagnitude(double[] re) {
		for (int i = 0; i < re.length; i++)
			re[i] = Math.sqrt(re[i]);
	} // toMagnitude()
	
	/** Computes the magnitude spectrum of a real sequence (in place).
	 *  @param re the real input and output data; length must be a power of 2
	 */
	public static void magnitudeFFT(double[] re) {
		powerFFT(re);
		toMagnitude(re);
	} // magnitudeFFT()

	/** Computes a complex (or real if im[] == {0,...}) FFT and converts
	 *  the results to polar coordinates (power and phase). Both arrays
	 *  must be the same length, which is a power of 2.
	 *  @param re the real part of the input data and the power of the output
	 *  data
	 *  @param im the imaginary part of the input data and the phase of the
	 *  output data
	 */
	public static void powerPhaseFFT(double[] re, double[] im) {
		fft(re, im, FORWARD);
		for (int i = 0; i < re.length; i++) {
			double pow = re[i] * re[i] + im[i] * im[i];
			im[i] = Math.atan2(im[i], re[i]);
			re[i] = pow;
		}
	} // powerPhaseFFT()
	
	/** Inline computation of the inverse FFT given spectral input data
	 *  in polar coordinates (power and phase).
	 *  Both arrays must be the same length, which is a power of 2.
	 *  @param pow the power of the spectral input data (and real part of the
	 *  output data)
	 *  @param ph the phase of the spectral input data (and the imaginary part
	 *  of the output data)
	 */
	public static void powerPhaseIFFT(double[] pow, double[] ph) {
		toMagnitude(pow);
		for (int i = 0; i < pow.length; i++) {
			double re = pow[i] * Math.cos(ph[i]);
			ph[i] = pow[i] * Math.sin(ph[i]);
			pow[i] = re;
		}
		fft(pow, ph, REVERSE);
	} // powerPhaseIFFT()
	
	/** Computes a complex (or real if im[] == {0,...}) FFT and converts
	 *  the results to polar coordinates (magnitude and phase). Both arrays
	 *  must be the same length, which is a power of 2.
	 *  @param re the real part of the input data and the magnitude of the
	 *  output data
	 *  @param im the imaginary part of the input data and the phase of the
	 *  output data
	 */
	public static void magnitudePhaseFFT(double[] re, double[] im) {
		powerPhaseFFT(re, im);
		toMagnitude(re);
	} // magnitudePhaseFFT()


	/** Fill an array with the values of a standard Hamming window function
	 *  @param data the array to be filled
	 *  @param size the number of non zero values; if the array is larger than
	 *  this, it is zero-padded symmetrically at both ends 
	 */
	static void hamming(double[] data, int size) {
		int start = (data.length - size) / 2;
		int stop = (data.length + size) / 2;
		double scale = 1.0 / (double)size / 0.54;
		double factor = twoPI / (double)size;
		for (int i = 0; start < stop; start++, i++)
			data[i] = scale * (25.0/46.0 - 21.0/46.0 * Math.cos(factor * i));
	} // hamming()

	/** Fill an array with the values of a minimum 4-sample Blackman-Harris
	 *  window function
	 *  @param data the array to be filled
	 *  @param size the number of non zero values; if the array is larger than
	 *  this, it is zero-padded symmetrically at both ends 
	 */
	static void blackmanHarris4sMin(double[] data, int size) {
		int start = (data.length - size) / 2;
		int stop = (data.length + size) / 2;
		double scale = 1.0 / (double)size / 0.36;
		for (int i = 0; start < stop; start++, i++)
			data[i] = scale * ( 0.35875 -
								0.48829 * Math.cos(twoPI * i / size) +
								0.14128 * Math.cos(2 * twoPI * i / size) -
								0.01168 * Math.cos(3 * twoPI * i / size));
	} // blackmanHarris4sMin()

	/** Fill an array with the values of a 74-dB 4-sample Blackman-Harris
	 *  window function
	 *  @param data the array to be filled
	 *  @param size the number of non zero values; if the array is larger than
	 *  this, it is zero-padded symmetrically at both ends 
	 */
	static void blackmanHarris4s(double[] data, int size) {
		int start = (data.length - size) / 2;
		int stop = (data.length + size) / 2;
		double scale = 1.0 / (double)size / 0.4;
		for (int i = 0; start < stop; start++, i++)
			data[i] = scale * ( 0.40217 -
								0.49703 * Math.cos(twoPI * i / size) +
								0.09392 * Math.cos(2 * twoPI * i / size) -
								0.00183 * Math.cos(3 * twoPI * i / size));
	} // blackmanHarris4s()

	/** Fill an array with the values of a minimum 3-sample Blackman-Harris
	 *  window function
	 *  @param data the array to be filled
	 *  @param size the number of non zero values; if the array is larger than
	 *  this, it is zero-padded symmetrically at both ends 
	 */
	static void blackmanHarris3sMin(double[] data, int size) {
		int start = (data.length - size) / 2;
		int stop = (data.length + size) / 2;
		double scale = 1.0 / (double) size / 0.42;
		for (int i = 0; start < stop; start++, i++)
			data[i] = scale * ( 0.42323 -
								0.49755 * Math.cos(twoPI * i / size) +
								0.07922 * Math.cos(2 * twoPI * i / size));
	} // blackmanHarris3sMin()

	/** Fill an array with the values of a 61-dB 3-sample Blackman-Harris
	 *  window function
	 *  @param data the array to be filled
	 *  @param size the number of non zero values; if the array is larger than
	 *  this, it is zero-padded symmetrically at both ends 
	 */
	static void blackmanHarris3s(double[] data, int size) {
		int start = (data.length - size) / 2;
		int stop = (data.length + size) / 2;
		double scale = 1.0 / (double) size / 0.45;
		for (int i = 0; start < stop; start++, i++)
			data[i] = scale * ( 0.44959 -
								0.49364 * Math.cos(twoPI * i / size) +
								0.05677 * Math.cos(2 * twoPI * i / size));
	} // blackmanHarris3s()

	/** Fill an array with the values of a Gaussian window function
	 *  @param data the array to be filled
	 *  @param size the number of non zero values; if the array is larger than
	 *  this, it is zero-padded symmetrically at both ends 
	 */
	static void gauss(double[] data, int size) { // ?? between 61/3 and 74/4 BHW
		int start = (data.length - size) / 2;
		int stop = (data.length + size) / 2;
		double delta = 5.0 / size;
		double x = (1 - size) / 2.0 * delta;
		double c = -Math.PI * Math.exp(1.0) / 10.0;
		double sum = 0;
		for (int i = start; i < stop; i++) {
			data[i] = Math.exp(c * x * x);
			x += delta;
			sum += data[i];
		}
		for (int i = start; i < stop; i++)
			data[i] /= sum;
	} // gauss()

	/** Fill an array with the values of a rectangular window function
	 *  @param data the array to be filled
	 *  @param size the number of non zero values; if the array is larger than
	 *  this, it is zero-padded symmetrically at both ends 
	 */
	static void rectangle(double[] data, int size) {
		int start = (data.length - size) / 2;
		int stop = (data.length + size) / 2;
		for (int i = start; i < stop; i++)
			data[i] = 1.0 / (double) size;
	} // rectangle()

	/** Returns an array of values of a normalised smooth window function,
	 *  as used for performing a short time Fourier transform (STFT).
	 *  All functions are normalised by length and coherent gain.
	 *  More information on characteristics of these functions can be found
	 *  in F.J. Harris (1978), On the Use of Windows for Harmonic Analysis
	 *  with the Discrete Fourier Transform, <em>Proceedings of the IEEE</em>,
	 *  66, 1, 51-83.
	 *  @param choice  the choice of window function, one of the constants
	 *  defined above
	 *  @param size    the size of the returned array
	 *  @param support the number of non-zero values in the array
	 *  @return the array containing the values of the window function
	 */
	public static double[] makeWindow(int choice, int size, int support) {
		double[] data = new double[size];
		if (support > size)
			support = size;
		switch (choice) {
			case RECT:		rectangle(data, support);			break;
			case HAMMING:	hamming(data, support);				break;
			case BH3:		blackmanHarris3s(data, support);	break;
			case BH4:		blackmanHarris4s(data, support);	break;
			case BH3MIN:	blackmanHarris3sMin(data, support);	break;
			case BH4MIN:	blackmanHarris4sMin(data, support);	break;
			case GAUSS:		gauss(data, support);				break;
			default:		rectangle(data, support);			break;
		}
		return data;
	} // makeWindow()

	/** Applies a window function to an array of data, storing the result in
	 *  the data array.
	 *  Performs a dot product of the data and window arrays. 
	 *  @param data   the array of input data, also used for output
	 *  @param window the values of the window function to be applied to data
	 */
	public static void applyWindow(double[] data, double[] window) {
		for (int i = 0; i < data.length; i++)
			data[i] *= window[i];
	} // applyWindow()

	/** Unit test of the FFT class.
	 *  Performs a forward and inverse FFT on a 1MB array of random values
	 *  and checks how closely the values are preserved.
	 *  @param args ignored
	 */
	public static void main(String[] args) {
		final int SZ = 1024 * 1024;
		double[] r1 = new double[SZ];
		double[] i1 = new double[SZ];
		double[] r2 = new double[SZ];
		double[] i2 = new double[SZ];
		for (int j = 0; j < SZ; j++) {
			r1[j] = r2[j] = Math.random();
			i1[j] = i2[j] = Math.random();
		}
		System.out.println("start");
		fft(r2, i2, FORWARD);
		System.out.println("reverse");
		fft(r2, i2, REVERSE);
		System.out.println("result");
		double err = 0;
		for (int j = 0; j < SZ; j++)
			err += Math.abs(r1[j] - r2[j] / SZ) + Math.abs(i1[j] - i2[j] / SZ);
		System.out.printf( "Err: %12.10f   Av: %12.10f\n", err, err / SZ);
	} // main()

} // class FFT