资讯动态

从零绘制声谱图:窄带与宽带STFT参数详解

发布时间:2026/10/14 15:54:03 来源:尧图企业网站定制
做语音处理绕不过声谱图这关。我第一次把一段话音转成声谱图时对着屏幕上那些横七竖八的条纹愣了好久后来才明白窄带声谱和宽带声谱根本就是两个视角的东西一个盯着频率看一个盯着时间看。这篇文章就把我常用的Python做法完整写出来包括怎么从零手写短时傅里叶变换STFT、怎么用scipy一行接口出图、怎么用librosa把图做得能直接放进论文或报告里以及我踩过的一些坑。1. 声谱图能给语音分析带来什么声谱图是语音信号最直观的“时频CT”。普通波形只能看到振幅随时间起伏但声谱图能同时展示“什么时间点、有哪些频率成分、能量多大”。语音里的基频轨迹、共振峰位置、清音和浊音的分界、辅音起始的冲直条全部能在一张图上找出来。有人觉得声谱图只是可视化工具其实不是。很多语音标注任务、发音评测、语音合成的前期特征分析都得靠人眼先扫一遍声谱图来建立直觉。比如我想看某个人发元音时第二共振峰是不是在移动波形上完全看不出来声谱图上一眼就知道。窄带和宽带是两种互补的观察方式窄带声谱适合看谐波和基频宽带声谱适合看共振峰和音段边界。做语音研究的人两个都得会看做工程的人也得知道参数怎么选才能得到自己想要的图。本文给的实现可以直接用在任意wav语音文件上不需要复杂环境主要依赖numpy、scipy和matplotlib这几件套。如果你已经装过Anaconda那常用的包基本都有了。如果你是零基础也没关系我会把每个参数的意义都拆开讲清楚保证你照着敲完代码能出图。2. 先搞懂窄带与宽带才能选对参数2.1 用“焦点”类比理解两种声谱图把语音信号想成一张照片。窄带声谱图就像长焦镜头拍特写对频率很敏感能把每一条谐波纹路拍得清清楚楚但代价是时间上比较模糊宽带声谱图就像广角镜头时间上很灵敏元音里那一条条“垂直栅栏”能看得特别清楚但频率方向就糊成一片。为什么会这样因为声谱图本质上是对一小段信号做傅里叶变换得到的。你选择的窗长直接决定了“频率看得细不细”和“时间上分辨得准不准”。窗越长参与FFT的信号越长频率分辨率越高但这一段时间内信号变化被平均掉了时间分辨率变差。窗越短越能抓住瞬间的变化但频率上只能得出粗糙的分布。2.2 三个关键参数到底在控制什么使用STFT做声谱图时最核心的三个参数是窗长window length、帧移hop length和FFT点数n_fft。窗长每一帧取多少个采样点。它决定一次分析所覆盖的时间长度也决定频率分辨率。帧移相邻两帧之间滑动多少个采样点。帧移越小时间轴上越“密”图越平滑但计算量也越大。FFT点数做FFT时补零后的总长度。很多人会忽略这一点FFT点数只影响频率轴的插值密度不影响真正的频率分辨率。真正的频率分辨率取决于窗长。频率分辨率有个非常简单的公式频率分辨率 采样率 / 窗长。比如采样率是16000Hz窗长取256点那么频率间隔就是16000/25662.5Hz。窗长取2048点频率间隔就是7.8Hz。前者对语音来说就是宽带的效果后者接近窄带的效果。时间分辨率则由窗长决定窗越长能分辨出的时间细节越差。比如2048点在16000Hz采样率下是128毫秒一般认为超过了语音中很多音素持续的长度所以时间上会把一些细节抹平256点只有16毫秒能大幅保留时间变化。2.3 窄带/宽带典型参数对照实际研究里窄带和宽带没有严格的统一定义习惯上以滤波器带宽大致区分。窄带对应的滤波器带宽通常在50Hz上下宽带在300Hz上下。换到Python参数上可以直接用下面这个对照表来选采样率宽带推荐窗长窄带推荐窗长常用帧移8000Hz64~128点8~16ms512~1024点64~128ms窗长的1/416000Hz256点16ms或128点8ms1024~2048点64~128ms窗长的1/444100Hz512点11.6ms或256点5.8ms2048~4096点46~93ms窗长的1/4如果你只是想快速画个图“看看”先用窗长512和2048对比一次大多数情况下就能很好感受到宽带、窄带的差异。3. 环境准备与音频规范化3.1 安装依赖库我平时用的库其实就四个numpy、scipy、matplotlib、librosa。前三个是基础最后一个librosa是音频特征提取专业户画出来的图自带一套漂亮的坐标轴和配色。安装命令很简单pip install numpy scipy matplotlib librosa如果你已经装了conda用conda也可以conda install numpy scipy matplotlib librosa其中librosa自带或间接依赖soundfile、audioread等音频解码库能读wav、flac、mp3等格式。本文示例以wav为主因为wav处理起来最稳scipy.io.wavfile可以直接读不需要额外解码器。3.2 音频文件检查和预处理做声谱图之前最好先确认几个事情采样率、声道数、位深。采样率决定了你算出来的横轴和纵轴刻度。很多语音数据集是16kHz单声道wav这比较好处理但如果是手机录音很可能采样率是44100Hz、双声道。如果对双声道直接做STFT两个声道会叠加在一起相位抵消图会变脏。我的习惯是先看音频属性再预处理成单声道浮点数数组。import numpy as np from scipy.io import wavfile sr, audio wavfile.read(speech.wav) print(采样率:, sr, 形状:, audio.shape, 类型:, audio.dtype)如果audio是二维数组就取平均转单声道如果dtype是int16就把数值除以32768转成-1到1之间的浮点数。这个归一化不会改变声谱图结构但后面做分贝转换时更统一。if audio.ndim 2: audio audio.mean(axis1) if audio.dtype np.int16: audio audio / 32768.0 audio audio.astype(np.float32)还有一些wav文件实际位深是24位或者32位浮点scipy的wavfile读起来会有兼容问题。这时候我建议直接用librosa.load读它能自动处理各种格式默认会重采样到22050Hz。想让语音分析保持原始采样率就用srNone想统一到16kHz做语音分析就传sr16000。重采样到16k还有一个好处是大幅减少计算量后面画长音频时你会感谢这个操作。4. 从零手写STFT声谱图是怎么算出来的很多人直接用现成函数但遇到图像不对时根本不知道往哪调。所以我强烈建议至少手写一次STFT二十行不到的代码却能帮你想明白整条链路。4.1 分帧语音是不断变化的短时信号语音不是平稳信号直接拿整段信号做FFT毫无意义得到的是全场平均频谱。所以要先分帧一帧一帧地处理。帧与帧之间有重叠因为窗函数会把边缘压低不重叠就会丢失信息。常见的做法是帧移为窗长的四分之一也就是75%重叠。def frame_signal(x, win_length, hop_length): n_frames 1 (len(x) - win_length) // hop_length frames [] for i in range(n_frames): start i * hop_length frame x[start:start win_length] if len(frame) win_length: frame np.pad(frame, (0, win_length - len(frame))) frames.append(frame) return np.array(frames)这个函数把语音切成一帧一帧的二维数组shape是(帧数, 窗长)。注意最后一帧可能不足一个窗长用零补齐就行对声谱图几乎没影响。4.2 加窗直接截断会让频率“炸开”直接截断相当于加了一个矩形窗矩形窗的频谱旁瓣特别高主瓣能量泄漏到旁边原本一条干净的谐波会变成一片毛刺。所以必须用平滑窗把帧的两端压到接近0。语音处理里最常用的是汉宁窗Hann或汉明窗Hamming我默认用Hann窗。win np.hanning(win_length) weighted_frames frames * win加窗之后每帧的时间长度没变但两端的能量被压低了。正因为如此重叠不能太小。75%重叠时同一时间点会被至少4个窗覆盖加起来能恢复出原始信号的能量形状这也是为什么帧移设窗长四分之一很流行。4.3 FFT与补零提高作图分辨率不等于提高频谱分辨率对每一帧加窗后做FFT。只取正频率部分用np.fft.rfft就够。如果你把FFT点数设置得比窗长大比如窗长256但FFT设2048这等于在窗后补零再变换。补零能让频谱曲线更“细腻”看起来点多但它不能区分两个靠得很近的频率分量因为真正的分辨能力已经被窗长决定了。这点我踩过坑以前为了图好看把n_fft调大以为分辨率提升了其实只是白算了一堆插值点。完整的手写STFT如下def manual_stft(x, win_length, hop_length, n_fftNone): if n_fft is None: n_fft win_length win np.hanning(win_length) n_frames 1 (len(x) - win_length) // hop_length spec [] for i in range(n_frames): start i * hop_length frame x[start:start win_length] if len(frame) win_length: frame np.pad(frame, (0, win_length - len(frame))) spec.append(np.fft.rfft(frame * win, nn_fft)) return np.array(spec).T # (频率点数, 帧数)做完这一步你就得到声谱图的原始数据了。要显示出来通常取复数谱的幅度再转成分贝spec_db 20 * np.log10(np.abs(spec) 1e-10)加1e-10是为了防止log0。幅度谱如果不转dB动态范围太大画图上会丢失低能量细节转成dB之后轻声的气音、辅音噪声都能看出来。5. 调用scipy.signal.stft快速生成声谱图5.1 stft接口参数怎么选手写STFT能帮助理解但进到工程里我更常用scipy.signal.stft。它内部把分帧、加窗、FFT全部封装好了参数也比较好理解nperseg表示窗长noverlap表示重叠的采样点数nfft表示FFT补零后的点数。如果你设帧移hop_length那noverlap就等于nperseg - hop_length。比如窗长256、帧移64重叠就是192。这也是很多教程里看起来“多了一个参数”的原因。窗函数建议用Hann设置windowhann。语音分析默认Hann很稳旁瓣衰减好画出来干净。如果想更极端地窄带可以增大nperseg比如2048或4096如果想更宽带就调小到128或256。5.2 完整可运行的窄带/宽带声谱图脚本下面给一段我平时常用的脚本把窄带和宽带两张图画在一起方便对比import numpy as np import matplotlib.pyplot as plt from scipy.io import wavfile from scipy.signal import stft def read_mono_wav(path): sr, x wavfile.read(path) if x.ndim 2: x x.mean(axis1) if x.dtype np.int16: x x / 32768.0 return sr, x.astype(np.float32) def draw_stft(sr, x, win_length, title, cmapmagma): hop_length win_length // 4 f, t, Zxx stft(x, fssr, windowhann, npersegwin_length, noverlapwin_length - hop_length) spec_db 20 * np.log10(np.abs(Zxx) 1e-10) plt.figure(figsize(12, 4)) plt.pcolormesh(t, f, spec_db, cmapcmap, shadingauto) plt.colorbar(labeldB) plt.ylim(0, sr // 2) plt.xlabel(时间(s)) plt.ylabel(频率(Hz)) plt.title(title) plt.tight_layout() plt.savefig(f{title}.png, dpi150) plt.close() sr, audio read_mono_wav(speech.wav) # 宽带窗长256频率分辨率约 16000/256 62.5Hz draw_stft(sr, audio, 256, broadband_spectrogram) # 窄带窗长2048频率分辨率约 16000/2048 7.8Hz draw_stft(sr, audio, 2048, narrowband_spectrogram)这个脚本的关键动作是分别用256和2048的窗长生成两张图。如果你把两张图并排看宽带那张图里元音段会有一条条近似垂直的暗纹那是声带振动一个周期一个周期造成的窄带那张图里元音段会有一条条水平亮线那是基频和各个谐波。我实际使用中还会把ylim适当调小比如语音用0到5000Hz因为语音大部分能量集中在4kHz以下画到8000Hz会把细节压缩掉。如果你在16000Hz采样率下把纵轴拉满到8000Hz高频段大量空白会让图变得很“空”。6. 用librosa绘制更专业的声谱图6.1 librosa的load与stftscipy版本很简洁但想要论文级别的好看效果我会转到librosa。librosa的specshow自带时间轴、频率轴和坐标网格还能直接设置对数频率轴这在展示语音谐波时特别有用。音频读取直接用librosa.load它对格式兼容性和重采样处理得更好import librosa audio, sr librosa.load(speech.wav, sr16000)这里把采样率固定为16000便于和之前的参数表对应。如果你的音频不是16k的librosa会自动重采样不需要你手动处理。STFT计算接口更直接窗长就是win_length帧移就是hop_length。额外还可以指定n_fft默认和win_length相同。如果想在窄带模式下做一点频率插值可以n_fft设成比窗长更大的值。D librosa.stft(audio, n_fft2048, hop_length512, win_length2048, windowhann)返回的D是复数矩阵行对应频率点列对应时间帧。6.2 用specshow美化输出librosa里展示声谱图通常配合amplitude_to_db函数把幅度谱转成以参考值0dB为标准的分贝值。再通过specshow设置x_axis和y_axislibrosa会自动算出时间刻度和频率刻度。下面这段代码是我常用的一套模板可以生成窄带、宽带两张图import librosa.display def plot_spec_librosa(audio, sr, win_length, title): hop_length win_length // 4 D librosa.stft(audio, n_fftwin_length, hop_lengthhop_length, win_lengthwin_length) S_db librosa.amplitude_to_db(np.abs(D), refnp.max) plt.figure(figsize(12, 5)) librosa.display.specshow( S_db, srsr, hop_lengthhop_length, x_axistime, y_axislog, cmapmagma ) plt.colorbar(format%2.0f dB) plt.title(title) plt.tight_layout() plt.savefig(f{title}.png, dpi150) plt.close() # 使用 plot_spec_librosa(audio, sr, win_length256, titlelibrosa_broadband) plot_spec_librosa(audio, sr, win_length2048, titlelibrosa_narrowband)之所以推荐y_axislog是因为人耳对频率的感知接近对数关系。低频区域会被放大基频和前面几个谐波的间隔看得更清楚如果用线性轴低频往往挤在最底下几行像素里很难观察。如果做共振峰分析线性轴反而更常用因为共振峰之间的频率间距在线性轴上更直观。两种轴都试一下看自己的目标选。我还习惯把cmap设置为magma或viridis暗色背景配亮色更能凸显语音细节。灰度图适合打印论文但屏幕上显示亮色渐变更容易辨认低能量成分。7. 常见问题与排查实录7.1 图像问题速查我整理了一份自己经常遇到的异常情况表很多问题都是参数设错了现象可能原因解决办法图里全是横条纹语音原本的共振峰看不见窗太长时间分辨率太差改用更小的win_length比如256谐波显示成一片糊看不清基频窗太短频率分辨率不足改用更大的win_length比如2048纵轴上频率最高点不对采样率设置错了或者没有正确读取打印sr检查或明确指定librosa的sr参数图整体颜色很暗几乎看不见内容分贝动态范围太大用amplitude_to_db并clip到从最大值向下80dB高频部分有奇怪的竖纹音频是双声道且被直接处理先转单声道再做STFT波形开头和结尾出现异常的强能量块边缘补零太长或音频文件本身带爆音截取中间稳定段分析或加静音检测7.2 音频读取问题如果你用scipy.io.wavfile读取wav发现dtype是float32、数据范围在-1到1之间这是正常的直接使用就行。如果读进来是int32或uint8归一化时要小心最好事先把数据归一化到一个统一幅度比如除以数据类型的最大值。遇到24位wav读取失败的场景我一般直接切到librosa.load因为它依赖soundfile对更多编码格式的兼容性更好不用自己折腾字节解析。遇到音量特别小的语音时声谱图画出来会比较暗可以先做一下自动增益把峰值拉高到-1dBFS再保存也可以在绘图时人为降低shading范围。7.3 长音频与内存优化计算STFT最怕碰到又长又高采样率的文件。比如44100Hz、10分钟的录音总时长是2646万采样点。如果取窗长4096、帧移1024约产生25800多帧频率点有2049个复数矩阵就要占用好几百MB加上转分贝和绘图复制内存很容易爆。我处理长音频的办法比较简单先在librosa.load里把采样率重采样到16000Hz甚至8000Hz然后设置更大的hop_length比如win_length的1/4或1/8。如果只是看全局趋势hop_length可以放宽到窗长一半时间上稍微糙一点但计算量骤降。还有一个技巧是分段分析每次只取10到20秒做一帧图片这样既能看到细节也不怕内存开裂。如果是做批量处理上百个文件建议导出为png而不是plt.show然后直接把plt.close()防止一段脚本跑完生成几十张未关闭的图窗口导致内存持续上涨。8. 实操心得与参数推荐做声谱图最大的心得是别一上来就抄别人的参数先弄清楚自己到底想看什么。想找基频窄带想找共振峰宽带。这是语音分析里最基础也最实用的分法。如果只是为了快速查验一段音频有没有坏帧我通常用宽带win_length256、帧移64就够了看得清楚还快。如果我想仔细确认某段元音的谐波结构我会用win_length2048而且把纵轴设置成对数频率轴。窗函数我不太轻易换。Hann窗在绝大多数语音场景都是最稳妥的选择旁瓣低、曲线平滑虽然主瓣比矩形窗宽但声谱图不是测频器对主瓣宽度没那么敏感。偶尔在特定场景下改用Hamming窗口是为了让频谱旁瓣稍微低一点。但如果你刚开始接触别在窗函数上浪费太多时间默认Hann就好。补零这个事我再强调一次。很多人以为加大n_fft就能提高频率分辨率实际上只是让画出来的曲线更圆滑真正的分辨率上限是窗长决定的。这个限制不是你代码能绕过去的是傅里叶变换本身的性质。所以如果你需要更高的频率分辨率请老老实实加长窗长不是加FFT点数。实际对比窄带和宽带图时我一般把两幅图上下排列横轴时间对齐纵轴频率范围一致。宽带图里清音的噪声区看起来像细碎杂点浊音段像垂直栅栏窄带图里浊音段是水平谐波线清音噪声区仍然是一片无规律的亮斑。两种图结合起来几乎能把一段语音里的所有主要语音学事件标出来。这也是我做语音数据标注前必做的检查步骤。如果你要拿声谱图做语音识别的输入特征通常不会用线性频率刻度而是转mel刻度后作为模型输入。但无论是mel谱还是线性谱底层都是STFT本文讲到的窗长和帧移取舍到那里依然成立。把底层的原理弄明白后面玩出花来都不慌。最后分享一个我保存图片的小习惯dpi设成150到300图尺寸设成12比4或12比5的长条。声谱图本来就是横向展示时间的信息竖长图会浪费大量显示空间横长图放到报告里也更好看。写代码封装成一个函数输入音频路径、win_length、输出文件名之后批量跑文件就很省事了。

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

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

免费获取报价 →
↑