Python统计WAV音频可听/非可听频率时域占比实现问题
核心问题说明
你当前用全局FFT统计频率bin数量的计算方式完全不可行,得到的结果和你需要的时域占比没有关联。全局FFT是把整段音频的所有时域信息混合后转换到频域,返回的频率数组长度仅由FFT点数、采样率决定,无法对应到具体时域片段的持续时长,你当前写法算出的结果只是「频率轴上非可听频段的频率点数量占总频率点的比例」,完全无法反映"5秒音频里2秒是可听内容"这类时域维度的统计结果。
正确实现思路
要得到时域维度的频段占比,必须基于短时傅里叶变换(STFT)做分帧检测,核心逻辑是把整段音频切分为多个短时间片段,逐片段判断频段属性后累计时长,步骤如下:
- 音频预处理:如果输入是多声道WAV,先按通道取均值转为单声道,同时做振幅归一化消除录音电平的影响
- 分帧加窗:将音频切为长度20ms-50ms的短帧(这个长度是音频检测的常用取值,符合人耳对声音的稳态感知区间),帧之间设置50%重叠减少截断误差,每帧加汉明窗抑制频谱泄漏
- 逐帧频段判定:对每一帧单独做FFT得到对应频谱,分别计算次声段(<20Hz)、可听段(20Hz-20kHz)、超声段(>20kHz)的能量占比;注意不要直接以"是否存在对应频率点"作为判定标准——正常可听音频的FFT结果也会有少量量化噪声落在非可听段,需要设置判定阈值,比如某类频段能量占该帧总能量的90%以上,就把该帧归属到对应类别
- 时长累计:帧移对应每帧的实际时间步长(帧移=帧长*(1-重叠率)),统计归属到不同类别的总帧数,乘以帧移对应的时长就是该类别的总持续时间,除以音频总时长即可得到占比
参考实现代码
from scipy.io import wavfile from scipy.signal import stft import numpy as np wav_path = '/path/to/wav/file' # 可调参数 frame_len_ms = 30 # 单帧时长30ms overlap_rate = 0.5 # 帧间50%重叠 energy_threshold = 0.9 # 频段能量占比判定阈值 # 读取音频 rate, wav_data = wavfile.read(wav_path, 'rb') # 多声道转单声道 if len(wav_data.shape) > 1: wav_data = np.mean(wav_data, axis=1) # 振幅归一化 wav_data = wav_data.astype(np.float32) / np.iinfo(wav_data.dtype).max total_duration = len(wav_data) / rate # 计算STFT得到时频谱 f, t, Zxx = stft( wav_data, fs=rate, nperseg=int(rate * frame_len_ms / 1000), noverlap=int(rate * frame_len_ms / 1000 * overlap_rate), window='hamming' ) # 计算每个时频点的能量 frame_energy = np.abs(Zxx) ** 2 total_energy_per_frame = np.sum(frame_energy, axis=0) # 标记不同频段的频率索引 infrasonic_mask = f < 20 audible_mask = (f >= 20) & (f <= 20e3) ultrasonic_mask = f > 20e3 # 逐帧计算各频段能量占比,加1e-8避免除零错误 infrasonic_ratio = np.sum(frame_energy[infrasonic_mask, :], axis=0) / (total_energy_per_frame + 1e-8) audible_ratio = np.sum(frame_energy[audible_mask, :], axis=0) / (total_energy_per_frame + 1e-8) ultrasonic_ratio = np.sum(frame_energy[ultrasonic_mask, :], axis=0) / (total_energy_per_frame + 1e-8) # 统计各类型有效帧数 infrasonic_frames = np.sum(infrasonic_ratio >= energy_threshold) audible_frames = np.sum(audible_ratio >= energy_threshold) ultrasonic_frames = np.sum(ultrasonic_ratio >= energy_threshold) # 单帧对应的实际时长(帧移步长) frame_step = frame_len_ms / 1000 * (1 - overlap_rate) # 输出统计结果 print(f"音频总时长: {total_duration:.2f}s") print(f"次声段时长: {infrasonic_frames*frame_step:.2f}s, 占比: {infrasonic_frames*frame_step/total_duration*100:.1f}%") print(f"可听段时长: {audible_frames*frame_step:.2f}s, 占比: {audible_frames*frame_step/total_duration*100:.1f}%") print(f"超声段时长: {ultrasonic_frames*frame_step:.2f}s, 占比: {ultrasonic_frames*frame_step/total_duration*100:.1f}%")
注意事项
- 如果音频采样率低于40kHz,根据奈奎斯特采样定理,音频本身不会包含20kHz以上的频率分量,此时超声段占比恒为0,不需要额外计算
- 能量阈值可以根据实际场景调整,如果音频本身底噪较高,可以适当降低阈值,避免把有效内容误判为混合片段
- 后续开发语音/非语音占比统计函数时可以复用这套分帧逻辑,只需要把频段判定规则替换为语音对应的特征规则即可——比如语音核心能量集中在300Hz-3400Hz,也可以搭配过零率等特征提升判定准确率,按同样的帧统计逻辑就能得到时域占比结果。
内容的提问来源于stack exchange,提问作者Jana.k
相关产品推荐
相关产品推荐

