使用java实现一维FFT

傅里叶变换基础

FFT是快速傅里叶变换,它是DFT的快速算法,但是计算结果与DFT等价。首先回顾一下傅里叶变换的计算公式:

首先,非周期性连续时间信号x(t)的傅里叶变换可以表示为:(公式1)
在这里插入图片描述

在离散的情况下,傅里叶变换的公式为:(公式2)
在这里插入图片描述
上面就是DFT的计算公式。DFT用代码实现是很简单的,因为,对于任意指定数值的K,只需要遍历N个采样点即可得到该K值下的频谱。不过DFT的复杂度为O(N^2),在N较大的情况下,计算速度是很慢的。

FFT

FFT本身是DFT的变体,所以我们需要先了解DFT的理论。下面先简单介绍FFT的原理:
还是对x(n)信号求傅里叶变换。x(n)本身可以拆分成下标为奇数子集和偶数子集。
在这里插入图片描述

x1(n)和x2(n)的长度都是N/2,x1(n)是偶数序列,x2(n)是奇数序列,则:(公式3)
在这里插入图片描述
其中X1(k)和X2(k)分别为x1(n)和x2(n)的N/2点DFT。由于X1(k)和X2(k)均以N/2为周期,且WN k+N/2=-WN k,所以X(k)又可表示为:(公式4)
在这里插入图片描述
写到这里就不难发现,FFT本质上是采用分治算法来计算的DFT。一个初始长度为N的初始数据,最后会被分治为很多长度为2 的数据,然后分别对这些长度为2的数据做DFT,最终等价于对整个N长度的数据做傅里叶变换。
分治其实是一种思想,具体的实现其实就是使用递归。在写Java程序之前,我们需要做一些准备工作。FFT的运算过程中涉及到复数的运算。公式4中包含了复数的加法和乘法。因此我们在Java程序中需要建立一个Complex(复数)类,除了real(实部)和imag(虚部)两个属性以外,需要给Complex类定义add方法和multiply方法。这样的话,Complex类不仅仅可以用作复数对象存储实部和虚部,还能实现复数的运算。Java中不能实现运算符的重载,这是挺遗憾的事情,这样的话就不能直接用加减乘除号来进行Complex对象之间的运算了。
以下是Complex类的定义:

// Struct to hold two primary type
    public static class Complex2 {
        public double real;
        public double imag;

        Complex2() {
            this.real = 0;
            this.imag = 0;
        }

        Complex2(double real, double imag) {
            this.real = real;
            this.imag = imag;
        }

        Complex2 add(Complex2 another) {
            return new Complex2(this.real + another.real, this.imag + another.imag);
        }

        Complex2 scalarMultiply(Complex2 another) {

            double x1 = this.real;
            double x2 = another.real;
            double y1 = this.imag;
            double y2 = another.imag;

            return new Complex2(x1*x2-y1*y2, x1*y2+x2*y1);
        }
    }

复数的加法是实部加实部,虚部加虚部。复数的乘法是类似于多项式相乘。正如代码所示。
下面进入正题。实现FFT主函数。FFT主函数必须分成两个函数来写,因为其中一个是递归函数,需要分离出来才能做递归。代码如下:

public static Complex2[] fft(List<Double> dataList) {

        // Get the size of dataList
        final int N = dataList.size();

        Complex2[] fftResult = new Complex2[N];

        // Traverse dataList by index k from 0 to size of dataList
        for(int k=0 ; k<N ; k++) {
            fftResult[k] = fftRecursion(dataList, N, k);
        }

        return fftResult;
    }

这是fft的主函数,主函数中主要是做了一个循环,因为我们需要求K频域上K从0~N-1变化过程中的频谱。对于每一个不同的K值,我们分别调用fftRecursion函数来实现分治算法。下面列出fftRecursion函数的实现:

/**
     * Realize FFT by divide-and-conquer algorithm
     *
     * @param ALineUnit This is List of fragemnt of ALine Points. The termination of recursion
     *                  is sometime when size of ALineUnit is 2
     */
    private static Complex2 fftRecursion(List<Double> ALineUnit, int unitLength, int k) {

        if(unitLength == 2) {

            Complex2 unit2 = new Complex2();

            // Apply FFT to data list (size:2)
            double xn;

            // Get real value
            for(int k1=0 ; k1<2 ; k1++) {
                for(int n=0 ; n<2 ; n++) {

                    xn = ALineUnit.get(n);

                    unit2.real += xn * cos(2*PI*k1*n/2);
                    unit2.imag -= xn * sin(2*PI*k1*n/2);
                }
            }

            return unit2;

        }

        else {
            // Cut odd and even component of the current unit
            List<Double> oddList = new ArrayList<>();
            List<Double> evenList = new ArrayList<>();

            for(int i=0 ; i<unitLength/2 ; i++) {
                oddList.add(ALineUnit.get(2*i + 1));
                evenList.add(ALineUnit.get(2*i));
            }

            return fftRecursion(evenList, unitLength/2, k).add(new Complex2(cos(2*PI*k/unitLength), -sin(2*PI*k/unitLength)).scalarMultiply(fftRecursion(oddList, unitLength/2, k)));
        }

    }

需要给递归设置一个终止递归的条件。也就是说当递归进行到每个数据包的长度已经变成2的时候,就可以对每个数据包做DFT,最后整体实现一个FFT的效果。
在递归还没有进行到数据包长度等于2之前,我们需要依照公式4的第一个公式对当前的数据包进行拆分,拆成奇数项和偶数项,然后对照公式4中的第一个公式,把k代进去(k在递归的过程中是个常数),同时做复数的加法和乘法(这个是我们在Complex类中就已经实现了的)。
以上。

使用FFT处理音乐频谱数据,首先需要将音乐文件读入内存,并将其转换为一维数组。Java中可以使用javax.sound.sampled包中的AudioInputStream类来读取音乐文件,并使用java.util.Arrays类中的静态方法将其转换为一维数组。 读取音乐文件并转换为一维数组的代码示例如下: ``` import javax.sound.sampled.*; import java.io.*; public class AudioReader { public static int[] read(String filename) throws UnsupportedAudioFileException, IOException { AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(new File(filename)); AudioFormat format = audioInputStream.getFormat(); int numChannels = format.getChannels(); int sampleSizeInBits = format.getSampleSizeInBits(); int numBytes = (int) (audioInputStream.getFrameLength() * format.getFrameSize()); byte[] bytes = new byte[numBytes]; int numSamples = numBytes / (numChannels * sampleSizeInBits / 8); int[] samples = new int[numSamples]; audioInputStream.read(bytes); int i = 0; for (int t = 0; t < numBytes; t += 2 * numChannels) { int sample = 0; for (int channel = 0; channel < numChannels; channel++) { sample |= (bytes[t + channel * 2 + 1] << 8) | (bytes[t + channel * 2] & 0xFF); } samples[i++] = sample; } return samples; } } ``` 上述代码中,我们使用AudioSystem.getAudioInputStream方法读取音乐文件,并获取音频格式信息。然后根据音频格式将音频文件转换为字节数组,再将其转换为一维数组。需要注意的是,由于Java中只有整型数组,所以我们需要将每个采样点的数据转换为整型。 读取音乐文件后,我们可以使用FFT算法对其进行处理,得到音乐频谱数据。对音乐文件进行FFT变换的代码示例如下: ``` import java.util.Arrays; public class FFT { public static void main(String[] args) throws Exception { int[] data = AudioReader.read("music.wav"); int n = data.length; // 补零操作,使数据长度变为2的幂次 int m = 1; while (m < 2 * n) { m <<= 1; } int[] x = Arrays.copyOf(data, m); // 一维FFT变换 fft(x); // 输出结果 for (int i = 0; i < m / 2; i++) { double freq = (double) i / m * 44100; double magnitude = Math.sqrt(x[2 * i] * x[2 * i] + x[2 * i + 1] * x[2 * i + 1]); System.out.println(freq + " " + magnitude); } } public static void fft(int[] x) { int n = x.length; if (n <= 1) { return; } // 分别计算奇偶数项 int[] even = new int[n / 2]; int[] odd = new int[n / 2]; for (int i = 0; i < n / 2; i++) { even[i] = x[2 * i]; odd[i] = x[2 * i + 1]; } // 递归计算FFT fft(even); fft(odd); // 合并结果 for (int k = 0; k < n / 2; k++) { double angle = -2 * k * Math.PI / n; double real = Math.cos(angle); double imag = Math.sin(angle); int re = even[k] + (int) (real * odd[k]) - (int) (imag * odd[k + n / 2]); int im = even[k] + (int) (imag * odd[k]) + (int) (real * odd[k + n / 2]); x[k] = re; x[k + n / 2] = im; } } } ``` 上述代码中,我们先使用AudioReader类读取音乐文件,并将其转换为一维数组。然后对该数组进行FFT变换,并将变换后的结果输出到控制台。需要注意的是,在输出结果时,我们需要计算每个频率对应的幅值。对于一维FFT变换的结果,数组下标为k的元素对应的频率为k / m * 采样率,其中m为数组长度,采样率为音频文件的采样率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值