4. 语音信号处理基础:声音的物理特性、采样与量化、时域与频域分析

做语音交互系统,说白了就是让机器人听懂人话。但在这之前,你得先搞懂声音本身是个什么东西。我刚开始接触这个领域时,总觉得信号处理是数学家的活儿,跟我做工程的没关系。后来踩了不少坑才明白——不懂信号处理,你连麦克风选型都搞不定。

这一章,咱们就聊聊声音的物理特性、怎么把模拟声音变成数字信号,以及时域和频域分析到底在干什么。嗯,内容有点硬核,但我会尽量用大白话讲清楚。

4.1 声音的物理特性

声音的本质是什么?说白了就是振动。声源振动,推动空气分子,形成疏密波,传到我们耳朵里。这个波有几个关键参数:

  • 频率(Frequency):每秒振动的次数,单位是赫兹(Hz)。人耳能听到的大概是20Hz到20kHz。低于20Hz叫次声波,高于20kHz叫超声波。我在做机器人语音交互时,通常只关心300Hz到3400Hz这个范围——这是电话语音的带宽,也是人说话的主要能量集中区。
  • 振幅(Amplitude):振动的幅度,决定了声音的响度。振幅越大,声音越响。但要注意,人耳对响度的感知不是线性的,而是对数关系。所以我们会用分贝(dB)来表示。
  • 相位(Phase):描述波在某个时刻的位置。相位差会影响声音的叠加效果。比如两个同频率的声波,相位差180度,就会相互抵消——这就是主动降噪的原理。

关键点:语音信号处理中,频率和振幅是最常用的两个参数。相位信息在波束成形和声源定位中很重要,但在语音识别预处理阶段,我们通常不太关心它。

我记得有一次做机器人远场语音交互,麦克风阵列怎么调都效果不好。后来发现是机器人外壳的共振频率刚好落在人声频段内,导致语音信号被严重扭曲。嗯,这就是没考虑物理特性的后果。

4.2 采样与量化

模拟声音是连续的,但计算机只能处理离散的数字。怎么把连续信号变成离散信号?两个步骤:采样和量化。

4.2.1 采样

采样就是每隔一段时间,记录一次声音信号的瞬时值。这个时间间隔叫采样周期,它的倒数就是采样率。

这里有个重要的定理——奈奎斯特采样定理:采样率必须大于信号最高频率的两倍,才能无失真地恢复原始信号。

举个例子:人声的最高频率大约在8kHz左右,那么采样率至少需要16kHz。实际上,电话系统用8kHz采样率(带宽限制在4kHz),而CD音质用44.1kHz采样率(覆盖20kHz的听觉上限)。

我的经验:做语音识别系统时,我一般用16kHz采样率。这个频率既能保证语音质量,又不会产生太多数据。我曾经试过用8kHz采样率做识别,结果准确率掉了好几个点——高频信息丢失了,像/s/、/sh/这些擦音就分不清了。

4.2.2 量化

采样得到的是连续值,但计算机只能用有限个离散值来表示。量化就是把连续值映射到离散值的过程。

量化精度用位深(bit depth)表示。常见的位深有8位、16位、24位。位深越大,量化级数越多,精度越高。

位深 量化级数 动态范围 典型应用
8位 256 48 dB 早期电话、低端对讲机
16位 65536 96 dB CD音质、语音识别
24位 16777216 144 dB 专业录音、高保真音频

量化会产生误差,叫量化噪声。说白了就是真实值和量化值之间的差距。量化噪声是不可避免的,但我们可以通过增加位深来降低它。

避坑指南:我曾经在项目里为了省存储空间,把语音数据从16位降到了8位。结果语音识别模型直接崩了——背景噪声被量化噪声淹没了,信噪比急剧下降。后来我学乖了,语音数据至少用16位,宁可多花点存储,也不能牺牲质量。

4.3 时域与频域分析

拿到数字语音信号后,怎么分析它?两个视角:时域和频域。

4.3.1 时域分析

时域分析就是看信号随时间的变化。最直观的方式就是波形图——横轴是时间,纵轴是振幅。

时域分析能告诉我们什么?

  • 短时能量:语音信号的能量随时间变化。说话时能量高,静音时能量低。我们可以用这个来检测语音的起止点(VAD,Voice Activity Detection)。
  • 过零率:单位时间内信号穿过零轴的次数。清音(如/s/、/f/)的过零率高,浊音(如/a/、/o/)的过零率低。这个特征在语音端点检测中很常用。
  • 自相关:信号与自身延迟后的相似度。可以用来估计基频(pitch),也就是声带振动的频率。

我个人习惯在调试语音系统时,先看时域波形。如果波形有明显的削顶或截断,说明增益设置有问题。如果波形被噪声淹没,说明信噪比太低。

4.3.2 频域分析

时域分析虽然直观,但有些信息在时域里看不出来。比如,一段语音里包含了哪些频率成分?每个频率的能量有多大?这就需要频域分析了。

把时域信号转换到频域,最常用的工具是傅里叶变换(FT)。对于离散信号,我们用离散傅里叶变换(DFT),实际计算时用快速傅里叶变换(FFT)

FFT的结果是频谱——横轴是频率,纵轴是能量(或幅度)。

频域分析能告诉我们什么?

  • 共振峰(Formant):声道谐振产生的能量集中区域。元音的区别主要靠前两个共振峰(F1和F2)的位置。比如/a/的F1在800Hz左右,F2在1200Hz左右;/i/的F1在300Hz左右,F2在2300Hz左右。
  • 谐波结构:浊音信号在频域上呈现基频及其整数倍谐波。清音信号则没有明显的谐波结构,频谱比较平坦。
  • 噪声分析:通过频谱可以判断噪声的类型和频率分布。比如空调噪声集中在低频,风扇噪声在中高频。

实用技巧:做语音增强时,我经常在频域里做处理。比如用谱减法去除噪声——先估计噪声的频谱,然后从带噪语音的频谱中减去它。这个方法简单有效,但要注意不要减过头,否则会产生音乐噪声(musical noise)。

4.3.3 时频分析

语音信号是非平稳的——它的统计特性随时间变化。单纯用傅里叶变换分析整个信号,会丢失时间信息。怎么办?用短时傅里叶变换(STFT)

STFT的思路很简单:把信号分成一小段一小段(帧),每帧大约20-30毫秒,假设在这一帧内信号是平稳的,然后对每一帧做FFT。这样我们就得到了一个二维的时频谱——横轴是时间,纵轴是频率,颜色深浅表示能量大小。

时频谱(也叫语谱图)是语音分析中最常用的工具之一。从语谱图上,你可以清晰地看到共振峰的轨迹、谐波结构的变化、以及噪声的分布。

我的习惯:每次拿到新的语音数据,我都会先画一张语谱图看看。如果语谱图清晰干净,说明数据质量不错。如果语谱图模糊或者有奇怪的条纹,那就要检查一下采集设备或者环境噪声了。这比听录音要高效得多。

4.4 在ROS中处理语音信号

好了,理论讲完了,咱们看看在ROS里怎么处理语音信号。

ROS中常用的音频消息类型是audio_common_msgs/AudioData,它包含一个字节数组和采样率信息。但说实话,这个类型用起来不太方便。我个人更推荐用sensor_msgs/Image来传输音频数据——把音频当作一维图像来处理,这样可以利用ROS的图像传输管道。

下面是一个简单的语音信号处理节点示例:

# 语音信号处理节点示例
import rospy
import numpy as np
from sensor_msgs.msg import Image
from std_msgs.msg import Float32MultiArray

class VoiceProcessor:
    def __init__(self):
        rospy.init_node('voice_processor')
        self.sub = rospy.Subscriber('/audio', Image, self.audio_callback)
        self.pub = rospy.Publisher('/spectrum', Float32MultiArray, queue_size=10)
        
    def audio_callback(self, msg):
        # 将音频数据转换为numpy数组
        audio_data = np.frombuffer(msg.data, dtype=np.int16)
        
        # 归一化到[-1, 1]
        audio_float = audio_data.astype(np.float32) / 32768.0
        
        # 计算短时能量
        frame_size = 512
        energy = np.sum(audio_float[:frame_size] ** 2)
        
        # 计算FFT
        spectrum = np.fft.rfft(audio_float[:frame_size])
        magnitude = np.abs(spectrum)
        
        # 发布频谱
        msg_out = Float32MultiArray()
        msg_out.data = magnitude.tolist()
        self.pub.publish(msg_out)
        
        rospy.loginfo(f"短时能量: {energy:.4f}")

if __name__ == '__main__':
    try:
        vp = VoiceProcessor()
        rospy.spin()
    except rospy.ROSInterruptException:
        pass

这个例子很简单,但包含了核心步骤:读取音频数据、归一化、计算时域特征(短时能量)和频域特征(FFT幅度谱)。

注意:在实际项目中,千万别在回调函数里做复杂的信号处理。ROS的回调是单线程的,处理时间太长会阻塞其他消息。我一般会在回调里只做数据缓存,然后另开一个线程做处理。或者用ROS的异步spinner。

嗯,这一章的内容就到这里。声音的物理特性、采样量化、时频分析,这些是语音信号处理的基础。你想想看,如果连这些都不懂,后面做语音增强、特征提取、语音识别,那真是寸步难行。

下一章,咱们聊聊语音特征提取——怎么把原始语音信号变成机器能理解的特征向量。到时候会用到这一章的知识,所以先把基础打牢。