目录

前言

学习的意义:

频域和时域图的展现

项目Git地址

一、理解时域、频域、FFT

所有信号都是若干正弦波的和

采样中的几个名词

不得不说的几个理解误区

二、加窗和窗函数的理解

什么是加窗?

理想是美好的,现实是骨感的

体现加窗的意义

形象加窗的意义

加窗函数

Hanning和Hamming窗

Blackman-harris窗

常见窗函数

三、总结

四、代码分享

说明:

波形图代码:

频谱代码:

采样点数补全方法:


前言

学习的意义:

  •     学习信号时域和频域、快速傅里叶变化(FFT)、加窗,以及如何通过这些操作来加深对信号的认识。
  •     本文中,相关图为我在搜集资料时看到的具有代表性的图,所以直接复制过来了,便于图文结合理解。 
  •     关于代码方面,后续会逐步更新到本片文章中。

频域和时域图的展现

  • 傅里叶变换有助于理解常见的信号,以及如何分辨信号中的错误。尽管傅里叶变化是一个复杂的数学函数,但是通过一个测量信号来理解傅里叶变换的概念并不复杂。从根本上说,傅里叶变换将一个信号分解为不同幅值和频率的正弦波。我们下面深入理解下这个意义的说法:

Java如何编写MongoDB java如何编写频率波_Java如何编写MongoDB

  • 先看一个时域上的电信号的连续波形图:

Java如何编写MongoDB java如何编写频率波_数字信号处理_02

项目Git地址

http://192.168.173.100:90/fanyh/fourier.git

一、理解时域、频域、FFT

所有信号都是若干正弦波的和

    我们通常把一个实际信号看做是根据时间变化的电压值。这是从时域的角度来观察信号。(通过A/D 数模转换模块实现,将离散的电压数字信号转换为联系的电压模拟信号)

    傅里叶定律:

    任意波形在时域都可以由若干个正弦波的加权和叠加而成(此处,正弦波和余弦波统称为正弦波)。

    怎么理解:
    有两个正弦波,其中一个的频率是另一个的3倍。将两个正弦波相叠加,就得到一个不同的信号。

Java如何编写MongoDB java如何编写频率波_fft_03

 

如上图,两个正弦波叠加后得到一个信号的波形。我们可以发现波在叠加时,交点处根据波的振动方向进行向量叠加,波峰根据方向向量进行叠加。 

 

我们现在尝试将黄色波的振幅缩减为1/3。此时,只有波峰受影响。

Java如何编写MongoDB java如何编写频率波_Java如何编写MongoDB_04

 

假如,我们现在将无数个正弦波递归按周期和波峰为基波的1/5叠加,直到波形趋近于噪声波形的临界(冲击波形),那么我们可以认得出得到的波形:

Java如何编写MongoDB java如何编写频率波_java_05

 

我们发现,按上面的方式进行叠加我们会得到一个趋近于方波的波形。所以我们可以知道,对于任意的波形,我们都可以通过改变叠加的正弦波的频率(周期)、幅度来构造任意的波形。 
     
    我们现在回头再看傅里叶变换的意义 — 对于任意的波形(信号),我们可以将其视为(拆解)任意数量的正弦波的叠加。并且,每一个波形的频率都可以对应在频谱上。那么针对于任意波形,我们都可以在频谱上对其视为(拆解)n个频率所对应的正弦波的叠加。 
    因此,我们可以将这n个频率不同的正弦波对应到频谱上,且每个正弦波的频率就是频谱的x轴的频率所对应的位置。

 

采样中的几个名词

 

信号分析,有几个不得不提前了解并且完全掌握的名词,这里我给大家总结下:

  • 波形周期 T,频率 f

比如:正弦波 — f(x) = sin(wt) = sin [w(t+T)]  = sin [wt+wT]   
     
    其中,上面的正弦波在 nT(n=1,2,…..,n) 周期内波形重复,所以我们称 T 为正弦波的振动周期;而对于周期T的倒数为频率 f ,所以对于上面的正弦波,我们称它的振动频率为 f = 1/T。

  • 采样样本容量N、采样频率 fs、采样定律
  1. 样本容量:我们在一个信号的周期内均匀采样所得到的的采样点数;
  2. 采样频率:在离散采样中,我们在 Ts 时间段内获取了所有的采样点,那么我们就称这些采样点的采样周期为 Ts。采样频率 fs = 1/Ts;
  3. 采样定律:根据采样定律,fs >= 2 fmax。即,采样频率要大于等于2倍的最大信号波振动频率 fmax。而实际生产中,我们发现一般我们的倍数为【2.56~4】才是最佳的。
  • 采样长度、分辨率 △f

    采样时,采样长度过长会导致计算量很大;采样长度过短,会导致频谱分辨率 △f 过低。因此,一般必须综合考虑采样频率和采样长度的问题。

    一般,在信号分析仪中,采样点数是固定的(如:N=1024,2048,4096,….),各档分析频率的范围: 
    fa = fs/2.56 = 1/(2.56△t)  

    分辨率为: 
    △f = 1/N△t = 2. 56fa/N = fs/N = (1/400,1/800,1/1600,…)fa

    而这就是信号分析仪的频谱分辨率选择中常说的400线、800线、1600线…。 
    谱线数 L = N/2.56

    采样时间 T;采样周期 Ts(一个点的采样时间);采样频率 Fs(每秒的采样频率);采样点数 N(2的整数次幂);最高分析频率 Fmax。有如下关系:

    T = Ts * N = N / Fs = N / (2.56 * Fmax) = lines / Fmax    

不得不说的几个理解误区

 

1、采样频率和最大波频 
    根据采样定律:采样频率(fs)>= 2*最大波频(fmax)。 
     
    第一点,实际生活中,2倍波频的采样频率往往是不能满足生产需求的。具体的原理跟波形混叠有关,具体的概念希望了解的人可以自己去了解,我这里不再说明。所以,我们往往根据实践和公式的推导可以得出最佳的系数:采样频率(fs)>= (2.56~4)*最大波频(fmax)。

2、频谱中最大频率 — 量程的定义 
    我们既然能将采样点经过FFT后转换为对应在频域上的一个个的点,那么我们如何画出这个频谱图呢?

    第一点,我们先确定频域上点与点之间的间隔 — △f = 1/N△t = 2. 56fa/N = fs/N = (1/400,1/800,1/1600,…)fa 
     
    根据公式,我们可以知道,频域中点与点之间的间隔就是我们所说的分辨率。怎么理解呢? 
    我们试着想象:频域上的每一个点都对应了一个原基波的叠加波,那么频谱上每一个点就都有了它的物理定义(为对应频率叠加波的能量),那么我们就知道了频域上每一个点在频域横坐标上的物理意义(对应叠加波的振动频率)。 
    我们有了上面的思考后就不难理解分辨率的意义 — 对于原基波的拆分的颗粒度的描述。(即,我们将一个原基波拆分的越细,那么就可以得到更多的不同频率上的波,那么我们对于这个基波的叠加就能更细致的了解)。 
    最后,我们回过头来看,频谱的最大量程的物理意义 — 原基波的对应叠加波中频率最高的一个叠加波的振动频率 (fmax)。

    不得不感叹,当一个个抽象的数据有了物理定义后,我们就能理解它的本质了。

    总结:

  • 既然我们知道了频谱的最大量程的物理意义,那么我们就可以推导计算出不同情况下的频谱量程的定义了:

                fmax = fs / 倍率系数  【2.56~4,常用2.56】

3、采样点数和采样频率不一定相等,若采样频率Fs = 采样点数N,则频率分辨率为标准1hz。(具体的理解看下面的例题) 

 

 

   例题1:
    已知两组参数:【最大量程200Hz,400线;最大量程400Hz,400线】,求上面两组情况各自对应的(1)采样频率(2)采样点数(3)频率分辨率。比例系数:2.56 
    
例题剖析: 
设:M为线数,Fa为最大量程,Fs为采样频率,N为采样点数,△f为频率分辨率

  1. 由题设可以得出:M  = Fa / △f = N / 2.56,所以代入数值可以得出,当M = 400Hz时,采样点数N = 2.56 * 400 = 1024;
  2. 当Fa = 200hz时,频率分辨率△f = Fa / M = 0.5hz;当Fa = 400hz时,频率分辨率△f = Fa / M = 1hz;
  3. 由于△f = Fs / N = Fa / M ;所以,当△f = 0.5hz时,Fs1 = 512hz/s;当△f = 1hz时,Fs2 = 1024hz/s

    例题2:
   在一般信号分析仪中,采样点数是固定的,一般取N = 2的整数次幂(细分为:256,512,1024,2048等几个档次),各档分析频率范围Fa取决于采样频率的高低。即,Fa = Fs / 2.56 = 1 / (2.56△t)

    则在频率轴上的频率间隔为:△f = 1 / T = 1 / (N△t)= 2.56 * Fc / N = Fc / M (M为谱线数)

    则谱线数M = Fc / △f = N / 2.56

    一台具体的分析仪器,当采样点数N(或者谱线数M)固定下来后,它的频率分析范围 Fc 取决于采样间隔△t(或者采样频率Fs);最低分析频率取决于采样长度T(或者频率分辨率)。

    例如:某台分析仪器的采样点数为N = 1024,采样时间间隔为△t = 0.4ms,采样长度为0.4096s,求频谱量程Fa,最低分析频率Fc,分辨率△f,谱线数M。

答: 
    从上面的描述可以知道已下信息:采样频率 Fs  = 1/△t = 2.5Khz ;采样总长度 Ts = 0.4096s,我们可以验证采样点N的个数:N = Ts / △t = 1024,看来描述的没有问题。【体会采样频率Fs 和 采样点个数N没有关系】

然后我们根据公式来推导目标结果:

  1. Fa = Fs / 2.56 = 2500 / 2.56 ≈ 977 Hz
  2. Fc = 1 / (2.56*Ts)≈ 1 Hz
  3. △f = Fs / N = 2.56*Fc / N = 2500 / 1024 ≈ 2.44 Hz
  4. M = Fa / △f = N / 2.56 ≈ 400 线

综上,我们可以知道这是一个400线的,最大量程为977Hz,频谱起始点为1Hz,分辨率(谱线间隔)为2.44Hz,采样频率2500Hz/s,采样点数1024的一个频谱分析仪器。 

二、加窗和窗函数的理解

     虽然频域提供了观察信号的新的视角,但是FFT也有各种限制,可以通过增加窗函数来增加信号的清晰度。

什么是加窗?

 

    使用FFT分析信号的频率成分的时候,分析的是有限的数据集合。FFT认为波形是一组有限数据的集合,一个连续的波形是由若干段小波形组成的。对于FFT而言,时域和频域都是环形的拓扑结构。【但是现实的离散采样,我们不能保证采集的波形是满足环形拓扑结构】

    时间上,波形的前后两个端点是相连的。如测量的信号是周期信号,采集时间内刚好有整数个周期,那么FFT的上述假设合理。 

Java如何编写MongoDB java如何编写频率波_Java如何编写MongoDB_06

如图,测量整数个周期(如上图)可以得到理想的FFT。

理想是美好的,现实是骨感的

    虽然我们都期望在离散采样的过程中,我们可以在连续的完整周期上进行采样,那么我们FFT后便可以得到完美的频谱结果。可是,在很多情况下,我们采样的点并不是在整个波形的整数周期上。因此,测量到的信号就会被从周期中间切断,与时间连续的原信号显示出不同的特征。有限数据采样会使测量信号产生剧烈的变化。这种剧烈的变换成为不连续性。

    采集到的周期为非整数时,端点是不连续的。这些不连续片段在FFT中显示为高频成分。这些高频成分不存在于原信号中。这些频率可能远高于奈奎斯特频率,在【0~Fs/2】这段频率区间产生混叠。使用FFT获得的频率,不是原信号的实际频率,而是一个改变过的频率(类似于某个频率的能量泄露至其他频率)。这种现象就叫做频谱泄露。频谱泄露使得好的频谱线扩散到更宽的频域范围内。 

Java如何编写MongoDB java如何编写频率波_Java如何编写MongoDB_07

如上图所示,我们采样的波形周期非整数周期,那么就会发生频谱泄露而产生的频域图。

体现加窗的意义

 

    通过上面的学习,我们知道了什么是频谱泄露这个问题,以及频谱泄露会导致频谱的高频段莫名出现高频数据段这个问题。那么如何解决频谱泄露这个常见问题呢?

    答案显而易见,可以通过加窗来尽可能的减少在非整数周期上进行FFT产生的误差。 

形象加窗的意义

 

    数字化仪器采集到的有限序列的边界会呈现出不连续性。加窗可以减少这些不连续的部分的幅值。加窗包含将时间记录乘以有限长度的窗,窗的幅值主键减小,在外边沿出为0。 
    加窗的结果是尽可能呈现出一个连续的波形,减少剧烈的变化。 

Java如何编写MongoDB java如何编写频率波_数字信号处理_08

 

如图,我们对于上述的不连续的波形加一个窗,使得FFT以在窗口内波形上的采样点为准,窗两边幅度逐渐收敛于0。此时我们认为窗内的波形为连续整数个周期的波形。所以我们可以FFT后可以得到一个频谱图,虽然这个频谱图不能还原为真实的样子,但是,相比较与没有加窗的频谱图来说,我们体现除了真实波形的能量最大的点所对应的频率。所以,加窗的意义在于尽可能的较少频谱泄露带来的高频段无法辨认的问题,但不能真正解决频谱泄露的问题。 

加窗函数

 

根据信号的不同,可选择不同类型的加窗函数。要理解窗对信号产生怎样的影响,就要先理解窗的频率特性。

首先,我们需要知道有哪些窗以及窗的特性:

  • 矩形窗:B=4π/N  A=-13dB  D=-6dB/oct  
  • 三角窗:B=8π/N  A=-27dB   D=-12dB/oct  
  • 汉宁窗:B=8π/N  A=-32dB   D=-18dB/oct  
  • 海明窗:B=8π/N  A=-43dB   D=-6dB/oct  
  • 布莱克曼窗:B=12π/N  A=-58dB  D=-18dB/oct  

B为主瓣宽度   A为最大边瓣峰值   D为衰减度
可以看出,矩形窗有最窄的主瓣,但是旁瓣泄露严重。汉宁窗和海明窗虽然主瓣较宽,但是旁瓣泄露少,所以实际中基本选择这两种窗函数。

     
    窗的波形图显示了窗本身为一个连续的频谱,有一个主瓣,若干旁瓣。主瓣是时域信号频率成分的中央,旁瓣接近于0。旁瓣的高度显示了加窗函数对于主瓣周围频率的影响。对强正弦信号的旁瓣响应可能会超过对较近的弱正弦信号的主瓣响应。一般而言,低旁瓣会减少FFT的泄露,但是增加了主瓣的带宽。 
    旁瓣的跌落速率是旁瓣峰值的渐进衰减率。增加旁瓣的跌落速率,可减少频谱泄露。

如何选择加窗函数

    选择加窗函数并非易事。每一种加窗函数都有其特征和使用范围。要选择加窗函数,必须预先估计信号的频率成分。

选择的判断依据:

  • 如果原始的信号具有强干扰频率分量,与感兴趣分量相距较远,那么就应该选择具有高旁瓣、高下降率的平滑窗。
  • 如果原始的信号具有强干扰频率分量,与感兴趣的分量相距较近,那么就应该选择具有低旁瓣的窗。
  • 如果感兴趣频率包含两种或者多种距离很近的信号,这时,频率分辨率就很重要了。在这种情况下,最好选用窄主瓣的平滑窗。
  • 如果一个频率成分的幅值精度比信号成分在某个频率区间内的精确位置更加重要,那么就选择宽主瓣的窗。
  • 若原始信号频谱较平或者频率成分较宽,那么可以不使用窗。

    总之,Hanning窗适用于95%的情况。它不仅具有较好的分辨率,还可以减少频谱泄露。所以,当不知道怎么选择窗的时候,不妨先用一下Hanning窗试一试。

    即使不使用任何窗,信号也会与高度一致的长方形窗进行卷积运算。本质上相当于对于时域信号进行输入信号的截屏,对离散信号也有效。该卷积有一个正弦波函数特性的频谱。基于该原因,没有窗叫做统一窗或者长方形窗。 

Hanning和Hamming窗

 

Hanning和Hamming窗都有正弦波的外形。两个窗都会产生宽波峰,低旁瓣的结果。

    不同点是:

  • Hanning窗在窗口的两端都为0,杜绝了所有不连续性。
  • Hamming窗的窗口两端不为0,信号中仍然会呈现出不连续的特性。
  • Hamming窗擅长减少最近的旁瓣,但是不擅长减少其他旁瓣。

Hanning和Hamming窗都适用于对频率精度要求较高,对旁瓣要求较低的噪声测量。 

Java如何编写MongoDB java如何编写频率波_fft_09

 

上图中,蓝线为Hamming,红线为Hanning;我们可以看到加对应的窗后在频谱的体现。 

Blackman-harris窗

类似于Hanning和Hamming窗。得到的频谱有较宽的波峰,旁瓣有压缩。

Java如何编写MongoDB java如何编写频率波_fft_10

常见窗函数

Java如何编写MongoDB java如何编写频率波_fft_11

三、总结

所有时域中的信号都可表示为一组正弦波。

FFT变换将一个时域信号分解为在频域中表示,并分析信号中的不同频率成分。

在频域中显示信号有助于发现信号中的干扰、噪声和抖动。

信号中如果包含非整数个周期,会发生频率泄漏。可通过加窗来改善该情况。

数字化仪采集到的有限序列的边界会呈现不连续性。加窗可减少这些不连续部分的幅值。

没有窗叫做统一窗或长方形窗,因为加窗效果仍然存在。

一般情况下,Hanning窗适用于95%的情况。 它不仅具有较好的频率分辨率,还可减少频谱泄露。

请始终比较窗函数的性能,从而找到最适合的一种窗函数。

四、代码分享

说明:

代码设计到一些上面所讲的信号分析的公式以及定义,代码中可能会有为什么要这样做的疑问,我会尽可能的备注清楚。

波形图代码:

下面的代码展示的是一个周期内的正弦波的波形图:

package com.runlion.fourier.demo.waveform;

import java.awt.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.swing.*;

/**
 * 波形图绘制 情景分析: 数据从socket接收,一个包200数据,根据给的数据绘制波形图
 *
 * @author fanyuanhang
 * @date 2020年05月28日 13:44:24
 */
public class DrawWaveForm extends JPanel {

    private static final long serialVersionUID = 8893226355968026163L;

    public static void main(String[] args) {
        createGuiAndShow();
    }

    /**
     * 接收数据的容器
     */
    private List<Double> collect;

    /**
     * 周期采样点数
     */
    private static final Integer SAMPLE_COUNT = 64;

    /**
     * 最多保存的数据个数
     */
    private static final Integer MAX_COUNT_OF_SIZE = 65;

    private DrawWaveForm() {
        this.collect = Collections.synchronizedList(new ArrayList<>());

        // 使用一个线程模拟正弦波数据生产
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(() -> {
            for (int i = 0; i <= SAMPLE_COUNT; i++) {
                double sinValue = getSinValue(i);
                System.out.println("第" + i + "次 : " + sinValue);
                addValue(sinValue);
            }
        });
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        int w = getWidth();
        int h = getHeight();

        int xDelta = w / MAX_COUNT_OF_SIZE;
        int length = collect.size();

        // 横坐标
        g2d.drawLine(0, h / 2, w, h / 2);

        for (int i = 0; i < length - 1; ++i) {
            g2d.drawLine(xDelta * (MAX_COUNT_OF_SIZE - length + i),
                    h / 2 + normalizeValueForYAxis(collect.get(i), h),
                    xDelta * (MAX_COUNT_OF_SIZE - length + i + 1),
                    h / 2 + normalizeValueForYAxis(collect.get(i + 1), h));
        }

    }

    private static void createGuiAndShow() {
        JFrame frame = new JFrame("测试波形图");

        frame.getContentPane().add(new DrawWaveForm());

        // Set frame's close operation and location in the screen.
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setSize(500, 500);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    /**
     * 规一化y轴方向的值. 使得value在y轴的值为[0, height]之间.
     *
     * @param value y坐标
     * @param height 高度
     * @return 归一化后结果
     */
    private int normalizeValueForYAxis(double value, int height) {
        return BigDecimal.valueOf(0.5 * height * value).setScale(0, BigDecimal.ROUND_HALF_UP)
                .intValue();
    }

    /**
     * 添加元素
     *
     * @param element 元素
     */
    private void addValue(double element) {
        // 循环使用一个接收数据的空间,最好是一个循环数组所以这里我采用有界
        if (collect.size() > MAX_COUNT_OF_SIZE) {
            collect.remove(0);
        }

        collect.add(element);
    }

    /**
     * 计算正弦值
     *
     * @param i 采样点数
     * @return 对应的幅度值
     */
    private double getSinValue(int i) {
        double sin = Math.sin(i * 2 * Math.PI / SAMPLE_COUNT);
        if (BigDecimal.valueOf(sin).abs().compareTo(BigDecimal.valueOf(0.01D)) <= 0) {
            return 0D;
        }
        // 因为上面是0点,所以反向输出
        return -sin;
    }
}

波形图输出:

Java如何编写MongoDB java如何编写频率波_java_12

频谱代码:

package com.runlion.fourier.demo.spectrum;

import com.runlion.fourier.demo.util.SampleNumberUtils;
import org.apache.commons.math3.complex.Complex;
import org.apache.commons.math3.transform.DftNormalization;
import org.apache.commons.math3.transform.FastFourierTransformer;
import org.apache.commons.math3.transform.TransformType;

import javax.swing.*;
import java.awt.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 频谱生成 功能: 根据波形的离散采样生成对应的频谱波形
 *
 * @author fanyuanhang
 * @date 2020年05月28日 17:08:27
 */
public class DrawSpectrumForm extends JPanel {

    private static final long serialVersionUID = 8578537190471097637L;

    public static void main(String[] args) {
        createGuiAndShow();
    }

    /**
     * 接收数据的容器
     */
    private List<Double> collect;

    /**
     * FFT的容器
     */
    private List<Double> fftCollect;

    /**
     * 周期采样点数
     */
    private static final Integer SAMPLE_COUNT = 64;

    /**
     * 最多保存的数据个数
     */
    private static final Integer MAX_COUNT_OF_SIZE = 64;

    private DrawSpectrumForm() {
        this.collect = Collections.synchronizedList(new ArrayList<>());

        // 使用一个线程模拟正弦波数据生产
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(() -> {
            for (int i = 0; i < SAMPLE_COUNT; i++) {
                double sinValue = getSinValue(i);
                addValue(sinValue);
            }
            fftCalculate();
        });
    }

    /**
     * FFT 计算
     */
    private void fftCalculate() {
        fftCollect = Collections.synchronizedList(new ArrayList<>());

        double[] dataArray = SampleNumberUtils.normalizeSampleList(collect);

        System.out.println("FFT数组 初始化完成");

        // 创建傅里叶方法实例
        FastFourierTransformer fft = new FastFourierTransformer(DftNormalization.STANDARD);
        Complex[] result = fft.transform(dataArray, TransformType.FORWARD);
        // 将傅里叶变换前数据和变换后数据打印出来,显示前200
        for (Complex complex : result) {
            // 频谱幅度值
            double abs = complex.abs() * 2 / SAMPLE_COUNT;
            System.out.println(abs);
            fftCollect.add(abs);
        }

        fftCollect.remove(fftCollect.size() - 1);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        int w = getWidth();
        int h = getHeight();

        int xDelta = w / MAX_COUNT_OF_SIZE;
        int length = fftCollect.size();

        // 横坐标
        g2d.drawLine(0, h / 2, w, h / 2);

        for (int i = 0; i < length - 1; ++i) {
            g2d.drawLine(xDelta * (MAX_COUNT_OF_SIZE - length + i),
                    h / 2 - normalizeValueForYAxis(fftCollect.get(i), h),
                    xDelta * (MAX_COUNT_OF_SIZE - length + i + 1),
                    h / 2 - normalizeValueForYAxis(fftCollect.get(i + 1), h));
        }

    }

    private static void createGuiAndShow() {
        JFrame frame = new JFrame("测试频谱图");

        frame.getContentPane().add(new DrawSpectrumForm());

        // Set frame's close operation and location in the screen.
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setSize(800, 800);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    /**
     * 规一化y轴方向的值. 使得value在y轴的值为[0, height]之间.
     *
     * @param value  y坐标
     * @param height 高度
     * @return 归一化后结果
     */
    private int normalizeValueForYAxis(double value, int height) {
        return BigDecimal.valueOf(0.5 * height * value).setScale(0, BigDecimal.ROUND_HALF_UP)
                .intValue();
    }

    /**
     * 添加元素
     *
     * @param element 元素
     */
    private void addValue(double element) {
        // 循环使用一个接收数据的空间,最好是一个循环数组所以这里我采用有界
        if (collect.size() > MAX_COUNT_OF_SIZE) {
            collect.remove(0);
        }

        collect.add(element);
    }

    /**
     * 计算正弦值
     *
     * @param i 采样点数
     * @return 对应的幅度值
     */
    private double getSinValue(int i) {
        double sin = Math.sin(i * 2 * Math.PI / SAMPLE_COUNT);
        if (BigDecimal.valueOf(sin).abs().compareTo(BigDecimal.valueOf(0.01D)) <= 0) {
            return 0D;
        }
        return -sin;
    }

}


频谱图输出:

Java如何编写MongoDB java如何编写频率波_数字信号处理_13

采样点数补全方法:

package com.runlion.fourier.demo.util;

import org.springframework.util.CollectionUtils;

import java.math.BigDecimal;
import java.util.List;

/**
 * 采样数学工具类
 *
 * @author fanyuanhang
 * @date 2020年06月03日 17:14:19
 */
public class SampleNumberUtils {
    /**
     * 补全采样点数为2的整数次幂
     *
     * @param originalList 原始采样数据集合
     * @return 补全后集合
     */
    public static double[] normalizeSampleList(List<Double> originalList) {

        if (CollectionUtils.isEmpty(originalList)) {
            throw new IllegalArgumentException("采样点数不能为空!");
        }

        int size = originalList.size();

        double log = Math.log(size) / Math.log(2);
        double ceil = Math.ceil(log);

        // 求整数次幂
        double pow = Math.pow(2, ceil);

        int diff = BigDecimal.valueOf(pow - size).setScale(2, BigDecimal.ROUND_HALF_UP).intValue();

        // 整数次幂长度
        int newLength = BigDecimal.valueOf(pow).setScale(0, BigDecimal.ROUND_HALF_UP).intValue();

        double[] dataArray = new double[newLength];


        for (int i = 0; i < size; i++) {
            dataArray[i] = originalList.get(i);
        }

        for (int index = 0; index < diff; index++) {
            // 补0
            dataArray[index + size] = 0D;
        }
        return dataArray;
    }
}