pyAudioKits是基于librosa和其他库的强大Python音频工作流支持。

API速查手册

通过pip安装:

pip install pyAudioKits

本项目的GitHub地址,如果这个项目帮助到了你,请为它点上一颗star,谢谢你的支持!如果你在使用过程中有任何问题,请在评论区留言或在GitHub上提issue,我将持续对该项目进行维护。

上节中对乐音和噪音的分析使用的方式均为时域分析的方法。本节我们将引入频域的概念,并介绍如何在频域分析音频信号。

傅里叶变换

连续时间信号傅里叶变换

我们说连续时间周期信号可以展开为频率为python 每个频段的功率谱密度 python频域分析_音视频的基波和频率为python 每个频段的功率谱密度 python频域分析_python_02的谐波。也就是说我们不仅可以使用原来的python 每个频段的功率谱密度 python频域分析_频域_03来表示连续时间周期信号,只要知道基波和谐波的频率以及振幅,就可以用频率和振幅来表示连续时间周期信号了。因此,我们现在假设有一个频率轴,上面有无数的频点python 每个频段的功率谱密度 python频域分析_频域_04,就可以尝试将原本时间轴上的一维信号python 每个频段的功率谱密度 python频域分析_频域_03映射到频率轴上,变成python 每个频段的功率谱密度 python频域分析_python_06,完成时域到频域的映射。

使用最小二乘法,保证时域表示和频域表示能量相差最小。为了达成这一条件的结论是:设所有频点的值为python 每个频段的功率谱密度 python频域分析_时域_07乘上python 每个频段的功率谱密度 python频域分析_时域_08。表示成频谱密度函数,有python 每个频段的功率谱密度 python频域分析_频域_09。可见周期信号的频谱密度函数是离散的冲激函数线性叠加。该函数可以通过python 每个频段的功率谱密度 python频域分析_时域_10还原为时域中的傅里叶级数表示python 每个频段的功率谱密度 python频域分析_音视频_11

那么非周期信号能不能映射到频域呢?答案是肯定的。假如基波频率python 每个频段的功率谱密度 python频域分析_频域_12无限小,即基波周期python 每个频段的功率谱密度 python频域分析_音视频_13无限大,则可以表示任意(无论是否周期)信号的频域特性,此时就从傅里叶级数推广到了傅里叶变换:基波频率python 每个频段的功率谱密度 python频域分析_音视频无限小,因此python 每个频段的功率谱密度 python频域分析_时域_15视为连续值python 每个频段的功率谱密度 python频域分析_音视频_16python 每个频段的功率谱密度 python频域分析_python_17。此时得到连续时间傅里叶变换的综合方程python 每个频段的功率谱密度 python频域分析_时域_10分析方程python 每个频段的功率谱密度 python频域分析_python_17。可见非周期信号的频谱密度函数是连续函数

当连续时间周期信号的周期逐渐扩大python 每个频段的功率谱密度 python频域分析_音视频就逐渐缩小,组成python 每个频段的功率谱密度 python频域分析_python_06冲激函数就越密集。当周期无限大时,python 每个频段的功率谱密度 python频域分析_音视频无限小,python 每个频段的功率谱密度 python频域分析_python_06成为连续函数,就得到了连续时间非周期信号频谱密度函数。

离散时间信号傅里叶变换

可是我们在计算机中储存的信号都是数字信号,也就是离散时间信号,它们怎么映射到频域呢?

还记得模拟信号到数字信号(在我们这里也就等价于连续时间信号到离散时间信号)使用了一个采样的操作。信号在时域乘上冲激串python 每个频段的功率谱密度 python频域分析_音视频_24可以实现采样。采样后的时域表示为python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_25。采样后的频域表示为python 每个频段的功率谱密度 python频域分析_时域_26,我们发现此时python 每个频段的功率谱密度 python频域分析_频域_27python 每个频段的功率谱密度 python频域分析_python_06在频谱上以采样频率为基数的周期性延拓的结果,其周期为python 每个频段的功率谱密度 python频域分析_频域_29且一个周期内就包含了所有的频谱信息。此时得到离散时间信号傅里叶变换的综合方程python 每个频段的功率谱密度 python频域分析_时域_30分析方程python 每个频段的功率谱密度 python频域分析_时域_31

此时综合方程和分析方程与采样周期python 每个频段的功率谱密度 python频域分析_python_32是有关的。为了使它们与采样周期python 每个频段的功率谱密度 python频域分析_python_32无关,我们将频域也用因子python 每个频段的功率谱密度 python频域分析_python_32(即采样率的倒数)归一化,记为python 每个频段的功率谱密度 python频域分析_python_35,得到离散时间信号傅里叶变换的综合方程python 每个频段的功率谱密度 python频域分析_音视频_36分析方程python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_37。此时python 每个频段的功率谱密度 python频域分析_音视频_38中,python 每个频段的功率谱密度 python频域分析_音视频_16的每python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_40实际代表频率python 每个频段的功率谱密度 python频域分析_音视频_41python 每个频段的功率谱密度 python频域分析_音视频_38python 每个频段的功率谱密度 python频域分析_时域_08为周期。

离散傅里叶变换

现在问题又来了,还记得我们为什么要对模拟信号python 每个频段的功率谱密度 python频域分析_python_44采样变成python 每个频段的功率谱密度 python频域分析_音视频_45吗?这是因为计算机无法储存原来的模拟信号,因为模拟信号是连续的,可能还是无限的、非因果的。那么频谱密度函数python 每个频段的功率谱密度 python频域分析_python_46实际上也面临着一样的问题。我们也需要对这个频谱密度函数进行一些“采样”操作。

令周期冲激串python 每个频段的功率谱密度 python频域分析_音视频_47,则有python 每个频段的功率谱密度 python频域分析_时域_48,此时python 每个频段的功率谱密度 python频域分析_频域_49,则有python 每个频段的功率谱密度 python频域分析_频域_50,从而在频域完成采样。使用python 每个频段的功率谱密度 python频域分析_音视频_51代替python 每个频段的功率谱密度 python频域分析_音视频_52有:

python 每个频段的功率谱密度 python频域分析_音视频_53

python 每个频段的功率谱密度 python频域分析_时域_54

时域python 每个频段的功率谱密度 python频域分析_音视频_55为周期进行周期延拓变为python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_56。原本有python 每个频段的功率谱密度 python频域分析_频域_57,现在则有python 每个频段的功率谱密度 python频域分析_python_58周期延拓后的信号python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_59可能拥有和python 每个频段的功率谱密度 python频域分析_时域_60不同的频率成分

原本python 每个频段的功率谱密度 python频域分析_音视频_38中,python 每个频段的功率谱密度 python频域分析_音视频_16的每python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_40实际代表频率python 每个频段的功率谱密度 python频域分析_音视频_41,现在python 每个频段的功率谱密度 python频域分析_时域_65中每个python 每个频段的功率谱密度 python频域分析_python_66实际代表python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_67的频率。称每个k为频谱中的一个频点,实际代表的频率为频谱分辨率。采样不能完整地表示出频谱,有些时候会遗漏频谱中的频率成分,这被称为栅栏效应。然而时频域依然满足能量相等的条件,被遗漏的频率成分的能量就会被泄露到附近的频点上,从而导致信号出现新的频率成分,这被称为频谱泄漏

还记得python 每个频段的功率谱密度 python频域分析_音视频_38的周期为python 每个频段的功率谱密度 python频域分析_音视频_69吗?这意味着我们只需要取频谱中连续的python 每个频段的功率谱密度 python频域分析_音视频_55个频点即可得到频谱的所有信息。加上实信号的频谱关于python 每个频段的功率谱密度 python频域分析_python_71对称,我们观察频谱时只需要取频谱中连续的python 每个频段的功率谱密度 python频域分析_频域_72个频点即可,即绘制频谱时一般只取python 每个频段的功率谱密度 python频域分析_音视频_73

由于python 每个频段的功率谱密度 python频域分析_时域_74为复数,频谱还分为幅度谱相位谱,分别为python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_75python 每个频段的功率谱密度 python频域分析_时域_76

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()

python 每个频段的功率谱密度 python频域分析_时域_77

aly.FFT(p1,16).plot()   #进行16点傅里叶变换,并默认绘制幅度谱

python 每个频段的功率谱密度 python频域分析_时域_78

x[n]采样自8Hz的正弦波x(t),在幅度谱上却体现不出8Hz的峰值。因为此时频谱分辨率为python 每个频段的功率谱密度 python频域分析_时域_79,大于信号频率8Hz,因此出现栅栏效应和频谱泄漏。

periodicExtension(p1,16).plot()

python 每个频段的功率谱密度 python频域分析_音视频_80

同时在时域我们也可以看到,python 每个频段的功率谱密度 python频域分析_时域_81在以16点为周期进行周期延拓得到的python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_56不再以0.125s为周期。

aly.FFT(p1,32).plot()

python 每个频段的功率谱密度 python频域分析_频域_83

使用32点傅里叶变换时,此时频谱分辨率为python 每个频段的功率谱密度 python频域分析_音视频_84,正好等于信号频率8Hz。因此我们可以看到8Hz的峰值被体现了出来,不会观察到栅栏效应和频谱泄漏。

periodicExtension(p1,32).plot()

python 每个频段的功率谱密度 python频域分析_时域_85

从时域上我们可以看到python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_56依然以0.125s为周期。

使用不同的频率轴刻度来观察真实频率python 每个频段的功率谱密度 python频域分析_频域_87、以角频率表示的真实频率python 每个频段的功率谱密度 python频域分析_时域_88、归一化频率python 每个频段的功率谱密度 python频域分析_音视频_16、频点的对应关系。

aly.FFT(p1,32).plot(xlabel="frequency/(rad/s)") #使用角频率表示的频率

python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_90

aly.FFT(p1,32).plot(xlabel="normalized frequency/(rad/s)")  #归一化角频率,0~采样率/2 就对应 0~π

python 每个频段的功率谱密度 python频域分析_python_91

aly.FFT(p1,32).plot(xlabel="freq point")	#频点。32点傅里叶变换在[0,采样率/2)之间有16个频点

python 每个频段的功率谱密度 python频域分析_python_92

再来看信号中有两个频率成分(8Hz和32Hz的情况)。

p2 = p1 + ak.create_Single_Freq_Audio(0.1,32,256,1)
p2.plot()

python 每个频段的功率谱密度 python频域分析_音视频_93

aly.FFT(p2,16).plot()

python 每个频段的功率谱密度 python频域分析_python_94

使用16点FFT时,频谱分辨率为python 每个频段的功率谱密度 python频域分析_时域_79,小于信号频率32Hz且可以被32Hz整除,但大于信号频率8Hz。因此在32Hz处不会出现频谱泄漏和栅栏效应,但在8Hz处会。

aly.FFT(p2,32).plot()

python 每个频段的功率谱密度 python频域分析_音视频_96

使用32点FFT时,频谱分辨率为python 每个频段的功率谱密度 python频域分析_音视频_84,小于信号频率且信号频率是频谱分辨率的整数倍,此时可以分辨出8Hz和32Hz的峰值,不会出现栅栏效应和频谱泄漏。

aly.FFT(p2,64).plot()

python 每个频段的功率谱密度 python频域分析_python_98

使用64点FFT时,频谱分辨率为python 每个频段的功率谱密度 python频域分析_python_99,频谱进一步细化。

aly.FFT(p2).plot()

python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_100

默认情况下,使用全256点进行FFT,频谱分辨率为python 每个频段的功率谱密度 python频域分析_音视频_101,不会产生频谱泄漏和栅栏效应。

aly.FFT(p2,512).plot()

python 每个频段的功率谱密度 python频域分析_频域_102

periodicExtension(p2,512).plot()

python 每个频段的功率谱密度 python频域分析_频域_103

使用512点FFT相当于在256个样本后补256个0再进行周期延拓。此时信号中出现新的频率成分,因此产生频谱泄漏和栅栏效应。

p3 = p2[0:48]
aly.FFT(p3).plot()

python 每个频段的功率谱密度 python频域分析_python_104

取前48个样本点对信号进行截断,此时使用全48点进行FFT,频谱分辨率为python 每个频段的功率谱密度 python频域分析_频域_105python 每个频段的功率谱密度 python频域分析_时域_106可以被32Hz整除,但不可以被8Hz整除。因此在32Hz处不会出现频谱泄漏和栅栏效应,但在8Hz处会。

p2.plot()

python 每个频段的功率谱密度 python频域分析_频域_107

periodicExtension(p3,48).plot()

python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_108

周期延拓的结果也显示了新的频率成分。

aly.FFT(p3,32).plot()

python 每个频段的功率谱密度 python频域分析_音视频_109

此时使用32点FFT依然不会观察到频谱泄漏和栅栏效应。

periodicExtension(p3,32).plot()

python 每个频段的功率谱密度 python频域分析_python_110

周期延拓时不会出现新的频率成分。

aly.FFT(p3,256).plot()

python 每个频段的功率谱密度 python频域分析_频域_111

由于本身信号被截断为48点,再使用256点FFT也只是对补0后的信号进行FFT。因此必然出现新的频率成分。

说了这么多大家应该也已经发现了,从时域上看周期延拓带来的新的频率成分,和从频域上看由栅栏效应带来的频谱泄漏,其实是同一现象的两种解释罢了。那么有没有一个统一的方法量化这一影响呢?就是说所谓新的频率成分到底是多少?

还记得我们一开始对音频信号进行过截断吗?截断的时候用到了窗函数python 每个频段的功率谱密度 python频域分析_python_112python 每个频段的功率谱密度 python频域分析_时域_113,所以python 每个频段的功率谱密度 python频域分析_音视频_114,也就是说python 每个频段的功率谱密度 python频域分析_时域_81的频谱是python 每个频段的功率谱密度 python频域分析_频域_116的频谱与窗函数频谱的卷积。python 每个频段的功率谱密度 python频域分析_音视频_117的频谱为python 每个频段的功率谱密度 python频域分析_音视频_118。假设python 每个频段的功率谱密度 python频域分析_时域_119采样自正弦波python 每个频段的功率谱密度 python频域分析_python_120,其频谱为python 每个频段的功率谱密度 python频域分析_时域_121。则python 每个频段的功率谱密度 python频域分析_频域_122。如果采样取python 每个频段的功率谱密度 python频域分析_python_123,则有python 每个频段的功率谱密度 python频域分析_频域_124,其中python 每个频段的功率谱密度 python频域分析_python_125python 每个频段的功率谱密度 python频域分析_时域_74中代表python 每个频段的功率谱密度 python频域分析_音视频的频点,python 每个频段的功率谱密度 python频域分析_频域_128为以python 每个频段的功率谱密度 python频域分析_时域_81中样本点数表示的正弦波周期。这样,当python 每个频段的功率谱密度 python频域分析_音视频_130python 每个频段的功率谱密度 python频域分析_音视频_55的整数倍且python 每个频段的功率谱密度 python频域分析_音视频_130python 每个频段的功率谱密度 python频域分析_音视频_133整数倍时,python 每个频段的功率谱密度 python频域分析_时域_74仅在python 每个频段的功率谱密度 python频域分析_音视频_135处有值,其余部分都是0,此时窗函数的频谱被隐藏了,我们只会看到python 每个频段的功率谱密度 python频域分析_频域_116的频谱;其他情况下,窗函数的频谱则会被显示出来

也就是说,想要屏蔽窗函数的频谱,就必须满足:

  1. 截断点数是离散傅里叶变换点数的整数倍
  2. 截断点数是信号所有分量的周期的整数倍

不满足这些条件的情况下,我们就会在频谱上观测到窗函数的频谱,这就是周期延拓带来的“新的”频率成分,以及频谱泄漏产生的“新的”频率成分。第二个条件是非常苛刻的,实际应用中往往无法满足。因此观测到窗函数的频谱是非常非常正常的现象。

input1 = ak.create_Single_Freq_Audio(0.02,8,256,1)
aly.FFT(input1,256).plot(0, 256)

python 每个频段的功率谱密度 python频域分析_频域_137

对于采样率为python 每个频段的功率谱密度 python频域分析_音视频_138的音频,取傅里叶变换的点数python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_139以计算幅度谱,则每个频点代表python 每个频段的功率谱密度 python频域分析_时域_140的真实频率,幅度谱的频点python 每个频段的功率谱密度 python频域分析_python_141python 每个频段的功率谱密度 python频域分析_音视频_142就涵盖了python 每个频段的功率谱密度 python频域分析_音视频_143python 每个频段的功率谱密度 python频域分析_频域_144上所有的功率信息。我们发现幅度谱上有两个峰值,其中左边的峰值出现在单音的频率python 每个频段的功率谱密度 python频域分析_python_145对应的频点上。实信号的幅度谱关于python 每个频段的功率谱密度 python频域分析_python_71偶对称,因此在对应python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_147的频点上也会出现一个峰值,而这个峰值经过以采样率python 每个频段的功率谱密度 python频域分析_音视频_138为周期的周期延拓后,就出现在了python 每个频段的功率谱密度 python频域分析_时域_149对应的频点上,即功率谱上右边的峰值。

import numpy as np

aly.FFT(input1,256).plot(0, 2*np.pi, xlabel="normalized frequency/(rad/s)")

python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_150

若使用归一化频率来作为横坐标,则我们发现幅度谱关于python 每个频段的功率谱密度 python频域分析_python_151对称。事实上,如果我们绘制python 每个频段的功率谱密度 python频域分析_python_152的幅度谱,则还会发现幅度谱以python 每个频段的功率谱密度 python频域分析_时域_153为周期,且关于python 每个频段的功率谱密度 python频域分析_时域_154偶对称。

import numpy as np

aly.FFT(input1,256).plot(0, 2*np.pi, plot_type = "phase", xlabel="normalized frequency/(rad/s)")

python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_155

实信号的相位谱也具有对称的特性,且也以python 每个频段的功率谱密度 python频域分析_时域_153为周期,但它是关于python 每个频段的功率谱密度 python频域分析_时域_154奇对称的。

能量谱和功率谱

除了频谱之外,能量谱和功率谱也是能够在频域描述信号特点的函数。能量谱和功率谱的理论基础是帕塞瓦尔定理,即时域内信号能量等于频域内信号能量。对于能量有限信号而言,根据帕塞瓦尔定理,其能量为python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_158,而能量谱密度就为python 每个频段的功率谱密度 python频域分析_python_159。对于功率有限信号而言,首先将python 每个频段的功率谱密度 python频域分析_频域_03截短为长度等于T的一个截短信号python 每个频段的功率谱密度 python频域分析_频域_161,使其成为能量有限信号。根据帕塞瓦尔定理,其能量为python 每个频段的功率谱密度 python频域分析_python_162,而信号功率为python 每个频段的功率谱密度 python频域分析_python 每个频段的功率谱密度_163,则有功率谱密度python 每个频段的功率谱密度 python频域分析_音视频_164

由于我们对音频信号进行了加窗截断,因此所有信号都是能量和功率有限信号。我们仅关心其功率谱密度,计算式为python 每个频段的功率谱密度 python频域分析_python_165

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")

python 每个频段的功率谱密度 python频域分析_python_166

功率谱也可以使用增益的形式来表示。

aly.PSD(input1, 256, dB=True).plot(0,32,xlabel="frequency/Hz")

python 每个频段的功率谱密度 python频域分析_python_167

当信号中同时有两个频率成分时,两个频率成分的功率信息都会被反映到功率谱上。

input2 = ak.create_Single_Freq_Audio(0.01,24,256,1)
aly.PSD(input1 + input2, 256).plot(0,32,xlabel="frequency/Hz")

python 每个频段的功率谱密度 python频域分析_python_168

其中由于input2的振幅是input1的一半,所以python 每个频段的功率谱密度 python频域分析_python_169上的功率谱密度是python 每个频段的功率谱密度 python频域分析_python_145上的四分之一。

噪音的频域特性

和确定性周期信号以及具有明显音高的乐音不同,噪音是没有明显音高,即没有明显周期性的。乐音可以在幅度谱和功率谱上体现出明显的峰值,而噪音的频率成分则呈现连续而随机的分布。

白噪音的频率成分均匀分布在整个频率范围内。

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()

python 每个频段的功率谱密度 python频域分析_python_171

python 每个频段的功率谱密度 python频域分析_python_172