音频采集:Android基于OpenSL ES的实现

前言

这篇文章简单介绍下移动端Android系统下利用OpenSL ES进行音频采集方法。
按照惯例先上一份源码 AudioRecordLib
OpenSL ES采集的核心实现在于 openslescore.cpp 这个文件。

权限申请

想要使用OpenSL ES,需要在AndroidManifest.xml的配置文件里面增加权限

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

OpenSL ES开发简介

什么是OpenSL ES

OpenSL ES全称为Open Sound Library for Embedded Systems,即嵌入式音频加速标准。OpenSL ES是无授权费、跨平台、针对嵌入式系统精心优化的硬件音频加速 API。它为嵌入式移动多媒体设备上的本地 应用程序开发者提供了标准化、高性能、低响应时间的音频功能实现方法,同时还实现了软/硬件音频性能的直接跨平台部署,不仅降低了执行难度,而且促进了高级音频市场的发展。

OpenSL ES架构原理

虽然OpenSL ES是基于C语言设计的API,但是其实基于对象和接口提供服务的,采用了面向对象的思想来开发API。
这里简单说一下OpenSL ES里面的对象和接口的概念:

  • 对象:类似于C++中类用来提供一组资源极其状态的抽象,也就是我们可以根据特定类型type(例如音频录制type)来获取一个音频录制的对象,但是对于这个对象我们并不能直接操作(换句话讲也就是我们不能直接在这个对象调用开始/结束录制的逻辑)。
  • 接口:接口是对象提供一组特定功能方法的抽象,也就是可以从对象中获取接口(例如从录制对象中获取录制接口),然后通过接口来改变对象的状态(例如通过接口设置开始录制)以便使用对象的功能(对于就是录制功能)。

PS:对象可以有一个或者多个接口的实例,但是接口实例肯定只属于一个对象,以上就是OpenSL ES的开发理念。

引用相关库文件以及头文件

怎么导入OpenSL ES库

CMake方式:CMakeList.txt中加入

#找打Android lib库里面的libOpenSLES.so的库
find_library( OpenSLES-lib
                OpenSLES )
#链接到你的native工程的库
target_link_libraries( your-native.so
                       ${OpenSLES-lib}
                     )

NDK Build方式:在Makefile文件Android.mk添加链接选项

LOCAL_LDLIBS = -lOpenSLES

引入头文件

#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>

创建引擎对象

简单介绍下入口slCreateEngine()这个全局方法:

SL_API SLresult SLAPIENTRY slCreateEngine(
    SLObjectItf             *pEngine,           //对象地址,用于传出对象
    SLuint32                numOptions,         //配置参数数量
    const SLEngineOption    *pEngineOptions,    //配置参数,为枚举数组
    SLuint32                numInterfaces,      //支持的接口数量
    const SLInterfaceID     *pInterfaceIds,     //具体的要支持的接口,是枚举的数组
    const SLboolean         *pInterfaceRequired //具体的要支持的接口是开放的还是关闭的,也是一个数组,这三个参数长度是一致的
);

一个较为完整的创建过程代码示例:

SLObjectItf engine_object;   //引擎对象
SLEngineItf engine_engine;   //引擎接口

//调用全局方法创建一个引擎对象(OpenSL ES唯一入口)
slCreateEngine(&engine_object, 0, NULL, 0, NULL, NULL);
//实例化这个对象
(*engine_object)->Realize(engine_object, SL_BOOLEAN_FALSE);
//从这个对象里面获取引擎接口
(*engine_object)->GetInterface(engine_object, SL_IID_ENGINE, &engine_engine);

当然调用每一个API后要检测其返回值是否等于 SL_RESULT_SUCCESS,限于篇幅就在上面代码没有处理,后续的示例代码也是同理。

设置IO设备(麦克风) 输入输出

我们需要设置采集设备的一些输入输出配置:

//设置IO设备(麦克风)
SLDataLocator_IODevice io_device = {
        SL_DATALOCATOR_IODEVICE,         //类型 这里只能是SL_DATALOCATOR_IODEVICE
        SL_IODEVICE_AUDIOINPUT,          //device类型  选择了音频输入类型
        SL_DEFAULTDEVICEID_AUDIOINPUT,   //deviceID 对应的是SL_DEFAULTDEVICEID_AUDIOINPUT
        NULL                             //device实例
};
SLDataSource data_src = {
        &io_device,                      //SLDataLocator_IODevice配置输入
        NULL                             //输入格式,采集的并不需要
};

//设置输出buffer队列
SLDataLocator_AndroidSimpleBufferQueue buffer_queue = {
        SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,    //类型 这里只能是SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE
        2                                           //buffer的数量
};
//设置输出数据的格式
SLDataFormat_PCM format_pcm = {
        SL_DATAFORMAT_PCM,                             //输出PCM格式的数据
        num_channels,                                  //输出的声道数量
        SL_SAMPLINGRATE_44_1,                          //输出的采样频率,这里是44100Hz
        SL_PCMSAMPLEFORMAT_FIXED_16,                   //输出的采样格式,这里是16bit
        SL_PCMSAMPLEFORMAT_FIXED_16,                   //一般来说,跟随上一个参数
        SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT,  //双声道配置,如果单声道可以用 SL_SPEAKER_FRONT_CENTER
        SL_BYTEORDER_LITTLEENDIAN                      //PCM数据的大小端排列
};
SLDataSink audioSink = {
        &buffer_queue,                   //SLDataFormat_PCM配置输出
        &format_pcm                      //输出数据格式
};

创建录制器

主要是创建录制对象和获取录制相关的接口:

SLObjectItf recorder_object;                         //录制对象,这个对象我们从里面获取了2个接口
SLRecordItf recorder_recoder;                        //录制接口
SLAndroidSimpleBufferQueueItf recorder_buffer_queue; //Buffer接口

//创建录制的对象,并且指定开放SL_IID_ANDROIDSIMPLEBUFFERQUEUE这个接口
const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
(*engine_engine)->CreateAudioRecorder(engine_engine,        //引擎接口
                                        &recorder_object,   //录制对象地址,用于传出对象
                                        &data_src,          //输入配置
                                        &audioSink,         //输出配置
                                        1,                  //支持的接口数量
                                        id,                 //具体的要支持的接口
                                        req                 //具体的要支持的接口是开放的还是关闭的
                                        );
//实例化这个录制对象
(*recorder_object)->Realize(recorder_object, SL_BOOLEAN_FALSE);
//获取录制接口
(*recorder_object)->GetInterface(recorder_object, SL_IID_RECORD, &recorder_recoder);
//获取Buffer接口
(*recorder_object)->GetInterface(recorder_object, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorder_buffer_queue);

设置数据回调并且开始录制

设置开始录制状态,并通过回调函数获取录制的音频PCM数据:

int8_t *pcm_data; //数据缓存区
//申请一块内存,注意RECORDER_FRAMES是自定义的一个宏,指的是采集的frame数量,具体还要根据你的采集格式(例如16bit)计算
pcm_data = static_cast<int8_t *>(malloc(sizeof(int8_t) * RECORDER_FRAMES));

//设置数据回调接口bqRecorderCallback,最后一个参数是可以传输自定义的上下文引用
(*recorder_buffer_queue)->RegisterCallback(recorder_buffer_queue, bqRecorderCallback, this);
//设置录制器为录制状态 SL_RECORDSTATE_RECORDING
(*recorder_recoder)->SetRecordState(recorder_recoder, SL_RECORDSTATE_RECORDING);
/在设置完录制状态后一定需要先Enqueue一次,这样的话才会开始采集回调
(*recorder_buffer_queue)->Enqueue(recorder_buffer_queue, pcm_data, RECORDER_FRAMES);

//数据回调函数
static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
    //从pcm_data获取RECORDER_FRAMES长度的PCM数据
    //注意这个是另外一条采集线程回调,可能需要加一个变量recodering控制退出采集
    if (recodering)
    {
        pcm_write(pcm_data, RECORDER_FRAMES);
        //取完数据,需要调用Enqueue触发下一次数据回调
        (*recorder_buffer_queue)->Enqueue(recorder_buffer_queue, pcm_data, RECORDER_FRAMES);
    }
}

停止录制和释放OpenSL ES资源

如果我们不需要采集了,需要调用接口停止采集并在适当的时机释放OpenSL ES相关资源。

//设置录制器为停止状态 SL_RECORDSTATE_STOPPED
(*recorder_recoder)->SetRecordState(recorder_recoder, SL_RECORDSTATE_STOPPED);

//只需要销毁OpenSL ES对象,接口不需要做Destroy处理。
(*recorder_object)->Destroy(recorder_object);
(*engine_object)->Destroy(engine_object);

//释放缓存区内存
free(pcm_data);

这样就完整的结束了OpenSL ES的采集业务。

播放PCM文件

Audacity这个工具可以导入pcm原始文件,并且提供了波形图查看和播放功能。
操作流程是:
文件 => 导入 => 原始数据 => 设置PCM数据格式 => 导入
具体效果图如下:

p1.png

结语

上一篇博客了介绍了Android利用AudioRecord进行录音导出PCM数据。
本文同步发布于简书CSDN

End!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容