这篇纯属是为了学习一下傅里叶变换过程然后更好看地呈现。
虽然看语谱图AU已经非常好用了,关于语谱图的介绍,可以去看这篇文章:【超详细】Cubase扒谱引出的语音学探讨-CSDN博客
关于频率分量的强度,语谱图是使用颜色亮暗来区分的,但是人类对颜色的感知毕竟不如空间维度更加直接,因此,扒谱机我考虑使用MATLAB编写代码,将语谱图进行三维可视化。
话不多说,开始AI。
(下文记录的是代码调试修改的过程,最终版的代码附在文章最后)
示范音频为截取自25高考英语听力的"现在是听力试音时间",它的频谱图是这样子的:

而到了三维,你会发现呈现效果并不是非常理想:
首先是这里,在音频末尾有幅度非常小的一系列数,拉长了坐标系显示范围
观察STFT的数组,发现确实这里有四行十分统一且都很小的数,如下图红框所示。

AI告诉我,这是因为:
- STFT 使用滑动窗口处理信号,当窗口滑动到信号末尾时,窗口内的信号会变得不完整
- 为了处理这种情况,MATLAB 的
stft
函数会自动进行零填充,导致末端出现小值
如果这些小值影响了可视化效果,可以通过以下几种方式处理:
方法 1:调整可视化范围
最简单的方法是在绘图时限制 z 轴(幅度)的显示范围:
Matlab
% 在三维绘图代码中添加以下行
zlim([-100 100]); % 设置幅度显示范围,根据实际情况调整
效果并不好,因为在频谱的较高频段存在幅值很低的区域,限制z轴显示也会把这些去掉
方法 2:修改 STFT 参数
可以通过调整 STFT 的参数来减少边界效应的影响:
% 增加FFT点数或调整窗口大小
nfft = 4096; % 增加FFT点数
window = hann(2048); % 增大窗口大小
noverlap = 1536; % 相应调整重叠样本数
似乎没什么本质区别,就是算得慢了一些。
方法 3:手动裁剪结果
如果确定末尾的小值都是无效数据,可以直接裁剪结果:
Matlab
% 假设您想保留到3秒处
cutoffTime = 3.0; % 设定裁剪时间点
idx = find(t <= cutoffTime); % 找到小于等于该时间点的索引
% 裁剪数据
t = t(idx);
% f = f(idx);这句是AI生成有误,频率是另一个维度,无需裁剪,否则会报错
stftDB = stftDB(:, idx);
% 重新绘图
surf(t, f, stftDB, 'EdgeColor', 'none');
又稍微改了下代码,针对我音频文件的时长,掐头去尾,效果不错。
cutoffTime1 = 2.9722; % 结束时间点
cutoffTime2 = 0.0348; % 开始时间点

从这个视角上观察,可以发现,携带有效信息的幅值基本上都-50dB以上,它以下的,是不是可以才进行适当的变换裁剪掉?或者说进行适度地压缩,把第三维的对比度留给携带信息更多的幅值部分?
一开始直接把-50dB以下的压平了,所以考虑了非线性压缩,实验一下:
怎么说呢......一言难尽
低频处一大片都是红红的,有点难绷......

因为音频信号的低频能量通常远高于高频,这会导致三维图中低频部分 "挤压" 高频部分的显示空间。对数频率轴可以有效解决这个问题,同时还能更符合人耳的听觉感知特性(人耳对频率的感知本身就是对数的)。
频域上的分辨率是可以了,但是颜色方面更难绷了
另外这个时候看低频分得很开,看高频的信息并不方便,借鉴一下AU中的功能:
AU 中语谱图通过滚轮切换放大低频 / 高频的核心原理是动态调整频率轴的显示范围 (即 "聚焦" 不同频率区间),本质是通过改变频率轴的上下限(ylim
)来放大特定频段,而非修改数据本身。这种交互本质是 "视野缩放",就像用放大镜在不同区域移动,数据不变但显示的局部范围变化。
AI生成代码报错遂放弃,毕竟本人是触控板党)笑死
最终为了照顾一下非常难绷的颜色,没有使用频率对数坐标。
把显示的颜色最大值加30,看起来就更清晰了。
另外发现了一个很有意思的现象------
红色箭头所指的频率带,有一条明显的沟
笔者怀疑,它和MP3的压缩有关。
MP3 压缩是一种 "感知编码",它基于人类听觉系统的特性,去除 "听不到" 的信息:
-
频率掩蔽效应:
- 强频率成分会掩盖相邻弱频率成分
- MP3 会去除这些被掩盖的弱频率成分
-
临界频带:
- 人耳对 2-5kHz 频率最敏感
- 对低频(<200Hz)和高频(>5kHz)敏感度较低
- MP3 会优先保留敏感频段的信息
-
量化噪声:
- 对不重要的频率成分使用较粗糙的量化
- 导致这些区域的频率信息丢失
实验用的音频本来就是mp3格式的,因此无法验证原音,也有可能是录音设备的问题(笔者的手机录音就有明显缺失的频带)
挖个坑,明天更新关于MP3压缩痕迹观测的实验。
用AU显示语谱图是这样子的
MATLAB却是这样子的
或许......只是聚焦范围不同而已,你看,把AU的频率聚焦范围狠狠往上调也是这个样子------

说好的后面附加代码:
Matlab
% 音频短时傅里叶变换(STFT)的三维可视化
% 功能:读取音频文件,计算STFT,支持开关非线性压缩功能,自动裁剪前后边缘帧
clear; clc; close all;
%% 1. 读取音频文件
audioFile = 'audio_sample.wav'; % 替换为你的音频文件路径
[signal, fs] = audioread(audioFile);
% 转换为单声道
if size(signal, 2) > 1
signal = mean(signal, 2);
end
%% 2. 设置STFT参数
window = hann(1024); % 汉宁窗
noverlap = 768; % 重叠样本数(75%)
nfft = 2048; % FFT点数
%% 3. 计算短时傅里叶变换
[stftMagnitude, f, t] = stft(signal, fs, ...
'Window', window, ...
'OverlapLength', noverlap, ...
'FFTLength', nfft);
% 转换为分贝值
stftDB = 20 * log10(abs(stftMagnitude) + eps);
%% 4. 非线性压缩(可开关)
enableNonlinearCompression = false; % true=启用,false=禁用
threshold = -50; % 有效信息阈值
compress_strength = 7; % 压缩强度
if enableNonlinearCompression
stft_compressed = stftDB;
low_values = stftDB < threshold;
% 非线性压缩公式
stft_compressed(low_values) = threshold - compress_strength * ...
log(1 + exp((threshold - stftDB(low_values))/compress_strength));
compression_status = '启用'; % 状态文本
else
stft_compressed = stftDB;
compression_status = '禁用'; % 状态文本
end
%% 5. 时间裁剪(根据前后各4帧自动去除)
% 获取总帧数
total_frames = length(t);
% 设置要去除的前后帧数(可调整)
frames_to_remove = 4;
% 计算有效帧范围(避免索引越界)
start_frame = frames_to_remove + 1;
end_frame = total_frames - frames_to_remove;
% 确保索引有效
if start_frame >= end_frame
warning('要去除的帧数过多,已自动调整为保留中间1帧');
start_frame = floor(total_frames/2);
end_frame = start_frame;
end
% 裁剪数据
t = t(start_frame:end_frame);
stft_compressed = stft_compressed(:, start_frame:end_frame);
% 显示裁剪信息
disp(['自动裁剪:去除前', num2str(frames_to_remove), '帧和后', num2str(frames_to_remove), '帧']);
disp(['裁剪后保留的帧数:', num2str(length(t))]);
%% 6. 三维可视化
figure('Name', '三维语谱图', 'Position', [100 100 1000 800]);
surf(t, f, stft_compressed, 'EdgeColor', 'none');
colormap('jet');
colorbar;
caxis([min(stft_compressed(:)) max(stft_compressed(:))+30]);
xlabel('时间 (s)', 'FontSize', 12);
ylabel('频率 (Hz)', 'FontSize', 12);
zlabel('幅度 (dB)', 'FontSize', 12);
title(['非线性压缩', compression_status, '后的三维语谱图'], 'FontSize', 14);
view(30, 60);
xlim([t(1) t(end)]);
ylim([0 fs/2]);
grid on;
box on;
set(gca, 'FontSize', 10);
%% 7. 二维语谱图
figure('Name', '二维语谱图', 'Position', [200 200 1000 600]);
imagesc(t, f, stft_compressed);
axis xy;
colormap('jet');
colorbar;
caxis([min(stft_compressed(:)) max(stft_compressed(:))]);
xlabel('时间 (s)', 'FontSize', 12);
ylabel('频率 (Hz)', 'FontSize', 12);
ylim([0 fs/2]);
title(['非线性压缩', compression_status, '后的二维语谱图'], 'FontSize', 14);
box on;
set(gca, 'FontSize', 10);
%% 8. 输出信息
disp('语谱图绘制完成!');
disp(['非线性压缩: ', compression_status]);
disp(['压缩阈值: ', num2str(threshold), ' dB']);
disp(['采样率: ', num2str(fs), ' Hz']);