COURSE 01 · AUDIO SIGNAL PROCESSING
从「声音是什么」讲到 MFCC
面向没有任何相关背景的读者。全程围绕一个项目:让程序听 1 秒钟的音乐片段,判断它来自古典、爵士还是摇滚。
23 讲247 张配图8 种交互实验23 个可运行脚本
00开始之前
01—05认识声音与数字录音
- 01程序要分辨古典、爵士和摇滚,先得跨过哪一步?免费给程序一张猫的照片,它拿到的是一块排得整整齐齐的颜色网格;给程序一段汽车驶过的声音,它拿到的是一长串按时间冒出来的数字。两件事都叫「分类」,但声音这一边多了一道工序——得先有人把那串数字整理成可以互相比较的证据,程序才有东西可比。这道工序就叫音频信号处理,也就是这 23 课要做的事。这一课不讲任何一种具体算法,只把三件事定下来:我们要让程序完成什么任务、这 23 课按什么顺序走、你需要什么基础。
- 02声音是怎么产生的,又怎么变成录音里那条线?免费上一课把任务定下来了:让程序分辨古典、爵士和摇滚。可程序拿到的是一长串数字,这串数字从哪儿来?这一课从最开头讲起——一个物体振动,推动它周围的空气,这层推挤一圈圈传到麦克风,麦克风把每一瞬的推挤强弱记成一个数。把这些数按时间画出来,就是录音软件里那条抖动的线,它叫波形。这条线里同时藏着三样东西:振动多快、振动多大、以及听起来是什么音色。这一课讲清前两样,音色留给第 03 课。
- 03音量条没动,为什么换一首歌就明显更吵?免费上一课把波形里的频率和振幅讲完了,还欠两样:强弱到底怎么量,音色又是什么。 这一课先从声源出发,一层层往耳朵走:声源每秒放出多少能量(声功率)→ 传到你这个位置每平方米有多少(声强)→ 从刚能听见到开始疼,中间差了十万亿倍 → 所以要用分贝把它压成一个好读的数 → 可分贝仍然不等于你听到的响度。最后回答那个「音量一样却不一样」的问题:高低和响度都对齐之后,剩下的差别叫音色,它由三样东西决定。
- 04连续的声音,怎样变成一串数字又变回来?完整版前两课讲的都是连续变化的东西——空气压力、频率、响度。可计算机一样都存不下:它存不了无穷多个时刻,也存不了无穷多种取值。这一课走完整条转换链:麦克风送出连续变化的电压 → 一个叫 ADC 的部件把它变成一串数字 → 计算成什么样都行 → 最后一个叫 DAC 的部件把数字变回连续电压,推动音箱。ADC 只做两件事:多久量一次(采样),每次量得多细(量化)。 这两件事各有各的代价,而且性质完全不同:一件事后能补救,另一件永远补不回来。
- 05该从录音里算出什么交给程序?——音频特征的五个维度完整版到上一课为止,一段 30 秒的音乐已经变成了一串六十多万个数字(每秒记两万两千多次,记了三十秒)。直接把这串数字丢给分类程序不行——太长、太细、而且单独一个数说明不了任何事。所以要先算出一些摘要,这些摘要就叫音频特征。搜「该用什么音频特征」会搜到一份长长的名词清单,可这些名字回答的不是同一种问题。这一课不背清单,而是给出一张分类地图:任何一个音频特征,都可以用五个维度定位——抽象层级、时间范围、音乐属性、信号域、用什么方法算出来。
06—10把录音变成可计算片段
- 06从整段录音到一串可比较的数,中间要走哪几步?完整版上一课把「该算什么」分成了五个维度,其中信号域那一维分了三档:时域、频域、时频。这一课讲前两档各自的完整流水线——从 ADC 出来的一串数字开始,到最后交给程序的那几个数为止,中间每一步是什么、为什么少不了。两条流水线的前半段是一样的(都要先切段),分岔发生在频域那条上:它多了「加窗」这一步。 而加窗又会带出一个新问题,逼出「帧必须重叠」这个结论。
- 07不做任何频率分析,只看那条曲线能问出什么?完整版上一课把流水线搭好了,「逐帧计算」那一格还空着。这一课填上时域这条的三个答案:振幅包络(这一帧最高多少)、均方根(这一帧整体多强)、过零率(这一帧穿过中线多少次)。三个都不需要任何频率分析,只看那条曲线本身。它们不是三个可以互换的名字,而是三个不同的问题——每一个都有自己的公式、自己的弱点、自己的用武之地。这一课把三个公式各拿一个八个数的小帧手算一遍,你拿笔就能核对。
- 08把振幅包络的公式,写成真能跑的代码完整版[上一课](07-三个时域特征-不做频率分析能问出什么.md)把三个公式讲完了,但一行实现都没写。这一课从头做第一个:振幅包络。我们会载入三段音乐、确认基本信息、画出波形、手写包络、把帧编号换算成秒,最后把包络叠到波形上比较三种曲风。中间还会发现一个容易漏掉的 `abs`:它在整段最大值上几乎看不出影响,逐帧检查时却会让三段音乐里 18% 到 36% 的帧被低估。
- 09先调库,再手写,然后逼它们对上完整版这一课实现剩下的两个时域特征。做法和上一课不同:先用 librosa 一行算出来,再从零手写一遍,然后把两个结果逐帧比对。 这一步不是走形式——数值对不上,就说明你和库对「一帧是哪些样本」的理解不一样,后面所有比较都不成立。均方根这边两边严丝合缝(逐帧最大差 8×10⁻⁹);过零率那边故意对不上,比值正好是 1024/1023——这正是第 07 课留下的那个「除以 K 还是除以 K−1」。
- 10电脑怎样从一条曲线里,找出它由哪些高低成分组成?完整版前四课把「只看那条曲线」能问的都问完了。可有些事它永远答不了——同时按下钢琴的三个键,麦克风只记下一条起伏的曲线,波形上完全看不出里面有三个音。这一课换个角度:不问它随时间怎么起伏,问它由哪些高低成分组成。 做法出乎意料地朴素:拿一支已知频率的波去和它逐点相乘,再求平均。 对上了平均值不为零,对不上正负抵消成零。把所有频率都试一遍,得到的那条曲线就是频谱。
11—15从波形进入时间—频率表示
- 11一个数,怎样同时装下「有多强」和「从哪里起步」?完整版上一课留下一个悬念:一支试探波会被起点位置抹掉,得用两支。 于是每个频率问出来的不是一个数,而是两个——强度,和从哪里起步。可「强度」是个普通的数,一个普通的数装不下两样东西。这一课引入的复数,正是为解决这件事而来:一个复数就是平面上的一个点,而一个点天生带着两个信息——离原点多远,朝哪个方向。 前者对应强度,后者对应起点。这一课不讲抽象的数学,只把上一课量到的那两个读数,真的拼成一个复数。
- 12把「有多强」和「从哪里起步」,写进同一个系数完整版上一课把复数准备好了:模记强度,角记起点。 这一课把它正式塞进傅里叶变换——每个频率算出一个复数系数,取它的模得到幅度谱,取它的角得到相位谱。那条看着吓人的公式,拆开之后做的还是第 10 课那件事:乘一支波,再求平均,只是欧拉公式把两支试探波合并成了一个 $e$ 的指数。最后走一次完整往返:变过去再变回来,逐点最大误差 $2.5\times10^{-13}$;而如果把相位扔掉再变回来,误差变成 2.05——差了八万亿倍。
- 13计算机只有有限个数字,怎样完成傅里叶变换?完整版上一课的公式假设时间连续,而且可以检查无穷多个频率;真实计算机手里只有有限个采样点。要让公式真正跑起来,需要把时间和频率各限制一次:留下 $N$ 个样本,也只计算 $N$ 个频率格。这样得到的计算叫离散傅里叶变换,缩写为 DFT。
- 14用 Python 读取真实声音的频谱:四段声音为什么长得不一样?完整版同样弹一个音,小提琴、钢琴和萨克斯听起来仍然不同;噪声又是另一种样子。这一课把四段录音交给 Python,比较它们在不同频率上的强弱分布,并把横轴读成真正的 Hz。
- 15整段频谱为什么找不到声音发生的时间:短时傅里叶变换完整版两段声音都先后出现一个低音和一个高音,只把顺序交换,整段频率结果就可能看起来完全一样。要让程序找回“哪个声音先出现”,需要把录音分成互相重叠的小段,每到一个位置就重新计算一次。
16—20把频谱整理成模型输入
- 16同一批数,为什么换个画法才看得见:用 Python 画声谱图完整版上一课已经把一段录音变成了一张「哪个高低成分、在哪一段时间、有多强」的大表格。这一课把它真的画出来。可第一次画出来的图几乎是全黑的——不是算错了,是上色的方式不对:这张表里 99.70% 的格子都不到最强那一格的百分之一,硬按比例分配颜色,它们全挤进同一种黑。换成分贝上色,再把纵轴改成按倍数刻,同一批数就全都看得见了。
- 17怎样按听感重新刻一把频率的尺子:梅尔刻度完整版上一课把纵轴改成按倍数刻,理由是音乐里一个八度就是频率翻一倍。可人耳并不完全按倍数听。有个老实验能说明这件事:C2 升到 C4,和 G6 升到 A6,两次的振动快慢只差 196 和 192 —— 几乎一样;可前者听起来跨了两个八度,后者只跨一个全音。同样的差值,听感差 12 倍。这一课把频率轴换成一把按听觉实验刻出来的尺子,再用十个三角形把 1025 行压成 10 行。
- 18形状对上了,数字却全不一样:用 Python 提取梅尔声谱图完整版上一课把那组梅尔滤波器——十个三角形,每个只管一段频率,中间最敏感、两边逐渐不管——按五步自己造了一遍。这一课让现成的库造一遍,然后把两份结果摆在一起——形状一模一样,数字一个都对不上。 顺着差异追下去,会挖出三个默认值:库默认取到的最高频率不是 8000、默认把每个三角形缩成等面积、默认用的根本不是同一条梅尔公式。三处改过来之后,最大差从 0.9999 降到 0.0147。剩下的那点残差,也有出处。
- 19把「谁在发声」和「发的什么音」分开:MFCC 是什么完整版一段人声里同时装着两样东西——声带振多快(决定音高),和嘴摆成什么形状(决定发的是「啊」还是「衣」)。在频谱上它们是相乘的,缠在一起谁也拿不出来。这一课做的事只有一步:取个对数,相乘就变成了相加;相加的两样,再变换一次就能落到一根轴的两端。本文把这条路上的每一步都量出来——包络 98.3% 在左边、激励 99.0% 在右边,切一刀就分开了;最后那步 DCT 把梅尔带之间 0.95 的相邻相关压到 0.15,前 13 个系数装下原谱 98.2% 的变化。
- 20一行代码替你做了哪四步:用 Python 取 MFCC完整版上一课把 MFCC 整条链子手写了一遍。这一课把它交给库:`librosa.feature.mfcc` 一行返回 13 行系数,`librosa.feature.delta` 再给出一阶和二阶变化率,拼起来就是常说的每帧 39 维。照例,一行拿到结果之后要做的第一件事是把它拆回上一课手写的那四步,逐位对齐——四个默认值全改对,最大差才降到 0.0000。中间还会撞上两件事:`delta` 不是「后一帧减前一帧」;13 个系数里没有哪一个单独分得开三种风格。
21—23用少量数字概括频谱
- 21一列频谱怎样压成三个数:带能量比、频谱质心与带宽完整版声谱图的一列有上千个频率格。它很完整,却不适合直接回答三个简单问题:低频相对高频有多少?频率的重心在哪里?频率围绕重心铺得多开?这一课给这三个问题各配一个数:带能量比、频谱质心和频谱带宽。我们还会造三列答案已知的频谱,证明这三个数观察的是不同侧面,谁也代替不了谁。
- 22用 Python 实现带能量比:2000 Hz 到底该切在哪一格完整版BER 的公式只有一个除法,代码却有两个不报错的陷阱:把“时间帧数”当成“频率格数”,以及用近似格距把 2000 Hz 切到错误一侧。本课用三段 30 秒音乐逐帧计算 BER。正确分界是第 186 格;一旦把 1292 个时间帧误当成频率格数,分界会跑到第 234 格,也就是实际约 2519 Hz。德彪西那段的中位 BER 会因此多出 4.37 dB。
- 23频谱质心与带宽:怎样量出频率的中心和展开完整版频谱质心和带宽经常只用两行库函数带过。本课反过来做:先按库的默认参数得到三段音乐的时间轨迹,再把两行函数拆成公式,自己逐帧重算。质心最大差只有 1.917e-05 Hz,带宽最大差只有 1.156e-05 Hz。对齐过程还会揭开一个容易混用的定义:平均绝对距离是 $p=1$,库函数默认算的是均方根距离 $p=2$,这三段音乐上两者能差到 1.30—1.54 倍。