专栏总目录
信号代表随着时间变化的量。声音源于空气压力的改变。声音信号代表的是空气压力随着时间的变化。
传声器是测量上述变化并产生表示所测声音的电信号的设备。传声器和扬声器都被称为换能器(transducer)。
1.1 周期信号
周期信号是在一段时间之后重复出现的信号。比如:敲钟时候,钟会震动从而产生声音。录制后绘制其信号如下图:
图1-1
该信号与三角函数类似,也就是说其形状和正选三角函数的形状一样。
上图信号是周期性的,这里选择显示了3个完整的重复,也被称为循环。每个循环的时长称为周期,上图信号周期大概2.3ms。
信号的频率是每秒钟内周期的数量,即周期的倒数。频率的单位是每秒钟循环次数,或者称为赫兹(Hertz),英文符号为”Hz“。
上图信号频率约为439Hz,比国际标准音的440Hz稍低。这个音符的音名叫做A,或者更确切地说是小字一组a。
音叉产生的是正弦信号,因为线性的变化是一种简单的谐波运动形式。大部分乐器产生的是周期信号,但是这些信号的形状不是正弦式的。如下图(小提琴演奏的片段):
图1-2
上图信号是周期性的,但是信号的形状更加复杂。周期信号的形状被称为波形。大部分乐器产生的波形比正弦信号的复杂。波形的形状决定了音乐的音色,也就是我们对声音品性的感受。对于比正弦信号更复杂的复杂波形,人们主观上认为它更加有内涵、更温暖而有趣。
1.2 频谱分析
任何信号都可以表示成一系列不同频率的正弦信号的叠加和。
【数学概念】离散傅里叶变换(Discrete Fourier Transform, DFT),DFT也就是将信号转换成频谱。频谱是指相加产生信号的正弦波的集合。
【算法】快速傅里叶变换(Fast Fourier Transform, FFT),它是计算离散傅里叶变换的一种高效方式。
图1-3
图1-3显示的是图1-2小提琴的频谱。x轴表示的是合成这个信号的频率范围,y轴表示各个频率元素的强度,或者说是振幅。
其中频率最低的元素被称为基频。图1-2所示的信号的基频在440Hz左右(实际上要低一点)。
此信号中,基频具有最高的幅度,所以它也是主频。通常情况下,感知到的声音的音高是由其基频决定的,即使它不是主频。
频谱中其他峰值的频率还有880、1320、1760以及2200,它们是基频的整数倍。这些频率元素被称为谐波,因为它们在乐理概念上跟基频和谐:
880Hz是小字2组a的频率,比基频高出一个八度。一个八度意味着基频频率的两倍。
1320Hz接近小字3组e,也就是小字2组a的纯五度。
1760Hz是小字3组a的频率,比基频高出2个纯八度。
2200Hz接近小字4组#c,它是小字3组a向上大三度关系。
这些谐波共同构成了A大调大三和弦A #C E,虽然不在一个八度内,其中一些音只是近似,因为西方音乐已经按照平均率做了调整。
1.3 信号
1.3.1 获取thinkdsp包
这本书提供的python模块thinkdsp中,包含信号和频谱分析的类和函数。下载地址:GitHub - AllenDowney/ThinkDSP: Think DSP: Digital Signal Processing in Python, by Allen B. Downey.
1.3.2 Signal类
将使用到matplotlib包,请提前安装好
安装方法:在cmd中输入下列命令:
pip install matplotlib
1.3.2.1 产生正、余弦信号
thinkdsp中显示信号的类,名为Signal。这个类是多个信号类型的父类,包括Sinusoid中包含正弦和余弦信号。
thinkdsp提供产生正弦和余弦信号的函数,调用方法如下:
from thinkdsp import CosSignal, SinSignal
# 产生一个频率440Hz、振幅为1、无偏移量的余弦信号
cos_sig = thinkdsp.CosSignal(freq=440, amp=1.0, offset=0)
# 产生一个频率880Hz、振幅为0.5、无偏移量的正弦信号
sin_sig = thinkdsp.SinSignal(freq=880, amp=0.5, offset=0)
其中,freq——频率,单位Hz;amp——振幅,单位未指定;offset
(偏移量)是指信号的初始值,它定义了正弦波在t=0时的初始位置,offset=0表示正弦信号从0开始。
在jupyter notebook中,可以在执行上面命令基础上,继续执行以下命令,显示信号图。
from thinkdsp import decorate cos_sig.plot() decorate(xlabel='Time (s)')
在idle中,或在.py文件中,可以通过以下命令生成图片,便于查看
from thinkdsp import CosSignal, SinSignal, decorate # 按照要求生成余弦和正弦信号 cos_sig = CosSignal(freq=440, amp=1.0, offset=0) sin_sig = SinSignal(freq=880, amp=0.5, offset=0) # 生成余弦信号图 cos_sig.plot() decorate(xlabel='Time (s)') # 将信号图保存为cosine_signal.png文件 import matplotlib.pyplot as plt plt.savefig('cosine_signal.png') # 清除已生成的信号图,为下一步做准备 plt.clf() # 生成正弦信号图 sin_sig.plot() decorate(xlabel='Time (s)') # 将信号图保存为sine_signal.png文件 plt.savefig('sine_signal.png')
1.3.2.2 信号叠加和操作
各个信号都有__add__方法,所以可以对他们直接使用 ”+“ 操作:
mix = sin_sig + cos_sig
其结果为SumSignal,表示了两个或者多个信号的叠加和。
在jupyter notbook中,在上述信号生成后,继续输入以下命令,实现信号叠加操作
mix = sin_sig + cos_sig # 生成信号叠加图 mix.plot() decorate(xlabel='Time (s)')
在idle中,或在.py文件中,可以通过以下命令生成图片,便于查看
from thinkdsp import CosSignal, SinSignal, decorate import matplotlib.pyplot as plt # 按照要求生成余弦和正弦信号 cos_sig = CosSignal(freq=440, amp=1.0, offset=0) sin_sig = SinSignal(freq=880, amp=0.5, offset=0) # 信号叠加操作 mix = sin_sig + cos_sig # 生成信号叠加图 mix.plot() decorate(xlabel='Time (s)') # 将信号图保存为mix_signal.png文件 plt.savefig('mix_signal.png')
Signal实际上是数学函数的Python表示。大部分信号(Signal)在全部的t值下做了定义——从负无穷到正无穷。
1.3.2.3 Wave对象
这一步操作用到scipy包,需要提前按装。
安装方法:在cmd窗口中输入下列命令:
pip install scipy
对Signal求值,即根据一系列时间点ts,来计算出对应信号值ys。在Wave对象中使用封装了表示ts和ys的Numpy数组。
Wave表示的是信号在一些列时间点下求出的值。每个时间点被称为帧(借用了电影和视频的概念)。测度本身被称为样本,虽然 ”帧“ 和 ”样本“ 在一些情况下可以互换使用。
Signal提供了make_wave,后者返回一个新的Wave对象:
wave = mix.make_wave(duration=0.5, start=0, framerate=11025)
duration——Wave的长度,单位为秒;start——开始的时间,单位秒;framerate——每秒帧数,该值为整数,表示每秒内的样本数。
11025帧每秒,是在音频文件中最常用的几个帧率之一,这些音频文件包括波形音频文件(Wavefrom AudioFile,WAV)和MP3。
上述代码产生的信号从t=0到t=0.5,取得5513个间距相等的帧(5513是11025的一半)。帧间的时间差被称为时间步长,值为1/11025秒,约等于0.0000907秒。
在jupyter notbook中,生成信号后,使用IPython.display包中的Audio函数,可以将信号转换为音频,进行聆听
wave = mix.make_wave(duration=0.5, start=0, framerate=11025) # 【第一种】获取音频方式 from IPython.display import Audio audio = Audio(data=wave.ys, rate=wave.framerate) audio # 【第二种】获取音频方式 wave.make_audio()
jupyter notebook中使用两种获取音频方式,显示如下图:
在idle中,或在.py文件中,可以通过以下命令生成该音频,便于聆听
from thinkdsp import CosSignal, SinSignal, decorate import numpy as np # 创建信号 cos_sig = CosSignal(freq=440, amp=1.0, offset=0) sin_sig = SinSignal(freq=880, amp=0.5, offset=0) # 叠加信号 mix = sin_sig + cos_sig # 生成叠加信号音频数据 wave = mix.make_wave(duration=0.5, start=0, framerate=11025) # 保存音频为WAV文件 from scipy.io.wavfile import write scaled = np.int16(wave.ys/np.max(np.abs(wave.ys)) * 32767) write('output.wav', wave.framerate, scaled) # 现在,您可以使用音频播放器打开并播放output.wav文件。
1.3.2.4 Wave的plot方法
Wave提供的plot方法,使用方法为pyplot,可以用于绘制波形。
wave.plot()
pyplot.show()
pyplot是matplotlib的一部分。Python默认安装,可以直接调用。
对于freq=440,在0.5秒之内有220个周期,所以其绘制结果开起来会像一大块色块。要放大显示几个周期的波形可以使用segment,它复制了Wave的一段,然后返回一个新波形:
period = mix.period
segment = wave.segment(start=0, duration=period*3)
period是Signal的属性,它返回的是信号的周期,单位为秒;start和duration的单位都是秒。这个例子从mix中复制了前3个周期,其结果是一个Wave对象。
如果绘制segment,它的结果如下图1-4所示,这个信号包含两个频率元素,所以它比饮茶的信号更为复杂,但是比小提琴的简单。
图1-4 两个三角函数混合信号的片段
在jupyter notbook中,在上述信号生成后,显示命令如下:
segment.plot() decorate(xlabel='Time (s)')
在idle中,或在.py文件中,可以通过以下命令生成该图:
from thinkdsp import CosSignal, SinSignal, decorate import matplotlib.pyplot as plt # 创建信号 cos_sig = CosSignal(freq=440, amp=1.0, offset=0) sin_sig = SinSignal(freq=880, amp=0.5, offset=0) # 信号叠加操作 mix = sin_sig + cos_sig # 生成叠加信号音频数据 wave = mix.make_wave(duration=0.5, start=0, framerate=11025) # 从mix中复制了前3个周期 period = mix.period segment = wave.segment(start=0, duration=period*3) segment.plot() decorate(xlabel='Time (s)') # 将信号图保存为mix_signal_2.png文件 plt.savefig('mix_signal_2.png')
1.3.2.5 作者在学习资料中的jupyter notebook测试中进行的其他操作
(1)wave.normalize()音量最大化
# 放大一个波形,使其峰值匹配音量做大值,不需要保存为变量,执行后wave就已经被调整
wave.normalize()
在jupyter notebook中显示normalize后的图
wave.plot() decorate(xlabel='Time (s)')
在idle中,或在.py文件中,可以通过以下命令生成该图:
from thinkdsp import CosSignal, SinSignal, decorate import matplotlib.pyplot as plt # 创建信号 cos_sig = CosSignal(freq=440, amp=1.0, offset=0) sin_sig = SinSignal(freq=880, amp=0.5, offset=0) # 信号叠加操作 mix = sin_sig + cos_sig # 生成叠加信号音频数据 wave = mix.make_wave(duration=0.5, start=0, framerate=11025) # 音量最大化处理 wave.normalize() wave.plot() decorate(xlabel='Time (s)') # 将信号图保存为normalized_signal.png文件 plt.savefig('normalized_signal.png')
(2)淡入淡出wave.apodize()
# 淡入淡出处理
wave.apodize()
显示波形图,可以在音量最大化操作后增加上述淡入淡出处理后显示。具体方法不再赘述,参考上一步。
1.4 波形读写
1.4.1 读取文件
thinkdsp提供的read_wave可以读取WAV文件并返回一个Wave对象:
violin_wave = thinkdsp.read_wave('input.wav')
测试脚本:
# 在下载资料包的code文件中有92002__jcveliz__violin-origional.wav测试音频
filename = '92002__jcveliz__violin-origional.wav'
# 如果在当前目录中没有找到92002__jcveliz__violin-origional.wav文件,则执行下载命令
if not os.path.exists(filename):
!wget https://github.com/AllenDowney/ThinkDSP/raw/master/code/92002__jcveliz__violin-origional.wav
from thinkdsp import read_wave
# 读取该.wav格式文件
wave = read_wave(filename)
jupyter notebook中,通过继续下列命令播放
wave.make_audio()
在idle中,或在.py文件中,通过继续执行下列命令获得音频文件
# 保存音频为WAV文件 from scipy.io.wavfile import write scaled = np.int16(wave.ys/np.max(np.abs(wave.ys)) * 32767) write('output.wav', wave.framerate, scaled) # 现在,您可以使用音频播放器打开并播放output.wav文件。
利用上面学过的segment方法,从以上音频中截取一段,开始时间为1.2,持续时间duration为0.6
start = 1.2
duration = 0.6
segment = wave.segment(start, duration)
jupyter notebook显示,继续执行下列命令
segment.plot() decorate(xlabel='Time (s)')
idle或在.py文件中执行,继续执行下列命令
import matplotlib.pyplot as plt from thinkdsp import decorate segment.plot() decorate(xlabel='Time (s)') # 将信号图保存为segment_signal.png文件 plt.savefig('segment_signal.png')
1.4.2 写入文件
Wave提供了write函数,它能写WAV文件:
wave.write(filename='output.wav')
可以通过媒体播放器播放WAV文件从而听到这个文件的声音。在Linux系统上我使用的是aplay,他是一个简单鲁棒(适应性好)的播放器,很多Linux发行版都有它。
1.4.3 在Unix系统上使用aplay播放器,作为子进程播放文件
thinkdsp同时还提供play_wave,它能以子进程运行媒体播放器(在Unix系统上使用aplay播放器测试):
thinkdsp.play_wave(filename='output.wav', player='aplay')
1.5 频谱
Wave提供了make_spectrum,它返回的是Spectrum:
spectrum = wave.make_spectrum()
[操作案例]:
延续1.4.1的segment操作数据,获取segment的频谱图
spectrum = segment.make_spectrum()
jupyter notebook显示方法:
spectrum.plot() decorate(xlabel='Frequency (Hz)')
使用idle或.py文件的输出显示图的方法:
import matplotlib.pyplot as plt from thinkdsp import decorate spectrum.plot() decorate(xlabel='Time (s)') # 将信号图保存为spectrum.png文件 plt.savefig('spectrum.png')
就像上述操作表现得那样,Spectrum提供了plot,执行下列代码,也可以实现显示上述图像:
import thinkplot
spectrum.plot()
thinkplot.show()
作者提供的thinkplot模块,提供了pyplot中一些函数的封装。 在使用thinkplot.show()之前,要安装pandas库
安装方法:在cmd命令窗口,输入以下命令
pip install pandas
可以通过调整spectrum.plot(high=1000)中,high参数值,调整x轴显示范围
Spectrum提供了3种修改频谱的方法:
(1)low_pass,它加载一个低通滤波器,也就是说低于某个截止频率的频率元素按照一定因数衰减(也就是在大小上降低)了。
(2)high_pass,它加载了一个高通滤波器,也就是说低于某个截止频率的元素被衰减量。
(3)band_stop,它让处于两个截止频率之间的波段内的频率元素衰减了。
下面的例子将所有高于600的频率衰减了99%:
spectrum.low_pass(cutoff=600, factor=0.01)
低通滤波器会移除明亮的高频声音信号,所以其结果的声音比较压抑而且昏暗。读者可以将Spectrum转化回Wave,然后播放来感受以下:
wave = spectrum.make_wave()
wave.play('temp.wav')
play方法将波形写入一个文件并播放它。如果读者使用Jupyter Notebook,就可以使用make_audio,它会创建一个音频部件用于播放声音。
1.6 波形对象
thinkdsp.py里面提供的大部分函数只是对Numpy和Scipy函数的稀薄封装(thin wrappers)。
thinkdsp的初始库包括Signa、Wave和Spectrum:
给定Signal,就可以创建一个Wave;
给定Wave,就能创建一个Spectrum;
反之亦然。
Wave对象包括3个特性:
包含信号参数的Numpy数组ys;
信号开始采样和取值的时间点数组ts;
每单位时间的采样数framerate。(时间单位通常是“秒”,但也可以不是,比如“天”)
Wave还提供了3个只读属性:start、end和duration。如果读者修改了ts,这些属性也会相应改变。
可以通过直接修改ts和ys来改变波形,例如:
wave.ts *= 2
wave.ys += 1
第一行波形扩大了2倍,会听起来更响亮,第二行将波形按时间移动了,让它晚一秒开始。
此外,Wave还提供了执行通用操作的方法。例如,与上面相同的两个变换可以写成以下形式:
wave.scale(2)
wave.shift(1)
可以在http://greenteapress.com/thinkdsp.html获取相关文档。
1.7信号对象
Signal是一个父类,它向各种信号提供通用的函数,比如make_wave。子类继承了这些方法并提供evaluate,也就是在给定的时间序列内对信号取值。
例如,Sinusoid是Signal的子类,定义如下:
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
__init__的参数有:
freq 频率,其含义为每秒周期数,单位是Hz。
amp 幅度。幅度的单位比较随意,通常设定为1.0,对应为传声器的最大输入或给扬声器的最大输出。
offset 其含义为信号周期的起始。offset的单位为弧度。
func 它是一个Python函数,用来对指定时间点的信号求值。通常它不是np.sin就是np.cos,对应的分别是正弦信号和余弦信号。
跟很多初始化方法一样,它也是把参数存起来以备未来使用。
Signal提供了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 是每秒帧数(采样数)。
n 代表采样的数量,而 ts 是采样时间的 NumPy 数组。
为计算 ys,make_wave 需要引用 evaluate,它是由 Sinusoid 提供的:
def evaluate(self,ts):
phases = PI2 * self.freq * ts + self.offset
ys = self.amp * self.func(phases)
return ys
下面来逐步分析这个函数
1. self.freq 是频率,代表每秒周期数,而ts的各个元素都是以秒计的,所以它们的乘积是从起始时间开始的周期数。
2. PI2是一个常数,其值为2π。乘上PI2之后就把周期转换成了相位。读者可以把相位理解为弧度形式的“从起始时间开始的周期数”,而每个周期的弧度是2π.
3. self.offset 是t = 0 时刻的相位。其作用是吧信号在时域上向左或向右移动一定距离。
4. self.func 是 np.sin或者np.cos,其值便是处于-1 ~ 1。
5. 乘上self.amp 之后产生的信号范围为 -self.amp到self.amp。
在数学意义上,evaluate 的形式如下:
其中A是幅度,f是频率,t是时间,是相位偏移。这里看起来像是写了大段代码,而仅仅为了一个简单的表达式求值,但是正如我们将要看到的,这段代码提供了处理所有信号的架构,而不仅是三级函数信号的。
写在本章最后:
本章我们了解了thinkdsp以及thinkplot及wave的基本操作,建议在播放音频前进行如下预处理。
在jupyter notebook中操作如下:
# 音量最大化 wave.normalize() # 淡入淡出 wave.apodize() # 展示图示 wave.plot() decorate(xlabel='Time (s)')
执行播放
wave.make_audio()
下一篇:《Python数字信号处理应用》学习笔记——第二章 谐波