pyAudioKits是基于librosa和其他库的强大Python音频工作流支持。
API速查手册
通过pip安装:
pip install pyAudioKits
本项目的GitHub地址,如果这个项目帮助到了你,请为它点上一颗star,谢谢你的支持!如果你在使用过程中有任何问题,请在评论区留言或在GitHub上提issue,我将持续对该项目进行维护。
上节中对乐音和噪音的分析使用的方式均为时域分析的方法。本节我们将引入频域的概念,并介绍如何在频域分析音频信号。
傅里叶变换
连续时间信号傅里叶变换
我们说连续时间周期信号可以展开为频率为的基波和频率为的谐波。也就是说我们不仅可以使用原来的来表示连续时间周期信号,只要知道基波和谐波的频率以及振幅,就可以用频率和振幅来表示连续时间周期信号了。因此,我们现在假设有一个频率轴,上面有无数的频点,就可以尝试将原本时间轴上的一维信号映射到频率轴上,变成,完成时域到频域的映射。
使用最小二乘法,保证时域表示和频域表示能量相差最小。为了达成这一条件的结论是:设所有频点的值为乘上。表示成频谱密度函数,有。可见周期信号的频谱密度函数是离散的冲激函数线性叠加。该函数可以通过还原为时域中的傅里叶级数表示
那么非周期信号能不能映射到频域呢?答案是肯定的。假如基波频率无限小,即基波周期无限大,则可以表示任意(无论是否周期)信号的频域特性,此时就从傅里叶级数推广到了傅里叶变换:基波频率无限小,因此视为连续值,。此时得到连续时间傅里叶变换的综合方程为,分析方程为。可见非周期信号的频谱密度函数是连续函数。
当连续时间周期信号的周期逐渐扩大,就逐渐缩小,组成的冲激函数就越密集。当周期无限大时,无限小,成为连续函数,就得到了连续时间非周期信号频谱密度函数。
离散时间信号傅里叶变换
可是我们在计算机中储存的信号都是数字信号,也就是离散时间信号,它们怎么映射到频域呢?
还记得模拟信号到数字信号(在我们这里也就等价于连续时间信号到离散时间信号)使用了一个采样的操作。信号在时域乘上冲激串可以实现采样。采样后的时域表示为。采样后的频域表示为,我们发现此时是在频谱上以采样频率为基数的周期性延拓的结果,其周期为且一个周期内就包含了所有的频谱信息。此时得到离散时间信号傅里叶变换的综合方程为,分析方程为
此时综合方程和分析方程与采样周期是有关的。为了使它们与采样周期无关,我们将频域也用因子(即采样率的倒数)归一化,记为,得到离散时间信号傅里叶变换的综合方程为,分析方程为。此时中,的每实际代表频率为。以为周期。
离散傅里叶变换
现在问题又来了,还记得我们为什么要对模拟信号采样变成吗?这是因为计算机无法储存原来的模拟信号,因为模拟信号是连续的,可能还是无限的、非因果的。那么频谱密度函数实际上也面临着一样的问题。我们也需要对这个频谱密度函数进行一些“采样”操作。
令周期冲激串,则有,此时,则有,从而在频域完成采样。使用代替有:
而时域以为周期进行周期延拓变为。原本有,现在则有。周期延拓后的信号可能拥有和不同的频率成分。
原本中,的每实际代表频率为,现在中每个实际代表的频率。称每个k为频谱中的一个频点,实际代表的频率为频谱分辨率。采样不能完整地表示出频谱,有些时候会遗漏频谱中的频率成分,这被称为栅栏效应。然而时频域依然满足能量相等的条件,被遗漏的频率成分的能量就会被泄露到附近的频点上,从而导致信号出现新的频率成分,这被称为频谱泄漏。
还记得的周期为吗?这意味着我们只需要取频谱中连续的个频点即可得到频谱的所有信息。加上实信号的频谱关于对称,我们观察频谱时只需要取频谱中连续的个频点即可,即绘制频谱时一般只取。
由于为复数,频谱还分为幅度谱和相位谱,分别为和。
def periodicExtension(p,N): #模拟进行3次周期延拓的函数。p为需要周期延拓的信号,N则是周期延拓的周期
L = 3 * N
p = ak.concatenate([ak.create_Single_Freq_Audio(0,0,p.sr,L/p.sr),p,ak.create_Single_Freq_Audio(0,0,p.sr,L/p.sr)])
p1 = p[0:L]
for i in range(N,len(p.samples)-L,N):
p1 = p1 + p[i:i+L]
return p1
import pyAudioKits.analyse as aly
p1 = ak.create_Single_Freq_Audio(0.1,8,256,1) #创建振幅为0.1,频率为8Hz,采样率为256Hz,持续时间1s的正弦信号
p1.plot()
aly.FFT(p1,16).plot() #进行16点傅里叶变换,并默认绘制幅度谱
x[n]采样自8Hz的正弦波x(t),在幅度谱上却体现不出8Hz的峰值。因为此时频谱分辨率为,大于信号频率8Hz,因此出现栅栏效应和频谱泄漏。
periodicExtension(p1,16).plot()
同时在时域我们也可以看到,在以16点为周期进行周期延拓得到的不再以0.125s为周期。
aly.FFT(p1,32).plot()
使用32点傅里叶变换时,此时频谱分辨率为,正好等于信号频率8Hz。因此我们可以看到8Hz的峰值被体现了出来,不会观察到栅栏效应和频谱泄漏。
periodicExtension(p1,32).plot()
从时域上我们可以看到依然以0.125s为周期。
使用不同的频率轴刻度来观察真实频率、以角频率表示的真实频率、归一化频率、频点的对应关系。
aly.FFT(p1,32).plot(xlabel="frequency/(rad/s)") #使用角频率表示的频率
aly.FFT(p1,32).plot(xlabel="normalized frequency/(rad/s)") #归一化角频率,0~采样率/2 就对应 0~π
aly.FFT(p1,32).plot(xlabel="freq point") #频点。32点傅里叶变换在[0,采样率/2)之间有16个频点
再来看信号中有两个频率成分(8Hz和32Hz的情况)。
p2 = p1 + ak.create_Single_Freq_Audio(0.1,32,256,1)
p2.plot()
aly.FFT(p2,16).plot()
使用16点FFT时,频谱分辨率为,小于信号频率32Hz且可以被32Hz整除,但大于信号频率8Hz。因此在32Hz处不会出现频谱泄漏和栅栏效应,但在8Hz处会。
aly.FFT(p2,32).plot()
使用32点FFT时,频谱分辨率为,小于信号频率且信号频率是频谱分辨率的整数倍,此时可以分辨出8Hz和32Hz的峰值,不会出现栅栏效应和频谱泄漏。
aly.FFT(p2,64).plot()
使用64点FFT时,频谱分辨率为,频谱进一步细化。
aly.FFT(p2).plot()
默认情况下,使用全256点进行FFT,频谱分辨率为,不会产生频谱泄漏和栅栏效应。
aly.FFT(p2,512).plot()
periodicExtension(p2,512).plot()
使用512点FFT相当于在256个样本后补256个0再进行周期延拓。此时信号中出现新的频率成分,因此产生频谱泄漏和栅栏效应。
p3 = p2[0:48]
aly.FFT(p3).plot()
取前48个样本点对信号进行截断,此时使用全48点进行FFT,频谱分辨率为。可以被32Hz整除,但不可以被8Hz整除。因此在32Hz处不会出现频谱泄漏和栅栏效应,但在8Hz处会。
p2.plot()
periodicExtension(p3,48).plot()
周期延拓的结果也显示了新的频率成分。
aly.FFT(p3,32).plot()
此时使用32点FFT依然不会观察到频谱泄漏和栅栏效应。
periodicExtension(p3,32).plot()
周期延拓时不会出现新的频率成分。
aly.FFT(p3,256).plot()
由于本身信号被截断为48点,再使用256点FFT也只是对补0后的信号进行FFT。因此必然出现新的频率成分。
说了这么多大家应该也已经发现了,从时域上看周期延拓带来的新的频率成分,和从频域上看由栅栏效应带来的频谱泄漏,其实是同一现象的两种解释罢了。那么有没有一个统一的方法量化这一影响呢?就是说所谓新的频率成分到底是多少?
还记得我们一开始对音频信号进行过截断吗?截断的时候用到了窗函数。,所以,也就是说的频谱是的频谱与窗函数频谱的卷积。的频谱为。假设采样自正弦波,其频谱为。则。如果采样取,则有,其中为中代表的频点,为以中样本点数表示的正弦波周期。这样,当是的整数倍且是整数倍时,仅在处有值,其余部分都是0,此时窗函数的频谱被隐藏了,我们只会看到的频谱;其他情况下,窗函数的频谱则会被显示出来。
也就是说,想要屏蔽窗函数的频谱,就必须满足:
- 截断点数是离散傅里叶变换点数的整数倍
- 截断点数是信号所有分量的周期的整数倍
不满足这些条件的情况下,我们就会在频谱上观测到窗函数的频谱,这就是周期延拓带来的“新的”频率成分,以及频谱泄漏产生的“新的”频率成分。第二个条件是非常苛刻的,实际应用中往往无法满足。因此观测到窗函数的频谱是非常非常正常的现象。
input1 = ak.create_Single_Freq_Audio(0.02,8,256,1)
aly.FFT(input1,256).plot(0, 256)
对于采样率为的音频,取傅里叶变换的点数以计算幅度谱,则每个频点代表的真实频率,幅度谱的频点到就涵盖了到上所有的功率信息。我们发现幅度谱上有两个峰值,其中左边的峰值出现在单音的频率对应的频点上。实信号的幅度谱关于偶对称,因此在对应的频点上也会出现一个峰值,而这个峰值经过以采样率为周期的周期延拓后,就出现在了对应的频点上,即功率谱上右边的峰值。
import numpy as np
aly.FFT(input1,256).plot(0, 2*np.pi, xlabel="normalized frequency/(rad/s)")
若使用归一化频率来作为横坐标,则我们发现幅度谱关于对称。事实上,如果我们绘制的幅度谱,则还会发现幅度谱以为周期,且关于偶对称。
import numpy as np
aly.FFT(input1,256).plot(0, 2*np.pi, plot_type = "phase", xlabel="normalized frequency/(rad/s)")
实信号的相位谱也具有对称的特性,且也以为周期,但它是关于奇对称的。
能量谱和功率谱
除了频谱之外,能量谱和功率谱也是能够在频域描述信号特点的函数。能量谱和功率谱的理论基础是帕塞瓦尔定理,即时域内信号能量等于频域内信号能量。对于能量有限信号而言,根据帕塞瓦尔定理,其能量为,而能量谱密度就为。对于功率有限信号而言,首先将截短为长度等于T的一个截短信号,使其成为能量有限信号。根据帕塞瓦尔定理,其能量为,而信号功率为,则有功率谱密度。
由于我们对音频信号进行了加窗截断,因此所有信号都是能量和功率有限信号。我们仅关心其功率谱密度,计算式为。
import matplotlib.pyplot as plt
input1 = ak.create_Single_Freq_Audio(0.02,8,256,1)
aly.PSD(input1, 256).plot(0,32,xlabel="frequency/Hz")
功率谱也可以使用增益的形式来表示。
aly.PSD(input1, 256, dB=True).plot(0,32,xlabel="frequency/Hz")
当信号中同时有两个频率成分时,两个频率成分的功率信息都会被反映到功率谱上。
input2 = ak.create_Single_Freq_Audio(0.01,24,256,1)
aly.PSD(input1 + input2, 256).plot(0,32,xlabel="frequency/Hz")
其中由于input2的振幅是input1的一半,所以上的功率谱密度是上的四分之一。
噪音的频域特性
和确定性周期信号以及具有明显音高的乐音不同,噪音是没有明显音高,即没有明显周期性的。乐音可以在幅度谱和功率谱上体现出明显的峰值,而噪音的频率成分则呈现连续而随机的分布。
白噪音的频率成分均匀分布在整个频率范围内。
monotone_without_wn = ak.create_Single_Freq_Audio(0.02,440,44100,1)
monotone_with_wn = monotone_without_wn.addWgn(10) #在单音上加上信噪比为10dB的高斯白噪音
wn = monotone_with_wn - monotone_without_wn #减去单音,只留下白噪音
aly.FFT(wn).plot(), aly.PSD(wn).plot()