1. 音频信号基础:声音的物理特性、时域与频域、采样定理与量化、音频文件格式解析

各位同学,欢迎来到《音频算法实战能力提升》的第一章。我是你们这门课的主讲,一个在音频信号处理领域摸爬滚打了十几年的老工程师。今天咱们不聊虚的,直接切入正题——音频信号到底是个什么东西?

很多人觉得音频算法就是调调参数、跑跑模型。其实不然。你想想看,如果连声音的本质都搞不清楚,后面做降噪、做编解码,那不就成了空中楼阁吗?我见过太多新手,上来就调FFT参数,结果频谱图都看反了。嗯,咱们先把地基打牢。

本章核心目标:理解声音从物理世界到数字世界的完整映射过程,掌握时域与频域的基本概念,搞懂采样定理和量化这两个“数字音频的基石”。

1.1 声音的物理特性:从振动到波形

声音的本质是什么?说白了,就是机械振动在介质(通常是空气)中的传播。你说话,声带振动;你敲桌子,桌面振动。这些振动挤压空气,形成疏密波,传到我们耳朵里,我们就听到了声音。

描述一个声音,我们通常用三个物理量:

  • 频率(Frequency):单位是赫兹(Hz),表示每秒振动的次数。频率决定了音调的高低。人耳能听到的频率范围大约是20Hz到20kHz。低于20Hz的叫次声波,高于20kHz的叫超声波。我在项目中遇到过有人想用麦克风采集蝙蝠的叫声做定位,结果发现普通麦克风的高频响应根本不够,最后换了超声波传感器才搞定。
  • 幅度(Amplitude):表示振动的强弱,决定了音量的大小。幅度越大,声音越响。
  • 相位(Phase):描述振动在时间轴上的起始位置。相位差是很多空间音频算法(比如波束成形)的核心。

一个最简单的纯音(比如音叉发出的声音),可以用正弦波来表示:

y(t) = A * sin(2π * f * t + φ)

其中A是幅度,f是频率,φ是相位。但现实世界的声音,比如音乐、人声、环境噪声,都是无数个不同频率、不同幅度、不同相位的正弦波的叠加。这就是傅里叶变换存在的意义——把复杂的声音拆解成简单的正弦波组合。

我的小习惯:每次拿到一个新的音频信号,我第一件事就是听,第二件事就是看波形。听能感知到有没有异常(比如爆音、削波),看波形能快速判断信号的动态范围和能量分布。别小看这一步,它能帮你省掉很多调试时间。

1.2 时域与频域:看声音的两个视角

同一个声音,我们可以从两个完全不同的角度去观察它。

时域(Time Domain),就是看声音的幅度随时间怎么变化。横轴是时间,纵轴是幅度。你打开Audacity或者Cool Edit,看到的那条波浪线,就是时域波形。时域直观,但信息量有限。比如一段音乐里同时有钢琴和吉他,你在时域波形上根本分不清谁是谁。

频域(Frequency Domain),就是看声音里包含了哪些频率成分,以及每个频率的强度是多少。横轴是频率,纵轴是幅度(或者能量)。频域图(频谱图)能让你一眼看出声音的“成分”。

时域和频域之间怎么转换?靠傅里叶变换。对于计算机来说,我们用的是离散傅里叶变换(DFT),而最常用的实现是快速傅里叶变换(FFT)。

我建议你记住一个核心公式:

X(k) = Σ_{n=0}^{N-1} x(n) * e^{-j(2π/N)kn}

这个公式看着复杂,但它的物理意义很简单:把时域信号x(n)和一系列不同频率的复指数信号做内积,看看每个频率成分的贡献有多大。

下面这张图,是我自己画的一个时域到频域的映射关系,帮你建立直观印象:

时域 → 频域 映射关系 时域信号 横轴:时间 (t) 纵轴:幅度 (A) 直观但信息混杂 例:无法区分叠加的乐器 FFT 频域信号 横轴:频率 (Hz) 纵轴:幅度/能量 成分清晰可见 例:钢琴在440Hz,吉他在880Hz 核心理解 时域和频域是同一信号的两个正交视角 时域损失了频率信息,频域损失了时间信息 实际工程中常使用时频分析(如STFT)兼顾两者

1.3 采样定理与量化:把连续世界装进离散计算机

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

1.3.1 采样定理(Nyquist-Shannon定理)

采样,就是在时间轴上每隔一段时间取一个点。这个间隔的倒数就是采样率(Sample Rate),单位是Hz。

采样定理告诉我们:要无失真地重建一个信号,采样率必须至少是信号最高频率的两倍。

fs ≥ 2 * f_max

其中fs是采样率,f_max是信号中的最高频率分量。

举个例子:人耳能听到的最高频率是20kHz,所以CD音质的采样率是44.1kHz(略大于2*20kHz)。为什么不是刚好40kHz?因为实际滤波器做不到理想的陡降,需要留一点过渡带。

我曾经踩过的坑:有一次做语音增强项目,麦克风采集的语音信号里混入了22kHz的超声波噪声。我用的采样率是44.1kHz,按说没问题。但我忘了前置抗混叠滤波器没做好,结果22kHz的噪声被折叠(Aliasing)到了22.05-22=0.05kHz,也就是50Hz的工频噪声区域。我花了整整两天排查,最后发现是采样前的模拟滤波没加。从那以后,我设计任何音频系统,第一件事就是检查抗混叠滤波器。

下表总结了常见音频场景的采样率选择:

应用场景 采样率 (kHz) 最高频率 (kHz) 说明
电话语音 8 3.4 窄带语音,带宽有限
宽带语音 16 7 VoIP、视频会议常用
CD 音质 44.1 20 音乐、消费音频标准
专业音频 48 22 电影、广播、后期制作
高采样率 96 / 192 40 / 80 高保真、超声研究

1.3.2 量化:把幅度变成数字

采样解决了时间轴离散化的问题。量化解决的是幅度轴离散化的问题。我们把连续的幅度值映射到有限个离散的等级上。

量化精度用位深(Bit Depth)表示。常见的位深有:

  • 8-bit:256个等级,动态范围约48dB。语音通信够用,但音乐会有明显的量化噪声。
  • 16-bit:65536个等级,动态范围约96dB。CD音质标准,人耳基本听不出量化噪声。
  • 24-bit:16777216个等级,动态范围约144dB。专业录音棚标准,给后期处理留足余量。

量化误差(量化噪声)是不可避免的。它的功率可以用一个简单公式估算:

量化噪声功率 ≈ (Δ²) / 12

其中Δ是量化步长。位深每增加1bit,信噪比提升约6dB。这就是为什么16-bit比8-bit好那么多。

我的建议:做算法开发时,内部处理尽量用32-bit浮点。浮点数的动态范围极大,几乎不用担心溢出和量化噪声。只在最终输出到硬件(比如保存为16-bit WAV文件)时才做定点化。这样可以避免很多精度问题。

1.4 音频文件格式解析:WAV、MP3、AAC、FLAC

搞音频算法,天天跟文件格式打交道。我简单说说最常见的几种。

1.4.1 WAV(波形音频文件格式)

WAV是微软和IBM联合开发的,是Windows平台上最基础的未压缩音频格式。它的结构很简单:一个44字节的文件头,后面跟着原始的PCM数据。

WAV文件头关键字段:

偏移量 长度(字节) 内容 示例值
0 4 RIFF标识 "RIFF"
8 4 文件格式类型 "WAVE"
12 4 "fmt "子块标识 "fmt "
16 4 子块1大小 16 (PCM)
20 2 音频格式 1 (PCM)
22 2 声道数 1 (单声道) / 2 (立体声)
24 4 采样率 44100
28 4 字节率 采样率 * 声道数 * 位深/8
32 2 块对齐 声道数 * 位深/8
34 2 位深 16
36 4 "data"子块标识 "data"
40 4 数据大小 音频数据总字节数

读取WAV文件的代码示例(Python):

import struct

def read_wav(filepath):
    with open(filepath, 'rb') as f:
        # 读取文件头
        riff = f.read(4)
        file_size = struct.unpack('<I', f.read(4))[0]
        wave = f.read(4)
        
        # 读取 fmt 子块
        fmt_id = f.read(4)
        fmt_size = struct.unpack('<I', f.read(4))[0]
        audio_format = struct.unpack('<H', f.read(2))[0]
        num_channels = struct.unpack('<H', f.read(2))[0]
        sample_rate = struct.unpack('<I', f.read(4))[0]
        byte_rate = struct.unpack('<I', f.read(4))[0]
        block_align = struct.unpack('<H', f.read(2))[0]
        bits_per_sample = struct.unpack('<H', f.read(2))[0]
        
        # 跳过可能的扩展字段
        if fmt_size > 16:
            f.read(fmt_size - 16)
        
        # 读取 data 子块
        data_id = f.read(4)
        data_size = struct.unpack('<I', f.read(4))[0]
        raw_data = f.read(data_size)
    
    return sample_rate, bits_per_sample, num_channels, raw_data

1.4.2 压缩格式:MP3、AAC、FLAC

WAV文件太大了。一首3分钟的CD音质歌曲,大约30MB。所以我们需要压缩。

  • MP3 (MPEG-1 Audio Layer 3):有损压缩,利用心理声学模型,去掉人耳不敏感的频率成分。128kbps是常见的“可接受”质量,320kbps接近无损。我早期做嵌入式开发时,MP3解码器是必啃的硬骨头。
  • AAC (Advanced Audio Coding):MP3的继任者,同等码率下音质更好。苹果生态的标配,也是YouTube、Netflix等流媒体平台的首选。
  • FLAC (Free Lossless Audio Codec):无损压缩,压缩比约50%-60%。音质和WAV完全一样,但文件小一半。适合音乐存档和发烧友。

下表对比了这几种格式:

格式 压缩方式 典型码率 音质 适用场景
WAV 无压缩 1411 kbps (CD) 完美 算法开发、专业制作
FLAC 无损压缩 ~700 kbps 完美 音乐存档、发烧友
MP3 有损压缩 128-320 kbps 好到可接受 音乐分发、便携设备
AAC 有损压缩 64-256 kbps 优秀 流媒体、视频平台

注意:做音频算法开发时,我强烈建议用WAV或FLAC作为中间格式。有损压缩会引入不可逆的失真,这些失真可能会干扰你的算法效果评估。比如你在MP3上调试好的降噪算法,换成WAV源文件后效果可能完全不同。嗯,我吃过这个亏。

好了,第一章的内容就到这里。音频信号的基础概念,是后面所有算法章节的基石。采样定理、量化、时频域转换,这些概念会贯穿整个课程。你最好能亲手写代码读一个WAV文件,画一下时域波形和频谱图,感受一下理论和实际的差距。