简介:万永革《数字信号处理的MATLAB实现》高清扫描版,是一本以大量实例和可运行程序贯穿始终的数字信号处理实践教材,主要面向数理基础相对薄弱、重视动手能力的读者,也适合本科生、研究生与工程技术人员作为案头参考。压缩包内共476个文件,整体约54.24MB,以364个.m脚本为主体,附带.dat/.mat数据文件、.txt/.doc说明文档、.tex源文件及高清PDF,文件类型覆盖程序、数据、文档三类,配合完整目录结构,方便按章节定位和调用。书内全部例题的MATLAB实现及其配套数据均已收录,理解算法思路后只需修改参数即可复用至实际工程场景,能有效降低上手门槛。目前已有530人学习下载,适合希望在MATLAB环境下系统掌握数字信号处理应用方法的读者。
1. 数字信号处理的MATLAB实现:先跑通源码包,再谈理解
如果你想找的是数字信号处理的MATLAB实现,而不是又一篇讲傅里叶变换定义的文章,这份资源值得下。它在做的事是把离散信号生成、差分方程求解、FFT频谱分析、FIR/IIR滤波器设计、功率谱估计拆成一段段能直接运行的脚本,让理论公式第一次变成可观察的曲线。它适合三类人:期末复习时想验证概念的本科生、课设前需要快速搭框架的同学、以及做信号处理Demo时不想重写基础函数的从业者。核心价值不是提供答案,而是告诉你MATLAB里那些函数真正要什么参数。我的结论比较直接:跑通它,比把书抄一遍更接近数字信号处理。
2. 拆资源包:按“时域→频域→滤波器”链路读脚本
资源包里的脚本数量看上去不少,但真正需要精读的不会超过二十个。按信号处理链路拆开,你会发现它无非三块:时域系统、频域变换、滤波器设计。时域脚本负责让你把离散信号和系统响应“看得见”,频域脚本负责把谐波成分找出来,滤波器脚本负责把不要的频率压下去。下面逐个拆,每一步都给出能直接复现的写法。
打开资源包后,我建议不要从第一行顺序读到结尾,而是按“时域→频域→滤波器”的顺序挑脚本看。先读时域基础,因为filter的用法会贯穿整个滤波器章节;再读FFT,因为频谱图是验证所有滤波效果的依据;最后读滤波器,这时候你已经知道横轴怎么换算、幅值怎么取单边,不会被图上奇怪的刻度带偏。
2.1 时域篇:信号生成与差分方程脚本怎么读
时域基础部分的脚本一般负责生成单位脉冲、阶跃、指数衰减序列,再用filter求解差分方程。很多同学一开始只盯着结果图,我却建议先看y = filter(b, a, x)这一行,因为它是后面所有滤波例程的地基。filter函数并不神秘,它按系统函数的分子分母系数逐点递归计算,输出长度默认和输入一样。下面这段简化脚本,就是这类例程最常见的骨架。
n = 0:15; % 离散时间轴,从0到15 x = (0.8).^n; % 指数衰减输入序列 b = 1; % 系统函数分子系数 a = [1, -0.8]; % 分母系数:y[n] - 0.8*y[n-1] = x[n] y = filter(b, a, x); % 求解差分方程,得到响应序列 subplot(2,1,1); stem(n, x, 'filled'); title('输入 x[n]'); subplot(2,1,2); stem(n, y, 'filled'); title('系统响应 y[n]');逻辑说明:filter不是狭义“滤波”函数,而是按差分方程递归求解的通用工具。b和a来自传递函数H(z)=B(z)/A(z),这里b=1表示输入只直接作用于当前输出,a=[1,-0.8]表示当前输出还依赖上一时刻输出乘以0.8。参数说明:subplot把两幅图纵向排开,stem画离散序列;如果这里改成plot,会把点连成线,掩盖离散信号的跳变特征。
读这类脚本时,我一般会把系数改成[1,-0.5]、[1,0.8],重新运行看波形变化。改分母系数是在验证系统是否稳定:极点接近单位圆时,序列衰减变慢;极点一旦超出单位圆,输出会指数发散。资源包里的例程通常只给稳定参数,但课设答辩时老师最爱问“系数变了会怎样”,所以动手改一改比背结论有用。
2.2 频域篇:FFT 脚本的取数与横轴换算
频谱分析脚本是资源包里最能拉开差距的部分。fft函数本身只有一行,但后续的取数方式直接决定图能不能看。常见错误是把fft结果直接plot(abs(X)),横轴停留在“点序号”,没法标Hz。另一个常见问题是忘了实信号频谱的镜像关系,导致正弦波幅值显示成真实值的一半。下面这段脚本是频域例程的标准起手式。
N = 512; % FFT点数,决定频率分辨率 fs = 1000; % 采样率,单位Hz t = (0:N-1)/fs; % 时间轴,单位秒 x = sin(2*pi*50*t) + 0.3*sin(2*pi*380*t); X = fft(x); % 复数频谱,长度等于N f = (0:N-1)*fs/N; % 把点序号换算成物理频率 plot(f, abs(X), 'LineWidth', 0.8); xlabel('频率 (Hz)'); ylabel('|X(f)|');这段代码里的频率换算必须记住:fft返回的复数数组,每个下标对应的数字频率是(idx-1)*fs/N,所以频率轴写成(0:N-1)*fs/N。画实信号频谱时,正负频率对称,一般只取前N/2+1个点做单边谱;如果直接显示全部,50Hz正弦的峰值是0.5而不是1。修整方式很简单:X(1)是直流,X(N/2+1)是奈奎斯特频率,这两个点不用乘,其余部分乘2。
如果要观察负频率,比如调制信号,可以在fft之后加X=fftshift(fft(x)),频率轴改成f=(0:N-1)*fs/N - fs/2。fftshift把零频移到中间,正负频率对称结构直接可见。这个函数在资源包的对称性示例里不一定单独出现,但读“信号调制”类脚本时总会遇到,提前掌握能少卡壳一次。
关于补零也要多说两句:fft(x,2048)是把序列补零到2048点做变换,曲线更平滑,但两个频率靠得近时不一定会分开,因为真实频率分辨率受原始信号长度限制。资源包如果用了补零,你需要在报告里写清“补零提高的是插值密度,不是真实分辨率”。
2.3 滤波器篇:设计函数与filtfilt的边界
滤波器例程是资源包的结尾部分,也是课设最常直接取用的部分。设计阶段一般调用fir1或butter,使用阶段常见filter,讲究一点会用filtfilt。设计时最需要算准的是归一化截止频率Wn,它是物理截止频率除以奈奎斯特频率之后得到的无量纲数。下面这段脚本代表FIR低通的标准流程。
fs = 1000; % 采样率 fc = 100; % 截止频率,单位Hz Wn = fc/(fs/2); % 归一化截止频率 b = fir1(32, Wn, 'low'); % 32阶FIR低通 x = randn(1, 1000); % 白噪声信号 y = filter(b, 1, x); % 普通滤波这里Wn=0.2,fir1内部在设计数字频率0.2π处的低通滤波器。阶数32表示滤波器有33个抽头,阶数越高过渡带越窄,但输出延迟和计算量同步上升。使用filter时注意起始瞬态:因为滤波器初始状态为零,输出开头几十个点受暂态影响,展示波形时最好截掉前一段。
如果资源包里使用了filtfilt,它的作用是先正向滤波再反向滤波,从而抵消相位偏移,让输出看起来没有延迟。代价是非因果特性,不能用在实际实时系统中。做课设演示时,我会把filter和filtfilt两种结果都跑一遍,报告里注明差异,这样的细节比单纯贴代码更容易得分。
2.4 随机信号与功率谱:pwelch 为什么不能乱用窗
功率谱估计脚本在资源包最后一段经常出现,用的通常是pwelch。它把长序列分成多段,每段加窗、做FFT、再对幅值平方做平均。这个方法比直接取FFT平滑,因为平均降低了方差,但代价是频率分辨率下降。常见的神逻辑是把pwelch输出和fft输出画在同一张图里,然后比较幅值大小,实际上两者量纲都不同,一个是功率谱密度,一个是线性谱。下面是pwelch的最小调用。
fs = 1000; N = 8192; rng(0); x = randn(N,1) + 0.5*sin(2*pi*100*(0:N-1)'/fs); [pxx, f] = pwelch(x, hamming(512), 256, 1024, fs); plot(f, 10*log10(pxx)); xlabel('频率 (Hz)'); ylabel('功率谱密度 (dB/Hz)');参数很好理解:512是窗长,窗长越长频率分辨率越高但方差越大;256是相邻窗的重叠点数,重叠越大段间相关性越高;1024是FFT点数;fs是采样率。改窗长和重叠能看到曲线平滑度的明显变化。窗函数选择也有讲究,矩形窗频率分辨率高但旁瓣泄漏大,汉明窗旁瓣低但主瓣稍宽。资源包里常用汉明窗,因为语音和噪声场景更看重旁瓣抑制。如果你把窗长从512改成128,谱曲线会剧烈抖动;这不是代码坏了,而是平均段数增加导致的方差减小和分辨率下降同时发生。先多跑几个参数,再下结论。
3. 跑起来:环境配置、参数设定与第一个可复现实验
源码包是否能跑,七成取决于运行环境,三成才是脚本本身。这里先把“能不能跑”的检查做完,再谈怎么把脚本改成自己的实验函数。很多第一次下源码包的同学会直接双击脚本文件,弹出一堆红色报错后就关掉了,其实问题往往出在工具箱缺失和路径冲突两件事上。
3.1 先查工具箱与路径,别急着双击
拿到源码包后第一件事不是读代码,而是检查运行环境。许多脚本会调用Signal Processing Toolbox里的函数,缺了它会直接报“未定义函数”。在命令窗口执行以下几行,5秒就能判断环境是否合格。
ver('signal') % 查看信号处理工具箱版本 which fir1 % 找fir1所在路径 license('test','Signal_Toolbox') % 返回1表示有授权如果which fir1返回的不是工具箱内置路径,而是某个以当前文件夹开头的路径,说明资源包里有一个同名文件把系统函数盖住了。这个问题比工具箱缺失更隐蔽:代码表面上能跑,但结果可能不是官方实现。在MATLAB课程设计中,最常见的人为冲突是自写fft函数,一旦出现,频谱图会变得很怪。
版本兼容性也需要看一眼。老一些的脚本用wavread读音频,新版MATLAB推荐audioread。如果运行提示函数已更名,一般不影响结果,但我会顺手改成audioread,避免以后换机器再报“考虑使用audioread”的警告。这个警告虽然不致命,但对答辩演示有点干扰。
3.2 建项目文件夹,处理路径与函数名冲突
资源包解压后最好放进一个纯英文路径,例如D:\DSP_Lab,不要带中文或空格。路径含中文时,某些版本的MATLAB读取数据文件会出玄学问题,比如load('data.mat')明明存在却报文件不存在。这属于环境问题,不是资源包的代码问题。
把文件夹加进搜索路径后,再检查一遍函数冲突。
cd('D:\DSP_Lab'); addpath('signal_scripts'); which fft -allwhich fft -all会列出所有名为fft的路径,如果排第一的不是toolbox内置路径,就要改名。自写的函数一律改成dsp_fft这种不冲突的命名。这一点在把脚本改造成函数时尤其重要。
运行脚本前还要注意很多旧例程第一行是close all; clear; clc;,clear会清空工作区变量。如果你的实验脚本先加载了音频,又调用了这一行,音频变量会消失,后面再引用就会报错。我一般会把这类开头的clear去掉,改成close all; clc;,保留工作区,方便调试。
3.3 把脚本改造成函数:参数外置是复用关键
源码包的脚本参数通常写死在代码里,比如fs=1000、N=512。课设中你需要反复换采样率、换点数、换截止频率,直接改脚本效率低,还容易改乱。我习惯拿到一段能跑的脚本后,先把它包成一个函数,把主要参数提到函数签名里。
function [X, f] = my_spectrum(x, fs, N) % 输入:x 时域序列,fs 采样率,N FFT点数 % 输出:X 频谱复数,f 频率轴 if nargin < 3 N = length(x); % 默认使用信号长度 end validateattributes(x, {'double'}, {'vector'}); % 确保输入是double向量 X = fft(x, N); f = (0:N-1)*fs/N; end函数化有一个好处:主脚本不用关心计算过程,只需要调用它。比如fs=8000; [X,f]=my_spectrum(x, fs, 1024);然后专心画图。nargin的作用是允许省略N时自动使用信号长度,这在信号长度不固定时很实用。validateattributes做参数守卫,输入不是double向量时直接报错,避免把单精度或字符串误传进来。
参数N的选择也有讲究:N越大,频率分辨率越细,但计算量变大;N取2的幂时FFT最快。如果信号只有8000点,你取N=65536,结果只是曲线更光滑,并没有真正提高分辨率。这一点在课设报告里写出来,是能体现理解深度的。
4. 避坑手册:跑这套DSP代码最容易翻车的五个位置
下面五条按“现象、原因、解决”的顺序列出来,都是我在帮别人看代码时反复遇到的同类问题。不一定每个资源包都有,但只要你用MATLAB做DSP,迟早会碰到其中之一。
4.1 频谱图的横轴显示的是点数而不是频率
现象:plot(abs(fft(x)))后,横轴是0、100、200这类整数,标不上Hz。
原因:fft输出的下标对应数字频率索引,必须经过换算才能变成物理频率。
解决:先算频率轴f=(0:N-1)*fs/N,再plot(f, abs(X));如果只是xlabel('频率/Hz')而不设置横轴数据,图上的刻度不会变。另一种更省事的方式是直接用freqz或periodogram,它们可以接受采样率参数,自动标Hz。
4.2 用plot画离散序列,把脉冲画成了锯齿
现象:单位脉冲序列用plot画出来是一条从0到1再回0的斜线,完全看不出离散样点。
原因:plot把相邻样点用直线连接,生成的是连续折线,对离散序列是不准确的表示。
解决:画离散序列用stem;如果你要显示包络,可以用plot(n,x,'o-')这种点线方式,但正式报告里仍建议stem。这个坑在资源包的时域例程里几乎不会出现,因为你直接复制代码就能看到正确画法,但自己动手时容易顺手写plot。
4.3 filter输出开头一段明显异常
现象:滤波后的信号前几十个点和稳态幅度差别很大,看起来像毛刺或者零值。
原因:filter是因果递归滤波,初始状态为零,开头需要若干样本把内部状态建立起来,这段暂态响应不是目标稳态。
解决:展示稳态波形时截掉开头一段,或者用filtfilt做零相位滤波。如果是实时数据处理,不能使用filtfilt,需要设计合理的初始状态,比如用filtic函数根据过去数据计算状态初值。课设里如果没做这个处理,报告上的滤波波形会很丑。
4.4 单边频谱幅度比预期小一半
现象:一个幅度为1的正弦信号,fft后在50Hz处峰值显示0.5。
原因:实信号的频谱在正负频率上对称分布,plot(abs(fft(x)))显示的是双边谱,能量被对半分掉了。
解决:绘制单边谱时取前N/2+1个点,乘以2,直流和奈奎斯特频率两个点不乘。注意这个“乘2”只适用于实正弦信号;如果信号本身是复指数,频谱只有单边,不需要乘2。这个细节很多教材不会强调,但答辩时很容易被追问。
4.5 butter滤波阶数过高导致输出异常
现象:用了butter(20, 0.4, 'low')后输出出现明显毛刺,甚至MATLAB提示“Matrix is close to singular”。
原因:IIR滤波器阶数过高会让极点靠近单位圆,量化误差和数值舍入误差被放大,滤波器接近不稳定状态。
解决:IIR阶数控制在8以内;需要更陡峭的过渡带时改用FIR,或使用等波纹设计法。这也是资源包里FIR例程比IIR多的原因:FIR没有反馈项,天然稳定,课设验收不容易翻车。如果必须用IIR,先调用freqz检查幅频响应,再决定最终阶数。
5. 从源码包到课设成品:做一个语音加噪与滤波实验
源码包拆完,下一步是把例程拼成一个能验收的完整项目。这里用一个最常见的语音滤波场景来串:读入语音,叠加固定频率干扰,设计低通滤波器,证明有效。这个思路你换到ECG信号、振动信号、音频降噪上都能复用,只是信号源和截止频率不同。
5.1 方案拼装:把四段例程组合成一个主脚本
典型的源码包会把“读文件、生成噪声、设计滤波器、播放对比”分散在多个脚本里,课设报告不需要这么多文件。我习惯新建一个main_filter_demo.m,把要点按流程合到一起。
[y0, fs] = audioread('speech.wav'); % 读取语音 y0 = y0(:, 1); % 取单声道 t = (0:length(y0)-1).'/fs; % 列时间轴 noise = 0.05 * sin(2*pi*1800*t); % 1.8k正弦干扰 x = y0 + noise; % 加噪信号 [b, a] = butter(4, 1200/(fs/2), 'low'); % 4阶IIR低通 yf = filtfilt(b, a, x); % 零相位滤波这个脚本看起来短,但每一行背后都有对应例程支撑。audioread是读取模块,sin是信号生成模块,butter和filtfilt是滤波器模块。组合时最需要改的参数是截止频率:这里的1200Hz是假设语音主要能量在1.2kHz以下,而1.8k单频是噪声。如果你的语音采样率是44100Hz,1200/(fs/2)只有0.0544,低通会非常窄,可能把语音也削掉。更稳妥的做法是先运行频谱分析脚本看噪声到底在哪个频点,再定截止频率。
5.2 验证:用频域图与信噪比证明滤波器有效
滤波器有没有效,不能只看“听起来安静了”这一句主观判断。课设验收需要数字指标和谱图。常见做法是算滤波前后的SNR。
snr_in = 10*log10( sum(y0.^2) / sum((x - y0).^2) ); snr_out = 10*log10( sum(y0.^2) / sum((yf - y0).^2) ); fprintf('SNR in = %.2f dB, out = %.2f dB\n', snr_in, snr_out);这里的y0是干净参考信号,x是加噪信号,yf是滤波输出。为什么用y0做参考?因为SNR的定义是“信号功率/噪声功率”,在没有加噪前录到的是参考信号源。如果资源包里没有干净参考信号,可以用一段静音段估计噪声功率,或者用频谱对比代替。
注意:如果用的是filter而不是filtfilt,输出存在延迟,直接做差算SNR会产生虚假误差,需要先把yf对齐到y0再计算。
5.3 参数标定的权衡
滤波器设计不是越大越好,也不是越陡越好。给一个直观的对比表:
| 参数 | 影响 | 建议值 |
|---|---|---|
| FIR阶数 | 阶数越高过渡带越窄,延迟和计算量越大 | 20到100之间,根据fs调整 |
| IIR阶数 | 阶数高到8以上开始有数值稳定风险 | 4到8 |
| 截止频率 | 太高滤不掉噪声,太低削掉有效信号 | 先用FFT看噪声频点再定 |
| 采样率 | 决定奈奎斯特频率与归一化折算 | 使用录音原始fs,不要随意resample |
这个表在报告里很有用,老师问“为什么选4阶”时可以直接答:兼顾衰减斜率和稳定性。如果源码包里的脚本直接给的是Wn而非Hz,写报告时把你的转换公式Wn=fc/(fs/2)放上去,这属于加分项。
5.4 验收时容易被问到的三个问题
第一个问题是“为什么不用带通?”如果1.8kHz噪声和语音频谱明显分离,低通足够;如果噪声落在语音频带中间,低通会把语音一起削掉,这时要改用自适应滤波或谱减法,而不是一味堆阶数。
第二个问题是“SNR提升为什么不明显?”因为正弦噪声能量集中在单频,低通能去掉它,但语音本身也有1.8kHz以上的成分,滤波后这些成分也被去掉。以干净语音y0为参考计算SNR时,yf与y0的差异反而可能变大。所以SNR只是指标之一,还要看时域波形和主观听感。
第三个问题是“能不能改成实时处理?”资源包里的filtfilt不能实时,因为它是非因果的。要改成实时,需要改用filter并接受相位延迟,或者用FIR滤波器配合延迟补偿。在报告里把这个边界写清楚,面试时非常加分。
6. 把参数验证写成固定流程:一个滤波器自检函数
做课设和拆源码包最大的区别是:拆包只要读懂,课设要对自己写的东西负责。我后来给自己定了一条规矩:任何滤波器交付之前,先跑一遍自检函数。
function verify_filter(b, a, fs) % 检查滤波器稳定性,并画幅频和相频响应 if length(a) > 1 && ~all(abs(roots(a)) < 1) error('滤波器不稳定:存在极点超出单位圆'); end [H, f] = freqz(b, a, 1024, fs); subplot(2,1,1); plot(f, abs(H)); grid on; xlabel('频率 (Hz)'); ylabel('|H(f)|'); subplot(2,1,2); plot(f, angle(H)*180/pi); grid on; xlabel('频率 (Hz)'); ylabel('相位 (度)'); endroots(a)计算的是IIR分母的极点,只要有一个极点落在单位圆外,这个滤波器就不适合实际使用。freqz返回频率响应,1024是频响计算点数,fs用于把横轴换算成Hz。这个自检函数把稳定性和可视化一次做完,省去每次手工敲freqz的麻烦。
我的习惯是在加噪声脚本里加一行调用:verify_filter(b, a, fs);如果报错,说明参数不合适,直接回第3.3节改阶数或截止频率。以前我总在阶数设得过高时看到输出毛刺,原因往往是极点越界,却没有快速定位的手段。从那以后,每次设计滤波器我都强制先跑一遍自检,再继续后面的处理,效果确实稳定很多。希望帮到你。
本文还有配套的精品资源,点击获取