简介:面向数字信号处理初学者的MATLAB演示程序,核心目标是对比方窗与海明窗在加窗傅里叶变换中的实际效果。程序先生成一个正弦波信号,再分别施加方窗和海明窗,调用FFT计算频域结果,直观展示不同窗函数对频谱泄漏和旁瓣抑制的影响,帮助理解有限长信号截断后产生的频域伪影。压缩包内仅含1个主MATLAB脚本(.m文件),体积仅759B,文件简短但逻辑完整,可直接运行并修改窗长参数观察频率分辨率变化,适合快速上手与课堂演示。已有456人学习该资源,尤其适合正在学习傅里叶变换与数字信号处理课程、需要动手验证窗函数原理的高校学生或自学者。通过反复运行程序并对比两种窗的频谱曲线,可以快速掌握方窗和海明窗的各自特点与适用场景,为后续信号分析时正确选择窗函数打下基础。
1. 加窗傅里叶变换到底在解决什么问题:泄漏与分辨率的取舍
做信号处理的朋友应该都遇到过这种怪事:明明信号里只有一个50Hz的正弦波,用Matlab做FFT后,频谱图却不是一根干净的谱线,而是底部拖着一大片“裙边”,旁边的频率上也有不小的幅值。更头疼的是,如果把两个频率很接近的正弦波叠在一起,FFT结果里它们糊成一团,根本数不清有几个峰。这不是FFT算法算错了,问题出在截断上——我们只能对有限长的一段信号做变换,截断等价于信号被乘了一个矩形窗。这个“加窗”动作会把纯净谱线展宽,产生频谱泄漏。想抑制泄漏,就要换更平滑的窗函数,比如海明窗。这份Matlab演示程序,就是把方窗和海明窗对同一段正弦信号的频谱影响并排画出来,让你一眼看懂泄漏到底长什么样、怎么用窗去压制它。适合刚接触FFT、想搞清楚频谱分析细节的开发者,也适合想快速验证窗函数效果的工程师。
2. 从方窗到海明窗:加窗的数学原理与Matlab里的频谱泄漏
2.1 为什么直接做FFT会“漏”:矩形窗的频域卷积效应
所谓“不做任何加窗处理”,在Matlab里其实是默认加了一个矩形窗,也就是方窗。矩形窗的时域表达式是一个长度为N的矩形脉冲,它的傅里叶变换是Dirichlet核,形状像一根中间高、两边振幅逐渐衰减的波浪线。一个无限长的正弦波频率为f0,理想频谱是位于f0处的一个冲激。当它被矩形窗截断为N点后,在频域上就相当于理想冲激谱与Dirichlet核做卷积。冲激变成了一个带有大量旁瓣的窄峰,主瓣有一定宽度,旁瓣高度只比主瓣低约13dB。那些旁瓣就是泄漏到邻近频率上的能量。
在Matlab里,直接调用fft(data)就相当于对data乘了一个矩形窗。很多人以为fft会把整段数据当成周期信号来处理,这是理解的误区。只有当信号截取的长度恰好包含整数个周期时,FFT结果才看起来“干净”,旁瓣刚好采样到零点,等于绕开了泄漏。但只要频率不是bin的整数倍,泄漏立刻出现。这就是为什么同一个正弦信号,采样点数变了,频谱形状也会变——不是算法不稳定,是矩形窗泄漏的“采样方式”变了。
2.2 海明窗的频域特性:主瓣变宽、旁瓣衰减
海明窗(Hamming窗)是加权余弦窗的一种,时域公式为:
w(n) = 0.54 - 0.46 * cos(2πn/(N-1))
它的设计目标很直接:把矩形窗的陡峭边缘变圆滑,减小截断瞬间的跳变,从而压低频谱旁瓣。付出的代价是窗口幅度谱的主瓣比矩形窗更宽,也就是说,想分辨两个频率很近的成分时,海明窗反而更吃力。海明窗的旁瓣峰值衰减能达到约-41dB,比矩形窗的-13dB低得多,能量不再“漏”到远方,而是被压缩在主瓣附近。
在Matlab中,生成海明窗用hamming(N)。要注意它旁边的兄弟hann(N)也叫汉宁窗,公式系数是0.5-0.5*cos(...),旁瓣衰减同样是-41dB量级,但海明窗的第一旁瓣略低,而汉宁窗的远端旁瓣衰减更快。很多中文资料把“海明窗”和“汉宁窗”混着翻译,其实在代码里就是两个函数、两组系数,选错的话频谱形状会有一点差异。本演示程序里的“海明窗”指的就是Matlab的hamming窗。
2.3 Matlab中生成窗函数:hamming、hann与rectwin的用法和参数
Matlab信号处理工具箱提供了窗函数生成函数,最常用的几个是:
- rectwin(N):返回N点矩形窗,也就是全1的向量。它和直接不乘窗等价。
- hamming(N):返回N点对称海明窗,系数如上述。
- hann(N):返回N点对称汉宁窗。
- blackman(N)、kaiser(N,beta)等,分别对应更复杂的窗。
这些函数默认返回列向量,生成后需要转置成与信号行向量相同的方向,或者直接用点乘。窗函数还有个坑:N是偶数时,hamming和hann的对称形式与周期形式(periodic)略有不同。默认是symmetric,适合做FIR滤波器;做频谱分析时,如果要用在FFT上,推荐用'periodic'选项,hamming(N,'periodic'),因为周期形式最后一个点几乎为零,能保持傅里叶变换的周期延拓性质,减少非整数周期泄漏。不少老版本Matlab默认不带这个选项,需要手动加。
另外,窗函数生成后,通常要检查一下它的数值范围:海明窗两端是0.08,而不是0,汉宁窗两端是0,这个差别导致了它们频谱旁瓣的细微不同。如果程序里把hamming写成了0.5-0.5*cos(2πn/(N-1)),那其实是在用汉宁窗。代码里最好直接调用库函数,避免自己手写公式出错。
3. 手写加窗傅里叶变换演示程序:正弦波加窗前后对比
3.1 程序主体结构:生成信号、加窗、FFT、绘图
这份演示程序的核心思路并不复杂:生成一个包含两个接近频率正弦波的测试信号,分别套上方窗和海明窗,然后各自做FFT,把幅度谱放在同一张图里对比。代码结构分四步:设定采样参数,生成原始信号;生成窗函数并加窗;做FFT并取幅值;绘图标注。下面是完整的可运行代码:
% 加窗傅里叶变换演示:方窗 vs 海明窗 clear; clc; close all; fs = 1000; % 采样率 1000 Hz N = 256; % 采样点数(窗口长度) t = (0:N-1) / fs; % 时间序列 % 两个频率接近的正弦波:80 Hz 和 82 Hz f1 = 80; f2 = 82; x = sin(2*pi*f1*t) + sin(2*pi*f2*t); % 方窗 = 矩形窗,直接取原始信号 x_rect = x; % 海明窗(周期形式) w_hamm = hamming(N, 'periodic'); x_hamm = x .* w_hamm'; % 做 N 点 FFT X_rect = fft(x_rect); X_hamm = fft(x_hamm); % 只取正频率部分,计算幅值 f = (0:N/2) * fs / N; % 频率轴 A_rect = abs(X_rect(1:N/2+1)) / N; A_hamm = abs(X_hamm(1:N/2+1)) / N; % 绘图:上半幅值线性谱,下半对数谱 figure('Position', [100 100 900 700]); subplot(2,1,1); plot(f, A_rect, 'b', 'LineWidth', 1.2); hold on; plot(f, A_hamm, 'r', 'LineWidth', 1.2); legend('矩形窗(方窗)', '海明窗'); xlabel('频率(Hz)'); ylabel('幅值'); title('线性幅度谱'); grid on; xlim([70 90]); subplot(2,1,2); plot(f, 20*log10(A_rect + eps), 'b', 'LineWidth', 1.2); hold on; plot(f, 20*log10(A_hamm + eps), 'r', 'LineWidth', 1.2); xlabel('频率(Hz)'); ylabel('幅度(dB)'); title('对数幅度谱(dB)'); legend('矩形窗', '海明窗'); grid on; xlim([70 90]);这段代码里,采样率fs、点数N、两个正弦频率f1和f2是最关键的三个旋钮。把f2改成85,能明显看到矩形窗下两个峰还能勉强分开;改成81,矩形窗频谱叠成一个宽包,海明窗因为旁瓣低,虽然主瓣变宽,但两个峰之间出现明显的凹分隔,反而更容易判断“这里有两个成分”。这就是窗函数选择的一句大实话:旁瓣是压下去了,但主瓣会变胖子,是否划算要看你要分离的信号有多近。
3.2 关键参数分析:采样率、点数、窗长和FFT点数
代码里N既是采样点数,也是窗长度和FFT长度,三者耦合在一起。实际工程中,这三个参数常常不是一回事:
- 采样率fs决定了频率轴覆盖范围,最大可分析频率是fs/2(奈奎斯特频率)。
- 采样点数N决定了时间窗长度,T = N/fs。时间窗越长,频率分辨率Δf = fs/N越小,越容易分辨相邻频率。
- 窗长度如果小于N,比如信号截取1000点,只对前256点加窗,那后面的部分等于被丢弃,等效分辨率还是由实际加窗长度决定,而不是N。
- FFT点数可以自己指定为任意值M,比如fft(x, 4096)。M大于N时,后面自动补零,频谱曲线被“插值”得更高清,但真实分辨率没变,补零只是让谱线更密集,有利于读峰值。
在本演示中,让三者一致能简化对比。但你要知道,如果有一天要分析一段很长的数据,不要直接用整个信号去FFT,那样计算量大、噪声多。正确姿势是分段加窗再平均,这就是Welch方法的思路。演示程序帮你把最小可运行结构摆出来了,进阶的方向一目了然。
3.3 绘图与标注:如何让旁瓣对比一目了然
在上面的绘图代码中,上半部分用线性幅度谱,适合看主瓣高度和两个峰的相对大小;下半部分用20*log10转成dB刻度,适合看旁瓣泄漏的“裙边”能压多低。dB图里矩形窗的第一旁瓣大约在-13dB处,海明窗则掉到-41dB附近,这个差距肉眼非常直观。
注意绘制dB谱时,幅值可能为0,取log会得到-Inf,所以加了eps(一个很小的正数)避免断线。这是频谱绘图里的一个常用技巧。另外,归一化幅度除以N之后,单频正弦波的峰值幅值应该接近1(对矩形窗来说),海明窗因为对信号整体能量有衰减,峰值会小于1,大约在0.51左右。如果你看到海明窗的峰值比矩形窗矮一大截,别慌,这是窗函数增益不同导致的,后面会讲怎么按窗因子修正幅值。
代码里的xlim([70 90])只显示目标频率附近的频段,避免整条频谱的DC分量和远端噪声把细节压扁。实际分析时,可以根据信号频率调整这个显示区间。legend中的“方窗”和“矩形窗”是同一个意思,Matlab没有内置“方窗生成函数”,rectwin生成的就是矩形窗。
4. 避坑指南:加窗FFT的五个常见问题
4.1 坑一:窗口长度与信号长度不一致,导致加窗后尾部突变
现象:把长度为1000的信号与一个256点的海明窗直接点乘,Matlab报错“矩阵维度必须一致”;或者不报错但结果诡异,比如频谱出现高频毛刺。
原因:窗函数长度小于信号长度,Matlab不会自动对齐,要么维度不匹配,要么如果用了某个向量乘法把前256点加权、后面原样保留,形成了阶梯状跳变,这相当于又叠了一个矩形窗,泄漏反而更严重。
解决:要么把信号截断到与窗相同长度(x = x(1:N);),要么把窗补零到信号长度(w = [hamming(N); zeros(M-N,1)])。做频谱分析时,更合理的做法是先从原始信号中取出需要分析的一段,再对这段加窗,让窗长等于分析段的长度。我的程序里直接让N统一,正是为了避免这种维度坑。
4.2 坑二:把hamming当hann用,或反之
现象:用hamming加窗后,频谱主瓣两侧出现了明显的、规律性的波纹,且旁瓣衰减达不到-41dB,看着像-31dB左右。
原因:hamming与hann的系数不同,海明窗两端不为零,汉宁窗两端为零。如果不小心写成hann,但图上标注“海明窗”,就会和理论值对不上。实际上hamming的第一旁瓣是-41dB,hann也是约-41dB,但形状略有差异;如果把对称/周期选项弄反,等效窗函数会变,也会影响旁瓣。
解决:在代码里固定用hamming(N,'periodic'),并注释说明这是周期形式,专门用于FFT。用hann时同理。不要自己手写余弦系数,除非你想复现教材上的公式。另外,可以打印第一次窗函数的值,hamming首尾是0.08左右,hann首尾是0,一眼就能分辨。
4.3 坑三:幅值谱没有归一化,单频正弦波的峰值不是1
现象:对一个幅值为1、频率为100Hz的正弦波做FFT,峰值却是25.6、32之类的数字,不同窗函数下峰值还不一样,跟预期差很远。
原因:fft函数的结果是复振幅,没有归一化。单频正弦波的峰值理论值是N/2(对正频率),所以幅度谱要除以N才对应连续傅里叶变换的幅值。加海明窗后,窗本身有增益,等效幅值还会再乘一个窗系数,不修正就会偏小。
解决:通用做法是幅度除以N。如果关心正频率分量,只取单边谱,那还要再乘2才能恢复原始正弦波幅值。我的演示里除以N是为了对比窗函数相对形状;要显示真实幅值,就需要对海明窗做增益补偿,公式是补偿系数 = N / sum(w)。这个技巧放到后面函数封装里。
4.4 坑四:FFT点数太少,频谱曲线太粗糙,峰值定位不准
现象:两个频率只差1Hz,采样率1000Hz,采样点数100,频率分辨率10Hz,频谱图上两个峰完全合并成一个,怎么看都只有一根。
原因:频率分辨率由fs/N决定,N太小时主瓣宽度、旁瓣零点间距都很大,相邻频率难以分离。这不是加窗能解决的,加窗只会让主瓣更宽。
解决:先延长采样时间,增加N,把频率分辨率降到目标频率差的一半以下。比如要分辨1Hz的间隔,至少让fs/N < 0.5Hz。然后可以再用fft(x, M)补零到M点,把谱线画得更平滑,便于读取峰值位置。注意补零不能提高真实分辨率,它只是把离散谱采样点加密了。
4.5 坑五:频率轴坐标算错,峰的位置对不上
现象:在80Hz处加了一个正弦波,FFT后峰值却出现在81Hz或79.5Hz,有时出现在接近90Hz的位置,排除谱泄漏还是不对。
原因:很多代码写f = (0:N-1)*fs/N,然后直接取前一半,这是对的,但有些人写成f = (0:N/2-1)*fs/N,少了最后一个点,或者用fftshift后没重新计算负频率轴,导致坐标偏移。更常见的是,Matlab的FFT输出下标从1开始,画图时用了0索引的数组,导致偏移一个bin。
解决:统一用f = (0:N/2)*fs/N,向量长度是N/2+1,对应fft结果的前N/2+1个点。下标1对应0Hz,下标2对应fs/N,这样不会错位。如果用了fftshift,频率轴要改成f_shift = (-N/2:N/2-1)*fs/N。我习惯每次写完都先用一个已知单频信号验证峰值位置,再分析真实数据。
5. 参数怎么调才能看清两个相邻频率:频率分辨率与零填充
5.1 频率分辨率的决定因素:采样点数与采样率
频率分辨率Δf = fs / N,这是DFT能区分两个频率成分的最小间隔。举个例子,fs=1000,N=256,那么Δf≈3.906Hz。如果一个信号同时含80Hz和82Hz的两个分量,差只有2Hz,小于Δf,理论上任何窗都没法把这两个峰值独立分开。很多文章说加窗能提高分辨率,这是个误导:加窗改变的是旁瓣泄漏和主瓣宽度,但不改变DFT频率网格的间隔。想真正提高分辨率,唯一诚实的办法是增大N,也就是采集更长时间的信号。
代码里的测试信号故意选了80Hz和82Hz,Δf是3.906Hz,两个频率差2Hz,其实低于一个bin。这种情况下,矩形窗频谱会呈现一个宽峰,峰值出现在80和82之间的某个位置;海明窗因为主瓣更宽,结果可能更像一个缓坡。要想让两个峰“长”出来,就应该把N增大到512或1024,让Δf降到1Hz以下。改一下代码里的N,就能直观感受到“时间窗越长,分辨率越高”这个硬道理。
5.2 用零填充(fft的N点)平滑谱线,但不能提高真实分辨率
Matlab的fft(x, M)允许指定FFT点数M。当M > length(x)时,系统自动在离散时间序列末尾补零。很多人以为补零能“提高分辨率”,做到一半就发现:原来两个靠得很近的峰,补零后依然黏在一起,只是曲线变圆润、峰顶变得光滑。这是因为补零没有增加任何真实数据信息,它只是在DFT的积分值之间做了sinc插值,让频谱曲线看起来更连续,频率网格加密了,但主瓣宽度、旁瓣位置仍然由原信号长度决定。
实际调试中,补零很有用:当你要精确测量峰值所在频率时,先补零到4096点,再找峰值的最大值,得到的位置比直接N点FFT更接近真实值,误差可以降低到零点几个bin。但如果你需要用两个峰之间的凹谷证明“存在两个分量”,补零帮不了忙,必须提高N。我的习惯是:先粗测用补零的FFT找峰位置,再确认分辨率时用原始长度的FFT看谷底深度。
5.3 加窗后主瓣宽度对分辨相邻频率的影响:方窗 vs 海明窗对比实验
把上面的程序稍作修改,让fs=1000,N=512(分辨率1.953Hz),两个频率设为80Hz和82Hz,差2Hz,刚好略大于一个bin。运行后你会看到:
- 矩形窗频谱中,两个峰虽然能看出起伏,但谷底抬高,几乎到了峰的一半高度,很难说清是几个峰。
- 海明窗频谱中,由于旁瓣被压制,谷底明显下降,两个峰的轮廓更清晰。虽然主瓣变宽,但旁瓣泄漏不再把谷底填满,所以小频率差的信号反而用海明窗更容易分辨。
这个结论反直觉:主瓣宽了,分辨率看似变差了,但实际检测能力却提升了。原因是矩形窗的旁瓣太高,相邻峰的旁瓣能量互相“串扰”,把谷底抬起来了。海明窗牺牲了主瓣宽度,换来更低的旁瓣,所以在信噪比适中的情况下,它的两窗分辨能力往往优于矩形窗。这也是频谱分析里一个经典权衡:没有免费的午餐。
6. 进阶技巧:把加窗封装成函数,并引入频谱归一化
6.1 自定义函数:输入信号、窗类型、FFT点数,输出幅值谱
把演示代码整理成一个可复用函数,能省下不少重复功夫。函数逻辑很简单:递增信号长度、选择窗类型、加窗、做FFT、生成频率轴和幅值谱。下面是函数实现:
function [f, amp] = windowedFFT(x, fs, winType, NFft) % windowedFFT 加窗FFT分析 % x: 输入信号(列向量或行向量均可) % fs: 采样率 % winType: 'rect' / 'hamming' / 'hann' % NFft: FFT点数(可选,默认等于信号长度) x = x(:); % 强制转成列向量 N = length(x); if nargin < 4 NFft = N; end switch winType case 'rect' w = rectwin(N); case 'hamming' w = hamming(N, 'periodic'); case 'hann' w = hann(N, 'periodic'); otherwise error('未知窗类型'); end xw = x .* w; X = fft(xw, NFft); % 单边谱 half = floor(NFft/2); f = (0:half) * fs / NFft; amp = abs(X(1:half+1)) / sum(w); end这段代码把之前的精髓都放进去了:强制列向量避免维度问题;用sum(w)做幅度归一化补偿窗增益;FFT点数NFft单独可调。调用方式非常简单:
[f_rect, a_rect] = windowedFFT(x, fs, 'rect'); [f_hamm, a_hamm] = windowedFFT(x, fs, 'hamming');6.2 幅值谱归一化方法:单频正弦波的幅值恢复
为什么要除以sum(w)而不是除以N?窗函数加在信号上,等效于改变了信号的能量。比如海明窗,N点相加的和为0.54N - 0.46sin(2πN/(N-1))/(2π/(N-1)),对大的N约等于0.54N。直接除以sum(w),等于把窗带来的衰减补偿回去,这样单频正弦波的幅值谱峰值几乎精确等于该正弦波的幅值。矩形窗的sum(w)=N,所以除以sum(w)和除以N是同一回事。
如果你只关心相对频谱形状,不关心绝对幅值,也可以只除以N。但一旦要比较不同窗函数下同一个信号的幅值,或者要测谐波幅值,就必须用sum(w)归一化。这是不少老手都会忽略的细节:我见过有人用海明窗测100Hz正弦波,峰值只有0.54,没做补偿,就直接下结论说信号幅值变小了。
6.3 一个完整示例:对比三种窗的峰值泄漏比
最后给一个能直接跑出结果的示例,用于总结三种窗的表现。它生成一个频率为非整数bin的正弦波,比如N=256,fs=1000,频率f0=95.3Hz,这样泄漏最强。分别用矩形窗、海明窗、汉宁窗,计算主瓣峰值与最大旁瓣泄漏值的比值。
fs = 1000; N = 256; t = (0:N-1)/fs; f0 = 95.3; % 故意选择非整数bin频率 x = sin(2*pi*f0*t); [f_r, a_r] = windowedFFT(x, fs, 'rect'); [f_h, a_h] = windowedFFT(x, fs, 'hamming'); [f_hn, a_hn] = windowedFFT(x, fs, 'hann'); % 找主瓣峰值及旁瓣最大泄漏(跳过主瓣附近3个点) [~, i_r] = max(a_r); [~, i_h] = max(a_h); [~, i_hn] = max(a_hn); skip = 4; leak_r = max([a_r(1:i_r-skip), a_r(i_r+skip:end)]); leak_h = max([a_h(1:i_h-skip), a_h(i_h+skip:end)]); leak_hn = max([a_hn(1:i_hn-skip), a_hn(i_hn+skip:end)]); fprintf('矩形窗 峰值泄漏比: %.1f dB\n', 20*log10(leak_r/max(a_r))); fprintf('海明窗 峰值泄漏比: %.1f dB\n', 20*log10(leak_h/max(a_h))); fprintf('汉宁窗 峰值泄漏比: %.1f dB\n', 20*log10(leak_hn/max(a_hn)));运行后,矩形窗的泄漏比大约在-13dB到-17dB之间,海明窗在-41dB到-45dB之间,汉宁窗在-31dB到-41dB之间。这个数值会随f0与bin的偏移量变化,但规律稳定:加窗能把旁瓣压低20到30dB。我第一次跑这个示例时,直接用矩形窗的峰值去和海明窗比,忘了做归一化,结果海明窗的峰值矮了一半,还以为程序写错了。后来把sum(w)归一化加上去,数据才漂亮起来。
从那以后,我每次做频谱分析,都会强制走一遍这个流程:先看分辨率够不够,再选窗类型,最后确认幅值归一化方式。尤其是要对比不同窗函数时,归一化那一步绝不能省。希望这个加窗演示程序和它的函数化封装,能帮你少踩几个我当年踩过的坑。
本文还有配套的精品资源,点击获取