第一章:声音和信号¶
信号 代表一个随时间变化的量。这个定义很抽象,我们用声音信号来作为一个更具体的例子。 声音是空气压力的变化产生的,声音信号则代表着空气压力随时间的变化。
麦克风能够测量这种变化并转换成可以表示声音的电信号。扬声器可以接收这种电信号并产生相应的声音。 像麦克风和扬声器这类的设备统称为 信号变换器,他们能够将一种信号变换为另一种信号。
本书会介绍信号处理的基础内容,包括信号的合成,变换和分析等。本书主要关注声音信号,但是同样的方法 完全可以运用于处理其他领域的信号,比如电信号,机械振动等。
这些方法同样也可以运用于其他随空间而非时间变化的信号,比如沿运动路径的高度变化信号。他们也可以运用于处理 多维的信号,例如图像或电影。图像可以看做是随二维空间变化的信号,电影则可以看做是随二维空间和时间同时 变化的信号。
但是,我们会从最简单的一维信号 声音信号 开始。
这章的代码 chap01.ipynb
可以在本书的 代码库 中找到,你也可以在 http://tinyurl.com/thinkdsp01 查看。
1.1 周期信号¶
我们先从 周期信号 开始。周期信号是按一定的周期重复出现的信号。例如当我们敲钟的时候,它会振动并发出声音, 如果把这个声音信号记录下来,可以画出如 图1.1 。
这个信号的形状与正弦曲线很相似,这就意味着它可以用三角函数来大致描述出来。
可以看出这个信号是周期的,在这个图中,我截取了三个重复循环( cycles )的信号,每个循环所经历的时间, 我们通常称作周期( peroid ), 这个信号的周期大概是 2.3 ms。
信号的频率( frequence )是指信号每秒重复循环的次数,是周期的倒数。频率的单位是赫兹(Hz)。
这个信号的频率大约是439Hz,略小于国际标准音高440Hz,用音名A表示,更准确说应该是A4,其中数字后缀表示所属八度音程, A4与中央C属于同一个八度,A5则比A4高一个八度。如果不熟悉标准音高记法(scientific pitch notation)可以参考: http://en.wikipedia.org/wiki/Scientific_pitch_notation 。
调音用的音叉会产生一个正弦信号,因为它尖端的振动是一个简谐运动。大部分乐器所产生的声音都是周期的,但是这些信号的形状 并不是简单的正弦曲线。例如,图1.2 显示了小提琴演奏的鲍凯利尼(Boccherini)的第5号E大调弦乐五重奏的第三乐章中 截取的一部分录音。
从图中我们可以看出,这个信号是周期的,但是信号的形状要比正弦波要复杂一些。我们把信号的形状称为信号的波形( waveform )。 大多数乐器产生的波形都比正弦信号复杂,而不同的波形实际上决定了不同乐器的音色,也就影响着我们对声音的心理感受。相比于正弦波, 人们通常会感觉这些复杂的信号更加的丰富,更加的温暖或者更加的有趣。
1.2 频谱分解¶
本书中一个重要的主题就是频谱分解。信号可以进行频谱分解是由于信号可以表示为不同频率的正弦信号的叠加结果, 这些不同频率的正弦信号的集合就是 频谱 。信号经过 离散傅立叶变换 就可以得到它的频谱, 离散傅立叶变换(简称DFT),也是本书最重要的一个数学概念之一。
一般情况下,我们通过一个叫做 快速傅立叶变换(FFT) 的算法来计算信号的DFT,FFT也是本书最重要的算法之一。
例如,图1.3 显示了 图1.2 中小提琴声音的频谱。其中x轴代表了组成这个信号的正弦波频率的范围, y轴显示了它们的强度(也叫做幅度)。
我们把频谱中最小的频率称作 基频 。这个信号的基频大概是440Hz(稍低一些)。
这个信号基频的幅度也是最大的,所以它也是这个信号的 主导频率 。通常我们对音高的感受取决于声音的 基频(即使它不是主导频率)。
这个频谱中其他幅度比较大的频率是880Hz,1320Hz,1760Hz和2200Hz,它们都是基频的整数倍,这些频率分量 称为信号的 谐波(harmonics) 。之所以叫做谐波,是因为它们和基频一起出现我们会觉得声音听起来比较和谐。
译者注
对于声音信号来说,谐波的另一个名称是泛音。实际上,对于任何周期性的信号,进行傅里叶变换后都会得到谐波。 因此,在翻译的时候,统一将harmonics翻译为谐波。
- 880Hz是A5的频率,它比基频高一个 八度 。一个八度的音程在频率上是两倍的关系。
- 1320Hz大约是E6的频率,它比A5高一个纯五度。如果你不熟悉音程(如纯五度)的概念,可以参考 https://en.wikipedia.org/wiki/Interval_(music).
- 1760Hz是A6的频率,它比基频高两个八度。
- 2200Hz 大约是C#7,它比A6高一个大三度。
这些谐波的音如果在同一个八度中都是三和弦的组成部分。其中有些音的频率是近似的,这是由于组成音阶的音的频率在 平均律下有一些小的调整。详见 http://en.wikipedia.org/wiki/Equal_temperament。
给定这些谐波的频率以及相应的幅度,我们就可以通过直接叠加的方法重构出整个信号。接下来我们会学习这个方法。
1.3 信号¶
我编写了一个Python模块 thinkdsp.py
,其中包含了我们用于处理信号和频谱的类和函数。
你可以在本书的 代码库 中找到这些代码。
thinkdsp
提供一个 Signal
类来表示信号,它是其他信号类的父类,包括正(余)弦信号 Sinusoid
等。
使用 thinkdsp
可以像下面这样来构造一个正弦信号或是余弦信号:
cos_sig = thinkdsp.CosSignal(freq=440, amp=1.0, offset=0)
sin_sig = thinkdsp.SinSignal(freq=880, amp=0.5, offset=0)
freq
参数代表频率(Hz), amp 参数代表幅度(无量纲,1.0代表可以录制和播放的最大的幅度),
offset
参数代表初始相位(弧度),它决定了信号在 0 时刻的值。例如,一个 offset=0
的正弦信号
在 0 时刻的值为 sin(0)
也就是 0 ,而 offset=pi/2
的正弦信号在 0 时刻的值为 sin(pi/2)
也就是 1 。
Signal
类中定义了 __add__
方法,因此我们可以使用 +
来对两个信号进行叠加:
mix = sin_sig + cos_sig
两个信号相加的结果为 SumSingal
,他可以代表任意两个信号的叠加结果。
Singal
实际上是由一个数学函数所定义的,大部分 Signal
定义了时间从正无穷到负无穷上信号的取值。
我们可以在一个时间区间上对一个信号进行求值( evaluate ),也就是输入一个时间的序列 - ts
,计算信号在这些时间点上
相应的值 - ys
。我们使用 Numpy
的数组来表示 ts
和 ys
,并且将他们包装成一个叫做 Wave
的类中。
Wave
表示信号在某个时间序列上的值,其中每个时间点叫做帧( frame ),也可以叫做采样( sample ),这两种叫法经常
可以混用。
Signal
类提供了一个 make_wave
返回一个新的 Wave
对象:
wave = mix.make_wave(duration=0.5, start=0, framerate=11025)
其中, duration
参数代表了需要求值的时间区间的长度(单位为秒), start
参数代表开始时间(单位为秒),
framerate
是一个整数,表示每秒的帧数,也叫做 采样率 (单位为Hz)
这个例子中,11025Hz的采样率经常用于音频信号的采集中,如 WAV 和 MP3 。该例中我们求得了信号在 t=0
到 t=0.5
区间内
等间隔的5513个采样值,我们把两次采样之间的时间间隔称为时间步长( timestep ),为采样率的倒数,这里的时间步长为 1/11025
大约为 91 微妙
Wave
提供了 plot
方法来画出波形图(使用 pyplot
模块实现):
wave.plot()
pyplot.show()
pyplot
是 matplotlib
库的一部分,是很常用的作图模块,他被包含在大部分的Python发行版中,当然你也可以手动安装它:
pip install matplotlib
这个440Hz的信号在0.5s内有220个周期,因此上面的代码画出的图形看起来会像是一条很粗的实线。
我们可以使用 segment
方法来截取一个更小的时间范围的波形:
period = mix.period
segment = wave.segment(start=0, duration=period*3)
period
是信号的一个属性,他返回信号的周期值(单位为秒)。
start
和 duration
的单位也是秒。这段代码截取了 mix
信号一开始的三个周期,其结果 segment
也是一个 Wave
对象。
我们画出 segment
的波形图,如 图1.4 。这个信号包含了两个不同频率的成分,因此它的波形看起来会比音叉发出的正弦信号要复杂一些,
但是比小提琴发出的声音信号要简单一些。
1.4 读写波形数据¶
thinkdsp
提供 read_wave
函数从 WAV 文件中读取数据并返回一个 Wave
对象:
violin_wave = thinkdsp.read_wave('input.wav')
Wave
对象提供了 write_wave
方法将数据写入到 WAV 文件中:
wave.write(filename='output.wav')
你可以用任意的媒体播放器来播放这些 WAV 文件。 在 UNIX 系统中,我通常使用 aplay
,这是一个简单而稳定的播放器,
多数的Linux发行版中都包含这个程序
thinkdsp
也提供了一个直接播放声音的函数 play_wave
,它会在一个子进程中运行播放器来播放音频数据:
thinkdsp.play_wave(filename='output.wav', player='aplay')
上面的代码中使用了默认的播放器 aplay
,当然你也可以通过 player
来指定其他的播放器。
译者注
我没有在Windows下找到播放器来替代 aplay
,如果有读者知道请
1.5 频谱¶
Wave
中提供了 make_spectrum
来生成频谱 Spectrum
spectrum = wave.make_spectrum()
Spectrum
同样也提供了 plot
方法用于作图:
spectrum.plot()
thinkplot.show()
我在 thinkplot
模块中包装了一些常用的 pyplot
方法,这个模块也包含在本书的 代码库 中。
Spectrum
提供了三个方法来对频谱进行变化:
low_pass
会对频谱应用一个低通滤波器,也就是说,高于给定截止频率的分量会被衰减(幅度减小), 衰减的程度由factor
指定,通常为一个 [0, 1] 的数,默认为 0 (完全衰减)。high_pass
会对频谱应用一个高通滤波器,也就是说,低于给定截止频率的分量会被衰减。band_stop
会对频谱应用一个带通滤波器,也就是说,在给定截止频率区间以外的分量会被衰减。
以下的代码将频谱的600Hz以上的频率成分衰减了99%:
spectrum.low_pass(cutoff=600, factor=0.01)
低通滤波器去除了声音中的明亮的高频声音,使声音变得比较低沉。你可以通过将频谱转换为波形后来播放它:
wave = spectrum.make_wave()
wave.play('temp.wav')
play
方法会将波形数据写入文件并且进行播放。如果使用 Jupyter notebooks
,你可以用 make_audio
生成一个可以直接播放的网页音频部件。
1.6 波形对象¶
其实 thinkdsp.py
中并没有什么复杂的东西,它提供的大多数方法仅仅是对 Numpy
和 Scipy
的包装。
其中主要有三个类: Signal
, Wave
和 Spectrum
。
给定一个 Signal
可以生成一个 Wave
,
给定一个 Wave
可以生成一个 Spectrum
,反之亦然。 图1.5 展示了这些关系。
Wave
包含三个属性: ys
是包含信号值的Numpy数组; ts
是对应的时间数组; framerate
是采样率。
其中单位时间通常是秒,但是有些例子中也会使用其他的单位时间,例如天。
Wave
还包含三个只读属性: start
, end
和 duration
,
这些属性由 ts
所决定,改变ts后这些属性会相应的改变。
我们可以通过直接改变 ts
以及 ys
来改变波形,例如:
wave.ys *= 2
wave.ts += 1
第一行代码将信号放大了两倍,使其音量变的更大。第二行代码将波形右移了一个单位时间,使其声音晚一秒钟才开始。
Wave
也提供了很多方法来进行更常规的操作,例如以下两个变换与之前的代码效果一样:
wave.scale(2)
wave.shift(1)
这些方法的文档在 http://greenteapress.com/thinkdsp.html 中。
1.7 信号对象¶
Signal
是所有信号的父类,其中提供了信号的基础方法,如 make_wave
。子类信号通过继承 Signal
并重写
evaluate
方法来实现。 evaluate
方法用于计算信号在任意时刻的值。
例如, Sinusoid
子类的定义如下:
class Sinusoid(Signal):
def __init__(self, freq=440, amp=1.0, offset=0, func=np.sin):
Signal.__init__(self)
self.freq = freq
self.amp = amp
self.offset = offset
self.func = func
其中构造参数包括:
- freq:信号的频率(Hz)
- amp:信号的幅度,通常单位为1
- offset:信号的初始相位,单位为弧度
- func:用于计算给定时间点的信号值的函数。可以为
np.sin
或np.cos
,对应为正弦信号和余弦信号。
Singal
类中的 make_wave
方法的代码如下:
def make_wave(self, duration=1, start=0, framerate=11025):
n = round(duration * framerate)
ts = start + np.arange(n) / framerate
ys = self.evaluate(ts)
return Wave(ys, ts, framerate=framerate)
其中, start
duration
为开始时间和持续时间(单位为秒),framerate
是采样率(单位为Hz)。
n
是采样点的总数, ts
用Numpy数组表示的采样时间
make_wave
会调用 evaluate
方法来计算信号在每个采样点的值 ys
,
例如: Sinusoid
中的 evaluate
是这样的:
def evaluate(self, ts):
phases = PI2 * self.freq * ts + self.offset
ys = self.amp * self.func(phases)
return ys
让我们详细解释一下这个函数:
self.freq
是频率,ts
是采样时间序列,因此他们的乘积为采样的cycle
PI2
是常数 \(2\pi\) ,把cycle
与 \(2\pi\) 相乘 就得到了相位( phase )。我们如果将波形循环一周的长度视作360°,即 \(2\pi\) , 那么相位就是信号在一周内所处的位置。self.offset
是初始相位,也就是t=0
时刻信号的相位。它实际上代表了波形的左右平移。- 如果
self.func
是np.sin
或np.cos
则计算的值会在[-1,1]的范围内。 - 乘以
self.amp
使得最终的结果范围为[-self.amp, self.amp]。
evaluate
用数字公式表示为:
其中 \(A\) 是幅度,\(f\) 是频率,\(t\) 是时间,\({\varphi _0}\) 是相位。 看起来好像我们用了很多代码来描述了一个简单的公式,实际上,我们得到了一个通用的框架来描述 所有类似的信号,而不仅仅是正余弦信号。
译者注
在 thinkdsp.py
中除了正余弦信号外,有很多信号都继承自 Sinusoid
,
包括三角信号 TriangleSignal
,方波信号 SquareSignal
,
锯齿信号 SawtoothSignal
等。这些信号的特征是都具有频率,幅度和初始相位的属性,
而唯一的区别就是它们的 evaluate
方法。
1.8 练习¶
在开始下面的练习之前,你可以从本书的 代码库 中下载本书的源码。
下面练习的答案可以参考文件 chap01soln.ipynb
练习1 如果你安装了 Jupyter
,使用它来打开 chap1.ipynb
,
阅读并且运行上面的代码示例。 如果没有 Jupyter
,可以在
http://tinyurl.com/thinkdsp01 浏览和运行它。
练习2 在 http://freesound.org 上下载一段清楚的声音,可以是音乐,语音或其他的声音,
使用代码截取其中音高固定的半秒声音,并计算并画出这段声音频谱,观察一下这个声音的音色
和它的频谱之间有什么样的关系。然后使用 high_pass
, low_pass
, band_stop
来
滤除其中的一些谐波分量,把他们在反过来转换为波形对象并播放,听一听与原来的声音有什么区别。
练习3 产生一些正弦信号和余弦信号,并将他们相加合成一个复合信号。 然后生成并画出信号的波形以及频谱。播放这个声音听一听,看看如果频率分量不是基频的整数倍的时候, 声音是怎么样的。
练习4 编写一个 stretch
函数,接收一个 Wave
对象以及一个伸缩因子,通过改变 ts
和 framerate
来让波形变快或变慢。提示:仅需要两行代码就能实现这个功能。