android平台OpenSL ES播放PCM数据

目录

  1. OpenSL ES是什么?
  2. 主要功能
  3. Android 平台的OpenSL ES
  4. 使用OpenSL ES 的优点
  5. API简要介绍
  6. 示例
  7. 参考

1. OpenSL ES是什么?

OpenSL ES(Open Sound Library for Embedded Systems,开源的嵌入式声音库)是一个免授权费、跨平台、C语言编写的适用于嵌入式系统的硬件加速音频库。它为移动和游戏行业的开发者提供标准化、高性能、低延迟的方法来实现音频功能,并致力于跨多个平台轻松移植应用程序。OpenSL ES由非营利性技术联盟Khronos Group管理。

OpenSL ES的设计目标是让应用程序开发人员能够访问高级音频功能,如3D定位音频和MIDI播放,同时努力在制造商和平台之间轻松实现应用程序移植。

简要来说 OpenSL ES 是一套针对嵌入式平台的音频功能API标准。

2. 主要功能

OpenSL ES主要功能包括:

  • 基本音频播放和录制。
  • 3D音频效果,包括3D定位音频。
  • 音乐体验增强效果,包括低音增强和环境混响。
  • 缓冲队列。

3. Android 平台的OpenSL ES

Android 2.3将OpenSL ES 1.0.1作为其NDK的一部分。在之后的版本中,实现的延迟有所改进。

Android 实现的 OpenSL ES 只是 OpenSL ES 1.0.1 的子集,并且进行了扩展。因此,对于 OpenSL ES API 的使用,我们需要特别留意哪些是 Android 支持的,哪些是不支持的。


image

不支持的功能:

  • 不支持 MIDI。
  • 不支持直接播放 DRM 或者 加密的内容。
  • 不支持音频数据的编解码,如需编解码,需要使用 MediaCodec API 或者第三方库。
  • 在音频延时方面,相比于JAVA的 API,并没有特别明显地改进。

4. 使用OpenSL ES 的优点

  • 相比于 Java API,避免音频数据频繁在 native 层和 Java 层拷贝,提高效率。
  • 相比于 Java API,可以更灵活地控制参数。
  • 使用 C 代码,可以做深度优化,比如采用 NEON 优化。

5. API简要介绍

OpenSL ES 虽然是 C 语言编写,但是它的接口采用的是面向对象的方式,并不是提供一系列的函数接口,而是以 Interface 的方式来提供 API,这是理解 OpenSL ES API 的一个比较重要的点。

它的大都数 API 需要这样访问:

//下面代码是对 Audio Engine 对象进行 “初始化”
SLEngineItf engineObject;
SLresult result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);

如果在 Android NDK 下开发过 C 代码,就应该不会太陌生,因为我们调用 “JNI* env” 的函数也是这个样子去调用的。

5.1 Object 和 Interface

OpenSL ES 有两个重要的概念 Object 和 Interface,“对象”和“接口”。
(1) 每个 Object 可能会存在一个或者多个 Interface,官方为每一种 Object 都定义了一系列的 Interface。
(2) 每个 Object 对象都提供了一些最基础的操作,比如:Realize,Resume,GetState,Destroy 等等,如果希望使用该对象支持的功能函数,则必须通过其 GetInterface 函数拿到 Interface 接口,然后通过 Interface 来访问功能函数。
(3) 并不是每个系统上都实现了 OpenSL ES 为 Object 定义的所有 Interface,所以在获取 Interface 的时候需要做一些选择和判断。

5.2 OpenSL ES的状态机制

OpenSL ES 有一个重要的概念:状态机制。如图所示:


OPENSL_ES_Object_state.PNG

任何一个 OpenSL ES 的对象,创建成功后,都进入 SL_OBJECT_STATE_UNREALIZED 状态,这种状态下,系统不会为它分配任何资源。

Realize 后的对象,就会进入 SL_OBJECT_STATE_REALIZED 状态,这是一种“可用”的状态,只有在这种状态下,对象的各个功能和资源才能正常地访问。

当一些系统事件发生后,比如出现错误或者 Audio 设备被其他应用抢占,OpenSL ES 对象会进入 SL_OBJECT_STATE_SUSPENDED 状态,如果希望恢复正常使用,需要调用 Resume 函数。

当调用对象的 Destroy 函数后,则会释放资源,并回到 SL_OBJECT_STATE_UNREALIZED 状态。

Engine对象是OpenSL ES API的入口点,这个对象使你能够创建OpenSL ES中所有其他对象。

Engine对象由全局的对象slCreateEngine()创建得到,创建的结果是Engine对象的一个SLObjectItf的接口。

5.3 Engine Object 和 SLEngineItf Interface

Engine Object是OpenSL ES 里面最核心的对象,
它主要提供如下两个功能:
(1) 管理 Audio Engine 的生命周期。
(2) 提供管理接口: SLEngineItf,该接口可以用来创建所有其他的 Object 对象。
(3) 提供设备属性查询接口:SLEngineCapabilitiesItf 和 SLAudioIODeviceCapabilitiesItf,这些接口可以查询设备的一些属性信息。

Engine Object 对象的创建和销毁的方法如下:

//创建
SLObjectItf engineObject;
slCreateEngine( &engineObject, 0, nullptr, 0, nullptr, nullptr );
//初始化
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
//销毁
(*engineObject)->Destroy(engineObject);

slCreateEngine的函数定义如下:

SLresult SLAPIENTRY slCreateEngine(
SLObjectItf *pEngine,
SLuint32 numOptions
constSLEngineOption *pEngineOptions,
SLuint32 numInterfaces,
constSLInterfaceID *pInterfaceIds,
constSLboolean *pInterfaceRequired
)

参数说明如下:
pEngine:指向输出的engine对象的指针。
numOptions:可选配置数组的大小。
pEngineOptions:可选配置数组。
numInterfaces:对象要求支持的接口数目,不包含隐含的接口。
pInterfaceId:对象需要支持的接口id的数组。
pInterfaceRequired:指定每个要求接口的接口是可选或者必须的标志位数组。如果要求的接口没有实现,创建对象会失败并返回错误码
SL_RESULT_FEATURE_UNSUPPORTED。

获取管理接口:

SLEngineItf engineEngine;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &(engineEngine));

下面就可以使用 engineEngine 来创建所有 OpenSL ES 的其他对象了。

5.4 Media Object

Media Object代表着多媒体处理功能的抽象,如呈现和捕获媒体流的对象player、recorder 等等。

可以通过 SLEngineItf 提供的 CreateAudioPlayer 方法来创建一个 player 对象实例,可以通过 SLEngineItf 提供的 CreateAudioRecorder 方法来创建一个 recorder 实例。

5.5 Data Source 和 Data Sink

数据源(Data source)是媒体对象的输入参数,指定媒体对象将从何处接收特定类型的数据(例如采样的音频或MIDI数据)。 数据接收器(Data sink)是媒体对象的输入参数,指定媒体对象将发送特定类型数据的位置。

OpenSL ES 里面,这两个结构体均是作为创建 Media Object 对象时的参数而存在的,Data source 代表着输入源的信息,即数据从哪儿来、输入的数据参数是怎样的;而 Data Sink 代表着输出的信息,即数据输出到哪儿、以什么样的参数来输出。

Data Source 的定义如下:

typedef struct SLDataSource_ {
      void *pLocator;
      void *pFormat;
} SLDataSource;

Data Sink 的定义如下:

typedef struct SLDataSink_ {
    void *pLocator;
    void *pFormat;
} SLDataSink;

其中,pLocator 主要有如下几种:

SLDataLocator_Address
SLDataLocator_BufferQueue
SLDataLocator_IODevice
SLDataLocator_MIDIBufferQueue
SLDataLocator_URI

也就是说,Media Object 对象的输入源/输出源,既可以是 URL,也可以 Device,或者来自于缓冲区队列等等,完全是由 Media Object 对象的具体类型和应用场景来配置。

数据格式(data format)标识数据流的特征,包括以下几种类型:

  • 基于MIME类型的格式
  • PCM格式

5.6 Metadata Extractor Object

播放器对象支持读取媒体内容的元数据。但是有时候只是读取元数据而不播放媒体内容是很有用处的。
Metadata Extractor Object可以用于读取元数据而不需要分配用于媒体播放的资源。

5.7 示例说明

(1) 音频播放场景:


OPENSL_ES_playback_audio.PNG

使用了Audio Player对象来实现播放音频功能。使用engine对象的 SLEngineItf接口来创建Audio Player,创建之后与Output mix相关联用于音频输出。输入以URI作为示例,Output Mix默认与系统相关的默认输出设备关联。

(2) 录制音频场景:


OPENSL_ES_audio_recorder.PNG

通过Audio Recorder对象来实现音频录制功能。

6. 示例

OpenSL ES播放PCM数据主要有如下7个步骤:
1.创建EngineObject
2.设置DataSource
3.设置DataSink
4.创建播放器
5.设置缓冲队列和回调函数
6.设置播放状态
7.启动回调函数

6.1 创建接口对象

SLresult OpenGLESPlayer::createEngine() {
    LOGD("createEngine()"); 
    SLresult result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    if(result != SL_RESULT_SUCCESS) {
        LOGD("slCreateEngine failed, result=%d", result);
        return result;
    }
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    if(result != SL_RESULT_SUCCESS) {
        LOGD("engineObject Realize failed, result=%d", result);
        return result;
    }
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &(engineEngine));
    if(result != SL_RESULT_SUCCESS) {
        LOGD("engineObject GetInterface failed, result=%d", result);
        return result;
    }
    return result;
}

6.2 设置混音器

    // set DataSource
    SLDataLocator_AndroidSimpleBufferQueue android_queue={SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
    SLDataFormat_PCM sLDataFormat_pcm={
            SL_DATAFORMAT_PCM,
            2,
            SL_SAMPLINGRATE_44_1,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_PCMSAMPLEFORMAT_FIXED_16,
            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立体声(前左前右)
            SL_BYTEORDER_LITTLEENDIAN
    };
    SLDataSource slDataSource = {&android_queue, &sLDataFormat_pcm};

6.3 设置DataSink

    //set DataSink
    const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
    const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
    ret = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, mids, mreq);
    if(ret != SL_RESULT_SUCCESS) {
        LOGD("CreateOutputMix failed, ret=%d", ret);
        return ret;
    }
    ret = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    if(ret != SL_RESULT_SUCCESS) {
        LOGD("Realize failed, result=%d", ret);
        return ret;
    }
    SLDataLocator_OutputMix outputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    SLDataSink audioSnk = {&outputMix, NULL};

6.4 创建播放器

    const SLInterfaceID ids[1] = {SL_IID_BUFFERQUEUE};
    const SLboolean req[1] = {SL_BOOLEAN_TRUE};
    ret = (*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject, &slDataSource, &audioSnk, 1, ids, req);
    if (ret != SL_RESULT_SUCCESS) {
        LOGD("CreateAudioPlayer() failed.");
        return ret;
    }
    ret = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);
    if (ret != SL_RESULT_SUCCESS) {
        LOGD("playerObject Realize() failed.");
        return ret;
    }
    ret = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);
    if (ret != SL_RESULT_SUCCESS) {
        LOGD("playerObject GetInterface(SL_IID_PLAY) failed.");
        return ret;
    }

6.5 设置缓冲队列和回调函数

    ret = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &simpleBufferQueue);
    if (ret != SL_RESULT_SUCCESS) {
        LOGD("playerObject GetInterface(SL_IID_BUFFERQUEUE) failed.");
        return ret;
    }

    ret = (*simpleBufferQueue)->RegisterCallback(simpleBufferQueue, pcmBufferCallBack, this);
    if (ret != SL_RESULT_SUCCESS) {
        LOGD("SLAndroidSimpleBufferQueueItf RegisterCallback() failed.");
        return ret;
    }
    return ret;
int64_t getPcmData(void **pcm, FILE *pcmFile, uint8_t *out_buffer) {
    while(!feof(pcmFile)) {
        size_t size = fread(out_buffer, 1, 44100 * 2 * 2, pcmFile);
        *pcm = out_buffer;
        return size;
    }
    return 0;
}

void pcmBufferCallBack(SLAndroidSimpleBufferQueueItf bf, void * context) {
    int32_t size = getPcmData(&buffer, pcmFile, out_buffer);
    LOGD("pcmBufferCallBack, size=%d", size);
    if (NULL != buffer && size > 0) {
        SLresult result = (*simpleBufferQueue)->Enqueue(simpleBufferQueue, buffer, size);
    }
}

6.6 设置播放状态

void OpenGLESPlayer::start() {
    (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
//    主动调用回调函数开始工作
    pcmBufferCallBack(simpleBufferQueue, this);
}

void OpenGLESPlayer::stop() {
    LOGD("stop");
    (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_STOPPED);
}

6.7 启动回调函数

void OpenGLESPlayer::start() {
    (*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_PLAYING);
//    主动调用回调函数开始工作
    pcmBufferCallBack(simpleBufferQueue, this);
}

完整示例:Github

7.参考阅读

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

推荐阅读更多精彩内容

  • Android音频系统详解 参考好文: Android 音频系统:从 AudioTrack 到 AudioFlin...
    爱雨520阅读 13,387评论 2 7
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_x阅读 15,967评论 3 119
  • 走吧,带着一路风尘 去寻找世间的纯真 抛却心头的担忧 一如既往的向远方奔走 走吧,带着昨天的云 今早的天空怎么少了...
    万里西风烈阅读 2,860评论 16 69
  • 有一点犹豫着写下这个标题,这个标题很大。大到没法写。 每个人都有自己对生死的看法和感悟。曾经有一段时间老是想到死,...
    飞翔的皮卡丘阅读 209评论 0 0
  • 嗨,这是第多少次来到我梦里了呢。是2017年12月,还是喜欢你的第五个不被知道的年分加上想不起的三个月和两个明不清...
    9th_of_October阅读 303评论 0 0