资讯动态

傅里叶变换频谱图从原理到实战:FFT加窗与Python实现

发布时间:2026/10/19 4:41:22 来源:尧图企业网站定制
简介傅里叶变换是信号处理、音频分析与通信系统中最常用的数学工具频谱图则能直观呈现信号在各频率上的能量分布情况。这份压缩包是《傅里叶变换频谱图你必须知道的》视频配套的代码与数据面向正在学习傅里叶变换、MATLAB基础操作的中级学习者可直接用于复现视频中的频域分析示例与实践。包内共2个文件包含一个MATLAB脚本用于构造信号、执行快速傅里叶变换并绘制幅度谱以及一个MATLAB数据文件提供可载入的示例数据压缩包整体约128KB便于下载后随时运行。目前已有2130人学习下载适合配合视频或教材通过运行代码亲手验证从时域到频域的转换过程。借助这些文件读者可以掌握快速傅里叶变换函数的基本用法、频谱坐标计算和一维频谱图的绘制方法为后续音频、图像等频域分析打下扎实基础。1. 傅里叶变换频谱图这套配套代码和样本把频域分析从玄学变成可复现的实验傅里叶变换频谱图这东西做音视频方向的同学大概率都遇到过这种情况看着谱图上的峰和谷能猜个大概但真要解释为什么这里有个峰、为什么底噪这么高、窗口长度到底怎么选就说不清了。这套《傅里叶变换频谱图你必须知道的》视频配套代码与数据解决的就是这个问题。它不是泛泛讲原理而是给了你现成的 Python 脚本和 WAV 样本把合成信号、FFT 计算、加窗、频谱绘制整条链路铺开改一个参数就能看到频谱图的变化。刚接触数字信号处理、想靠动手实验啃下频谱图的初学者以及做音视频处理但一直没时间系统捋一遍 FFT 细节的开发者拿到手先跑一遍再对着参数改频域分析里那些“玄学”就都有了落点。2. 资源拆包代码与数据文件到底怎么分工合作2.1 目录结构从配置到输出每个文件在一个环节只干一件事这类视频配套的代码资源最怕的是把所有逻辑塞进一个脚本改起来无从下手。这套资源的常见组织方式是把配置、信号生成、频谱分析、入口脚本分开拿到手你会看到类似下面的结构文件/目录职责说明config.py全局参数采样率、FFT 点数、窗函数类型、输出目录core/signal_gen.py信号生成合成多频正弦波、白噪声读取 WAVcore/spectrum_analyzer.py频谱分析FFT 计算、单边幅度谱、峰值提取main_fft_demo.py入口脚本串联整个流程支持命令行参数data/样本数据单音、和弦、噪声、语音片段等 WAV 文件output/生成结果跑完后输出的频谱图和峰值记录拿到资源后建议先别急着运行把 config.py 打开看一眼。集中管理参数是这个资源做得比较舒服的地方改动值通常只涉及这一个文件fs 44100 # 采样率决定频率轴显示上限 fft_size 2048 # FFT 点数调大则频率分辨率更高 window hann # 窗函数可选 hann / hamming / boxcar output_dir output # 频谱图输出目录这里的几个参数互相牵连。采样率 fs 决定频率轴能显示的最高频率奈奎斯特频率就是 fs/2FFT 点数 N 决定频率分辨率fs/N 是每个频点之间的间隔窗函数类型决定频谱泄漏的程度。窗口开大了时间分辨率变差开小了频率分辨率变差配合视频里讲的“不确定性原理”在 config 里改一版跑一版体会最深。从资源分类看它被归在音视频方向的综合资源里说明不只适合写代码的人用做后期、做语音交互的也能拿它当参考比如你想确认某个底噪频段到底从哪来的把素材丢进去跑一遍频谱比用耳朵听靠谱得多。如果你手里还有配套的文档资料配合 config.py 的注释一起看改参效率会高很多。2.2 数据文件说明为什么选这些样本而不是随便放一段音乐data 目录下的样本常见的有这么几类单音正弦波、包含基频与谐波的和弦、白噪声片段、以及一段带底噪的语音或音乐素材。用合成信号而不是直接放一首歌是刻意为之。真实音乐的频谱太复杂峰和峰叠在一起新手很难把频谱图上的特征和理论公式对应起来合成信号则干净得多440Hz 就是 440Hz谱线位置一眼能对出来适合用来验证“我的 FFT 没写错”。这几类样本的用途可以这样区分单音样本用来验证频率轴换算和幅度归一化和弦样本用来观察“多个频率分量的叠加在频谱上是线性的”两个峰各自独立不会互相干扰白噪声样本用来观察频域底噪的形态配合不同窗函数看噪声谱的变化带底噪的语音样本则最接近真实场景用于综合练习你能清楚看到语音能量集中在中低频段而高频段基本是噪声的天下。读这些 WAV 时推荐用 scipy.io.wavfile 读取它返回采样率和 int16 数组直接转 float 后再做分析不容易踩格式坑。一个容易忽略的细节是采样率统一。如果资源里既有 44.1kHz 又有 22.05kHz 的样本分析互相对比时频率轴的刻度会不一致看起来像“同一段音频在不同设置下频谱不一样”。我一般会在跑主脚本前先写个小循环把 data 目录下所有 wav 的采样率打印一遍确认一致再进入分析流程。这样后面所有频谱图共享同一个频率轴基准对比才有意义。3. 核心模块拆解一个信号从时域到频谱图的完整链路这一章是整套代码里信息量最大的部分。我把链路拆成三段讲先合成信号再做 FFT 和频率轴映射最后加窗并绘制频谱图。每一段对应一个可以独立运行的小片段配着视频看逻辑非常清楚。3.1 合成信号用代码生成“看得懂”的频谱视频里演示的第一个实验通常是先造一个已知频率的信号保证后面频谱图上的每个峰你都有理论值可对照。合成信号的脚本长这样import numpy as np fs 44100 # 采样率单位 Hz决定频率分析范围上限 duration 3.0 # 信号时长秒越长频率分辨率越高 t np.arange(int(fs * duration)) / fs # 生成时间轴步长是 1 个采样周期 f1, a1 440.0, 1.0 # 基频 440Hz幅度 1.0 f2, a2 1320.0, 0.5 # 三次谐波 1320Hz幅度 0.5 signal a1 * np.sin(2 * np.pi * f1 * t) \ a2 * np.sin(2 * np.pi * f2 * t) # 保存为 16bit WAV方便后续主脚本统一走读文件路径 pcm (signal * 32767).astype(np.int16)这里最关键的是采样率和时间轴的关系。np.arange(int(fs * duration)) / fs 生成的是以 1/fs 为间隔的时间序列长度正好是 fs*duration 个采样点。做音视频的人应该都清楚fs/2 以上的频率没法从离散信号里还原所以两个分量都选在 20kHz 以内留足余量。第二个分量选 1320Hz 不是随手写的它是 440Hz 的三倍频对应乐理上基频的第三次谐波这种整数倍关系在频谱图上会呈现为等间距的谱线非常直观。信号归一化到 float 范围 [-1, 1] 之后再乘 32767 转 int16是为了让生成的 WAV 在播放器和分析脚本里表现一致。如果直接存 float32 的 wav有些读取库默认认为范围是 [-1,1]有些认为是 [-32768, 32767]来回折腾很烦。这里有个小技巧保存前用 scipy.io.wavfile.write 写文件它会按传入的 dtype 决定量化位数int16 是最稳的选择。3.2 FFT 计算与频率轴映射从时域到频域最关键的一步拿到信号之后就要做离散傅里叶变换了。视频里强调过一句FFT 输出的是一堆复数不是直接能画图的幅度值。很多人在这一步栽跟头所以代码里要把取模、取单边谱、归一化分开写清楚N 4096 # FFT 点数决定频率分辨率 fs/N frame signal[:N] # 取前 N 个采样点做一帧 win np.hanning(N) # 加 hann 窗抑制频谱泄漏 spectrum np.fft.fft(frame * win) # 复数频谱 freqs np.fft.fftfreq(N, d1.0 / fs) # 每个频点对应的实际频率 half N // 2 # 单边谱只取正频率部分 amplitude np.abs(spectrum[:half]) * 2.0 / N先说 spectrum 这一步。np.fft.fft 不做任何归一化它返回的复数数组里每个元素的模长和信号的幅度不是一个量纲如果你直接 plt.plot(np.abs(spectrum))纵轴数值会比真实幅度大 N/2 倍左右看起来像几百甚至上千。所以 amplitude 那里要乘 2.0 再除以 N除以 N 是因为 FFT 的反变换归一化系数是 1/N不除回去能量就虚高乘 2 是因为我们扔掉了负频率那一半要把那部分能量补回来。再解释 freqs。np.fft.fftfreq(N, d1/fs) 是标准做法d 是采样周期也就是相邻采样点的时间间隔。如果你手写频率轴等价写法是 np.arange(N) * fs / NN 和 fs 配合得到每个 bin 的中心频率。这个换算是最容易错的地方写错除以 N 或者把 fs 漏掉峰值位置直接偏到十万八千里外。顺带提一句 FFT 点数的影响N4096、fs44100 时频率分辨率约 10.77Hz。如果信号里有 440Hz 和 1320Hz 两个分量这个分辨率足够分开但如果两个频率只差 5HzN 就得加大到八千以上或者用 zero-padding 把频谱插值得更光滑。提示np.fft.fftfreq 的第一个参数是点数 N不是采样率 fs写反了频率轴会错得毫无规律。3.3 窗函数与频谱绘制让频谱图不再“漏”能量加窗的作用视频里讲得很细。直接对信号做矩形截断等价于乘了一个矩形窗而矩形窗的频域形状是带旁瓣的信号频率若不在整数个 bin 上能量就会漏到相邻频点去。hann、hamming 这些窗牺牲了主瓣宽度换来了更低的旁瓣让频谱图干净不少。下面这段代码把加窗、能量校正和绘制放在一起# 加窗后信号总能量会下降需要用窗的能量做幅度校正 corr N / np.sum(win) # 典型值接近 2具体取决于窗型 amplitude_corrected amplitude * corr import matplotlib.pyplot as plt plt.figure(figsize(10, 4)) x_axis freqs[:half] y_axis 20 * np.log10(amplitude_corrected 1e-12) # 转 dB避免 0 值取对数报错 plt.plot(x_axis, y_axis) plt.ylim(-100, 0) plt.xlabel(Frequency (Hz)) plt.ylabel(Amplitude (dB))为什么纵轴要转成 dB因为线性坐标下基频 440Hz 那个 1.0 幅度的峰和 0.5 幅度的谐波虽然只差一倍但画出来视觉上可能差很多转成 dB 后1.0 是 0dB0.5 是 -6dB每次衰减 6dB更符合人耳对响度的感知也更容易观察旁瓣衰减速度。加 1e-12 是为了防止纯 0 值进去 np.log10 变成负无穷导致图像在零点处断裂。关于幅度校正 corrhann 窗的系数平均值约 0.5所以 N / np.sum(win) 约等于 2。如果你看到频谱峰值比理论值小了约一半基本就是没做这一步校正。plt.ylim(-100, 0) 的下限可以结合数据动态看如果底噪确实很低压到 -120dB 也没问题但建议保持固定这样对不同窗函数的效果对比才是同一尺度。4. 环境与运行三步把频谱图跑出来并核对结果4.1 环境准备依赖装少一点排查成本就低一点这套代码的数据链路很简单依赖不多。推荐 Python 3.8 以上版本核心只需要 numpy、matplotlib、scipy 三件套。建好虚拟环境后直接安装python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate pip install numpy scipy matplotlib不推荐一口气装一大堆数据分析全家桶。依赖越少跑不起来时排查范围就越小。scipy 在这里主要用来读 WAVscipy.io.wavfile.read 返回的 int16 数据可以直接转 float处理单声道样本最稳。如果你手头只有 mp3 或 m4a 格式的音频再额外装一个 librosa 负责解码正常流程用不到它。如果 pip 下载慢或者超时先确认网络状态再用 pip 的 -i 参数指定一个镜像源安装通常能解决。遇到依赖版本冲突时先看报错信息指向哪个包常见情形是 numpy 版本过新导致 scipy 的二进制接口不兼容这时只要把 numpy 降一个主版本再装 scipy 就能跑通。这类问题属于环境问题而不是代码问题别急着改代码。4.2 跑通主脚本命令行参数与输出文件说明主脚本 main_fft_demo.py 支持命令行参数方便切换样本和 FFT 参数。一个典型调用是python main_fft_demo.py --data data/chord.wav --fft-size 2048 --window hann --output output/chord_spectrum.png常用参数如下参数取值示例作用--datadata/chord.wav指定要分析的音频文件--fft-size2048FFT 点数建议设成 2 的幂改大则频率分辨率变高--windowhann / hamming / boxcar窗函数类型boxcar 即不加窗--outputoutput/chord_spectrum.png频谱图保存路径--save-peakspeak_freqs.txt可选把检测到的峰值频率写入文本跑完后先核对两件事。第一控制台或 peak_freqs.txt 里输出的峰值频率是否接近理论值——chord.wav 里面应该有三个明显的峰分别对应基频和两个泛音误差在个位数 Hz 以内都算正常。第二看 output 目录下生成的频谱图纵轴峰值是否接近 0dB对应幅度 1.0 的信号如果整体低了大约 6dB多半是窗函数校正那一步没生效。如果 --fft-size 比当前帧长度还大常见做法是脚本自动补零再变换跑之前瞄一眼控制台有没有相关日志主脚本一般不会强行改你的参数而是给出警告后继续运行。提示共振峰检测不要只看频谱图的最高峰先按频率区间分段找局部最大值否则低频强能量会把高频小峰全遮蔽掉。这套代码里 --save-peaks 输出的就是分段峰值的结果。4.3 用 Jupyter 逐格运行观察中间变量的变化如果你不想在命令行里反复试参数把核心分析代码放到 Jupyter Notebook 里一行一行跑效率更高。视频配套资源里常见附带一个 demo_notebook.ipynb里面把信号生成、FFT、绘制三个步骤拆成独立单元格。核心思路是每跑一个格子就打印一下当前变量的 shape 和取值范围确认没有 NaN 或全零数组再继续往下%matplotlib inline spectrum np.fft.fft(frame * win) print(spectrum.shape, spectrum.dtype) # (4096,) complex128 print(np.abs(spectrum[:10])) # 前几个点的幅度看看是否为 0 附近这种调试习惯非常管用。FFT 出来的结果如果不是预期的 complex128说明输入的 frame 可能不是一维数组前几个频点幅度如果异常大说明信号里有直流分量没去除。先确认中间变量再绘图能省下不少对着图形猜原因的时间。如果想对比不同窗函数的效果可以在一个单元格里把 hann、hamming、boxcar 三种窗的频谱画在同一张图上用三根不同颜色的线区分。视频里讲窗函数那一段时这个对比图是最直观的辅助材料。对比时记得固定其他所有参数保持 fft_size、输入信号完全一致唯一变量是窗函数这样你才能确定差异来自窗而不是别的因素。5. 避坑手册频谱图调试中四条血泪经验5.1 现象谱线周围出现一大片“裙边”主峰旁边全是衰减振铃第一次跑通时最容易看到的现象明明只有 440Hz 一个频率频谱图上却从 400 到 480 铺开一串锯齿状衰减的副瓣。原因在于默认的截断方式是矩形窗矩形窗的频域响应旁瓣衰减很慢信号频率没有落在整数 bin 上时泄漏尤其明显。解决方法是把窗函数从默认的 boxcar 换成 hann 或 hamming旁瓣衰减能降到 40dB 以上主峰立刻干净很多。如果换窗后还想更光滑就增大 --fft-size 到 8192或用 zero-padding 把频谱采样点加密但注意 zero-padding 只是让曲线更平滑并不会提高真实的频率分辨率。想确认这个问题到底是窗引起的还是信号本身多频叠加把信号长度改成恰好包含整数个信号周期此时截断点刚好落在正弦波的零相位处矩形窗的旁瓣会明显收敛裙边几乎消失。用这个对照实验你就能把“窗的选择”和“信号本身特性”两个因素剥离开。5.2 现象生成的是 440Hz 正弦波峰值却出现在 441Hz 或 439Hz我第一次复现时峰值跑到 441Hz半天没想明白为什么整数频率会偏掉。原因出在频率分辨率上fs44100、N4096 时频率分辨率约 10.77Hz440Hz 并不在任何一个 bin 中心FFT 只能把它分配到最近的 441Hz 上。这不是写错是离散傅里叶变换的固有特性。解决方法是先确认频率轴计算没有 bug用 np.fft.fftfreq(N, d1/fs) 代替手写公式排除低级错误若仍然对不上接受这个量化误差报告峰值频率时四舍五入到整数就好。真要减少误差拉大 N 是唯一正路。一个快速验证方法生成 440Hz 单音后打印 freqs[argmax(amplitude)] 和理论值 440 的差若差值小于 fs/(2N)说明行为正常不用再纠结那 1Hz。很多音高检测、自动定调的工具会做二次插值来修正这个偏差但那属于进阶话题演示阶段不必引入。5.3 现象信号幅度明明是 1.0频谱峰值却有 200 多这个坑几乎每个人都会踩。numpy 的 FFT 不做归一化1.0 幅度的正弦波变换后幅值大约是 N/2再取绝对值后纵轴数值大得离谱看起来像是“数据错了”。原因就是缺了 3.2 节里 amplitude np.abs(spectrum[:half]) * 2.0 / N 这一步除法。解决方法是先确认已经取了单边谱并除以 N如果用了窗函数还要再乘上 N / np.sum(win) 做能量补偿。有个快速核对方法跑一个已知 1.0 幅度单音样本频谱峰值应接近 0dB差 6dB 就是只除了 N 没乘 2差 3dB 就是窗校正没加。这个坑最容易在改代码时复发因为移植到别的项目时别人可能把 amplitude 计算直接写成了 np.abs(fft_result)看起来“简化”了实际幅度全变了。我现在的习惯是任何有关频谱幅度的代码必须显式写出乘 2 除 N 和窗校正宁可多写三步也不省事。5.4 现象低频段整片抬高像一座“底噪山”如果信号本身带直流偏置0Hz 处会有一个能量巨大的峰它的旁瓣把 0-200Hz 整段都抬高了看起来像是底噪异常。原因有两个层面一是录音设备或生成环节引入了直流分量二是 FFT 对直流分量特别敏感旁瓣泄漏直接污染低频段。解决方法是分析前先做去均值signal signal - np.mean(signal)这个操作对绝大多数音频分析场景是无损的因为音频信号本来就该围绕 0 波动。去完均值再看低频底噪通常会恢复到正常水平。排查顺序建议固定下来先看 0Hz 处的幅度值如果远大于 1Hz 处幅度说明直流分量没处理再检查是否去均值最后才考虑换窗函数。如果你没有去均值就急着调窗、加大 N会发现治标不治本根源在直流分量不在泄漏本身。资源里的语音样本一般没有直流偏置但你自己录的素材经常有所以这段经验到二次开发时会非常关键。6. 二次开发把演示脚本改成自己的频谱分析组件看完视频、跑通示例之后接下来的需求通常很具体我要分析自己手头的一批音频不是那几个演示样本。这种时候最忌讳直接改 main_fft_demo.py 的代码因为主脚本里绘制逻辑、文件读写、参数解析全都混在一起改一个取值就可能动到别的地方。我的习惯是先做一次函数化重构把频谱分析逻辑从入口脚本里抽出来变成可以 import 的组件class SpectrumAnalyzer: def __init__(self, fs, fft_size2048, windowhann): self.fs fs self.fft_size fft_size self.win np.hanning(fft_size) if window hann else np.ones(fft_size) def analyze(self, frame): if len(frame) self.fft_size: raise ValueError(frame 长度不足先做补齐或减小 fft_size) amp np.abs(np.fft.fft(frame[: self.fft_size] * self.win)) amp amp[: self.fft_size // 2] * 2.0 / self.fft_size amp * self.fft_size / np.sum(self.win) # 窗能量校正 freqs np.arange(self.fft_size // 2) * self.fs / self.fft_size return freqs, amp重构完的类只干两件事分析、返回频率轴与幅度数组绘制交给调用方决定用什么样式。这样你再想去分析一批真实文件只需要在外面套一个循环读 WAV、调 analyze、记录峰值完全不用碰内部逻辑。另外一个值得做的扩展是支持实时输入比如从麦克风拿一段 PCM 流按 1024 或 2048 个采样点切帧边录边刷新频谱。方向虽然从“离线分析”变成“实时显示”核心还是这个类只是把“读文件”换成“读缓冲区”。从那以后我每次拿到一段不明来历的音频都会强制先跑一遍无参数分析的版本固定 fft_size 到 2048用 hann 窗只看原始频谱长什么样再决定要不要加窗、要不要滤波、FFT 点数该取多少。顺序反了效果就是对着一个被窗和参数掩盖过的结果瞎调半天。这套配套代码最大的价值不是那几个演示图而是它逼着你把“先看原始频谱再定制参数”这个流程练成肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑