第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运算的时候使用,在析构函数中要销毁这个浮点类型数组。接下来,看一下从时域信号到频域信号的正向FFT变换的接口,代码如下:

void fft_forward(float* input, float* output_re, float* output_im);

从接口中也可以看出,第一个参数是输入的时域信号(浮点类型表示),第二个参数和第三个参数分别代表了转换为频域信号之后实部和虚部的两个数组,这个函数的具体实现如下。

首先要将输入数据复制到在构造函数中开辟的数组中,然后调用MayerFFT进行FFT变换:

memcpy(m_fft_data, input, sizeof(float) * nfft);

MayerFFT::mayer_realfft(nfft, n_fft_data);

待MayerFFT做完时域到频域转换之后,实部和虚部的数据也已经存放到n_fft_data中去了,只不过是实部和虚部是对称存储的,我们需要按照顺序取出来,但是首先要将直流分量(第一个元素)的虚部置为0,代码如下:

output_im[0] = 0;

for(int i = 0; i < nfft / 2; i++) {

output_re[i] = n_fft_data[i];

output_im[i + 1] = n_fft_data[nfft-1-i];

}

这样就可以利用开源的MayerFFT做了声音信号的时域到频域的转换。接下来,再做一个逆FFT操作,即把频域数据变换为时域数据,接口如下:

void fft_inverse(float* input_re, float* input_im, float* output)

从接口中可以看出来,输入的是频域信号,分为实部和虚部,输出是时域信号即一个浮点型的数组。来看一下具体的实现,先将输入的实部和虚部按照MayerFFT中存储复数的存储格式还原回去,代码如下:

int hnfft = nfft/2;

for (int ti=0; ti<hnfft; ti++) {

m_fft_data[ti] = input_re[ti];

m_fft_data[nfft-1-ti] = input_im[ti+1];

}

m_fft_data[hnfft] = input_re[hnfft];

然后调用MayerFFT进行逆FFT运算,运算之后的结果还是存储到m_fft_data这个浮点数组中,而此时这个浮点数组中存储的就是时域信号了,最终,把时域信号拷贝到输出参数中,代码如下:

MayerFft::mayer_realifft(nfft, m_fft_data);

memcpy(output, nfft_data, sizeof(float) * nfft);

这样就可以将频域信号又转换回时域信号了,这种逆FFT运算会在一些音频处理中有特殊的作用,比如变调效果器(PitchShift)中就会用到这种操作。MayerFFT的运算都是在CPU上进行计算的,跨平台特性可以做的比较好(因为只有一个cpp的实现文件),但是性能是它的瓶颈,而在移动平台上最需要注意的就是性能问题,所以在下面会给出在各个平台上的优化处理。

3. iOS平台的vDSP加速

前面已经讲解如何使用工具类MayerFFT来将时域信号的音频转换为频域的表示。而在移动平台上我们最关心的就是效率,所以下面要针对于移动平台上给出相应的实现优化。在iOS平台上可使用iOS提供给开发者的vDSP来做FFT的优化操作。苹果为开发者提供的vDSP无论在iOS上还是在MacOS上都可以使用,当然,在使用之前,必须要将Accelerate这个framework引入到我们的项目中,具体做法就是在工程文件的Build Phases里面的Link Binary With Libraries中添加Accelerate.framework这个库,然后再要使用到vDSP的类中用以下代码引入头文件:

#include <Accelerate/Accelerate.h>

现在就可以使用vDSP来加速运算了,vDSP中提供了很多函数来完成DSP运算,本节只介绍FFT的运算以及与FFT运算相关的函数。使用FFT需要先构造出一个指针类型的OpaqueFFTSetup的结构体,需要调用函数如下:

OpaqueFFTSetup *fftsetup;

int m_LOG_N = log2(nfft);

fftsetup = vDSP_create_fftsetup(m_LOG_N,kFFTRadix2);

第一个参数是做FFT转换时,使用的窗口大小(为取log2之后的数值),在vDSP中,FFT的窗口一般是2的N次方,所以这里传入以2为底取对数的数值;第二个参数一般传递iOS提供的枚举类型kFFTRadix2,这样就构造出了fftSetup这个结构体类型。然后分配一个DSPSplitComplex的复数类型作为FFT的结果输出,代码如下:

size_t halfSize = nfft / 2;

splitComplex.realp = new float[halfSize + 1];

memset(splitComplex.realp, 0, sizeof(float) * (halfSize + 1));

splitComplex.imagp = new float[halfSize + 1];

memset(splitComplex.imagp, 0, sizeof(float) * (halfSize + 1));

如上述代码所示,由于声音做FFT的结果是对称的,因此只需要去取半部分的数据,那么,对于复数的实部和虚部分配空间时,也只需要分配一半的大小就可以了。结构体中的realp代表了实部的部分,imagp代表了虚部的部分,当我们分配好这个结构体之后,就可以进行FFT运算了。首先,把要做FFT的时域信号放入上面构造好的复数的结构体中:

vDSP_ctoz((DSPComplex*)input, 2, &splitComplex, 1, halfSize);

输入的时域信号是float类型的数值,首先强制类型转换为复数类型,然后利用vDSP_ctoz这个函数,将复数的实部和虚部分开存储,即原始的复数结构体是交错(interleaved)存放的,而转换之后就是平铺(Plannar)存放的。将转换之后的结构体作为FFT运算的输入:

vDSP_fft_zrip(fftsetup, &splitComplex, 1, m_LOG_N, kFFTDirection_Forward);

如上述代码,第一个参数是最开始构造的OpaqueFFTSetup指针类型的结构体,第二个参数是时域信号填充的复数结构体,后续的参数指定了行距和大小,最后一个参数代表做正向的FFT,FFT的结果还是会放入到这个复数结构体中,我们可以转换为自己的float指针类型的输出,代码如下。

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

outputRe[i] = splitComplex.realp[i];

outputIm[i] = splitComplex.imagp[i];

}

待使用完FFT之后,要将分配的OpaqueFFTSetup指针类型的结构体销毁掉,并且还要将分配的复数结构体内部的实部和虚部部分的数组销毁掉,代码如下:

if (fftsetup) {

vDSP_destroy_fftsetup(fftsetup);

fftsetup = NULL;

}

if (splitComplex.realp) {

delete[] splitComplex.realp;

}

if (splitComplex.imagp) {

delete[] splitComplex.imagp;

splitComplex.imagp = NULL;

}

这样就销毁完毕了,当然也可以利用vDSP来做逆FFT(IFFT)的运算,从名字上来看,就知道这是FFT运算的一个逆过程(从频域信号转换为时域信号),上面在讲解做FFT运算的时候,提到过最后一个参数代表了做正向的FFT,如果使用kFFTDirection_Inverse则代表要做逆向的FFT。一个完整的做逆FFT的代码如下:

void fft_inverse(float* input_re, float* input_im, float* output) {

DSPSplitComplex tsc;

tsc.realp = input_re;

tsc.imagp = input_im;

vDSP_fft_zrip(fftsetup, &tsc, 1, m_LOG_N, kFFTDirection_Inverse);

vDSP_ztoc(&tsc, 1, (DSPComplex*)output, 2, halfSize);

float scale = 1.0 / m_nfft;

vDSP_vsmul(output, 1, &scale, output, 1, m_nfft);

}

上述代码中首先将实部和虚部放到复数的结构体中,然后调用FFT运算函数,注意,此时最后一个参数传递代表要做逆FFT运算。接着调用vDSP_ztoc函数将平铺(Plannar)分布的复数转换为交错(interleaved)分布的output中,最终逆FFT的结果需要除以窗口大小才可以还原成原来的时域信号,其中在除以窗口大小时,使用了vDSP提供的vDSP_vsmul函数来实现性能的提升。

4. Android平台的Ne10加速

在Android平台上,我们能做的优化就是使用Neon指令集来加速运算,Neon指令集其实也是一种单指令多数据的计算模式,而开发者直接使用Neon指令集来实现运算的加速以及实现FFT的话成本太高了,一则是FFT的实现过于复杂,再一个来说测试成本也比较繁琐,所以下面使用开源的Ne10这个库来实现Android平台的性能提升。

Ne10这个库的介绍与安装可以参见本书的附录部分,而本节所介绍的仅仅是如何使用Ne10这个库来实现FFT与逆FFT的运算。首先引入Ne10的头文件:

#include “NE10.h”

然后构造一个FFT运算的配置结构体,注意,这个配置项结构体我们选用的是实数到复数(r2c)的配置项,因为这种FFT配置项在做音频的FFT运算的时候更加适合,代码如下:

ne10_fft_r2c_cfg_float32_t cfg;

cfg = ne10_fft_alloc_r2c_float32(nfft);

构造这个结构体需要传入的参数是使用FFT运算时的窗口大小,由于我们使用的是Ne10这个库,所以在进行FFT运算时需要将声音时域信号构造成Ne10需要的结构体来进行输入,由于这里输入的是float类型的数组,因此Ne10中定义的也是float类型的数据,代码如下:

ne10_float32_t* in;

in = (ne10_float32_t*) NE10_MALLOC (nfft * sizeof (ne10_float32_t));

为了获得FFT运算之后的结果,也需要构造出输出结构体来接受FFT的运算结果,因为FFT的输出是复数的结构,分为实部和虚部,所以在Ne10中也采用了复数的结构,不过它是有单独的结构体来表示的,代码如下:

ne10_fft_cpx_float32_t* out;

out = (ne10_fft_cpx_float32_t*) NE10_MALLOC (nfft * sizeof

(ne10_fft_cpx_float32_t));

准备好以上内容后,就可以做真正的FFT运算了,将输入的音频时域信号的float数组拷贝到上述定义的输入结构体in中,然后调用Ne10这个库提供的FFT运算函数进行FFT运算。注意,在这里我们使用的运算函数是实数到复数的FFT运算,这种FFT运算更适合在音频场景下做FFT运算,最终得到的结果会放到上述分配的结构体out中,代码如下:

memcpy(in, input, sizeof(float) * m_nfft);

ne10_fft_r2c_1d_float32_neon(out, in, cfg);

for (int i = 0; i < m_nfft / 2; i++) {

output_re[i] = out[i].r;

output_im[i] = out[i].i;

}

经过FFT运算之后的结果就存在于结构体out中,取出前半部分数据赋值给输出的实部的float数组和虚部的float数组中。最终在做完所有的FFT运算之后,需要释放掉分配的资源,包括FFT配置项、输入结构体、输出结构体,代码如下:

NE10_FREE(in);

NE10_FREE(out);

NE10_FREE(cfg);

其实类似于iOS平台的vDSP的优化方案,Ne10提供的FFT方案肯定也提供了逆FFT的运算操作,代码如下:

for (int i = 0; i < m_nfft / 2; i++) {

out[i].r = input_re[i];

out[i].i = input_im[i];

}

ne10_fft_c2r_1d_float32_neon(in, out, cfg);

memcpy(output, in, sizeof(float) * m_nfft);

可以看到,调用逆FFT运算时,调用的是c2r的FFT函数,即使用从复数到实数的转换函数来完成逆FFT的运算,最终再把in这个结构体中的实数拷贝到整个函数的output即时域信号的float数组中去。

8.3 基本乐理知识

本节来学习一下基本的乐理知识,为什么要学习乐理知识呢?因为在处理声音的时候,有一部分是与伴奏或者背景音乐有关的,或者说与唱歌或听歌处理相关的,这就需要我们要掌握一些基本的乐理知识来解决了。

8.3.1乐谱

为了能记录之前发生的事情,人们撰写出了历史,而不是口口相传,导致后世不知前世之事。而类似的问题发生在各个领域,比如医学、数学、化学、物理等各个领域。同样在音乐界也不例外,人们为了能使美好的乐曲保留下来,并且便于学习和交流,创造出了各种各样的记谱方法,而这一些记谱方法就是我们要说的乐谱。而记谱的方法也有很多种,像壁画一样,世界各地的人都会创造音乐,也都会有自己的记谱方法,比如在中国古代广为流传的《工尺谱》。但是现在仍然被我们广为应用并且熟悉的有两种记谱方法,其中一种就是用阿拉伯数字表示的《简谱》,还有就是国际上流行通用的《五线谱》。

下面来看一下《我是一个粉刷匠》这首歌曲的第一句,使用简谱记谱方法和五线谱的记谱方法有何区别,简谱记谱方法如图8-11所示。

图8-11

在图8-11中,左上角用于指明节拍信息和谱号,具体节拍和谱号的意义先简单了解一下:谱号G指明是高音谱号;2/4代表是拍号,每一拍(代表时间长度)是四分音符的时值,每一小节有两拍(代表节奏信息)。乐曲的第一行指明了具体的音符排练顺序以及节奏信息,5 3代表的是Sol Mi两个连起来算作一拍,而小节之间是使用 | 进行分割的,第二个小节的第二拍do自己占用了这一拍的时值,所以前两个小节连接起来就是sol mi sol mi sol mi do,大家自己可以体会一下。这是一首儿歌,音符都在一个音组之内(一个八度之内),而有一些歌曲可能要跨越多个音组,对于低音就在数字下面加点来表示,高音则在数字上面加点来表示,加的点越多则代表越低或越高。接下来,看一下五线谱的记谱方法,如图8-12所示。

图8-12

图8-12中其实同时具有简谱和五线谱的表示方法,这里看五线谱部分,第一个∮代表了是高音谱号,2/4代表拍号,表示每一拍都是四分音符,每一小节有两拍。剩下的就是谱表部分,在制作五线谱,首先得画出五根线,而五根线画出来之后中间就形成四个空行,这在五线谱中称之为间,所以五线谱由五条平行的“线”和四条平行的“间”组成。而线和间的命名也比较简单,从下向上依次是第一线、第一间、第二线、第二间、第三线、第三间、第四线、第四间、第五线。而我们的音符就画在这一些线和间上,具体画在哪一个线还是哪一个间上所表达的音高必定是不一样的,而这九个音高必定不能满足我们想表达的音符,这应该如何办呢?五线谱的上边和下边都可以再加线和间,向上即所谓的上加一间,上加一线,直至上加五线,向下即所谓的下加一间,下加一线,直至下加五线。但是即使是这样,也才能够表示29个音符(本来可以表示9个音符,上边可以加五间五线,下边可以加五间五线),能表示的音符还是不够多,所以就有了谱号这个东西,即在五线谱的最开始都要标记到底是高音谱号还是低音谱号或者中音谱号,用的最多的就是高音谱号和低音谱号,高音谱号如图8-13所示。

图8-13

高音谱号也称为G谱号,对于高音谱号,下加一线是do(是中央C的do),三间就是高八度的do,上加两线是再高一个八度的do,对于度数后面也会详细讲到,大家可以理解为更高的一组音。低音谱号如图8-14所示。

图8-14

低音谱号也称为F谱号,对于低音谱号上加一线是do(是中央C的do),二间是低八度的do,下加二线是再低八度的do。如图8-15所示,图中将简谱、五线谱、唱名与钢琴键盘画在了一个图中,大家可以对照着看一下,以便加深理解。

图8-15

可见不同谱号也就代表了在五线谱中每个线或者每个间所表达的音高是不一样的。五线谱之所以是世界范围内通用的记谱方法,就是因为它是最科学也是最容易理解的记谱方法,大家可以看从下到上就是音高在不断增长,一目了然。

五线谱由三部分组成,分别是谱号、谱表和音符,其中前两部分在不知不觉中已经介绍完了,剩下的就是音符了,音符其实比较复杂,因为它涉及比较多的概念,所以在下面的部分,分别从音符的音高和时值两个方面来进行介绍。

8.3.2音符的音高与十二平均律

如何描述一个乐谱呢?常用的有五线谱、简谱等。而无论是五线谱,还是简谱,都是在表示音符的音高和音符的时值。当然时值是由节拍信息定义的,而在本节讨论的是音符的音高部分。对于音符也有两种表述方式,第一种就是大家经常唱的do re mi fa sol la si,即唱名,也是大家最常使用的;除此之外还有另外一种表示方式,就是音名,即C D E F G A B,这种标记方式叫做音名。

由于基本乐理的概念比较多,所以我们逐一来理清楚,读者可以看到图8-16中所示的键盘图片,可以看到一个中央C的白键,音名记为c1,我们从c1向右数,一直数到c2,这一串连续的音称之为一个音阶,同时c2这个白键所发出声音的频率恰好是c1这个白键发出声音频率的2倍。一个音阶同时也称为一个音组,在键盘上中央C所在的这一个音组称为小字一组,向右数下去的每个组分别是c2所在的小字二组,c3所在音组是小字三组,c4所在的音组是小字四组。那么,向相反的方向数的话,即c所在的音组是小字组,C所在的音组是大字组,C1所在的音组是大字一组,可这么多组如何记忆呢?其实很简单,大家只要记住音名就可以了,首先找到比中央C低八度的音名为c的这一组,所有的音名都是小写字母表示,所以称之为小字组,从这一音组向右数每增加一个八度音名都会在小写字母后边加1,同时音组就成为小字几组(比如中央C所在的音组成为小字一组),然后再来看比中央C低2个八度的音名是C的这一组,所有的音名都是大写字母表示,所以称之为大字组,从这一组向左数,每低一个八度就音名就在大写字母后边加1,而所在的音组就是大字几组,这样应该就能比较简单地记住音名及所有的音组了。

在这里,不得不在引入一个音程的概念,所谓音程是指两个音符之间的音高关系,一般用度来表示,还是对照着图8-16中的钢琴键盘来看,从c1到c1称之为一度,从c1到c2称之为两度,那么以此类推,从c1到c2之间我们常说差了八度,所谓的八度实际上指的就是音程之间的关系。

图8-16

可以数一下,所有的键(包括黑键和白键)加起来恰好是十二个,而相邻键之间称之为一个半音,即e1和f1之间是一个半音,而c1和d1之间是两个半音即是一个全音,而从c1到c2有12个半音,这也就引出了即将和大家介绍的概念——十二平均律,十二平均律只是一个比较好入门的一个理论,以下是十二平均律的定义:

十二平均律,亦称“十二等程律”,世界上通用的把一组音分成十二个半音音程的律制,各相邻两律之间的振动数之比完全相等。

钢琴就是十二平均律制的乐器,国际标准音规定,钢琴的a1(小字组的A音,其实就是中央C这个音节的A音)的频率是440Hz,并且规定每相邻半音的频率比值为2^(1/12)≈1.059463。根据这两个规定就可以得出钢琴上每一个琴键音的频率,比如a1的左边的黑键升g1的频率就是:

440 / 1.059463 = 415.305Hz

a1右边的黑键升a1的频率就是:

440 * 1.059463 = 466.16372Hz

而依照这种运算,计算出来a的频率是220Hz,a2的频率是880Hz,恰好差了12个半音频率是一倍的关系。而这种定音方式就是“十二平均律”。为什么钢琴称为乐器之王,是因为钢琴的音域范围从A2(27.5Hz)至c5(4186Hz),几乎囊括了乐音体系中的全部乐音。

而有的读者文言功底比较深厚,可能知道中国传统五声音阶是:

宫gōng、商shāng、角jué、徵zhǐ、羽yǔ

这是我国五声音阶中五个不同音的名称,对应于唱名的话,宫等于Do、商等于Re、角等于Mi、徵等于Sol、羽等于La。那比现代乐谱中少了Fa和Xi这两个音,其实在我们的古音阶中有变宫与变徵分别对应于Fa和Xi。关于中国的五声音阶最早的记载出现在春秋时期,可见其实音乐是不分国界不分时间的,每个国家都有自己的乐律,而中国音乐史上著名的“三分损益法”就是古代发明制定音律时所用的生律法。对应于中国民族的传统乐器中的古筝,其实只有这五个音作为一个音阶,另外大家非常熟悉的沧海一声笑,其实也是只有这五个音以及相差八度的五个音共同组成的歌曲,并且大家听到的中国风的歌曲大都是采用了宫调式的主旋律,比如流行歌曲中的东风破、青花瓷、烟花易冷、红尘客栈、庐州月等。

8.3.3 音符的时值

所谓音符的时值就是表示这个音符所持续的时间,平时大家衡量时间是有单位的,而音符是如何体现自己的单位的呢?其实就是靠长得不一样。音符一般由三部分组成,分别是符头、符干、符尾,让我们拿一个简单的音符来看,如图8-17所示。

图8-17

大家应该很熟悉这个音符,因为在很多地方都以此音乐符号来代表音乐,那这个音符是什么音符呢?这个音符其实是一个八分音符,具体音符一共有多少种表示呢?我们来看图8-18所示。

图8-18

在图8-18中,一拍的单位一般是一个四分音符,用一个实心符头和一个符干表示,为什么符干有的向上画,有的向下画的?其实主要就是为了在五线谱中更加容易被识谱者观察。在五线谱中规定符头要左低右高呈椭圆形,在五线谱三线以上符干要向下画并且在符头的左边,在三线以下的符干要向上画并且在符头的右边,符杆的长度一般以一个八度为单位。

大家看了之后,可能觉得图8-12所展示的粉刷匠的五线谱中的音符并不存在于这个表中,不要着急,其实在五线谱中还有一种画法叫共用符尾,像粉刷匠中的第一个小节就是有4个八分音符共用一个符尾,这中共用符尾的记谱方法更便于识谱。

另外还有一些休止符、变化音等比较特殊的音符标记方法,在这里不在一一讨论,其实我们也只是想通过学习五线谱来学习基本的乐理知识,所以很细节的东西我们就不再讨论,毕竟我们也是希望掌握基本的原理以助于在日常工作中更好地实现App上的功能,如果读者有兴趣,可以买一本基本乐理的书去深入学习一下。

8.3.4 节拍

节拍是指强拍和弱拍的组合规律,有很多有强有弱的音,在长度时间内,按照一定的顺序反复出现,形成有规律的强弱变化,使得整个乐谱更有节奏感。根据强、弱的不同组合可以形成各种情绪,各种不同风格的乐曲来,因此节拍非常重要,它等于是音乐大厦的基石,且必须是有规律、有秩序的。

在前面小节讲解过乐谱上的音符除了记录音符的音高,还要记录音符的时值,而所谓时值的表示,就是通过节拍来表示的。在介绍节拍之前,先来介绍一下拍号。

拍号是乐谱小节的书写标准,在乐谱中是以一个分数的形式来表示的。比如4/4,分母的4表示的是以一个四分音符为一拍,分子的4表示的是每小节有四拍。其实拍号是一个相对的时间单位,它只能表示出每一个小节里面有几个拍子以及每个拍子的时值,但是具体占多长时间该怎么表示呢?这又要引入另外一个概念,即BPM。

BPM = Beat Per Minute,每分钟节拍数的单位。最浅显的理解就是在一分钟的时间之内,声音节拍的数量(相当于拿一个节拍器在一分钟之内发出节拍的数量),这个数量的单位便是BPM,也叫做拍子数。BPM就是每分钟的节拍数,是全曲速度标记,是独立在曲谱外的速度标准,一般以一个四分音符为一拍,60BPM为一分钟演奏均匀60个四分音符(或等效的音符组合)。由于60BPM对应的曲目速度为一分钟均匀演奏60个四分音符(或等效音符组合),所以一个四分音符(或等效音符组合)的时值应为1秒,而对应的提供给演奏者显示的演奏速度。一般情况下,歌曲分为慢速(节奏)歌曲、中速歌曲、快速(节奏)歌曲,对应于节拍的话,慢速每分钟40—69(60左右)拍;中速90拍左右;快速108—208(120左右)拍。

8.3.4 MIDI格式

MIDI(Musical Instrument Digital Interface)乐器数字接口,是20世纪80年代初为解决电声乐器之间的通信问题而提出的。MIDI是编曲界最广泛的音乐标准格式,可称为“计算机能理解的乐谱”。它用音符的数字控制信号来记录音乐,一首完整的MIDI音乐只有几十KB大,而且能包含数十条音乐轨道。MIDI里面存储的不是声音,而是音符、控制参数等指令,而能解析MIDI的设备会根据MIDI文件里面的指令来播放。具体音符在MIDI中是如何表示的呢?还是使用一张键盘图来看一下音符对应的MIDI的名字以及MIDI值,如图8-19所示。

对于键盘图大家应该会比较熟悉了,我们借助于键盘图来了解MIDI是再合适不过的了。可以看到中央C的频率是261.63Hz,而所对应的MIDI名称是C4,对应的MIDI值是60。对于MIDI值,以及MIDI的名称该如何记忆呢?其实也很简单,前面在讲音组时,已经知道键盘最低的音组是大字二组(当然大字二组只有A和B两个音),大字二组的第一个音A就是MIDI的A0,第二个音B就是MIDI的B0,在图8-19中,向下数就可以数出所有的MIDI名称来了。而对于MIDI值的话,我们只要记住中央C(c1)的MIDI值是60或者小字一组的A音(a1)是69,然后根据十二平均律就可以全部都计算出来了。

图8-19

具体的MIDI制作就不在这里详细展开了,可以使用某一些电子钢琴,甚至现在有一些App都可以将乐者弹奏的音符(包括音高和时值)记录下来。一般由作曲人或者音乐编辑将制作出一首伴奏的MIDI,然后会将这个MIDI文件加密制作成私有的格式,等客户端下载了这个MIDI文件之后,在进行解码,最终能解析出对应时间上的音符。那我们解析出了对应的时间上的音符能做什么呢?其实有很多种用处,如果我们知道一首歌曲对应的MIDI信息,在K歌应用中就可以给用户做打分,即评测用户唱的音高和MIDI中的音高是不是匹配,从而作为打分的依据;也可以做一些节奏修正和音高修正,因为MIDI中包含了时间信息和音高信息,我们可以对用户唱的歌曲进行对齐节奏和修正音高等操作。所以了解MIDI格式是非常重要的,尤其是对于音乐属性的App来讲,大家可以基于MIDI能做很多事情。

至此,基本乐理也接近尾声了,毕竟我们不可能通过一节的内容将别人一本书的内容全部都讲解出来,所以笔者就根据自己的理解与工作中需要用到的知识点和大家一块进行了讨论,如果读者对基础乐理比较感兴趣的话,可以自行深入学习。从下一节开始会进行混音效果器的介绍,让大家了解一下具体的混音过程,以帮助大家在工作中对声音做出更好的处理。

8.4 混音效果器

在音乐的App中,进行混音处理是必不可少的一项工作,而混音这门学科也是非常复杂的,一个优秀的混音师也是非常值钱的。作为一个优秀的混音师,不单单是要有编曲经验,还得会乐器懂乐理,并且耳朵要好用,可以分辨什么样的声音是好声音,同时还要会使用混音的工具。本节会介绍一些混音中的基础知识,也包括一些常用混音工具的使用。针对最常用的四种效果器会分别给出介绍,让大家明白这四种效果器分别能影响声音哪一方面的特性,应该如何合理地使用这些效果器的组合来美化我们的声音,从而产生一首优秀的作品。

8.4.1 均衡效果器

均衡效果器又称均衡器(Equalizer),最大的作用就是决定声音的远近层次,而我们时常听到别人说这首歌曲是重金属风格的歌曲,或者说这首歌曲是舞曲风格等,其实就与声音的远近层次有关。不同的歌曲风格区别在于声音在不同频段的提升或衰减不同,要想完成一首具体风格的作品就离不开均衡效果器。

均衡效果器具有美化声音的作用,即调整音色,每个人由于自己声道以及颅腔、口腔的形状不同,导致音色不同,有可能这个用户所发出的声音在低频部分比较薄弱,我们就可以在低频部分予以增强,使得整个声音听起来更加温暖;有可能这个用户所发出的声音在高频部分又过于强烈(薄弱),我们可以在高频部分予以减弱(增强),可以使声音听起来不那么刺耳(更加嘹亮),使得这个人的声音听起来更丰满,更悦耳。当然专家级别的混音师在为歌手处理后期混音阶段,会有更复杂的调节方法,比如这个歌手的声音低频部分有瑕疵,那么去提高一点中频部分来掩盖住有瑕疵的低频段的声音。

其实上面所描述的就是均衡效果器的使用场景了,明白了使用场景,下面就来具体的介绍一下均衡器。均衡器(Equalizer)最早是被发明用来补偿频率缺陷的,因为那时音频设备的信号品质很差,在传输过程中损失非常严重,到最后除非进行信号补偿,否则信号就会变得极差,工程师发明均衡器,就是为了补偿损失的频率,尽可能地还原声音。而现在均衡器更多的应用在掩盖歌手的某一个频段的声音缺陷,或者增强某一个频段的声音优势上。

那接下来看一下声音的频率分布。

1)超低频。1Hz-20Hz之间,大约是4个八度的范围。这个声音,人的耳朵是听不到的,如果音量很大,我们的耳朵能够感觉到一种压力感,比如地震就可以产生这种频率。一般来说这个频段和音乐没有关系。

2)非常低频。20Hz-40Hz之间,一个八度(频率差两倍)的范围。这个频率也是很低的,一般远距离的雷声以及风声在这个频段里。这个频段的音效,在音乐中还是会经常用到的。

3)低频。40Hz-160Hz之间,2个八度的范围。电贝斯的声音便属于这个频段,当然低音提琴、钢琴也都拥有这个音域。这就是音乐中常用的频段了。男低音也可以发出这个频段中的一部分声音。

4)低中频。160Hz-315Hz之间,1个八度。这个八度音,男中音可以发出。单簧管、巴松管、长笛也拥有这个频段的声音。

5)中频。315-2500Hz之间,3个八度。这是人耳最容易接受的声音频段。我们从电话听筒里听到的声音一般就是属于这个频段。如果没有低频和高频,单独听这个频段,是很干涩的。

6)中高频。2500Hz-5000Hz之间,1个八度。人耳对这段音程是最敏感的。声音的清晰度和透明度都是由这个频段来决定的。音乐的音量也主要由这个频段影响,人声的泛音也会在这个频段出现。我们知道,公共广播用的喇叭,就是专门设计成3000Hz左右的频段。

7)高频。5000Hz-10000Hz之间,1个八度。这个频段会使音乐更明亮。多种高音乐器都拥有这个频段的声音,人的唇齿音也在这个频段内。

8)超高频。10000Hz-20000Hz之间,1个八度。这是可听频率范围内最高的音程了,需要很高的泛音,才可以达到这个范围,在音乐中很少见,而且人耳对这个频段已经很难辨别。但是,这个频段丰富的泛音可以起作用于其他频段的声音,对音色有很大的影响。

了解了声音的分布之后,我们可以使用最简单的Audacity工具打开一段声音,在菜单中的特效选项下选择均衡(Equalizer),可以看到均衡器的调节菜单,如图8-20所示。

图 8-20

在图8-20中,中间部分有一个曲线,横轴是频率,纵轴是dB(0dB以上代表增强,0dB以下代表减弱),如果我们点击变平坦按钮会看到这个曲线会是在0dB上的一条水平的直线,而如果我们打开选择曲线的菜单会看到有一些默认的曲线,其中包括以下曲线。

  • Bass Boost: 低音增强;
  • Bass Cut: 低音截断(类似于高通滤波器);
  • Treble Boost: 高音增强;
  • Treble Cut: 高音截断(类似于低通滤波器);
  • Telephone: 代表电话音质(频率分布在400Hz-3000Hz);
  • AM Radio:代表收音机音质(频率分布在50Hz-400Hz);

选择其中一个预制的效果器可以看到曲线的变化,点击预览按钮可以针对加入这个效果器的音频效果进行预览。当然自己也可以进行拖曳曲线来对某一个频率进行增强或者减弱,然后进行预览,尝试着听一下效果。当然,也可以选择图形化的均衡单选框,如图8-21所示。

图 8-21

在图8-21中,出现了各个频率上的滑动块,可以通过滑动块来代表将这个频率的声音进行增强还是减弱,同时在曲线上也可以看到效果,调整完毕之后点击预览按钮可以试听效果,如果最终确定了所有参数,点击确定按钮,就可以将这一组均衡效果器作用到声音上了,可以听一下整个声音的效果。

上面描述了Audacity这个工具里面如何使用均衡器来修正声音,同时在一些更专业的工具比如LogicPro和Cubase里面,均衡效果器的使用都是非常类似的,在这里也就不再一一介绍,接下来的重点就是分析一下对于均衡器,我们需要设置哪些参数。

其实最直观的参数就是频率,即修正(增强或者减弱)哪一个频率附近的声音,所以第一个最主要的参数就是frequency(代表哪一个频率),如何修正呢?即增强多少,减弱多少,也就是使用参数gain(代表增益是多少),其实除该参数之外,还有一个参数是可以用的,但很多人想不到,就是bandWidth(代表频宽),均衡器修正的不是某一个单一频率上的声音而是一个频段的声音,所谓频段其实就是从一个频率作为中心点左右都扩充一定的频率,就形成了一个频段,而具体这个频段有多大,就是用这个bandWidth来表示的。bandWidth常用的表示单位有两个,一个是O,即Octave,代表一个音程即一个八度,基本乐理中我们描述过,一个八度体现在频率上就是2倍的频率,如果我们定义的中心频率为2KHz,频宽为1.0(单位为Octave),增益为3dB的话,那么对应的到图中的曲线就是从1KHz开始进行提升,到2KHz提升到峰值3dB,最后到3KHz以后不再进行提升,这就完全描述了对这个频段的增强;而另外一个是Q,即Quality Factor ( 质量系数 ),代表了一个音程调整的有效影响斜率,也就是大家常说的Q值,其实这和前面第一种表示方法达到的效果是一样的,实际上Q和O是有一定的换算关系的,因为我们在不同平台或者开源算法中使用EQ的时候,要填入的bandWidth单位不一定是什么,所以我们要知道两者是如何进行换算的。其中我们知道O值(有多少个八度)如何计算出Q值呢?如图8-22所示。

图 8-22

而如果我们知道Q值如何计算出O值(有多少个八度)呢?如图8-23所示。

图 8-23

知道了两个公式,其实只要给出任意一个值都可以计算出以另外一个单位描述的值了。

均衡效果器的作用以及应用场景大家也基本上清楚了,本节也仅仅是均衡器的一个入门,真正的混音师在使用均衡器的时候,很少会对某个频段上的声音进行能量增强,反而是经常会把其他频段上的声音能量进行衰减,所以在使用的时候,并不是一味地增加能量,而是要根据具体情况具体分析。后面会讨论具体如何在Andorid和iOS平台上如何实现均衡效果器。

8.4.2 压缩效果器

压缩效果器又称压缩器(Compressor),是在时域上对声音强度所进行的一个处理,压缩器可以简单地理解为,当音频的音量剧增的时候,自动将音量调整的小一点的功能。这也是大多数压缩器的工作方式,压缩器就是改变输入和输出信号电平大小比率的效果器,如图8-24所示。

图 8-24

在图8-24中,最重要的一个概念是门限值(Threshold),即到达了这个门限值才会进入到压缩器的工作范围,而整体增益(Unity Gain)就是输入信号和输出信号完全一样,也就是对输入信合不做任何改变,即压缩比为1:1。这就又提到了另外一个重要的概念,即压缩比,其实这个概念很简单,可以理解为图中直线的斜率。如果将压缩比调整为2:1,大于门限值的输入信号将以2:1的比例被压缩,比如2dB的输入信号在经过压缩器之后就被压缩掉了1dB,这也就是我们图中的2:1这个曲线;但是如果将压缩比改为20:1的曲线,即比率设置成为20:1,这时压缩器就变成了一个限制器,输入信号过了门限值之后每增加20dB的电平,输出只能增加1个dB,所谓限制器其实就是常说的峰值限制器(Peak Limiter)。在压缩器里面还有两个非常重要的参数,一个是作用时间(attack time),另外一个是释放时间(release time),下面分别介绍这两个参数的意义。

作用时间(attack time),又称为起始时间,决定了压缩器在超过门限值后多久会触发压缩器来工作,前面说过,超过了门限值之后就到了压缩器的工作范围,但是不带表压缩器就会立马工作,而这里的起始时间实际上就是来触发压缩器工作。为什么要使用这个参数来触发压缩器工作呢?假设输入电平有一个瞬时峰值我们的压缩器就进入工作,这就达不到压缩器的最初目的了,因为压缩器的存在就是为了使得整个音频作品平稳地在一定的能量范围内,所以设置一个起始时间,让压缩器躲过这些瞬时峰值而持续工作。

释放时间(release time)和起始时间正好相反,它决定了压缩器在低于门限信号多长时间之后停止工作,如果释放时间过短,那么在信号低于门限之后压缩器会立即停止工作,就会导致抽泵现象,声音会听起来非常不舒服。

最后一个参数就是输出增益(Output Gain),即增益补偿,比如我们压缩了3dB的增益,然后使用增益补偿提升了整个输出信号,那么就可以将压缩掉的动态空间补偿回来。

明白了以上参数之后,下面使用Audacity打开一个音频文件,然后在特效的菜单中选择压缩器,如图8-25所示。

图 8-25

在图8-25中,参数噪音底可以不用去管,其余的可以调整的参数包括阈值(实际上就是上面所讲的门限值)、比率(就是压缩比)、上升时间(就是AttackTime)、衰减时间(就是releaseTime),而压缩后增长到0dB就是上面所讲的输出增益了,我们可以自己调节这几个参数观察曲线的变化,点击预览可以试听效果,如果效果满意的话,点击确定可以将压缩器作用到音频文件中。

其实压缩器在日常工作中可以以多种其他效果器的形式出现,但是原理都是一样的,比如上面所提到的峰值限制器(Peak Limiter),将压缩比调整到足够大(一般我们认为压缩比率在20:1以上的压缩器就是限制器)的压缩效果器就是一个峰值限制器了;除此之外,唇齿音消除器(De-Esser)也是一种应用场景,因为唇齿音一般都是/ci/、/si/等高频的声音,频率一般是4KHz到8KHz,做一个可调频率的压缩效果器来压缩特定的频率,可以将这一些人声中的嘶嘶声给衰减掉,从而达到悦耳的效果;另外还有噪声门(Nosie Gate)也是压缩器的一种应用场景,即我们规定一个门限值,门限以上的声音可以通过,门限以下的声音被视为噪音被完全切掉,这也是压缩效果器的另外一个特殊的应用场景。

8.4.3 混响效果器

混响效果器又称混响器(Reverb),在介绍混响器之前先讨论一下什么是混响。其实混响在大部分场景下都会产生的,我们可以设想老师讲课的一个场景,老师的声音经过多次反射,假如有5条声音反射线(实际上有成千上万条)到达学生耳朵,老师每说一句话,学生实际上听到的就是6句话(一句话直接传到学生耳朵里,还有反射的5句话),但是由于这一些反射声到达时间间隔太近了,所以学生实际上市分辨不出6句话,而是1句带有混响感觉的话,混响效果器就是这样工作的,把很多路声音(由于经过不同的反射源反射,所以能量不同)进行很多很多次的叠加(因为反射的距离有长有短,所以到达听者耳朵的时间就不同,所以叠加的时间也不同)。而所谓的混响器就是接受一个输入的声音,然后进行[某种计算],就可以达到6个声音(实际上是成千上万个声音)叠加的效果。而这里面所谓的某种计算在数学中叫做“卷积”计算,英文是“convolution”。如果把教师的声音看做一个单一的脉冲,通过计算之后我们得到却是一个完整的声波,如图8-26所示。

图 8-26

在图8-26所示的这个脉冲图中,就是含有6个脉冲(实际上是成千上万个脉冲)的声波,也就是在这个房间里,从老师到学生座位的混响特征。在声学上,由于这个混响特征是由脉冲得到的,所以称之为脉冲反应,即impulse response,简称IR。很显然,在不同的空间里这个脉冲图并不相同,也就是说不同空间里的混响特征不同,进一步也就是说不同空间里的IR是不相同的。我们将一个输入声音作为源声音,这个声音通过与IR的卷积得到的结果就是这个声音源在这个混响空间内所产生的最终混响结果。其实在几乎在任何场景下都会产生混响,只不过有一些混响效果在人的耳朵里不太容易分辨,像比较专业的录音棚里,墙壁上做了很多突出的吸音棉,可以最大程度的减少混响的影响,而在混音阶段再给作品增加混响。

在浴室里面自己发出一个声音,最终我们自己听到的声音其实是代表了浴室这个空间的混响特征,在小礼堂里面,或者在一个非常开阔的大舞台上演唱歌曲,所听到的混响效果肯定是不同的,总之每个空间都有自己独特的混响特征也就是有自己独特的IR。为了模拟出各个空间的混响效果,也就是为了定制出不同的场景混响,我们就可以制定不同场景下的IR,然后将声音源与特定场景下代表的IR进行卷积就可以得到这个场景下的混响效果了。那我们如何确定特定场景下的IR呢?

第一种就是采样IR混响,Sony、Yamaha都出过采样混响,所谓采样混响全部是真实采样得来的wave文件,可以存放与任何存储器,采样混响的IR都是录音采样得来。在想要获得混响特征的地方,例如小礼堂、音乐厅舞台上安置音箱,座位席中安置立体声话筒,然后播放一系列测试信号,以脉冲信号为主,各种速度的全频段正弦波连续扫描为辅,录得声音,然后经过计算得到IR。用这种采样方法得到的IR,是最真实也是效果最好的一种,当然这种IR的制作也是极为昂贵的。

第二种就是算法混响,也是最常见的混响效果器,目前大多数的数字混响效果器以及软件混响都是这种类型的。这类混响器虽然不带有真实的IR,但是却提供了很多方法可以让你对它自带的原始脉冲序列进行修改,比如通过改变空间大小、早反射时间、衰减时间、阻尼等参数来修改IR,以达到控制混响效果的目的,为了性能的考虑,这种IR的脉冲个数其实是有限的,并不会像第一种采样混响中有无限的脉冲信号。

为了方便研究,声学上把混响分为几个部分,并规定了一些习惯用语。混响的第一个声音也就是直达声(Direct sound),也就是源声音,在效果器里叫做 dry out (干声输出),随后的几个明显相隔比较开的声音叫做“早反射声”(Early reflected sounds),它们都是只经过几次反射就到达了的声音,声音比较大,比较明显,它们特别能够反映空间中的源声音、耳朵及墙壁之间的距离关系。后面的一堆连绵不绝的声音叫做 reverberation。大多数的混响效果器会有一些参数选项给你调节,现在就来讲讲这些参数具体是什么意思。

(1)空间大小(Room Size)

空间可以体现出声场的宽度和纵深度,不同的效果器在这个参数上有不同的的算法体现,但是这个参数是非常重要的。

(2)余响大小(Reverbrance)

如果说早反射声可以决定空间的距离,而余响则代表了空间的构造,即空间里面的物体多少,以及墙壁的材质,墙壁及室内物体的表面材质越松软,则代表吸音的能力越强,余响则越小。

(3)阻尼控制(Damping)

这个代表了混响声音减弱的程度,对应到实际场景中就是场景里面的物体多少,物体越多,并且物体表面越不光滑,衰减的就越厉害,可以根据我们要想得到的实际场景去设置这个参数

(4)干湿比(Dry Wet Mix Ratio)

有的混响算法会有这个参数,干信号表示原始信号,湿信号表示混响信号,而干湿比就是代表了最终输出信号的干声和湿声的比例。设置为100%,则意味着只要湿声不要干声。

(5)立体声宽度(Stereo Width)

有的混响效果器有这样的参数,如果把这个值设大,那么效果器在产生IR的时候会使左右声道差异变大,最终就会产生立体声的感觉。

和前面的内容一样,我们也是打开Audacity,在特效菜单中选择Reverb,如图8-27所示。

图 8-27

在图8-27中,可以看到一些可调节的参数,这些参数前面已经都一一介绍过了,大家可以自己调节参数进行预览,并且可以点击Dry Preview来预览干声,最后点击确定,即可将这个混响效果器作用到音频文件上。

混音效果器的介绍先介绍到这里了,我们不可能使用一节的内容讲别人一本书的内容都给讲解清楚,并且针对于我们开发者来讲知道这一些基础知识,也可以满足我们的日常开发需求了,接下来会讲解如何在这两个平台上实现这一些效果器。

8.5 效果器实现

在8.4节中已经了解了各个混音效果器的作用,本节将会讲解如何在Android和iOS平台上实现各个效果器,并最终集成到我们的App中去,可以试听效果。

8.5.1 Android平台的实现

在Andorid平台上实现上面介绍过的效果器有很多种方法,如果我们从头开始一个一个效果器去书写,显然不是一个合理的方案。我们应该去寻找优秀的开源仓库来实现这三种类型的效果器,比如sox开源库,它在音频处理界是一个非常优秀的框架,号称音频处理界的瑞士军刀。所以我们就先来编译sox,然后看一下它能做什么吧。

1. sox编译与命令行使用

Sox是最为著名的声音处理开源库,已经被广泛移植到Windows、Linux、MacOSX等多个平台,Sox项目是由Lance Norskog创立的,后来被众多的开发者逐步完善,现在已经能够支持很多种声音文件格式和声音处理效果。它默认支持的输入输出是wav文件,如果想要支持mp3等格式,需要预先安装libmp3lame库来支持这种格式的的编码与解码。那我们就下载这个库的源码,编译出二进制的命令行工具,先试着使用一下里面的三种效果器。Sox的源代码放在SourceForge上,主页在如下的链接中:

https://sourceforge.net/projects/sox/

进入Sox的主页之后,找到Code目录,下载整个源码目录,我们使用git将整个目录clone下来。所以首先建立一个sox目录,然后进入到这个目录中,执行如下命令:

git clone https://git.code.sf.net/p/sox/code sox-code

当上面这一行命令执行结束之后,进入sox-code目录,可以看到仓库的源代码已经全部被下载下来了,接下来的工作就是将源码编译成为二进制命令行工具。先查看源码目录下面的INSTALL文件,这个文件中指明了如果要编译的源(即代码仓库)是使用git下载的源码,则要先执行如下命令:

autoreconf -i

这个命令执行完毕之后,会在源码目录下生成configure、install-sh等文件,由于我们要编译最基本的Sox的二进制命令行工具出来,所以应建立一个shell脚本config_pc.sh,键入以下代码:

#!/bin/bash

CWD=`pwd`

LOCAL=$CWD

./configure \

–prefix=”$LOCAL/pc_lib” \

–enable-static \

–disable-shared \

–disable-openmp \

–without-libltdl \

–without-coreaudio

然后给config_pc.sh以及configure增加执行权限,并在源码目录下面,新建pc_lib目录,最终执行这个shell脚本文件:

./config_pc.sh

当这个shell脚本执行结束之后,代表配置结束,接下来就可以执行安装命令了:

make && make install

执行成功之后,进入到pc_lib目录下,可以看到这个目录里被安装脚本生成了bin、lib、include等目录,各个目录的作用本书中已经讲过很多遍了。进入bin目录,可以看到play、rec、sox等二进制文件,其中,sox就是我们要运行的二进制命令行工具了,而play则可以在处理的同时直接播放一个音频文件,类似于FFmpeg中的ffplay工具,至于rec,则是录制声音的工具。由于我们在config_pc.sh中关闭了硬件设备的配置选项,所以play和record工具不能使用,我们只使用sox来处理音频文件,输入是wav格式的音频文件,输出也是wav格式的音频文件。那我们就使用sox这个二进制命令行工具,对一个输入文件分别完成前面提到的三种效果器。

首先是均衡效果器,在前面已讲过均衡器的设置,整个参数分为N组参数,每一组参数代表对具体频率的增强或者减弱,每一组参数中包括频率、频带宽度、增益,sox的均衡器参数设置中也是一样的,来看下面这条命令:

sox song.wav song_eq.wav equalizer 89.5 1.5q 5.8 equalizer 120 2.0q -5

上面这条命令前两个参数分别代表输入文件和输出文件,它们后面有两个均衡器,第一个均衡器在89.5Hz作为中心频率,频带宽度为1.5q(具体Q值代表的意义,在之前的章节中已经提到过),增加5.8dB的能量;第二个均衡器是在中心频率为120Hz,频带宽度为2.0q,减少5dB的能量。如果想再给声音多作用几个均衡器,在后面依次再写上几组就可以了。待执行完命令之后,可以听一下输出文件的效果,或者使用Audacity软件打开处理前和处理后的音频文件,使用频谱图来观察一下处理前后的频谱分布的变化。

其次是压缩效果器,在前面也讲过压缩器的设置,整个参数包括门限值、压缩比、Attack Time、Decay Time等,sox中的压缩效果器使用库中的compand来实现,先来看一下命令:

sox song.wav song_compressor.wav compand 0.3,1

-100,-140,-85,-100,-70,-60,-55,-50,-40,-40,-25,-25,0,-20 0 -100dB 0.1

前两个参数依次是输入文件和输出文件,后面的compand代表的是效果器的名称,在sox中使用compand效果器来实现压缩-扩展器(Compressor-Expander),后面的参数以空格分开,首先是0.3和1,分别代表了Attack Time和Decay Time,至于它们所代表的含义前面已经介绍过了,接下来的一组参数代表了压缩器的转换函数表,每个数值的单位都是dB,稍后会详细解释这一条曲线,继续来看下面三个参数分别是0,-100,0.1,第一个0代表的是增益,即压缩完毕之后可以给一个整体增益作用到输出上,这里就不给任何增益了,第二个-100代表的初始音量,可以设置成为-100dB,代表初始音量从一个几乎为静音的音量开始,最后的0.1是延迟量。在实际的音频处理场景中,压缩器对于声音的忽然升高有很好的抑制作用。

现在来看一下由压缩器转换函数表绘制出的一个压缩曲线,如图8-28所示。

图 8-28

在图8-18中,红色直线为一条斜率为1的直线,实际上就是不作任何处理时的曲线,而蓝色曲线就是我们的压缩曲线。整个蓝色曲线分为四部分,可以看到在能量比较低的部分(-100dB到-80dB)将输出能量降低,相当于底部噪声部分给压低了;在中间能量部分(-75dB到-45dB)有所提升;而接下来一部分(-40dB到-25dB)我们保持不动,可以看到蓝色曲线和红色曲线重合;在接下来比较高能量部分(-20dB到0dB)再进行压缩处理,就形成了整个曲线。当然,输入输出点数越多,曲线就会画得越平滑,处理得到的声音效果也会越好。大家可以听一下经过压缩器处理完毕的声音,是不是整个音量的动态变化范围被压缩了呢?而这也就是压缩效果器的作用。

最后是混响效果器,对于混响器的参数之前也详细的介绍过了,直接来看如何使用sox给一个声音增加混响:

sox song.wav song_reverb.wav reverb 50 50 90 50 30

第一个参数是reverbrance即余响的大小,先设置为50听听效果,第二个参数是HF-damping即高频阻尼,设置为50,第三个参数是room-scale即房间大小,这里设置为90,代表一个比较大的房间,第四个参数代表立体声深度,设置越大则代表立体声效果越明显,这里设置为50,最后一个参数是pre-delay即早反射声的时间,单位是毫秒,这里设置为30毫秒。执行完以上命令,读者听一下处理完的声音,会发现有一个比较明显的混响效果了。

这里介绍了如何编译Sox,以及使用Sox这个二进制命令行工具,下面会把它交叉编译到Android平台,并且介绍它的SDK的使用。

2. sox的交叉编译

这里会将Sox交叉编译到Android平台,并且介绍如何在Andorid平台使用Sox的SDK来使库中的效果器工作。首先,要将Sox这个开源库交差编译出一个静态库以及头文件,以方便我们在Android的NDK开发的编译阶段和链接阶段分别引用。新建立config_armv7a.sh,键入以下代码,来编译出静态库与头文件:

#!/bin/bash

NDK_BASE=/Users/apple/soft/android/android-ndk-r9b

NDK_SYSROOT=$NDK_BASE/platforms/android-8/arch-arm

NDK_TOOLCHAIN_BASE=$NDK_BASE/toolchains/arm-linux-androideabi-4.6/prebuilt/darw

in-x86_64

CC=”$NDK_TOOLCHAIN_BASE/bin/arm-linux-androideabi-gcc –sysroot=$NDK_SYSROOT”

LD=$NDK_TOOLCHAIN_BASE/bin/arm-linux-androideabi-ld

CWD=`pwd`

PROJECT_ROOT=$CWD

./configure \

–prefix=”$PROJECT_ROOT/lib/armv7″ \

CFLAGS=”-O2″ \

CC=”$CC” \

LD=”$LD” \

–target=armv7a \

–host=arm-linux-androideabi \

–with-sysroot=”$NDK_SYSROOT” \

–enable-static \

–disable-shared \

–disable-openmp \

–without-libltdl

然后执行config_armv7a.sh(如果没有执行权限,要加上执行权限),最终可以看到在当前目录里的lib目录下有一个armv7的目录,armv7目录中会有我们非常熟悉的include、lib目录,里面就是我们需要的头文件sox.h与静态库文件libsox.a,至此交叉编译工作就完成了,接下来就来看看如何使用Sox库中提供的API在代码层面使用各种效果器。

3. SDK介绍

要使用Sox库中提供的API,就要从它的官方实例中开始,在Sox的根目录下进入到src目录下,在src目录下有几个以example开头的C文件,这就是提供给开发者参考的使用Demo。打开example0.c这个文件,首先可以看到,在这个文件的开头引用了sox.h这个头文件,然后再来看一下main函数,因为主要使用API的流程都是在main函数中,所以下面逐步看一下。在使用sox这个库之前,必须初始化整个库的一些全局参数,需要调用如下代码:

sox_init();

上述函数返回一个整数,如果返回的是SOX_SUCCESS这个枚举值,则代表初始化成功了。在整个应用程序中,如果没有调用sox_quit方法,是不可以再一次调用sox_init,否则会造成Crash。接下来初始化输入文件,代码如下:

sox_format_t* in;

const char* input_path = “/Users/apple/input.wav”;

in = sox_open_read(input_path, NULL, NULL, NULL);

初始化好了输入文件之后,再来初始化输出文件,代码如下:

sox_format_t* out;

const char* output_path = “/Users/apple/output.wav”;

out = sox_open_write(output_path, &in->signal, NULL, NULL, NULL, NULL);

这样就初始化好了输出文件,可以看到输入和输出文件都是wav格式的,因为我们并没有集成其他编码格式的工具,所以就是直接用的wav格式。下面来使用效果器,Sox中提供的效果器种类比较多,为了方便开发者使用,Sox使用类似责任链设计模式的方式来设计整个系统,所以我们使用的时候需要先构造一个效果器链出来,然后将需要使用的效果器一个一个地加到这个链里面,最终传入输入文件中数据以及接受这个效果器链处理完的数据,就可以完成音效的处理工作了,所以首先我们先来构造这个效果器链:

sox_effects_chain_t* chain;

chain = sox_create_effects_chain(&in->encoding, &out->encoding);

上述代码就构造出了一个效果器链,重点来看一下里面的两个参数,这两个参数实际上就是告诉效果器链输入音频的数据格式和输出音频的数据格式,比如声道、采样率、表示格式等。而我们从最开始初始化的输入文件格式和输出文件格式中可以拿到数据格式,sox会存储到encoding这个属性中。接下来就需要向效果器链中增加效果器了,但是在增加实际的效果器之前,我们需要先考虑一个问题,就是如何将输入音频数据提供给效果器链,以及如何将效果器链处理完的音频数据写到文件中去。对于这个问题,其实Sox已经帮我们提供了对应的API,为了方便开发者,sox的作者把输入和输出分别构造成了一个特殊的效果器,待我们创建出提供输入数据的效果器之后,需要添加到效果器链的第一个位置;然后创建出输出数据的效果器,添加到效果器链的最后一个位置上。

下面首先来看一下为效果器链提供输入数据的特殊效果器的构造:

sox_effect_t* inputEffect;

inputEffect = sox_create_effect(sox_find_effect(“input”));

上述代码就构造出了一个用于给效果器链输入数据的特殊效果器,但是具体这个特殊效果器的数据从哪里来呢?答案就是我们上面初始化的输入文件,所以我们要将输入文件配置到这个效果器中,代码如下:

char* args[10];

args[0] = (char*) in;

sox_effect_options(inputEffect, 1, args);

可以看到上述代码将之前构造的输入文件格式的结构体强制转化为char指针类型的参数,并配置给了效果器,其实在sox中都是以char指针类型的参数,来配置效果器的。配置好了之后,要将这个效果器增加到效果器链中,并且将这个效果器释放掉,代码如下:

sox_add_effect(chain, inputEffect, &in->signal, &in->signal);

free(inputEffect);

至此我们给效果器链提供输入数据的特殊效果器就已经创建成功,并且进行了配置,最终成功的添加到了效果器链中,而这个过程也是任何一个效果器从创建到配置到添加到销毁的整个过程。

接下来就是我们想要使用的最核心的效果器部分了,这里以一个非常简单的增加音量的效果器作为讲解,在接下来的章节中会依次针对本章中重点介绍的三个效果器进行讲解,音量调整效果器添加代码如下:

sox_effect_t* volEffect;

volEffect = sox_create_effect(sox_find_effect(“vol”));

args[0] = “3dB”;

sox_effect_options(volEffect, 1, args);

sox_add_effect(chain, volEffect, &in->signal, &in->signal);

free(volEffect);

可以看到,使用这个音量效果器给整个音频文件增加3个dB的音量,整个过程也比较简单,但是有的读者会问到,若想使用一个效果器,从哪里可以找到这个效果器的名称呢?其实所有的效果器的名称都被定义在effects.h这个头文件中,读者可以自己去查阅。

接下来配置另外一个比较特殊的效果器,即接受效果器链处理完的数据,并将数据输出到文件中的效果器,代码如下:

sox_effect_t* outputEffect;

outputEffect = sox_create_effect(sox_find_effect(“output”));

args0 = (char*)out;

sox_effect_options(outputEffect, 1, ags);

sox_add_effect(chain, outputEffect, &in->signal, &in->signal);

free(outputEffect);

上述代码也比较简单,主要是把我们前面所构造的输出文件配置给output这个特殊效果器,最终再将效果器添加到整个效果器链中。至此我们这个效果器链就已经构造好了,整个结构如图8-29所示。

图 8-29

当构造好了这个效果器链,如何让整个效果器链运行起来呢?其实也很简单,只需要执行以下代码:

sox_flow_effects(chain, NULL, NULL);

这个方法执行结束,其实整个处理流程也就结束了,经过我们核心效果器——声音变化效果器处理之后的音频数据就被全部写入到output.wav这个文件中了,当然,完成之后还是要销毁掉这个效果器链:

sox_delete_effects_chain(chain);

然后需要关闭掉输入和输出文件:

sox_close(out);

sox_close(in);

最后需要释放掉sox这个库里面的全局参数,代码如下:

sox_quit();

至此我们就可以使用sox提供的SDK来处理音频文件了,大家可以熟悉一下,下面会继续讲解本章中的最重要的三个混音效果器在sox中的使用。

4. 均衡器的实现

下面来使用sox的均衡器,其实在前面已经介绍过命令行工具中如何使用均衡效果器,有了前面的基础,我们也基本上可以猜测出本节的代码如何来书写,代码如下:

sox_effect_t* e;

e = sox_create_effect((sox_find_effect(“equalizer”)));

首先根据均衡器的名字创建出效果器,然后使用中心频率、频带宽度,以及增益来配置这个效果器,代码如下:

char* frequency = “300”;

char* bandWidth = “1.25q”;

char* gain = “3dB”;

char* args[] = {frequency, bandWidth, gain};

int ret = sox_effect_options(e, 3, args);

由于均衡器的参数有3个,所以这里配置参数的第二个参数传递3,待执行完这个配置函数之后,返回的ret是SOX_SUCCESS则代表配置成功。最后将这个效果器添加到效果器链中,代码如下:

sox_add_effect(chain, e, &in->signal, &in->signal);

free(e);

至此,就将一个均衡器加入到我们的效果器链中了,但是一般情况下会有多个均衡器同时作用到音频上,如果有多个则创建多个均衡器,依次添加到效果器链中。

均衡效果器就讲解完毕了,但是一般情况下我们在处理音频的时候,还会加上高通和低通,类似于均衡器,都属于滤波器。高通就是高频率的声音可以通过这个滤波器,低频的声音就被过滤掉了,有另外一种叫法就是低切。低通就是高通的逆过程,也被称之为高切。这里只展示高通滤波器,代码如下:

sox_effect_t* e;

e = sox_create_effect(sox_find_effect(“highpass”));

char* frequency = “80”;

char* width = “0.5q”;

char* args[] = {frequency, width};

sox_effect_options(e, 2, args);

sox_add_effect(chain, e, &in->signal, &in->signal);

相对比与普通的均衡器,高通滤波器不需要增益这个参数,所以只需要这两个参数就够了,低通效果器的名字是【lowpass】。

在sox库中对于均衡器的具体实现是biquad(源码文件是biquads.c),而biquad也是大部分均衡器以及高通、低通等的实现方式。Biquad又称为双二阶滤波器,双二阶滤波器是双二阶(两个极点和两个零点)的IIR滤波器,它可以不用将声音转换到频域而给声音做频域上的某一些处。至此均衡器的所有内容就讲解完毕了,接下来会讲解压缩效果器。

5. 压缩器的实现

这里介绍如何使用sox中的压缩器,压缩效果器前面已经讲解得比较多了,所以我们就直接上代码,首先创建出压缩效果器:

sox_effect_t* e;

e = sox_create_effect(sox_find_effect(“compand”));

然后给这个压缩器配置参数,下面挨个介绍一下,首先是作用时间与释放时间,代码如下:

char* attackRelease = “0.3,1.0”;

然后是压缩比,在sox中用了更灵活的压缩曲线来控制压缩比,使用的是构造一个函数转换表的方式来实现,代码如下:

char* functionTransTable = “6:-90,-90,-70,-55,-31,-31,-21,-21,0,-20”;

最后是整体增益、初始化音量以及延迟时间,代码如下:

char* gain = “0”;

char* initialVolume = “-90”;

char* delay = “0.1”;

构造方这一些参数之后,利用这一些参数配置一下这个压缩效果器,代码如下:

char* args[] = {attackRelease, functionTransTable, gain, initialVolume, delay};

sox_effect_options(e, 5, args);

最终将这个效果器加入到效果器链中,并销毁这个效果器,代码如下:

sox_add_effect(chain, e, &in->signal, &in->signal);

free(e);

大家尝试着运行一下,最终拿出处理完毕的音频文件,进行播放,感受一下在代码层面使用SDK调用压缩效果器处理的音频是否和命令行工具处理出来的音频一致。

6. 混响器的实现

下面介绍如何使用sox的混响器,有了之前效果器的经验,我们首先创建出混响效果器,代码如下:

sox_effect_t* e;

e = sox_create_effect(sox_find_effect(“reverb”));

然后给这个混响效果器配置参数,混响效果器的参数比较多,下面来一一介绍,首先是是否纯湿声的参数:

char* wetOnly = “-w”;

然后是混响大小、高频阻尼以及房间大小:

char* reverbrance = “50”;

char* hfDamping = “50”;

char* roomScale = “85”;

最后是立体声深度、早反射声时间以及湿声增益:

char* stereoDepth = “100”;

char* preDelay = “30”;

char* wetGain = “0”;

将这一些参数一块配置到效果器中,代码如下:

char* args[] = {wetOnly, reverrance, hfDamping, roomScale, stereoDepth,preDelay,

wetGain};

sox_effect_options(e, 7, args);

最终将这个效果器加入到效果器链中,并运行程序。大家可以试听一下处理之后的音频效果,其实在使用混响效果器的时候,一般在混响效果器之前增加一个echo效果器往往可以获得比较好的效果,大家可以尝试着增加一下,由于篇幅的关系,这里就不再赘述了。

在sox库中Reverb使用经典的施罗德(Schroeder)混响模型来实现,施罗德(Schroeder)混响模型使用4个并联的梳状滤波器和2个串联的全通滤波器来建立混响模型。梳状滤波器提供了混响效果中延迟较长的回声,而延时较短的全通滤波器则起到了增加反射声波密度的作用,如图8-30所示:

图 8-30

那现在我们就来分析一下施罗德混响模型的优缺点,优点是,通过设置六个滤波器的参数,可以模仿出前期反射和后期混响效果,全通滤波器可以在一定程度上减轻梳状滤波器引入的渲染成分;缺点是产生的混响效果缺少早期反射声,这样会造成声音缺乏空间立体感而且不清晰。对于它的缺点我们可以去进行改造和优化,其中大家最为常用的一种手段就是使用干声的echo来填充早期的反射声,改造后结构如图8-31所示:

图 8-31

如图8-31所示,在混响中一定要将isWetOnly属性设置为True,即只要湿声,然后使用Echo来填充早起反射声部分,所以将湿声与Echo加起来之后,作为混响的湿声部分,然后在与干声以一定的干湿比混合起来作为最终的输出结果。

至此在Android平台上的效果器实现已经讲解完毕了,读者可以把sox里面的效果器都玩一遍试试效果,说不定哪一个效果器在你以后的工作场景下会用到。特别的说明,这一些效果器都是以C语言书写的,所以理论上是通过交叉编译就可以运行在iOS平台上,但是iOS平台的多媒体库非常强大,是否有更高效的处理方式呢?下一节就介绍iOS平台上如何使用更高效的方式来完成音频处理。

8.5.2 iOS平台的实现

相比较于Android的开发者来说,iOS平台上的开发者应该感到非常幸运,因为苹果已经为开发者提供了足够强大的多媒体方面的API,所以我们的原则是首先来看iOS平台本身的音频处理API都有什么,能否满足我们的效果器实现。查阅开发者文档中不难得出,使用AudioUnit是可以来处理音频的,而这个结论在第4章中就有提到过,此外,还提到过AudioUnit有多种类型,其中有一种类型就是EffectType的AudioUnit,所以我们就来看一下如何使用AudioUnit来实现本章中提到的三种效果器。

1. 均衡器的实现

下面使用AudioUnit来实现均衡器,在AudioUnit中对于均衡器有两种类型的实现,第一种实现是叫做ParametricEQ的实现,这种类型均衡器的设置非常类似于sox中的均衡效果器,如果想要给声音多个均衡器,则需要增加多个此类型的效果器。第二种实现是叫做NBandEQ,从名字上来看,这种均衡器可以同时对多个频带进行设置,不用为了对多频带进行调整而添加多个效果器。在实际的生产过程中,笔者常用的是NBandEQ,读者应该根据自己的场景来选择具体的均衡效果器,在这里分别进行介绍一下这两种类型的AudioUnit的使用,还是按照之前AUGraph的方式来使用。我们使用这两种实现方式实现同一个需求,完成给3个中心频率的增加或减少能量。

(1)ParametricEQ

先来看一下它的描述,类型是Effect的类型,子类型就是ParametricEQ,厂商肯定都是Apple,代码如下:

CAComponentDescription equalizer_desc(kAudioUnitType_Effect,

kAudioUnitSubType_ParametricEQ,

kAudioUnitManufacturer_Apple)

然后按照这个描述将均衡器AUNode加入到AUGraph中,并且根据这个AUNode取出这个效果器对应的AudioUnit,代码如下:

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

AUNode equalizerNode;

AUGraphAddNode(playGraph, &equalizer_desc, &equalizerNode);

equalizerNodes[i] = equalizerNode;

}

AUGraphOpen(playGraph)

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

AUGraphNodeInfo(playGraph, equalizerNodes[i], NULL,

&equalizerUnits[i]);

}

根据之前章节中讲述的AudioUnit配置过程,来给这个效果器配置参数,至于均衡器的参数有哪一些分别代表了什么意义,前面已经讲解得非常清楚了,所以这里就直接上代码了:

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

int frequency = [[frequencys objectAtIndex:i] integerValue];

float band = [[bands objectAtIndex:i] floatValue];

int gain = [[gains objectAtIndex:i] integerValue];

AudioUnitSetParameter(equalizerUnits[i],

kParametricEQParam_CenterFreq,

kAudioUnitScope_Global, 0, frequency, 0);

AudioUnitSetParameter(equalizerUnits[i],

kParametricEQParam_Q,

kAudioUnitScope_Global, 0, band, 0);

AudioUnitSetParameter(equalizerUnits[i],

kParametricEQParam_Gain,

kAudioUnitScope_Global, 0, gain, 0);

}

注意,配置频带的参数不是Q值也不是以O(Octave八度)为单位的,而是以Hz为单位,中心频率的设置以及增益的设置都是和之前一致的。配置好参数之后,就将这个效果器连接到数据源(RemoteIO或者Audio File Player),然后可以去试听一下效果或者将数据保存下来。

(2)NBandEQ

先来看一下它的描述,类型是Effect类型,子类型为NBandEQ,代码如下:

CAComponentDescription n_band_equalizer_desc(

kAudioUnitType_Effect, kAudioUnitSubType_NBandEQ,

kAudioUnitManufacturer_Apple);

从名字上也可以看出,所谓的NBandEQ,其实这一个效果器就可以满足为多个频带增强或者减弱能量的需求,在这里不再展示构造AUNode以及从具体的AUNode中获取出AudioUnit的代码,而是直接把设置参数的代码展示如下:

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

float frequency = [[frequencys objectAtIndex:i] floatValue];

float band = [[bands objectAtIndex:i] floatValue];

float gain = [[gains objectAtIndex:i] floatValue];

AudioUnitSetParameter(nBandEqUnit, kAUNBandEQParam_FilterType + i,

kAudioUnitScope_Global, 0, kAUNBandEQFilterType_Parametric,0);

AudioUnitSetParameter(nBandEqUnit, kAUNBandEQParam_BypassBand + i,

kAudioUnitScope_Global, 0, 1, 0);

AudioUnitSetParameter(nBandEqUnit, kAUNBandEQParam_Frequency + i,

kAudioUnitScope_Global, 0, frequency,0);

AudioUnitSetParameter(nBandEqUnit, kAUNBandEQParam_Gain + i,

kAudioUnitScope_Global, 0, gain, 0);

AudioUnitSetParameter(nBandEqUnit, kAUNBandEQParam_Bandwidth + i,

kAudioUnitScope_Global, 0, band, 0);

}

乍一看可能会觉得这里的参数怎么多。下面就逐一来解释一下各个参数的含义,首先是NBandEQ,要想给哪一个Band设置就直接在某一个参数后面加几即可,第一项设置是选择EQ类型,其中EQ类型包括高通、低通、带通等,这里选择的就是Parametric类型的普通EQ;而第二项参数就是Bypass的设置,也就是是否直接通过而不做任何处理,0代表这个不对这个频带不作处理,1代表对这个频带进行处理;剩余的三个参数是前面讲过的,但是需要注意的是,BandWidth设置的单位是O(Octave,八度),所以如果你的频宽单位是Q值的话,这里要进行一下转换。

接着可以将这个效果器以AUNode的形式连接到数据源(RemoteIO或者Audio File Player)后面,然后可以去试听效果以及生成处理后的音频文件。

2. 压缩器的实现

下面使用AudioUnit来实现压缩器,相比上一节的均衡器,压缩器的设置可以说是比较简单的,首先来看一下压缩器的描述,类型就是Effect类型,而子类型是DynamicProcessor,代码如下:

CAComponentDescription compressor_desc(kAudioUnitType_Effect,

kAudioUnitSubType_DynamicsProcessor,

kAudioUnitManufacturer_Apple);

接着利用这个描述(compressor_desc)构造AUNode,然后找出对应的AudioUnit来:

AUNode compressorNode;

AudioUnit compressorUnit;

AUGraphAddNode(mGraph, &compressor_desc, &compressorNode);

AUGraphNodeInfo(mGraph, compressorNode, NULL, &compressorUnit);

之后就是给这个AudioUnit来设置参数了,代码如下:

AudioUnitSetParameter(compressorUnit, kDynamicsProcessorParam_Threshold,

kAudioUnitScope_Global, 0, -20, 0);

AudioUnitSetParameter(compressorUnit, kDynamicsProcessorParam_HeadRoom,

kAudioUnitScope_Global, 0, 12.937, 0);

AudioUnitSetParameter(compressorUnit, kDynamicsProcessorParam_ExpansionRatio,

kAudioUnitScope_Global, 0, 1.3, 0);

AudioUnitSetParameter(compressorUnit,

kDynamicsProcessorParam_ExpansionThreshold,

kAudioUnitScope_Global, 0, -25, 0);

AudioUnitSetParameter(compressorUnit, kDynamicsProcessorParam_AttackTime,

kAudioUnitScope_Global, 0, 0.001, 0);

AudioUnitSetParameter(compressorUnit, kDynamicsProcessorParam_ReleaseTime,

kAudioUnitScope_Global, 0, 0.5, 0);

AudioUnitSetParameter(compressorUnit, kDynamicsProcessorParam_MasterGain,

kAudioUnitScope_Global, 0, 1.83, 0);

这里的参数比较简单,和之前介绍的压缩器参数是一致的,主要是有门限值、压缩比、作用时间和释放时间等。

最后将这个效果器连接到数据源AudioUnit的后面,然后可以试听效果,如果满意最终可以试着生成一个目标音频文件。

3. 混响器的实现

下面使用AudioUnit来实现混响器,代码如下:

CAComponentDescription reverb_desc(kAudioUnitType_Effect,

kAudioUnitSubType_Reverb2, kAudioUnitManufacturer_Apple);

这里利用这个描述构造出AUNode,并且取出对应的AudioUnit,代码如下:

AUNode reverbNode;

AudioUnit reverbUnit;

AUGraphAddNode(mGraph, &reverb_desc, &reverbNode);

AUGraphNodeInfo(mGraph, reverbNode, NULL, &reverbUnit);

然后就是设置参数了,代码如下:

AudioUnitSetParameter(reverbUnit, kReverb2Param_DryWetMix,

kAudioUnitScope_Global, 0, 15.65, 0);

AudioUnitSetParameter(reverbUnit, kReverb2Param_Gain,

kAudioUnitScope_Global, 0, 9.3, 0);

AudioUnitSetParameter(reverbUnit, kReverb2Param_MinDelayTime,

kAudioUnitScope_Global, 0, 0.02, 0);

AudioUnitSetParameter(reverbUnit, kReverb2Param_MaxDelayTime,

kAudioUnitScope_Global, 0, 0.25, 0);

AudioUnitSetParameter(reverbUnit, kReverb2Param_DecayTimeAt0Hz,

kAudioUnitScope_Global, 0, 1.945, 0);

AudioUnitSetParameter(reverbUnit, kReverb2Param_DecayTimeAtNyquist,

kAudioUnitScope_Global, 0, 10, 0);

AudioUnitSetParameter(reverbUnit, kReverb2Param_RandomizeReflections,

kAudioUnitScope_Global, 0, 1, 0);

这里的参数和前面大部分的混响设置也差不多,大家可以自己调试各项参数,分别来设置参数试听效果。设置好参数之后,可以将这个混响器连接到数据源之后,并试听效果,如果满意最终可以试着生成一个目标音频文件。

至此,本章的内容就全部结束了,本章内容比较多,从数字音频的表示形式,到基本乐理知识,以及到最后各种效果器的介绍以及实践,读者可以依据自己工作中的需求逐一去学习和应用。

8.6 本章小结

本章从声音的时域、频域表示开始进行讲解,并且讲解了FFT的物理意义,掌握这一些基本的表示对于数字音频的理解是很有帮助的;然后讲解到了一些基本的乐理知识,掌握这一些乐理知识之后相信读者对于声音的理解可以达到一个更高层次的理解;最后介绍了混音效果器,在8.4和8.5小结从各个效果器的原理以及实现进行了分析,并且在各自平台的优化策略也做出了总结。本章内容比较多,读者可以慢慢阅读,深入理解。

音频效果器的介绍与实践相关推荐

  1. 开发音频频谱_ToneBoosters音频效果器插件合集

    ToneBoosters Plugin Bundle for mac是一款Mac平台的ToneBoosters音频效果器插件包,包含超过十年在数字信号处理和听觉感知等领域的科研和产品开发.结合先进的信 ...

  2. 车牌识别算法介绍与实践(转)

    源: 车牌识别算法介绍与实践 转载于:https://www.cnblogs.com/LittleTiger/p/10101820.html

  3. RabbitMQ系列(三)RabbitMQ交换器Exchange介绍与实践

    RabbitMQ交换器Exchange介绍与实践 RabbitMQ系列文章 RabbitMQ在Ubuntu上的环境搭建 深入了解RabbitMQ工作原理及简单使用 RabbitMQ交换器Exchang ...

  4. Python内置四大数据结构之字典的介绍及实践案例

    Python字典的介绍及实践案例 一.字典(Dict)介绍 字典是Python内置的四大数据结构之一,是一种可变的容器模型,该容器中存放的对象是一系列以(key:value)构成的键值对.其中键值对的 ...

  5. 数据分析与挖掘中常用Python库的介绍与实践案例

    数据分析与挖掘中常用Python库的介绍与实践案例 一.Python介绍 现在python一词对我们来说并不陌生,尤其是在学术圈,它的影响力远超其它任何一种编程语言, 作为一门简单易学且功能强大的编程 ...

  6. Gensim介绍以及实践

    目录 前言 一.相关概念介绍以及拓展 1-0.安装 1-1.文档 1-2.语料库.数据预处理.词典的定义 1-3.向量 1-4.模型(以TF-idf为例)的初始化以及保存 二.文档相似度的计算 三.常 ...

  7. 基于Python的岭回归与LASSO回归模型介绍及实践

    基于Python的岭回归与LASSO回归模型介绍及实践 这是一篇学习的总结笔记 参考自<从零开始学数据分析与挖掘> [中]刘顺祥 著 完整代码及实践所用数据集等资料放置于:Github 岭 ...

  8. 基于Python的线性回归预测模型介绍及实践

    基于Python的线性回归预测模型介绍及实践 这是一篇学习的总结笔记 参考自<从零开始学数据分析与挖掘> [中]刘顺祥 著 完整代码及实践所用数据集等资料放置于:Github 线性回归预测 ...

  9. 关于Axure RP软件的介绍——软件工程实践第二次个人作业

    关于Axure RP软件的介绍--软件工程实践第二次个人作业 Axure RP是一个非常专业的快速原型设计的一个工具,客户提出需求,然后根据需求定义和规格.设计功能和界面的专家能够快速创建应用软件或W ...

  10. 02 Cadence Tensilica Xtensa HiFi 音频解码器相关介绍

    <Cadence Tensilica Xtensa HiFi 音频解码器相关介绍> 作者 将狼才鲸 日期 2022-01-13 Cadence HiFi名词解释:Cadence Tensi ...

最新文章

  1. 【Android 逆向】Android 逆向通用工具开发 ( Windows 平台静态库程序类型 | 编译逆向工具依赖的 Windows 平台静态库程序 )
  2. Android系统服务
  3. 要多大内存才满足_佛龛的尺寸要多大?
  4. 华为云大数据存储的冗余方式是三副本_华为TaurusDB技术解读(转载)
  5. 保障实时音视频服务体验,华为云原生媒体网络有7大秘籍
  6. mysql cnf参数_系统运维|MySQL my.cnf参数配置优化详解
  7. 将职业教育职业化 - 各IT培训中心必须完成的使命
  8. js实现二级联动菜单
  9. 线性代数及其应用第一章总结
  10. 你好2019,我是全新的CPDA数据分析师课程
  11. S3C2440 移植RTL8187L无线USB网卡记录(已经解决)
  12. 圣安地列斯服务器没有响应,Windows10系统玩不了侠盗猎车圣安地列斯怎么办?解决方案...
  13. 软件项目工作量评估方法简述之功能点方法(FPA)
  14. 云脉文档管理系统高效管理海量纸质文档
  15. 写给还在迷茫中的朋友,一名6年程序员的工作感悟!!!
  16. 蓝牙HC05模块探究-设置AT指令
  17. PWM开发SG90舵机
  18. 港科夜闻|香港科大再获[商科]评审全港第一!
  19. 串行、并行都是什么?为什么串行可以高速?
  20. ATI/NVIDIA显卡功耗表

热门文章

  1. Linux缺少rz和sz命令
  2. CASIA WebFace | face recognition data | 人脸识别数据集 | 云盘分享 |
  3. 删除远程桌面登录的记录(mstsc)
  4. 地图文件.osm格式与.pbf格式相互转换
  5. 南京邮电大学c语言实验报告4,南京邮电大学软件设计实验报告..doc
  6. 南邮物联网学院计算机考研,研友分享南京邮电大学物联网学院两个专业的一点看法...
  7. 初级算法代码-位移密码
  8. 基于单片机的电机转速PID控制
  9. 【RBF预测】基于RBF神经网络预测模型matlab源码
  10. python计算条件概率_统计算法_概率基础