浅聊OpenSL ES音频开发

浅聊OpenSL ES音频开发

导语

开发Android上的音频应用,一般是使用Android提供的AudioRecord采集音频,使用AudioTrack播放音频,使用MediaCodec来编解码,但这些API均是Android提供的JAVA层API,无论是采集、播放还是编解码,都需要将音频数据从java层拷贝到native层,或从native层拷贝到java层,影响性能。为了开发更加高效的Android音频应用,则建议使用Android NDK提供的OpenSL ES API接口,它支持在native层直接处理音频数据。

当前腾讯视频自研播放器的音频输出逻辑,都是将解码后的音频数据都抛到Java层采用AudioTrack进行渲染输出。为了提高效率减少JNI拷贝,合理调整播放引擎与上层逻辑的架构,我们做了音频渲染模块的重构,将音频渲染模块下沉到播放引擎层,采用OpenSL在native层进行音频输出,并将现有的java层AudioTrack作为备份逻辑。

一、OpenSL ES API接口及使用流程

OpenSL ES是一种针对嵌入式系统特别优化过的硬件音频加速API,无授权费并且可以跨平台使用,它提供的高性能、标准化、低延迟的特性实现为嵌入式媒体开发提供了标准。关于OpenSL ES的介绍可以参考官方文档《OpenSL_ES_Specification_1.0.1.pdf》,这里不再赘述。在OpenSL ES中,一切API的访问和控制都是通过接口完成的,以下是OpenSL ES音频播放场景。


浅聊OpenSL ES音频开发_第1张图片

从上图可以看出,一般播放实现思路是:

1.创建并初始化Audio Engine(音频引擎,是和底层交互的入口)

2.打开OutputMix(音频输出),配置相关参数DataSource和DataSink,创建播放器AudioPlayer和缓冲队列BufferQueue

3.设置输出的callback回调,实现输出渲染逻辑

OpenSL使用回调机制来访问音频IO,但它的回调里并不会把音频数据作为参数传递,回调方法仅仅是告诉我们:BufferQueue已经就绪,可以接受/获取数据了。我们可以在回调函数中直接调用Enqueue方法往音频设备中放入数据,也可以仅通知程序进行渲染。

二、音频播放逻辑实现

因这里音频播放的数据来源于解码器的输出,为了不影响解码线程,这里采用异步渲染的方式。主要流程如下图所示,主要包括几大模块:初始化、数据缓存、音频数据渲染、播放消息处理四大模块。


浅聊OpenSL ES音频开发_第2张图片

初始化

初始化主要包括OpenSL ES引擎初始化、播放器AudioPlayer初始化、渲染线程创建等。OpenSL ES初始化没有什么特别,具体可参考《Android音频开发之OpenSL ES》。需要值得注意的是,这里需要根据解码后的音频数据来设置采样率、声道、采样格式、缓冲队列size大小等,然后根据这些参数来初始化播放器。因此是在渲染线程真正要渲染时才进行播放器初始化的。

数据缓存

因采用异步渲染,这里设置了10个size的缓冲队列,解码后的音频数据输出时调用onAudioData,之后调用inputQueue,将音频数据拷贝到缓冲队列中,同时发送信号量通知渲染线程进行渲染。而在渲染线程中,调用outputQueue来获取一帧数据进行渲染。这里有两点比较重要:

1.缓冲队列数据淘汰策略

当解码线程输出数据比较快而渲染线程播放比较慢时,有可能会出现缓冲队列满的情况,此时如何处理比较关键,一般有三种方案:

1)丢弃当前帧数据DISCARD_CURRENT_ONE

2)丢弃最早的未播放数据DISCARD_LAST_ONE

3)等待buffer空闲WAIT_BUFFER_FREE

前两种方案,因为丢帧可能会出现轻微杂音现象,且第一种方案因什么也不做直接快速返回,有可能导致音视频不同步问题。所以这里选择了WAIT_BUFFER_FREE方案。如果缓冲队列未满,则直接拷贝数据到缓冲buffer中,若缓冲队列已满,则进行等待,待一帧数据已调用了Enqueue方法放到SL的BufferQueue中,才发送缓冲队列空闲信号量停止等待。

2.双缓冲队列设置

一般情况下,只设置一个缓冲队列,从缓冲队列中获取一帧数据,调用Enqueue方法将数据送入SL的BufferQueue后即可释放该buffer来接收新数据。但经过测试发现,该方法并未真正copy数据到SL的BufferQueue中,而是直接保存了buffer地址,待播放时直接播放buffer中数据。即调用了Enqueue方法后不能立即标记该buffer为Free来接收新数据,因为可能这一帧数据还未播放。因此就出现了一个问题,有可能已经从缓冲队列中拿出的buffer调用了Enqueue之后,在还未播放时就被新一帧数据覆盖了,这样就会导致出现杂音。如下图所示,第三个buffer还未播放就被新数据覆盖了:


浅聊OpenSL ES音频开发_第3张图片

为了解决该问题,这里采用了双缓冲队列,一个是输入数据的bufferQueue,用于存放onAudioData接收的数据;另一个是播放的bufferQueue,用于存放调用Enqueue进行播放的数据,即作为SL的BufferQueue。然后在渲染线程中判断SL的BufferQueue是否已经满了,如果已经满了,则等待直到callback回调一帧数据已播放完成。从而保证,未播放的数据buffer不会被新数据覆盖。


浅聊OpenSL ES音频开发_第4张图片

音频数据渲染

只要缓冲队列中有数据就调用OnDealRender进行渲染,渲染时先初始化AudioPlayer(已经初始化且采样率、声道等参数没有变化时不需要重新初始化),初始化失败后需要退出线程,设置init为false,由播放器切换为其他的渲染模式。真正渲染时,先判断SL Buffer是否足够,如果不足,即buffer满了,则需要等待,直到播放完一帧数据后有callback回调,或者有其他事件打断。如果足够,则调用Enqueue方法送数据。这里特殊一点的是,等待之后需要根据状态判断一下是否需要改变播放状态,或者停止渲染,因为等待过程中可能会有播放状态改变。

SLresult result = SL_RESULT_SUCCESS;

SLAndroidSimpleBufferQueueState slState = {0};

bool isSuccess = initAudioPlayer(info->sampleRate, m_sampleFormat, info->channelLayout, info->size);

if (isSuccess && m_bqPlayerBufferQueue != NULL ){

//先判断buffer是否足够,足够的话enqueue数据,buffer不足时,等待callback回调,直到buffer足够为止

result = (*m_bqPlayerBufferQueue)->GetState(m_bqPlayerBufferQueue, &slState);

while (slState.count >= SL_BUFFER_QUEUE_SIZE ) {

sem_wait(m_pSemSLState);//sl buffer不足,需要等待,直到播放完一帧数据后释放buffer,buffer free;或者有暂停、flush及stop事件来为止

if (!m_isPauseOn && m_curPlayState == SL_PLAYSTATE_PAUSED) { //这里要把playing状态设置回来,因为有可能这里卡住(sl buffer满了,又因为之前处于暂停状态,导致没有播放完的buffer空闲出来)

(*m_bqPlayerPlay)->SetPlayState(m_bqPlayerPlay, SL_PLAYSTATE_PLAYING);

m_curPlayState = SL_PLAYSTATE_PLAYING;

}

result = (*m_bqPlayerBufferQueue)->GetState(m_bqPlayerBufferQueue, &slState);

}

if (m_isStopPlay || m_isNeedFlush) { //如果是因为flush及stop事件,则及时返回,不再enqueue

return;

}

result = (*m_bqPlayerBufferQueue)->Enqueue(m_bqPlayerBufferQueue, info->audioData, (info->size)*sizeof(BYTE));

sem_post(m_bufFreeLock); //标记buffer空闲

} else {

//初始化失败,先退出渲染线程,然后设置init为false,由播放器改为其他渲染模式,而不是一直重试

m_pInitOk = false;

m_isRenderThreadRunning = false;

}

OpenSL中播放完一帧数据就会回调AudioPlayer注册的回调函数,在这里发送信号量停止渲染过程中的等待:

//音频输出回调

void SLAudioRender::outputCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) {

SLAudioRender* render = (SLAudioRender*)pContext;

if (render != NULL) {

sem_post(render->m_pSemSLState);

}

}

播放消息处理

因是视频播放中的音频渲染场景,所以涉及到播放操作的处理,即seek、pause、resume、stop、静音和调节音量等操作,这些操作都会影响到音频渲染。为了简化渲染前的判断逻辑,这里采用消息处理机制,维护一个消息队列,当调用这些操作接口时,直接发送一个消息到消息队列中即可。然后在渲染线程中,每一次循环开始时,先判断消息队列中是否有消息,有消息时优先处理状态消息,无消息时则处理一帧数据。

以seek为例,seek时调用flushRender,发送AudioRender_MSG_FLUSH消息:

m_isNeedFlush = true;

MsgInfo* info = new MsgInfo();

info->msgID = AudioRender_MSG_FLUSH;

info->param = m_isNeedFlush;

addMsgToRenderThread(info);

然后渲染线程处理如下:

while (pAudioRender->m_isRenderThreadRunning) {

sem_wait(m_pSemRenderThread);//等待信号量

if(!pAudioRender->m_isRenderThreadRunning)

{

return NULL;

}

//先判断m_msgList中是否有消息,优先处理m_msgList中的状态消息,无消息时然后再处理render data

if(pAudioRender->m_msgList.empty())

{

AudioRenderInfo* info = pAudioRender->outputQueue();//获取一帧数据

if (info != NULL) {

pAudioRender->OnDealRender(info);//真正进行音频渲染

}

} else { //处理m_msgList中的消息

MsgInfo* msgInfo = pAudioRender->popMsgToRenderThread();

switch (msgInfo->msgID) {

DEAL_MSG_WITH_FUNC(AudioRender_MSG_STOP, pAudioRender->DealMsg_Stop);

DEAL_MSG_WITH_FUNC(AudioRender_MSG_FLUSH, pAudioRender->DealMsg_Flush);

DEAL_MSG_WITH_FUNC(AudioRender_MSG_PAUSE, pAudioRender->DealMsg_Pause);

DEAL_MSG_WITH_FUNC(AudioRender_MSG_RESUME, pAudioRender->DealMsg_Resume);

DEAL_MSG_WITH_FUNC(AudioRender_MSG_SET_MUTE, pAudioRender->DealMsg_SetMute);

DEAL_MSG_WITH_FUNC(AudioRender_MSG_SET_VOLUME, pAudioRender->DealMsg_SetVolume);

default:

break;

}

}

}

真正的处理逻辑在DealMsg_Flush方法中:

if (m_bqPlayerBufferQueue != NULL) {

(*m_bqPlayerBufferQueue)->Clear(m_bqPlayerBufferQueue);

}

pRender->clearBufferQueue();

m_isNeedFlush = false;

三、总结

本文主要分享使用OpenSL ES进行视频播放过程中的音频渲染,包括数据缓存的处理与相关策略,播放操作的处理、音频渲染策略等。这中间也踩了很多坑,比如buffer满了之后如何处理比较好,什么时机调用Enqueue(之前是每次渲染前都等待callback回调后才调用Enqueue,经测试发现,某些手机会因为数据不连贯而出现轻微杂音),seek等播放操作来了之后,放在哪里进行处理比较合适,不会影响到数据渲染等。

经过测试优化,最终采用了本文的逻辑,但后续还可以继续优化逻辑,提供更多的功能,将音频后处理放到渲染逻辑里来。

你可能感兴趣的:(浅聊OpenSL ES音频开发)