音频效果器的介绍与实践

本文深入介绍了音频处理的基础知识,包括数字音频的时域和频域表示,如波形图、频谱图和语谱图。通过实例展示了如何使用快速傅里叶变换(FFT)进行时域到频域的转换,以及理解FFT的物理意义。此外,还探讨了基本乐理,如音高、基频、共振峰等。最后,讲解了音频处理中的混音效果器,如均衡器、压缩器和混响器的实现,包括Android和iOS平台的实践方法,以及如何利用sox库和AudioUnit API进行音频处理和优化。
摘要由CSDN通过智能技术生成

第8章 音频效果器的介绍与实践

前七章不仅了解了音视频的基础概念,还在Android和iOS平台完成了两个比较完整的应用,一个是视频播放器的应用,一个是视频录制应用,所以可以把前七章称之为基础篇或者说是入门篇。而从现在开始,将进入一个新的篇幅——提高篇,这部分内容旨在为基础篇中完成的两个应用添加一些必要的功能(比如添加音频滤镜、视频滤镜),做一些性能优化(比如硬件解码器的使用),实现一些公共基础库的抽象与构建(音频处理、视频处理的公共库)等。

本章将学习音频处理相关的知识,在第1章已介绍过一些音频背景与相关的基础知识,本章会在此基础上进行更加深入的讲解。此外,一些基本的乐理知识也会在本章中进行介绍与讲解。让我们马上开始吧!

8.1数字音频基础

第1章已经讲解过音频的模拟信号与数字信号的概念,本章所面对的都属于数字音频,所以本节会以更加直观的方式从各个角度来了解一下数字音频。在开始本节之前建议读者先下载一个音频编辑工具(如:Audacity、Audition、Cubase等),Audacity在我们的资源目录中提供了一个Mac版本的安装文件,如果读者使用的是MacOS环境,可以直接安装。由于本章内容中很多操作都基于Audacity工具操作的,所以先简单介绍一下Audacity。Audacity是一个集播放、编辑、转码为一体的一个工具软件,在平常的工作中,它是必不可少的一个工具。在本节开始之前,大家可以在本章资源目录中找出对应的音频文件pass.wav放到Audacity中,完成操作之后,直接映入眼帘的就是下面要讲的第一种表示形式,即波形图的表示。

8.1.1波形图

声音最直接的表示就是波形图,英文叫waveform。横轴是时间,纵轴根据表示的意义不同有多种格式,比如说有用dB表示的、有用相对值表示的等,但是可总体理解为强度的大小。下面先来看一下当笔者读出pass[pɑ:s]这个单词时,所产生的波形图,如图8-1所示。

图 8-1

当横轴的分辨率不够高的时候,波形图看起来就像图8-1一样。如果不是一个单词,而是一段话,其波形图就会有多个这样子的波形连接起来,而所有波形的轮廓可以叫做整个声音在时域上的包络(envelope),包络整体形状描述了声音在整个时间范围内的响度。一般来说,每一个音节对应一个这样的三角形,因为每一个音节通常都会包含一个元音,而元音听起来比辅音更加响亮(如图8-1中的0.05-0.18秒)。但是也有例外,比如:类似/s/的唇齿音持续时间比较长,也会形成一个比较长的三角形(如图8-1中的0.18-0.4秒);类似/p/的爆破音会在瞬时聚集大量能量,在波形上体现为一个脉冲(如图8-1中的0.02-0.05秒)。如果把横轴时间单位的分辨率提高,比如只观察20毫秒的波形,可以看到波形图的更精细的结构,如图8-2所示。

 

图 8-2

图8-2中的左边图片就是放大了0.08-0.10秒部分的波形图情况,这部分是元音,大家可以注意到这个波形是有周期性的,大约有3个周期多一点(每个周期大约是7ms左右),这也是所有浊音在时域上的特性。相反的,再来看一下8-2中的右边图片,该图放大了波形图中0.2-0.22秒部分的波形,这部分是清音部分,是没有任何周期性可言的,并且频率(过零率,即在横轴精度一致的的情况下的波形的疏密程度)比元音也高很多。

上面所讲的特性都是我们从波形图上可以直观看出来的,可以知道,波形图表示的其实就是随着时间的推移,声音强度变化的曲线,是最直观也最容易理解的一种声音的表示形式,也就是通常称所说的声音的时域表示。看完了声音的时域表示,再从另外一个维度来看一下声音是如何表现的,也就是它的频域表示——声音的频谱(spectrum)图的表示。

8.1.2频谱图

使用图8-1中0.08-0.11秒的这一段声音来做FFT,得到频谱展示图,如图8-3所示。

图 8-3

在解释频谱图之前先理解一下什么是FFT。FFT是离散傅立叶变换的快速算法,可以将一个时域信号变换为频域表示的信号,有些信号在时域上是很难看出什么特征的,但是如果变换到频域之后,就很容易看出特征了,这就是很多信号分析采用FFT变换的原因。所以我们将一小段波形做FFT之后取模,注意这里必须是一小段波形(一般情况下是20-50ms),如果这段波形表示的时间太长其实就没有意义了。对音频信号做FFT的时候,是把虚部设置为0,得到的FFT的结果是对称的,即音频采样频率是44100,那么从0-22050的频率分布和22050-44100的频率分布是一致的。下面基于此来理解一下图8-3,横轴是频率,表示范围就是0-22050,而纵轴表示的就是当前频率点能量的大小,我们直接能看到的就是频域的包络,如果把横轴表示的单位改为指数级(即把分布比较密集的地方使用更加精细的单位来表示),就可以显示出频域上能量分布的精细结构。图8-4表示了频域分布的精细结构。

图 8-4

从图8-4中可以看出,每隔170Hz左右就会出现一个峰,而这恰恰是我们在波形图(8-2左边的图片)中所看到的波形周期(为6ms左右)所对应的频率。从图中也可以看出语音不是一个单独的频率信号,而是由许多频率的信号经过简谐振动叠加而成的。图中的每一个峰叫做共振峰,第一个峰叫基音,其余的峰叫泛音,第一个峰的频率(也是相邻峰的间隔)叫作基频(fundamental frequency),也叫音高(pitch),常记作f0,对于人声来讲,声带发声之后会经过我们的口腔、颅腔等进行反射最终让别人听到,但是这里基频指的就是声带发出的最原始的声音所代表的频率。所以如果声带不发声的声音,比如唇齿音(/z/ /c/ /s/等)一般就无法检测出基频。

继续看图8-4的频谱图,该图有很多峰,每个峰的高度是不一样的,这些峰的高度之比决定了音色(timbre)。不过对于语音的音色来说,一般没有必要精确地描写每个峰的高度,而是用“共振峰”(formant)来描述的。共振峰指的是包络的峰,可以看到,第一个共振峰的的频率就是170Hz,第二个共振峰的频率为340Hz,第三个共振峰大约是510Hz,第四个共振峰是680Hz,第五个共振峰大约是850Hz,第六个共振峰是1020Hz左右,再往后边的共振峰相对于前面的这几个共振峰就弱了很多,所以一般前几个共振峰的形状决定了这个声音的音色。接着再看一下0.2-0.22秒波形的频谱图表示,如图8-5所示。

图 8-5

观察图8-5可以发现,在低频率部分几乎没有峰(1000Hz那里由于能量太小,可以忽略),第一个峰值都出现在5000Hz以上,这种情况下也就无法计算出基频来了,如果对应于人的发声部位,其实就是我们的声带不发声,这一类一般称之为清音。清音通常没有共振峰,也就没有基频,没有音高。

上面的频谱图只能表示一小段声音,而如果我们想观察一整段语音信号的频域特性,应该怎么办呢?这将涉及下一节介绍的语谱图,其实在第3章中讲解ffplay时在显示面板上绘制的就是语谱图。

8.1.3语谱图

我们可以把一整段语音信号截成许多帧,把它们各自的频谱“竖”起来(即用纵轴表示频率),用颜色的深浅来代替当前频率下的能量强度,再把所有帧的频谱横向并排起来(即用横轴表示时间),就得到了语谱图,它可以称为声音的时频域表示。语谱图读者可以理解为一个三维的概念,如果称横轴为X轴,那么表示的是时间;纵轴为Y轴,表示的是频率;还有一个Z轴,表示就是当前时间点,当前频率所代表的能量值(能量值越大,颜色越深)。使用Audacity软件打开pass.wav之后,在这一轨声音的左侧选择频谱图(在Audacity中语谱图称之为频谱图)的视图模式,来看一下这段声音的语谱图,如图8-6所示。

图 8-6

在图8-6中,横轴是时间,纵轴是频率,颜色越深的地方其实代表声音的能量越大。所以对应着图8-1的波形图可以看到,0.0-0.05秒是在/p/这个爆破音的时候,其频率基本上都在1000Hz以下;而到了0.05-0.15秒,元音/ɑ:/的频率就非常明显,并且颜色已经非常非常深,是可以计算出基频来的;再随着时间的推移到了0.2秒以后的/s/,所有的频率基本上都到了5000Hz以上了,这一段声音是无法再进行计算基频的,属于清音部分。语谱图的好处是可以直观地看出共振峰频率的变化。

对于清音和浊音这里也介绍一下,因为这对于后续在基频检测以及针对频域数据做处理的时候会有很大帮助。语音学中,将发音时声带振动的音称为浊音,声带不振动音称为清音。辅音有清有浊,也就是大家常说的清辅音、浊辅音,而多数语言中,元音皆为浊音,鼻音、半元音也是浊音。我们可以尝试这发出/a/这个音,同时用手触摸喉部,此时,手是可以感觉出喉咙的振动的,而在我们发b/p/、d/t/、g/k/等音的时候喉咙是不振动的,这一些音都是清辅音,还有一种是鼻音,比如/m/、/n/、/l/等都是浊辅音。清音是无法检测出基频也就无从知道它所代表的音高,浊音一般都是可以检测出基频来的,所以也可以计算出它表示的音高。

8.1.4深入理解时域与频域

根据之前的介绍,想必读者已经比较清楚声音在时域和频域上的表示了,但是有的读者可能还是不太清楚到底声音的波形是如何产生的,又是如何跟频域联系起来的。先来生成一段单一频率的声音,然后在进行逐步叠加不同频率的声音,以此作为我们的声源,从而逐步分析快速傅里叶变换(FFT)能为我们做一些什么。

首先写一个函数来生成频率为440Hz,单声道,采样频率为44100Hz(注意采样频率代表的波形的平滑程度),时长为5s的声音,代码如下:

double sample_rate = 44100.0;

double duration = 5.0;

int nb_samples = sample_rate * duration;

short* samples = new short[nb_samples];

double tincr = 2 * M_PI * 440.0 / sample_rate;

double angle = 0;

short* tempSamples = samples;

for (int i = 0; i < nb_samples; i++) {

float amplitude = sin(angle);

*tempSamples = (int)(amplitude * 32767);

tempSamples += 1;

angle += tincr;

}

//Write To PCM File

delete[] samples;

代码中我们生成的就是一个相位为零的正弦波,使用Audacity软件将生成的PCM文件以裸数据(raw data)的方式导入进来,放大横轴的刻度可以看到波形图,如图8-7所示。

图 8-7

从图8-7中可以看出,时间为0的地方正处于正弦波幅度为0的地方,所以相位为0,并且还可以看出,一个周期大约是2.27ms,其实恰好代表了生成的这段声音的频率是440Hz,有的读者可能会问,那采样率在波形图中又代表着什么呢?其实采样率在波形图中代表着整个波形的平滑程度,采样点越多波形就会越平滑。紧接着选中20ms的音频来做傅里叶变换看一下得到的频谱图,如图8-8所示。

图 8-8

在这个频谱图中可以看到,波峰就是440Hz,可见傅里叶变化之后我们得到了这个波形在频域上的表示,并且是正确的,但是我们所听到的声音永远不会只是一个单调的正弦波,而是有很多波叠加而成的,所以稍微改动生成波形的代码来生成一个更加复杂的声音,仅需要修改生成幅度的那一行代码,如下:

float amplitude = (sin(angle) + sin(angle * 2 + M_PI / 3) +

sin(angle * 3 + M_PI / 2) + sin(angle * 4 + M_PI / 4))

* 0.25;

代码中使用了四个正弦波叠加,并且每个正弦波都有自己的相位,相位是随机给的,至于后面乘以0.25,是因为我们后续要将这个值在转换为SInt16表示的值,所以将其转换为-1到+1的范围之内。使用Audacity软件打开生成的这个PCM文件,将横轴的刻度拉大,波形图如图8-9所示。

图 8-9

可以看到,这个波形图就没有一个单一的正弦波(图8-7)看起来那么规范了,显然这是有很多个波叠加而成的,并且不同的波还有自己的相位,因为在时间为0的时候能量不是从0开始的,但是观察这个波形图,还是可以看出它具有周期特性的,每一个周期大约是2.25ms左右,其实根据代码我们也可以看出,最主要的频率还是440Hz所产生的正弦波的频率,所以我们选中20ms的波形图来观察一下它的频谱图,如图8-10所示。

图 8-10

在图8-10中,第一个波峰在440Hz,第二个波峰在880Hz,第三个波峰在1320Hz处,第四个波峰在1760Hz处,其实这个频谱图就非常类似于我们人所发出的非清音的频谱图,这个频谱图的基频就是440Hz。

好了,看了这么多波形图和频谱图的对比,想必读者已经比较熟悉声音在时域上的表示,以及时域和频域的转换了,在接下来的小节中,笔者会带着大家将声音的时域信号转换为频域信号,然后提取特征甚至做一些操作,让我们马上开始吧!

8.2 数字音频处理

本节讨论音频的处理,根据8.1节的介绍可知,其实声音主要的表示形式就是时域和频域的表示,而音频处理就是针对于声音分别在时域和频域上的处理,本节会详细介绍如何从时域和频域方面对于音频做一些处理。对于时域方面的处理,比较简单,不需要额外进行转换的操作,因为一般情况下拿到的音频数据就是时域表示的音频数据。但是要对音频的频域方面做处理的话,那么就得将拿到的音频数据先转换为频域上的信号,然后再进行处理了。那么如何转换为频域上的信号呢?在8.1节中曾提到过,使用FFT,即使用傅里叶变换,所以下面首先来学习傅里叶变换。

8.2.1 快速傅里叶变换

离散傅里叶变换简称是DFT,由于计算速度太慢,所以就演变出了快速傅里叶变换即我们常说的FFT,在处理音频的过程中常使用MayerFFT这个实现。在本节中不会讨论傅里叶变换的原理以及公式推导,而是讲解FFT的物理意义以及如何使用FFT将时域信号变为频域信号,以及如何利用逆FFT将频域信号重新变换为时域信号,同时在iOS平台会使用vDSP来提升效率,在Android平台的armv7的CPU架构以上,会使用neon指令集加速来提升性能,这样的安排相信会使得读者更加深入地了解FFT,并且还可以迅速地将优化应用于自己的日常工作中。

1. FFT的物理意义

FFT是离散傅立叶变换的快速算法,可以将一个时域信号变换到频域。有些信号在时域上是很难看出什么特征的,但是如果变换到频域之后,就很容易看出特征了。这就是很多信号分析(声音只是众多信号的一种)采用FFT变换的原因。虽然很多人都知道FFT是什么,可以用来做什么,怎么去做,但是却不知道做FFT变换之后结果的意义,本节就来和大家一块分析一下FFT的物理意义。

声音的时域信号可以直接用于FFT变换,假如N个采样点经过FFT之后,就可以得到N个点的FFT结果,为了方便进行FFT运算,通常N取值为2的整数次幂,比如:512、1024、2048等。根据采样定理,采样频率要大于信号频率的两倍,所以假设采样频率为Fs,信号频率为F,采样点数为N,那么FFT之后的结果就是N个点的复数,每一个复数分为实部a和虚部b,表示为:

z = a + b * i

每个点对应着一个频率点,而这个点的复数的模值就是这个频率点的幅度值,可以计算为:

amplitude = sqrt(a * a + b * b);

而每个复数都会有一个相位,其实在物理意义上代表的就是这个周期的波形的起始相位是多少,相位的计算如下:

phase = atan2(b, a);

由于输入是声音信号,声音信号在时域上表示为一个一个独立的采样点,因此在要做FFT变换之前,需要先将其变换为一个复数,即将时域上的某一个点的值作为实部,虚部统一设置为0,由于所有输入的虚部都是0,从而导致FFT的结果就是对称的即前面半部分和后边半部分的结果是一致的。所以在对声音信号做FFT之后,只需要使用前半部分就可以了,后半部分的其实是对称的,不需要使用。那FFT得到的结果与真实的频率有什么关系呢?

还是用8.1.4节中的生成音频文件的代码来说明,利用以下公式来生成一段采样率为44100Hz的音频文件,如下:

float amplitude = (sin(angle) + sin(angle * 2 + M_PI / 3) +

sin(angle * 3 + M_PI / 2) + sin(angle * 4 + M_PI / 4))

* 0.25;

拿到这个音频文件后,先去做一个FFT,具体如何操作,这里先不讨论,先把FFT的计算当做一个黑盒子,给它输入音频的时域信号,得到的就是频域信号,我们来理解一下它的物理意义。由于这个音频文件的采样率为44100,做FFT的窗口大小是8192,那么生成的FFT的结果,第一个点的频率就是0Hz,而最后一个点的频率就是44100Hz,而一共是8192个点,所以相邻两点之间表示的频率差值就是:

44100 / 8192 = 5.3833Hz

这就是我们通常所说的,使用8192作为窗口大小来给采样频率44100Hz的声音样本做傅里叶变换,得到的结果分辨率是5.3833Hz。接下来,我们在FFT的结果数组中找出第一个峰值(即第一个最大的值),可以发现是Index位置为82的元素,我们可以计算出来它代表的频率是:

5.3833 * 82 = 441.43Hz

由于声音源是由4个波叠加而成的,因此找到的第一个峰值则是频率最低的峰值,其实也是440Hz所代表的峰值,那为什么我们得到的结果却是441.43Hz呢?这就是前面所说的分辨率问题了,如果我们要想准确地算出440Hz,那就需要增加窗口大小以提高频带分布的分辨率,才能使计算出来的频率更加准确。接着来看第二个峰值,它是在Index为163的位置,计算它代表的频率是:

5.3833 * 163 = 877.478Hz

得到了第二个波峰的频率信息,接着可以计算出第三个波的频率为5.3833 * 245 = 1319Hz,第四个波的频率为5.3833 * 327 = 1760.3Hz,这和在前面图8-10看到的波峰分布情况是一致的。每个点的峰值以及相位的计算也可用上述公式计算出来,这里不再赘述。其实FFT就是把多个波叠加后的时域信号,可以按照频率将各个波拆开,进行更加清晰的展示。下面的小节会更加详细地讲解如何做FFT,以及如何在移动平台上进行优化。

2. MayerFFT的使用

在C++语言中,进行FFT变换时,最常使用的就是MayerFFT的实现,本节就来看一下如何使用MayerFFT将音频文件做一个FFT转换。

首先,下载一个MayerFFT的实现,它的实现虽然比较复杂(我们不做讨论),但是已经比较好地封装在一个类中,这个类也可以在代码仓库中的本章代码部分找到,下面写一个类文件FFTRoutine将具体的实现封装起来,然后提供接口给外界调用。

首先来看一下构造函数和析构函数:

FFTRoutine(int nfft);

~FFTRoutine();

可以看到构造函数中有一个参数nfft,这个参数代表FFT运算钟一个窗口的大小,这也是做FFT最基本的设置,为避免频繁的内存开辟和释放操作,在构造函数的实现中,要开辟一个nfft大小的浮点类型的数组,以供做FFT运算的时候使用,在析构函数中要销毁这个浮点类型数组。接下来,看一下从时域信号到频域信号的正向F

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值