Android的声音编程--使用OpenSL ES Audio

OpenSL ES? 是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速API。Android NDK 给出了使用OpenSL ES来实现native-audio的例子。本文介绍了如何使用OpenSL ESAndroid中进行声音编程。从OpenSL ES的编程编译链接设置到创建声音引擎、创建声音播放器、设置播放缓冲等具体步骤。

编译和链接设置

使用OpenSL ES的前提条件是在源代码中包含 OpenSL ES的头文件如下所示
#include <SLES/OpenSLES.h>
gcc中的链接选项link option中链接OpenSL ES的库文件
-lOpenSLES 
OpenSL ES音频编程步骤

具体的使用步骤如下:
1.
创建声音引擎
2.
创建声音播放器
3.
设置播放缓冲

OpenSL ES的接口

OpenSL ES 主要操作的是接口(interface),大体来说有如下几个接口:
SLObjectItf 
:对象接口, 
SLEngineItf 
:引擎接口
SLPlayItf
:播放接口
SLBufferQueueItf :
缓冲队列接口
SLEffectSendItf
:音效发送接口
SLMuteSoloItf : 
声道关闭 /单声道接口
SLVolumeItf : 
声量接口
初始化声音引擎
除了 slCreateEngine 这一条函数是用来创建声音引擎对象接口以外,其它的所有操作都是通过接口的成员函数完成的,现在研究如何初始化声音引擎

初始化声音引擎需要3个接口,要将它们声明为全局变量:
SLObjectItf _aud;/* 
声音引擎的对象接口 */
SLEngineItf _aud_eng;/* 
声音引擎 */
SLObjectItf _aud_mix;/* 
混音器对象接口 */

首先创建声音引擎的对象接口 
slCreateEngine(&_aud, 0, NULL, 0, NULL, NULL); 
创建之后的接口并不能立即使用,我们首先要通过接口的子函数实现(Realize)它:
(*_aud)->Realize(_aud, SL_BOOLEAN_FALSE);/* 
通过_audRealize子函数实现声音引擎的对象接口 */
实现之后,从声音引擎的对象中抓取声音引擎,在这里通过接口的子函数抓取接口GetInterface),抓取对像是 _aud,抓取类型是引擎,抓取后存放的内存位置是先前声明的引擎接口_aud_eng的指针位置。指令如下:
(*_aud)->GetInterface(_aud, SL_IID_ENGINE, &_aud_eng);
这样声音引擎就初始化了

第三步要做的是从声音引擎中创建 ”输出混音器“ 对象,这样才可以将声音播放出来。
注意,同声音引擎的对象一样, 由于 ”输出混音器“ 是对象,必须创建后在实现(Realize)它。
由于输出混音器创建的同时需要两个音效参数,所以先准备好这两个参数:第一个参数类型是声音接口编号(SLInterfaceID),是一个数组,可以将其看成一个音效配置列表,在其中放置不同的音效编号。第二个参数是一个逻辑数组:是否强制实现声音接口编号中对应音效,如果某个音效的逻辑为真(SL_BOOLEAN_TRUE),则在无法实现该音效时视为输出混音器创建失败,否则将忽略该音效。因为安卓的声硬件对音效的支持不同,所以最好不要强制实现,所以在第二个数组中全部填写SL_BOOLEAN_FALSE 

现在开始创建输出混音器,环境回响(SL_IID_ENVIRONMENTALREVERB )是比较常见的音效,将非强制性的使用该音效。

SLInterfaceID effect[1] = {SL_IID_ENVIRONMENTALREVERB}; /*
环境回响音效 */
SLboolean effect_bool[1] = {SL_BOOLEAN_FALSE}; /* 
回响音效强制实现逻辑 */
(*_aud_eng)->CreateOutputMix(_aud_eng, &_aud_mix, 1, effect, effect_bool);/* 
从声音引擎中创建输出混音器” */
(*_aud_mix)->Realize(_aud_mix, SL_BOOLEAN_FALSE); /* 
实现刚创建的输出混音器” */

这样一来,所有的初始化工作就全部完成了。完整代码如下:

SLObjectItf _aud;/* 
声音引擎对象 */
SLEngineItf _aud_eng;/* 
声音引擎 */
SLObjectItf _aud_mix;/* 
输出混音器对象 */

/* audio_init: 
初始化OpenSL ES */
int audio_init ()
{
SLInterfaceID effect[1] = {SL_IID_ENVIRONMENTALREVERB}; /* 
音效 */
SLboolean effect_bool[1] = {SL_BOOLEAN_FALSE}; /*
音效强制实现逻辑 */

slCreateEngine(&_aud, 0, NULL, 0, NULL, NULL);/* 
创建声音引擎对象 */
(*_aud)->Realize(_aud, SL_BOOLEAN_FALSE);/* 
实现声音引擎对象 */
(*_aud)->GetInterface(_aud, SL_IID_ENGINE, &_aud_eng);/* 
从声音引擎对象中抓取声音引擎 */
(*_aud_eng)->CreateOutputMix(_aud_eng, &_aud_mix, 1, effect, effect_bool);/* 
通过声音引擎创建输出混音器对象,并且非强制性的开启环境混响效果 */
(*_aud_mix)->Realize(_aud_mix, SL_BOOLEAN_FALSE);/* 
实现混音器对象 */

return;
}

创建播放器对象

初始化引擎之后,还要创建一个播放器对象,就可以在播放器中输出声音了
播放器对象和输出混音器一样,是对象,创建后需要进行实现:
SLObjectItf _aud_plyobj; /*
播放器对象 */
(*_aud_eng)->CreateAudioPlayer(_aud_eng, &_aud_plyobj, &sndsrc, &sndsnk, 3, ids, req);
/* 
这是创建声音播放器对象aud_plyobj的函数 */
前两个参数分别把 声音引擎  播放器引擎的指针位置填上就可以了
真正需要填写的是后面4个参数  
sndsrc: 
声音数据源
sndsnk:
声音输出池(data sink),也就是声音输出设备
3:
功能清单数目,我们要让播放器支持3种功能
ids: 
功能清单,要让播放器支持的功能
req:
功能强制实现逻辑
下面逐一来分析这5个参数

sndsrc是声源,类型是SLDataSource, 是一个有两个成员的结构,两个成员分别是:
pLocator: 
数据定位器(DataLocator)的指针位置
PFormat:
数据定位器指向数据的格式指针位置,在这里使用pcm 声音格式
那么首先看这两种结构: 
1:
数据定位器 就是定位要播放声音数据的存放位置,分为4种:内存位置,输入/输出设备位置,缓冲区队列位置,和midi缓冲区队列位置。由于我们可以灵活的对缓冲区队列进行安排并设置播放后回传函数,而且我们使用的是pcm声音格式,所以我们采取第三种数据定位器:
SLDataLocator_BufferQueue loc_bufq; /* 
缓冲区队列定位器 */
2:
数据格式,由于我们采用的是pcm声音格式,所以我们使用:
SLDataFormat_PCM format_pcm; /* pcm 
数据格式 */
接下来,我们要填写 ”缓冲区队列定位器“  “pcm数据格式“ 这两种结构的内容 

我们要在SLDataLocator_BufferQueue类型变量 loc_bufq中填上的是
SLDataLocator_BufferQueue loc_bufq = { /* 
缓冲区队列定位器 */ 
SL_DATALOCATOR_BUFFERQUEUE, /* 
必须填成左边这样,表示数据定位器将定位缓冲区队列 */ 
2 /* 
缓冲队列里的缓冲数目,这里我们填写2个缓冲 */ 
};
我们要在SLDataFormat_PCM类型变量 format_pcm中填上的是
SLDataFormat_PCM format_pcm = { /* pcm 
数据格式 */
SL_DATAFORMAT_PCM, /* 
必须填成左边这样,表示pcm 数据格式 */
1, /* 
这里是声道数目,多声道数目将交错存放,而不是整体存放,我们使用单声道 */
SL_SAMPLINGRATE_8, /*
采样率,我们使用 8kHz的采样率,质量不是很好 */
SL_PCMSAMPLEFORMAT_FIXED_16, /*
采样大小,我们使用固定的 16bit采样 */ 
SL_PCMSAMPLEFORMAT_FIXED_16,/*
容器大小,和采样大小保持一致就不会失真,我们采用 16bit容器*/ SL_SPEAKER_FRONT_CENTER, /* 声道映射掩码,在这里,我们将不用的声道映射到不同的扩音器中,在这里,由于我们的声音是单声道,所以我们将其映射到安卓设备的主扩音器也就是前中扩音器:SPEAKER_FRONT_CENTER */
SL_BYTEORDER_LITTLEENDIAN /* 
尾序:小端模式 */
};

 

这样,我们这完成了声源参数:
sndsrc
我们还要完成剩下的4个参数,也就是:
sndsnk:
声音输出池(data sink),也就是声音输出设备
3:
功能清单数目,我们要让播放器支持3种功能
ids: 
功能清单,我们要让播放器支持的功能
req:
功能强制实现逻辑 

从声音输出池开始,声音输出池和声源的成员差不多:
pLocator: 
数据定位器(DataLocator)的指针位置
PFormat:
数据定位器指向数据的格式指针位置

但声音输出池需要的是SLDataLocator_OutputMix 输出混音器数据定位器,而且不需要交代数据格式,所以只要填写输出混音器数据定位器就可以了:

SLDataLocator_OutputMix loc_outmix = { /* 
输出混音器数据定位器 */ 
SL_DATALOCATOR_OUTPUTMIX, /* 
必须填成左边这样,表示数据定位器将定位输出混音器 */ 
NULL /* 
不需要交代数据格式 */ 
};

 

最后只有播放器功能清单要完成了,我们要求播放器具备3种功能 ,它们的编号如下:
SL_IID_BUFFERQUEUE
:缓冲区队列功能(循环播放,随机播放…)
SL_IID_EFFECTSEND
:音效功能(环境回响,均衡器…)

SL_IID_VOLUME
:音量功能(音量调节,3d声源…)
这样来填写 
SLInterfaceID ids[] = { /* 
播放器功能列表 */
SL_IID_BUFFERQUEUE, /* 
缓冲区队列功能 */
SL_IID_EFFECTSEND,/* 
音效功能 */
SL_IID_VOLUME /* 
音量功能 */
};
而强制逻辑和之前提到的req完全一样,但这3种功能是常用功能,一般的安卓设备都应具备,所以我们强制要求实现,实现不了则视为错误:
SLboolean req[] = {
SL_BOOLEAN_TRUE, /* 
强制实现id中对应的 缓冲区队列功能 */
SL_BOOLEAN_TRUE, /* 
强制实现id中对应的 音效功能 */
SL_BOOLEAN_TRUE /* 
强制实现id中对应的 音量功能 */
};

整合以上步骤,我们终于得到了一个声音播放器对象 _aud_plyobj,最终指令如下:
(*_aud_eng)->CreateAudioPlayer(_aud_eng, &_aud_plyobj, &sndsrc, &sndsnk, 3, ids, req);
/* 
从声音引擎_aud_eng中创建 声音播放器对象_aud_plyobj
同其它对象一样,我们来实现声音播放器对象_aud_plyobj: 
(*_aud_plyobj)->Realize(_aud_plyobj, SL_BOOLEAN_FALSE); /*
实现声音播放器对象 */
这样一来,声音播放器对象就创建好了。
OpenSL ES
主要操作的是接口(interface),所以要从播放器中获得一些接口,这样在对接口进行操作时,就能控制声音播放器。在这里,我们从声音播放器中获得4种不同类型的接口,首先进行全局变量声明:
SLPlayItf _aud_ply; /* 
播放接口(播放,暂停 */
SLBufferQueueItf _aud_buf; /*
缓冲区接口 */
SLEffectSendItf _aud_bufefx; /*
音效接口,在本播放器中将用于缓冲区 */
SLVolumeItf _aud_vol; /*
音量接口 */
然后通过GetInterface成员函数从播放器对象中获得接口:
(*_aud_plyobj)->GetInterface(_aud_plyobj, SL_IID_PLAY, &_aud_ply); /*
获得播放接口, 存至_aud_ply*/
(*_aud_plyobj)->GetInterface(_aud_plyobj, SL_IID_BUFFERQUEUE, &_aud_buf); /*
获得缓冲区接口,存至 _aud_buf */
(*_aud_plyobj)->GetInterface(_aud_plyobj, SL_IID_EFFECTSEND, &_aud_bufefx); /*
获得音效接口,存至 _aud_bufefx */
(*_aud_plyobj)->GetInterface(_aud_plyobj, SL_IID_VOLUME, &_aud_vol); /*
获得音量接口,存至 _aud_vol */
从现在起,所有对上述4个接口的操作都将影响到播放器

为了真正实现缓冲区连续播放的功能,我们要为缓冲区接口_aud_buf注册一个回调函数:
(*_aud_buf)->RegisterCallback(_aud_buf, _newsnd_cb, NULL);
_newsnd_cb
函数将在缓冲区播放完毕时自动执行,接下来我们研究这个函数怎么实现

前面创建了引擎和播放器,如果我们命令播放器播放,会不会马上有声音呢?答案是否定的,因为我们根本没有提供数据

提供播放数据
其实自行设计_newsnd_cb回调函数将要实现的功能只有一个,就是为播放器提供将要播放的数据,这样播放器就会自动对其进行播放了
short *snd; /* 16 bit, 8kHz
单声道 pcm 声音采样 */
unsigned sndlen; /*
声音采样数据的大小*/
int num_snd; /* 
该采样的剩余播放的次数 */

/* _newsnd_cb: 
我们自己创建的缓冲区队列播放回调函数,第一项填写目标缓冲区接口,第二项填NULL即可 */
void _newsnd_cb(SLBufferQueueItf bq, void *context)
{
if (–num_snd > 0 && snd && sndlen > 0) /* 
如果采样的剩余播放的次数大于0,声音数据有效,而且 声音数据的长度大于0 */
(*bq)->Enqueue(bq, snd, sndlen); /* 
操作缓冲区队列接口,将长度为 sndlen的声音数据snd排进队列 */
}
这样,缓冲区队列没有声音可以播放时,将呼出上述回调函数,而该回调函数将检查剩余播放次数,并在有播放次数的情况下将 声音 排进队列,从而让缓冲区队列可以播放队列中的第一个声音,也就是刚排进去的声音。当然,由于这个函数是自定义的,灵活度很高,所以大家可以自由发挥,借助它实现随机播放,或是互联网 流媒体播放 

现在 我们完成了 OpenSL ES声音引擎和播放器创建,只需要操作_aud_ply 播放接口,即可启动播放器:
(*_aud_ply)->SetPlayState(_aud_ply, SL_PLAYSTATE_PLAYING); /* 
将播放状态设为正在播放” */
为了测试我们的播放器是否正常,我们尝试通过函数合成一个时长为一秒种的 正弦波采样,将其播放5:
#include “math.h”
short snd_sin[8000]; /* 
时长为1秒的 16bit 单声道 正弦波采样 */ 
void create_sine_wave(void)
{
int i;
for (i = 0; i < 8000; ++i) /* 
声音时长1秒,所以创建8000个单声道 采样 */
snd_sin[i] = sinf(i*180/M_PI)*32767; /* 
通过正弦函数创建单个采样值 */ 
}

这样就能通过输出声音是否为正弦波来判断播放器是否正常了

最后是将正弦波播放5次的 演示程序:
http://pan.baidu.com/s/1uw672

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章