【Aegisub】生成音频频谱的简单介绍

Aegisub可以做各种各样的频谱效果,比如

如果要看效果,可以拖到文章末尾,看一些效果的示例。
先讲一些零散的概念:
比特率(bit rate)指每秒传送的比特(bit)数,比特率越高,单位时间传送的数据量(位数)越大。一个二进制数有几位就有几个bit(位),比如11101有5位那么就有5bit(把比特翻译成中文就是位)
字节(btye)是计量存储容量的一种单位,1个字节能储存8位的二进制数,那么最大的8位二进制数就是11111111了,换算成十进制就是255,所以0到255一共有256个数,一个字节只能记录256个数,字节叫byte,所以字节率byte rate就用比特率除以8,比如每秒传送1536000比特,那么比特率就是1536000bit/s,而字节率就是1536000/8=192000btye/s,所以当然字节率也一样是一个传输速率的单位。
模拟信号,简单说就是用电信号去模拟出其他信号(比如声波),它的幅度值随时间连续变化。比如你打电话有一个声波,那手机采集到声波以后就用电信号模拟出声音的波形,那如果手机直接将这个模拟信号传输到小伙伴的手机上,那因为会有各种各样的干扰,那么到小伙伴的手机上的时候声音的波形就变了样,如图1.01,最后红色的波形就变样了

这样小伙伴听到的声音就失真了。所以模拟信号的传播就相当于一直要照着前面的画,那一个个的画下去,越往后波形的变形就越严重,所以模拟信号在传输的过程中抗干扰性比较差。所以在传输之前,需要将模拟信号转为数字信号(模数转换),数字信号只有两种波形,一种是高的一种是低的,高就是1低就是0,比如图1.02

数字信号的抗干扰能力比较好,数字信号只有两种状态,所以可以轻松地还原出原来的数据。把模拟信号转变为数字信号首先就要对模拟信号采样,假设每秒钟采样1次,如图1.03

采样以后就进行量化,量化成一些等级,如图1.04

图1.04
比如分了5个等级,那Lv1就可以是001,Lv2就是010了(二进制),Lv5就是101了,那么这样编码以后,就有图1.05了

那么这样的话在哪个时间是哪个等级就知道了,比如在3s时是100,也就是等级4(Lv4)。所以如果我们提高采样率和量化等级,形状上当然就会越来越接近这个曲线,也就能更好地还原出模拟信号
采样率(sampling rate):声音信号在”模数”转换过程中单位时间内采样的次数。比如你每秒钟采样23333次,那么采样率就是23333Hz(赫兹Hz是频率的单位,指每秒钟重复的次数,如声音是由振动产生的,那100Hz就可以表示每秒振动100次)。然后比如1kHz就等于1000Hz
人能听到的声波的频率范围是20Hz到20000Hz,那么由于采样定理(采样率必须至少是模拟信号频率的两倍),所以咱们的音频的采样率需高于20000*2=40000Hz,一般CD的标准是44100Hz的采样率,也就是44.1kHz的采样率。一般常见的采样率就是44.1kHz或者48kHz。所以假设采样率是Fs,那么原本信号的的频率就不会超过Fs/2
在老外写的Yutils里,有读取音频信息的函数,首先你需要准备一个音频文件,格式需要是wav,那么如果你不是wav格式的就把它变为wav格式的即可。然后只要把你音频文件的路径复制好,作为参数传给_G.Yutils.decode.create_wav_reader函数即可获得一个能提供音频信息的表(表里有很多函数,能提供音频信息),如图1.06

wav=_G.Yutils.decode.create_wav_reader('F:\\TEMP\\新建文件夹\\ヒーロー supercell.wav') 当然函数返回的表赋值给了名叫wav的变量,那这个名字你也可以取xxx,比如xxx=_G.Yutils.decode.create_wav_reader('F:\\TEMP\\新建文件夹\\ヒーロー supercell.wav')然后函数的参数就是填的一个字符串,显然现在的用的音频就是叫ヒーロー supercell了。那么在有了函数返回的表以后,就可以获取音频信息了,比如利用表里的sample_rate函数,就能知道你这个音频的采样率,如图1.07

废话,当然是44100了,不然就是48000,因为刚刚讲过采样率一般就是44100Hz或48000Hz
然后表里的channels_number函数,可以知道音频有几个声道,如图1.08

code行:xxx = _G.Yutils.decode.create_wav_reader('F:\\TEMP\\新建文件夹\\ヒーロー supercell.wav')
template行:!xxx.channels_number()!
显然,channels_number()函数返回音频通道的数量,常用的有单声道和立体声之分,反正这个函数最后得到的不是1就是2,其实这个函数没啥用
然后,samples_per_channel函数返回每个声道的采样点个数,如图1.09

这个当然不管是哪个声道,它们的总的采样点个数是一样的,比如声道1采样点一共是13745711,那声道2总共的采样点数也是13745711
byte_rate函数返回音频的字节率,这个概念刚刚讲过。

所以可以看到现在的字节率(byte rate)是176400byte/s,那么就可以算出比特率,是176400*8即1411200bit/s,即1411.2kbit/s
bits_per_sample函数返回每个声道的采样精度,采样精度决定了记录声音的动态范围,它以位(bit)为单位,比如8位、16位。8位可以把声波分成256等级(2的8次方),16位可以把同样的波分成65536等级(2的16次方)的信号。可以想象,位数越高,声音的保真度越高,因为被分成了更多的等级。

当然每个声道的采样精度都是一样的,就像刚刚讲的采样个数一样,各个通道都是一样的。16位是常用的采样精度。另外,函数名称里的per当然就是"每"的意思,如per second就是每秒,per week就是每周。那么其实知道了位深度(即采样精度)、采样率、通道数,就可以知道它的传输速率了,44100*16*2=1411200bit/s,也就是刚刚上文中利用字节率算出的比特率,当然是一样的结果了,所以它每秒钟传输1411200个bit
min_max_amplitude函数,返回音频可能的最小最大值,这个其实可以直接根据位深度(即采样精度)算出来。比如位深就是刚刚的16bit,那么声波被分为65535级,那此时的最大值就是2^16/2-1=32767,最小值就是-2^16/2=-32768了,那么如果采样精度是8bit就说明最大是127最小是-128

min_max_amplitude函数的作用是单位化幅度值,举个例子,比如你在30000级那么如果采样精度变成8bit的话,你在哪一级呢,30000/32768*128=117.1875,当然因为8bit的精度比16bit低,所以当然此时就不能保持117.1875级了,而是会取整。在单位化幅度值以后,得到的结果就会在-1到1之间了,比如30000/32768=0.91552734375就在-1到1之间,其实也很好理解,你知道30000级马上要接近现在等级的最大值了,那么到底有多接近呢,30000/32768≈0.92马上就到1了,振幅还是很大的

简单粗略地说,声音的响度决定于声源振动的幅度(振幅),振幅越大,响度越大,分贝(dB)可以作为振幅的测量单位
音频的时长可以利用刚刚的函数得到,比如用samples_per_channel就知道了总的样本数,然后用sample_rate就知道了采样速率,也就是知道了每秒钟采样的个数,所以总共音频有多少秒就可以算了,比如 “总样本数/采样率”,比如算出来音频时长是174.78秒
position函数用于设置样本流的位置,相当于定位的作用,所以这个函数是需要用的。先简单地理解一下,咱们做频谱效果,那个频谱是动的嘛,比如逐帧,那么每一帧都会有一个频谱图,那你就需要获取相应时间的一些样本,假设采样率是48000Hz、音频时长是10秒,那么总的采样数就是480000了,这时用position函数定位,函数的参数范围是0到4800000,此时假设1秒有30帧,那么音频一帧的采样数就是48000/30=1600,那么照理说在第0帧的时候你就可以采样1600次,然后下一次采样你就可以用函数position(1600)定位在这里,然后再取样1600次就完成了第一帧到第二帧的采样了,然后又定位在3200,所以实际上,当你用position函数定位的时候,你采样数可以自己设定,但是采样数是有上限的,比如现在本身音频的总的采样数是480000,那么你就算定位在0处(即position(0)),此时你采样也不能采666666666次,因为本身最多就只有480000个样本,但是你可以采样比如2048次。position函数可以说是一定要用的,因为咱们要自己采样就一定要定位,不然怎么知道你要在音频的什么位置开始采样。
samples函数,用来采样,参数填你要采样的个数,配合position函数使用,先用position函数定位,然后再用samples函数在这个位置往后采样n次,如图1.10

先用position函数定位音频在第80000个样本处,然后用samples函数从这里开始往后采样2048个样本,然后1表示采样是采样的第一个通道,而如果你想采样第二个声道,那就可以用2,即图1.11这样

还有就是你设定的采样个数就算超出了音频总的采样数,采样也不会多出来,比如音频本身总的采样数是20000,然后你position定位在18000,即position(18000),然后你用samples函数采样2048个,实际上最后函数返回的采样出来的表里只会有2000个样本,不会有2048个。
利用samples函数采样以后就得到了你那些采样点的等级,比如上图第一个的等级是4937,那么因为现在等级绝对值最大是32768,那么单位化振幅以后就是4937/32768≈0.15067,所以此时的振幅比较小
因为要做音频频谱需要用FFT(快速傅里叶变换),那么下面开始慢慢讲这一部分的东西
声波的振幅是随时间变化的,所以自变量是时间(即横轴是时间),那么声音信号就是时域信号。现在看一个正弦函数f(x)=sinx

它的横坐标是时间、纵坐标是幅度,这就是一个时域信号函数

y=sinx的频率f是1/T,函数周期是2π,所以频率f=1/T=2π分之一。那么现在假设横轴是频率,纵轴是幅度,显然y=sinx只有一个固定频率2π分之一,那么这个函数在频域中的图像就是图1.12这样的(2π分之一大约是0.1591549430919)

所以原本是横轴是时间的时域信号函数就变为了横轴是频率的频域信号
对于一般的正弦函数y=Asin(2πfx+φ),它其中的f就是频率,φ就是初始相位,A是振幅,如果上图中的旋转臂转得越快,那么频率就越高,在零时刻旋转臂和水平方向的夹角就是初始相位φ,另外这其中的2πf被成为角速度(或角频率),所以一般的正弦函数也可以写成y=Asin(ωx+φ),当然其中ω就是角频率。还有就是函数的周期T为1/f当然也是2π/ω了
为什么会有时域和频域的概念呢?比如有图1.13这么个音频的波形图

比如截了这一小段时间的波形图,横轴是时间、纵轴是幅值(范围-1到1)。假设想让音量大一些,应该怎么做呢?因为上面的波形图的振幅对应的其实就是声音的强度,如果想让音量大一些,只需要将整体的振幅同比例扩大即可。这个需求很容易满足。但如果你喜欢低音效果,你想加强上面这段音乐的低音部分,更加厚重一些,那应该怎么做呢?要知道时域的图像看起来杂乱无章,想找到低音部分根本无从下手,跟不用说将低音部分加强了。因为高中低音在时域中是杂糅在一起的,没法便利地将他们剥离开来,随便改动波形图中的一小部分,都会同时影响到高中低音。所以如果仅仅对时域信号进行处理是无法完成这个需求的。
在1807年傅里叶提出了:任何连续的周期信号都可以由一组正弦曲线组合而成(即傅里叶级数)。比如有一个方波

那么用很多个正弦曲线组合在一起就能得到一个近似的方波

可以看到,一开始第一行是sint,然后在此基础上加上1/3sin3t就得到了第二行,即第二行的图像就是sint+1/3sin3t的图像。然后又加上1/5sin5t得到了第三行,然后第四行当然就是函数f(t)=sint+1/3sin3t+1/5sin5t+1/7sin7t的图像了,显然越来越接近方波了,而最后一行可以看到随着组合的正弦曲线越来越多,图像就越来越接近方波。那么就可以把时域的方波信号变成频域信号,如图1.15


当然从左到右横坐标(频率)逐渐变大,然后纵坐标就是每个频率对应的振幅了,所以这样就知道这个方波它频率低的部分振幅大,频率高的振幅小。关于从时域到频域,图1.16应该能更直观的感受

原本红色的时域信号,能分解成很多个正弦曲线,将这些正弦曲线按照频率从小到大来排列,然后每个频率的正弦曲线的幅度给一个个记录下来,就得到了频域图像,也就是一个频谱。那么现在这个频谱是横坐标是频率、纵坐标是该频率对应的幅度值,这样的频谱叫做振幅谱。那既然信号可以分解为正弦函数的和,那么就知道了这个信号是由哪些频率的正弦波构成,也知道了对应频率的波在信号中的幅度信息和相位信息。信号可以被分解,那其实分解以后也还可以重新组合回去,所以这个过程是可逆的,那么意味着没有信息的丢失,所以在分解以后,不仅是知道了各个频率对应的幅度、还知道了各个频率对应的相位,因为你分解出来的这些正弦波也是有它们各自的相位的,相位不一样那组合的最终结果当然就可能不一样,所以除了横轴是频率纵轴是幅值的幅度谱,还有横轴是频率纵轴是相位的相位谱,这样,一个波形图就可以变成频谱图(幅度谱和相位谱)了,有了频谱(振幅谱和相位谱)同样也就可以还原出波形图了。有了频谱以后,比如就可以进行滤波,比如有些高频的噪音就可以直接去掉,而如果没有把信号从时域变到频域就没办法想去除高频就去除高频。
那么我们一般的信号都不是周期信号,连续周期信号可以展开成傅里叶级数(一堆正弦波),那非周期信号呢?那就是傅里叶变换了,用来将非周期的时域信号变为频域信号。傅里叶这孩子的名字开头是F,所以用大写的F来表示傅里叶变换,变换后的自变量是频率(横坐标是频率),如图1.17

其中自变量是角频率ω,那么算出来的F(ω)就是一个复数,当然刚刚也说了时域到频域的变换是可逆的,所以时域变到频域以后不会丢失信息,所以你在角频率ω时就一定能知道此时的振幅和相位,所以F(ω)这个复数就一定能提供幅度信息和相位信息,那么角频率ω对应的幅度是多少呢,就是F(ω)这个复数的模(这个大家应该都知道,说直接一点,假设这个复数F(ω)的实部是an虚部是bn,那么它的模就是√(an²+bn²)),角频率ω对应的相位就是这个复数F(ω)的主辐角(简单来说就是该复数在复平面上所对应的点与原点的连线和x轴正半轴之间形成的角度,取值范围当然是0到2π了)。那么这样你就可以有幅度谱和相位谱了,因为在角频率是ω时,你可以算出F(ω),而F(ω)这个复数又能得到振幅值和相位值。这样时域信号f(t)就可以变为频域信号了,而如果是频域信号变为时域信号的话就是傅里叶逆变换了

OK,既然连续的非周期信号可以连续傅里叶变换了,那你看啊,咱们不是数字信号吗,这你瞧瞧不是采样的吗,不是离散的吗,不是不是连续的吗,那现在就不是要连续傅里叶变换了,而是要用离散傅里叶变换,那离散傅里叶当然就不是积分了而是累加,如图1.18,假设总共采样了N个点,那么离散傅里叶变换以后就知道第n个频率(频率大小是2πn/N)所对应的振幅(即F(n)这个复数的模)了

那这里的e的次方可以用欧拉公式来算

这样,原本你要算的e的-i2πnt/N次方,就可以更舒服的计算了。这样可以方便地算出F(n)的实部和虚部了。比如现在采样了4个点(即N=4),那么比如求F(2)就是图1.19这样的了

也就是从t=0开始累加,一直加到t=3(即N-1,即4-1=3),然后其中n=2、N=4。所以,同样的道理,离散傅里叶变换以后就可以得到F(0)、F(1)、F(2)、F(3)了(总共4个采样点,第0个点到第3个点)。因为有欧拉公式,那比如F(2)的实部就可以这么算了:

同样F(2)的虚部也用上欧拉公式计算即可。所以,比如你就可以写一个离散傅里叶变换的函数

因为Lua的table下标是从1开始的,所以这里的t就该是t-1,当然n也就该是n-1,然后这样离散傅里叶变换以后,第0个点的结果就是F[1]了(Lua下标从1开始),那么F[1]这个复数的实部就是F[1].real,虚部就是F[1].i了。
我们将离散傅里叶变换简称为DFT(即Discrete Fourier Transform),那傅里叶变换当然简称FT(即Fourier Transform)了。对于N个点的离散傅里叶变换,把N称为DFT的区间长度。
可以看到,随着采样点N的增加,DFT的用时就会变得很长,因为要算很多加法乘法,所以在1965年有两个外国人写了一个DFT的优化算法,这个优化的快速算法仅仅是让代码执行速度更快,其他原理什么的并没有变化,这个优化算法称为快速傅里叶变换,简称FFT(即Fast Fourier Transform)。比如采样了N个点,直接对N个点进行DFT执行速度很慢,所以就把N个点分成一个奇序列(odd)和一个偶序列(even),然后这样就拆成了两个N/2个点的DFT计算了,然后奇序列和偶序列又可以继续分成奇序列偶序列,一直这样下去分,直到分到序列里只有2个点为止,这样对两个点做DFT计算就很快了,这样原本N个点的DFT就变成了很多只有两个点的DFT,代码执行速度就快多了

那么当然,因为要这么一直分一直分,所以快速傅里叶变换就要求采样数N需要是2的次方,因为比如只把N分一次、分成奇序列和偶序列,那么N就必须被2整除,所以要能一直分下去分到最后每个序列只有2个离散点的话,采样点数N就需要是2的次方(比如2的11次方之类的)。那其实在Yutils里本身就写的有FFT代码,所以就算你不太会写,也压根不用担心,知道这些原理就行了

那么FFT以后我们就知道各个频率从小到大的点所对应的振幅和相位是多少了,咱们做效果不太需要相位,所以只用振幅信息来做振幅谱就行了。假设采样率是Fs,那么N个从小到大的频率就是把Fs/2给分成N等份而已(上文说了采样率Fs是原始信号频率的2倍)。另外,这里提一句,由于共轭对称性,N个点最后会舍去一半,保留N/2个点,所以比如你采样2048个点,那么最后会保留前面N/2个频率的所对应的振幅(和相位)信息。
好了好了,现在咱们想要的振幅有了吧,再捋一捋,首先你自己采样N个点(养成好习惯的话,就像刚刚说的,N就要是2的次方),就得到原本信号上N个时刻的幅度值,然后有了这N个幅值,用FFT得到从小到大的各个频率所对应的幅值(称为频率分量),然后将这些从小到大的频率作为横坐标,它们对应的幅值(即频率分量)作为纵坐标,这不就得到了咱们一开始讲的振幅谱了吗?
还没完,你可能会问,频谱不是和时间没有关系吗,时域变到频域以后,横轴是频率纵轴是幅度(或者相位等),频谱和时间没关系那么为什么你看到的频谱图是动态的呢?非常好,如果你不这么问,那你岂不是文章前面的内容没看懂?确实,频谱是和时间没有关系的,比如有个时域信号持续50s,那么你可以取0到50s的这整段信号,来做FFT,得到一个频谱,这个频谱图就是静止的,不会随时间变化,不管你是0到50s的哪一时刻,频谱图都不变都还是那个频谱图,那为什么特效做的频谱是动态的呢?其实因为FT(傅里叶变换)有局限性:频谱中没有时间信息,所以不同时域信号的频谱没啥区别,它就无法有效地反映信号在窄区间上的突变,傅里叶变换对频谱的描绘是“全局性”的,不能反映时间维度局部区域上的特征,人们虽然从傅立叶变换能清楚地看到一整段信号包含的每一个频率的分量值,但很难看出对应于频率域成分的不同时间信号的持续时间和发射的持续时间,FT在平稳信号的分析和处理中有着突出贡献的原因在于,人们利用它可以把复杂的时间信号变换到频率域中,然后用相对简单的频谱特性去分析和发现原信号的特性,但是一般,咱们的音频都不是平稳信号,基本上都是非平稳信号,所以为了将时域和频域相联系,就要用到短时傅里叶变换,也简称STFT(即Short-Time Fourier Transform),它将信号的时域和频域联系起来,我们可以据此对信号进行时频分析。那么怎么STFT呢,那就是局部平稳化:把长时间的非平稳信号看成是一系列短时平稳信号的叠加,然后再对这些一个个短时间的信号做FFT即可。如图1.20

假设图1.20第一行是一个有50s的信号,现在不整个对0到50s做FFT,而是将这些信号分帧,"切成"一段段小的信号,对这些一段段小的信号做FFT,一般情况下按经验认为,20-40ms内的语音帧可近似看做平稳信号,这样对短时间内的信号做FFT得到一小段时间里的频谱图,就让频谱和时间联系上了。(要是对"全局时间"做FFT就只能得到一张频谱图,如图1.21)

所以现在大家知道了为什么要用position函数了吧,因为假设音频总共的采样数是194560个,那这个样本数是你整个音频时间里的样本数,可是咱们要做的是STFT,所以需要用position函数定位,然后音频定位在指定位置以后才能用samples函数获取position位置后的n个样本,然后获得了样本以后用min_max_amplitude函数单位化幅度值,这样你就有了这n个样本所对应的各个幅值了(也就是有了f(n)),那么就可以FFT得到F(n)了,然后求出F(n)的模|F(n)|就可以做幅度谱了。所以此时你就知道position函数定位以后用samples函数取样不是取的越多越好,因为咱们只需要取一帧时间内的样本,根据刚刚前文说的一般语音帧,20-40ms、音频采样率一般44.1kHz、采样数要是2的次方,所以综合下来,咱们一般采样数就是1024或者2048,因为采样率是44100Hz,那么意味着每ms采样44.1个,那比如40ms就采样了1764、20ms就采样了882个,然后采样数是2的次方,所以一般就取1024或2048。position函数的定位就根据帧持续时间和采样率得到了,比如你要在第2333帧到第2334帧采样,那么position的定位值就该是math.round(2333*fdur*Fs*0.001),即position(math.round(2333*fdur*Fs*0.001)),其中fdur是一帧持续时间、Fs是采样率,由于采样率是每秒采样数,而fdur单位是ms,所以要统一单位,将采样率变成每毫秒的采样数,也就是Fs*0.001,然后这个定位当然是定位在第几个样本处,所以肯定是整数,所以要math.round一下,到第2333帧时,已经采样了math.round(2333*fdur*Fs*0.001)个点,要在第2333帧到第2334帧采样,那么开始采样的位置就是math.round(2333*fdur*Fs*0.001)了,然后再用samples函数取样2048个即可。
好了,采了样,也把采样的样本都幅度值单位化了,这样就可以FFT了,因为Yutils里有相应的函数,所以就可以直接用,使用_G.Yutils.decode.create_frequency_analyzer函数进行样本解析,该函数输入两个参数,第一个是你的样本(装有一堆[-1,1]的幅度值的table),第二个参数是填你的音频采样率,这个函数同样也是返回一个表,这个表里有很多函数,你可以利用这些函数做频谱效果,不过Yutils本身没有把各个频率的幅值给保留下来,所以为了做振幅谱,你可以改一下Yutils里的代码,

可以看到把data这些样本给FFT了,然后又只保留了每个复数的模,所以在用了其中的magnitude函数取模以后,data就是只剩下幅度信息了,可是Yutils原本没有在frequencies表里加入幅度信息,所以你需要在里面加入amp振幅信息,各个data[i]就是振幅,所以就amp=data[i]即可。这样就有了N/2个频率对应的振幅了,但是我们要做频谱图,并不需要N/2这么多个频率,你想,咱们采样数N一般是2048,那么横坐标你全部摆出来不就有1024个了吗,那你要看1024个条形块吗,对吧,咱们做频谱,一般也就几条而已,比如下图

你要是有1024个,那每一条岂不是非常细、非常窄了?而且也不好看。所以我们要计算某个频率区间范围内的平均幅度值,比如你这1024个频率中第2个频率是23,它所对应的振幅是31,然后第3个频率是46,它所对应的振幅是54,第4个频率是70,它所对应的振幅是11,那么假设你现在要用一个条形块表示20Hz到70Hz的幅度情况,那么你就可以把这之间的3个振幅加起来,然后除以3平均一下,即(31+54+11)/3=32,所以你就可以设定频率在20Hz到70Hz之间时,用仅仅一个条形块来表现振幅情况,比如设定该条形块的高度是32(即(31+54+11)/3),所以这么看来,咱们现在的任务就是选择横坐标(频率)以及对应的频率分量(即振幅)了,那既然现在我们使用的条形块数量会较少,那么就意味着展现的频谱图就没有那么细节而是更加"平均"了,所以想一想人能听到的范围是20Hz到20000Hz,如果你要用30个条形块的话,你会选择横坐标(频率)的范围就是20到20000吗,我们知道音频一般低频部分比较多,高频部分就几乎没有,假设你就选择横坐标的范围是20到20000,那总共30块,那20Hz到2018Hz只用3块条形就表示完了,剩下的27块就表示2018到20000的高频部分了,然而音频本来就没啥高频部分啊,你看着27块几乎没啥变化的条形块你开心吗,对吧?(20000-20=19980,分成30块,每一块的的频率跨度就有19980/30=666Hz,这样第一块条形是20Hz到686Hz,这样就只用了一个条形块就概括了20Hz到686Hz这么大长度的频率区间的平均振幅,那岂不是丢掉了很多细节信息,你就不知道20Hz到686Hz之间的比如50Hz到200Hz的平均振幅是什么样的,同样的,第二个条形块就是686Hz到1352Hz,第三个条形块就是1352Hz到2018Hz的平均幅度情况,这样,只用3个条形块就笼统地概括了20Hz到2018Hz的幅度值大概情况,可是啊,一般的音频明明低频部分的振幅大,高频部分就没啥幅度,结果你用3条就表示了20到2018,用剩下27条去表示频率高的部分的幅度情况??那这频谱还看个水煮牛肉啊?) 所以由于一般音频本身就是低频部分的振幅较大,所以高频部分咱们根本也不需要太在乎,所以回到刚刚说的横坐标的选取,横坐标的范围就不要选择20到20000,而是选择20Hz到2000Hz,这个也是AE(After Effect)的默认数值。也就是说,咱们现在忽略高频部分,只关注低频部分,我们用比如30个条形块来表示20Hz到2000Hz的相应的振幅情况,这样就可以看到更多频率较低的部分的振幅情况了。那么(2000-20)/30=66,这样每一条跨度是66Hz,那第一个条形块就表示20Hz到86Hz的平均振幅了,第二个条形块就表示86Hz到152Hz的平均振幅了,这么下去,低频部分的细节就展现的很清楚了,就不会把很大一个范围的给用一个条形块笼统地平均地概括了。那么还有一个方法,如果你非要选择横坐标的范围是20到20000,那么你可以让每一个条形块表示的区间跨度不一样,也就是横坐标不是均匀分布的,比如你可以第一条表示20Hz到50Hz,第二条表示50到100,第三条表示100到180,第四条表示180到280,第五条表示280到400,以此类推这么下去,然后高频部分因为没啥振幅,所以你就笼统地表示高频部分即可,比如让第29条表示5000到8000,最后第30条表示8000到20000,这样的话,你也可以展现足够的低频部分的细节、省略掉没啥东西的高频部分,那你说怎么不均匀的选择横坐标呢,这个很简单很灵活,比如你可以用上指数函数,那样不就可以让你的第n个横坐标和第n-1个横坐标的差距越来越大了。当然,我自己是喜欢直接将横坐标的范围设定为20到2000的,高于2000Hz的频率一般就直接忽略掉。
选择好了横坐标就该算幅度了,幅度就像刚刚说的,就是算个平均值就行了。假设你FFT以后得到了1024个频率以及其对应的振幅(即FFT的结果取模,因为FFT的结果是复数,包含振幅和相位信息,刚刚已经说过多遍),那比如你要算20Hz到80Hz的平均幅度,那么就遍历这1024个频率,看哪些频率在20Hz到80Hz的,把这些频率对应的振幅加起来,如果一共加了cnt次,那么当然要平均振幅的话,加起来的这个振幅除以cnt就得到了20Hz到80Hz所对应的平均振幅了。不过这样算平均振幅的函数在Yutils里也是没有的,所以你可以自己在Yutils里添加这么一个函数:

就添加在frequency_range_weight函数后面即可。如果不会的话可以看文章下面的下载链接。显然这个函数就能计算freq_min, freq_max这个范围的平均幅度值了。那么有了横坐标和纵坐标不就已经"完成"了频谱图了吗,也就是现在把20Hz到2000Hz分成比如30份,然后每一份的对应的平均幅度也有了,那这样不就得到了振幅谱了吗?
然后还有一些要注意的点,比如我们要人为设定最大幅度,比如你做频谱效果,你不希望这些条形块的高度超过300,那么你就要设定最大幅度为300。怎么设定呢,就是你得到每一帧的频谱图以后,你再遍历所有帧、找到这所有频谱图里幅度最大的数值是多少,然后算这个最大值和300的比例,然后就把这个最大的幅度给缩放成300,然后当然,其他所有帧的频谱图的所有条形块都以这同样的比例来缩放一下,这样,我们就控制了整个频谱效果的最大高度了,你就可以自由的设定条形块的高度了,比如你想它高一点,那么就设定最大高度是400或666等等等等的。
第二个需要注意的点是,有的时候一些条形块的振幅很小、高度很小,可能看起来高度低的和高度高的相比差距很大,那么你可以人为设定让条形块之间的高度差距小一点,那么怎么做呢,你可以给每个条形块的高度加一个指数,设定0到1之间的一个指数,就能缩小高度差,比如25和9本身的差距是16,但是你加个指数,25^0.5和9^0.5,就变成了5和3,这样两个数的差距就变小了,当然你加的指数越接近0得到的两个数差距就越小,你的指数越接近1,计算后两数差距就越接近原来两数的差距。
还有就是,除了振幅谱,你也可以做比如能量谱和功率谱,能量谱当然横坐标是频率纵坐标就是能量了,那么能量怎么算呢,就是振幅的平方即可,也就是说你FFT得到的复数的模不就是振幅吗,那你这个复数的模的平方就是能量了,所以能量也是直接就这么算出来即可。那么功率呢,功率就是能量除以区间长度嘛,刚刚讲过采样数N称为DFT的区间长度,所以功率也是直接就能算出来的,就是用你的振幅平方一下,然后再除以N即可。当然如果算功率谱,最后也要将条形块之间的高度差给弄小一点。为什么要功率谱呢,因为功率谱不受频率分辨率的影响(刚刚没有讲频率分辨率,简单地说下,STFT不是分帧做FFT吗,那截取信号的时间越长,则时间分辨率越低,频率分辨率越高;截取信号越短,时间分辨率越高,频率分辨率越低),所以功率谱不受频率分辨率的影响的话,功率谱就有它的意义。不过,对于做特效来说,直接用振幅谱就可以了,因为做功率谱的计算量会大不少,而且效果上看起来也没多少区别。功率谱计算量更大是因为要平方然后除以区间长度,比如本来算得了1024个振幅就可以做振幅谱了,但是做功率谱的话,还需要把这1024个数每一个都平方再除以N(区间长度),然后得到了条形块高度后也还要缩小条形块之间的高度差,所以计算量肯定更大,但是特效上又没多少区别,所以就可以选择做振幅谱即可。
再多讲下_G.Yutils.decode.create_frequency_analyzer函数,参数刚刚说了输入样本和音频采样率,比如你取了2048个点,然后将每个点对应的振幅都单位化、使幅度在[-1,1]后,就可以把这个装有2048个幅度值的表作为第一个参数,然后音频采样率当然不用多说。然后这个函数会返回一个表,这个表里装有很多函数,比如你设定变量xxx=_G.Yutils.decode.create_frequency_analyzer(samples, 48000),那么xxx就是一个表了,表里的frequencies函数能提供N/2个频率以及其对应的振幅(因为采样数是N在FFT以后就去掉了一半,刚刚说过,然后振幅信息是刚刚说的你需要自己在Yutils里加的),所以此时你用tbl=xxx.frequencies()就可以获得这些信息了,这样比如tbl[3].freq就是第三个频率了,然后tbl[3].amp就是第三个频率对应的振幅了,这些其实大家自己看Yutils的代码应该很清楚

然后也说了,现在咱们自己加了frequency_range_amp函数,代码在上文里也有,那么添加了这个函数以后就可以用来求某个频率范围内的平均振幅了,所以你就可以用在xxx表里的frequency_range_amp函数了,即现在是xxx.frequency_range_amp,它返回的就是一个数字,就是你[freq_min, freq_max]这个范围对应的平均幅值了。
然后现在就是看看频谱可以做哪些花样了。首先,直接条形块排成一行或者一列是可以的,那么比如一行可以每个条形块设定不同的颜色等等、然后条形块的定位点不一样的话,"缩放"效果就可以不一样,如图1.22和图1.23


图1.22是矩形的底部中心定位在了(0,0)点,而图1.23里的矩形是中心定位在了(0,0)点,所以一个缩放的时候是往上的,一个缩放的时候是向着两边的
再比如,可以做环形频谱,而环形频谱也有各种做法:




甚至你可以自定义路径的生成频谱效果:



你要是愿意胡来,甚至可以在文字路径上附着频谱,这就相当地胡来了:

或者也可以做柱体的频谱:

而且频谱也可以利用上各种标签,比如透明啊模糊啊等等等等


Yutils下载:https://wwr.lanzoui.com/iU71Opznd4f 密码:9tec
剩下的代码就继续在视频里面讲了。
语音信号处理之分帧与窗函数(拓展知识,不看影响不大)
前面说了一般的信号是非平稳信号,所以不拿整个时长的信号做FFT,而是截取出来的一小段信号(一帧)来做FFT,因为在短时间内信号就可以看成是平稳信号了

如图1.24,不用一整段信号而是分成一帧帧的,比如红框框出来的部分是一帧。
那么,前面文章中讲的是分完帧然后就直接做FFT了,但其实在语音信号处理中,并不是分帧以后直接就拿这一小段信号做FFT的。分帧又叫做信号截断,在做信号截断时,是无法满足周期截断的,所以就会导致频谱泄露。
比如现在周期截断得到了一帧信号,如下图

对周期截断的帧做FFT就得到频谱:

那如果是非周期截断得到的帧呢,比如

对非周期截断信号做FFT:

可以看到,在主频点附近的频点上,也有着许多不可忽视的能量。即主频点能量泄露到附近频点上了
所以因为非周期截断得到的帧无法满足FFT的条件:信号要么从-∞到+∞ ,要么为周期信号,所以进而做FFT时,会导致频谱泄露。
但是想要周期截断信号又很难做到,那么这时就需要用到加窗了。取出来的一帧信号,在做傅里叶变换之前,要先进行「加窗」的操作,即与一个「窗函数」相乘:

加窗后的信号减小了两端的坡度,使窗口边缘两端不引起急剧变化而平滑过渡到零,此时,信号的起始时刻和结束时刻幅值都为0,也就是说在这个时间长度内,信号可以视为周期信号,这样可以减轻语音帧的截断效应了。所以加窗以后就可以减轻频谱泄漏(不能避免频谱泄露,只能减轻)了,但是加窗以后两端的信号被削弱了,所以截断信号的时候不要背靠背地截取,而是要相互重叠一部分,分帧一般采用交叠分段的方法,这是为了使帧与帧之前平滑过渡,保持其连续性。前一帧和后一帧的交叠部分称为帧移(英文:overlap)。帧移与帧长的比值一般取为0到1/2。
不同的信号会选择不同的窗函数,甚至有时会用指数窗:

那么短时傅里叶变换STFT因为需要加窗,所以时域信号f(t)就要先乘以窗函数再做FFT,那么如果你要加窗的话,就要自己写代码了,因为Yutils里是没有关于STFT的代码的,只有一个FFT的局部函数。