7.2.3 AudioTrack(Native空间)分析

1.new AudioTrack和set分析

Native的AudioTrack代码在AudioTrack.cpp中。这一节将分析它的构造函数和set调用。


[—>AudioTrack.cpp]

AudioTrack:AudioTrack()//我们使用无参构造函数。

:mStatus(NO_INIT)

{

//把状态初始化成NO_INIT。Android的很多类都采用了这种状态控制。

}


再看看set调用,这个函数有很多内容。代码如下所示:


[—>AudioTrack.cpp]

/*

还记得我们传入的参数吗?

streamType=STREAM_MUSIC,sampleRate=8000,format=PCM_16

channels=2,frameCount由计算得来,可以假设一个值,例如1024,不影响分析。

flags=0,cbf=audiocallback,user为cbf的参数,notificationFrames=0

因为是流模式,所以sharedBuffer=0。threadCanCallJava为true。

*/

status_t AudioTrack:set(int streamType,uint32_t sampleRate,int format,

int channels,int frameCount,uint32_t flags,callback_t cbf,void*user,

int notificationFrames,const sp<IMemory>&sharedBuffer,

bool threadCanCallJava)

{

//前面有一些判断,都是和AudioSystem有关的,以后再分析。

……

/*

audio_io_handle_t是一个int类型,通过typedef定义,这个值的来历非常复杂,涉及AudioFlinger和AudioPolicyService,后面会试着将其解释清楚。

这个值主要被AudioFlinger使用,用来表示内部的工作线程索引号。AudioFlinger会根据情况创建几个工作线程,下面的AudioSystem:getOutput会根据流类型等其他参数最终选取一个合适的工作线程,并返回它在AF中的索引号。而AudioTrack一般使用混音线程(Mixer Thread)。

*/

audio_io_handle_t output=AudioSystem:getOutput(

(AudioSystem:stream_type)streamType,

sampleRate,format,channels,

(AudioSystem:output_flags)flags);

//调用creatTrack

status_t status=createTrack(streamType,sampleRate,format,channelCount,

frameCount,flags,sharedBuffer,output);

//cbf是JNI层传入的回调函数audioCallback,如果用户设置了回调函数,则启动一个线程。

if(cbf!=0){

mAudioTrackThread=new AudioTrackThread(*this,threadCanCallJava);

}

return NO_ERROR;

}


再来看createTrack函数,代码如下所示:


[—>AudioTrack.cpp]

status_t AudioTrack:createTrack(int streamType,uint32_t sampleRate,

int format,int channelCount,int frameCount,uint32_t flags,

const sp<IMemory>&sharedBuffer,audio_io_handle_t output)

{

status_t status;

/*

得到AudioFlinger的Binder代理端BpAudioFlinger。

关于这部分内容,我们已经很熟悉了,以后的讲解会跨过Binder,直接分析Bn端的实现。

*/

const sp<IAudioFlinger>&audioFlinger=AudioSystem:get_audio_flinger();

/*

向AudioFinger发送createTrack请求。注意其中的几个参数,在STREAM模式下sharedBuffer为空。

output为AudioSystem:getOutput得到一个值,代表AF中的线程索引号。

该函数返回IAudioTrack(实际类型是BpAudioTrack)对象,后续AF和AT的交互就是围绕IAudioTrack进行的。

*/

sp<IAudioTrack>track=audioFlinger->createTrack(getpid(),

streamType,sampleRate,format,channelCount,frameCount,

((uint16_t)flags)<<16,sharedBuffer,output,&status);

/*

在STREAM模式下,没有在AT端创建共享内存,但前面提到了AT和AF的数据交互是通过共享内存完成的,这块共享内存最终由AF的createTrack创建。我们以后分析AF时再做介绍。下面这个调用会取出AF创建的共享内存。

*/

sp<IMemory>cblk=track->getCblk();

mAudioTrack.clear();//sp的clear

mAudioTrack=track;

mCblkMemory.clear();

mCblkMemory=cblk;//cblk是control block的简写。

/*

IMemory的pointer在此处将返回共享内存的首地址,类型为void,static_cast直接把这个void类型转成audio_track_cblk_t,表明这块内存的首部中存在audio_track_cblk_t这个对象。

*/

mCblk=static_cast<audio_track_cblk_t*>(cblk->pointer());

mCblk->out=1;//out为1表示输出,out为0表示输入。

mFrameCount=mCblk->frameCount;

if(sharedBuffer==0){

//buffers指向数据空间,它的起始位置是共享内存的首部加上audio_track_cblk_t的大小。

mCblk->buffers=(char*)mCblk+sizeof(audio_track_cblk_t);

}else{

//STATIC模式下的处理

mCblk->buffers=sharedBuffer->pointer();

mCblk->stepUser(mFrameCount);//更新数据位置,后面要分析stepUser的作用。

}

return NO_ERROR;

}


(1)IAudioTrack和AT、AF的关系

上面的createTrack函数中突然冒出来一个新面孔,叫IAudioTrack。关于它和AT及AF的关系,我们用图7-3来表示:

7.2.3 AudioTrack(Native空间)分析 - 图1

图 7-3 IAudioTrack和AT、AF的关系

从图7-3中可以发现:

IAudioTrack是联系AT和AF的关键纽带。

至于IAudioTrack在AF端到底是什么,在分析AF时会有详细解释。

(2)共享内存及其Control Block

通过前面的代码分析,我们发现IAudioTrack中有一块共享内存,其头部是一个audio_track_cblk_t(简称CB)对象,在该对象之后才是数据缓冲。这个CB对象有什么作用呢?

还记得前面提到的那个深层次思考的问题吗?即MemoryHeapBase和MemoryBase都没有提供同步对象,那么,AT和AF作为典型的数据生产者和消费者,如何正确协调二者生产和消费的步调呢?

Android为顺应民意,便创造出了这个CB对象,其主要目的就是协调和管理AT和AF二者数据生产和消费的步伐。先来看CB都管理些什么内容。它的声明在AudioTrackShared.h中,而定义却在AudioTrack.cpp中。


[—>AudioTrackShared.h:audio_track_cblk_t声明]

struct audio_track_cblk_t

{

Mutex lock;

Condition cv;//这是两个同步变量,初始化的时候会设置为支持跨进程共享。

/*

一块数据缓冲同时被生产者和消费者使用,最重要的就是维护它的读写位置了。

下面定义的这些变量就和读写的位置有关,虽然它们的名字并不是那么直观。

另外,这里提一个扩展问题,读者可以思考一下:

volatile支持跨进程吗?要回答这个问题需要理解volatile、CPU Cache机制和共享内存的本质。*/

volatile uint32_t user;//当前写位置(即生产者已经写到什么位置了)

volatile uint32_t server;//当前读位置

/*

userBase与serverBase要和user及server结合起来用。

CB巧妙地通过上面几个变量把一块线性缓冲当作环形缓冲来使用,以后将单独分析这个问题。

*/

uint32_t userBase;//

uint32_t serverBase;

void*buffers;//指向数据缓冲的首地址。

uint32_t frameCount;//数据缓冲的总大小,以Frame为单位。

uint32_t loopStart;//设置打点播放(即设置播放的起点和终点)。

uint32_t loopEnd;

int loopCount;//循环播放的次数。

volatile union{

uint16_t volume[2];

uint32_t volumeLR;

};//和音量有关系,可以不管它。

uint32_t sampleRate;//采样率

uint32_t frameSize;//一单位Frame的数据大小。

uint8_t channels;//声道数

uint8_t flowControlFlag;//控制标志,见下文分析。

uint8_t out;//AudioTrack为1,AudioRecord为0。

uint8_t forceReady;

uint16_t bufferTimeoutMs;

uint16_t waitTimeMs;

//下面这几个函数很重要,后续会详细介绍它们。

uint32_t stepUser(uint32_t frameCount);//更新写位置。

bool stepServer(uint32_t frameCount);//更新读位置。

void*buffer(uint32_t offset)const;//返回可写空间的起始位置。

uint32_t framesAvailable();//还剩多少空间可写。

uint32_t framesAvailable_l();

uint32_t framesReady();//是否有可读数据。

}


关于CB对象,这里要专门讲解一下其中flowControlFlag的意思:

对于音频输出来说,flowControlFlag对应着underrun状态,underrun状态是指生产者提供数据的速度跟不上消费者使用数据的速度。这里的消费者指的是音频输出设备。由于音频输出设备采用环形缓冲方式管理,当生产者没有及时提供新数据时,输出设备就会循环使用缓冲中的数据,这样就会听到一段重复的声音。这种现象一般被称作“machinegun”。对于这种情况,一般的处理方法是暂停输出,等数据准备好后再恢复输出。

对于音频输入来说,flowControlFlag对应着overrun状态,它的意思和underrun一样,只是这里的生产者变成了音频输入设备,而消费者则变成了Audio系统的AudioRecord。

说明 目前这个参数并不直接和音频输入输出设备的状态有关系。它在AT和AF中的作用必须结合具体情况才能分析。

图7-4表示CB对象和它所驻留的共享内存间的关系:

7.2.3 AudioTrack(Native空间)分析 - 图2

图 7-4 共享内存和CB的关系

注意 CB实际是按照环形缓冲来处理数据的读写的,所以user和server的真实作用还需要结合userBase和serverBase来看。图7-4只是一个示意图。

另外,关于CB,还有一个神秘的问题。先看下面这行代码:


mCblk=static_cast<audio_track_cblk_t*>(cblk->pointer());


这看起来很简单,但仔细琢磨会发现其中有一个很难解释的问题:

cblk->pointer返回的是共享内存的首地址,怎么把audio_track_cblk_t对象塞到这块内存中呢?

这个问题将通过对AudioFlinger的分析得到答案。

说明 关于audio_track_cblk_t的使用方式,后文会有详细分析。

(3)数据的Push or Pull

在JNI层的代码中可以发现,在构造AudioTrack时,传入了一个回调函数audioCallback。由于它的存在,导致了Native的AudioTrack还将创建另一个线程AudioTrackThread。它有什么用呢?

这个线程与外界数据的输入方式有关系,AudioTrack支持两种数据输入方式:

Push方式:用户主动调用write写数据,这相当于数据被push到AudioTrack。MediaPlayerService一般使用这种这方式提供数据。

Pull方式:AudioTrackThread将利用这个回调函数,以EVENT_MORE_DATA为参数主动从用户那pull数据。ToneGenerator则使用这种方式为AudioTrack提供数据。

这两种方式都可以使用,不过回调函数除了EVENT_MORE_DATA外,还能表达其他许多意图,这是通过回调函数的第一个参数来表明的。一起来看:


[—>AudioTrack.h:event_type]

enum event_type{

EVENT_MORE_DATA=0,//表示AudioTrack需要更多数据。

EVENT_UNDERRUN=1,//这是Audio的一个术语,表示Audio硬件处于低负荷状态。

//AT可以设置打点播放,即设置播放的起点和终点,LOOP_END表示已经到达播放终点。

EVENT_LOOP_END=2,

/*

数据使用警戒通知。该值可通过setMarkerPosition()设置。

当数据的使用超过这个值时,AT会且仅通知一次,有点像Water Marker。

这里所说的数据使用,是针对消费者AF消费的数据量而言的。

*/

EVENT_MARKER=3,

/*

数据使用进度通知。进度通知值由setPositionUpdatePeriod()设置,例如每使用500帧通知一次。

*/

EVENT_NEW_POS=4,

EVENT_BUFFER_END=5//数据全部被消耗

};


请看AudioTrackThread的线程函数threadLoop。代码如下所示:

[—>AudioTrack.cpp]


bool AudioTrack:AudioTrackThread:threadLoop()

{

//mReceiver就是创建该线程的AudioTrack。

return mReceiver.processAudioBuffer(this);

}

[—>AudioTrack.cpp]

bool AudioTrack:processAudioBuffer(const sp<AudioTrackThread>&thread)

{

Buffer audioBuffer;

uint32_t frames;

size_t writtenSize;

//处理underun的情况。

if(mActive&&(mCblk->framesReady()==0)){

if(mCblk->flowControlFlag==0){

mCbf(EVENT_UNDERRUN,mUserData,0);//under run通知

if(mCblk->server==mCblk->frameCount){

/*

server是读位置,frameCount是buffer中的数据总和。

当读位置等于数据总和时,表示数据都已经使用完了。

*/

mCbf(EVENT_BUFFER_END,mUserData,0);

}

mCblk->flowControlFlag=1;

if(mSharedBuffer!=0)return false;

}

}

//循环播放通知

while(mLoopCount>mCblk->loopCount){

int loopCount=-1;

mLoopCount—;

if(mLoopCount>=0)loopCount=mLoopCount;

//一次循环播放完毕,loopCount表示还剩多少次。

mCbf(EVENT_LOOP_END,mUserData,(void*)&loopCount);

}

if(!mMarkerReached&&(mMarkerPosition>0)){

if(mCblk->server>=mMarkerPosition){

//如果数据的使用超过警戒值,则通知用户。

mCbf(EVENT_MARKER,mUserData,(void*)&mMarkerPosition);

//只通知一次,因为该值被设为true。

mMarkerReached=true;

}

}

if(mUpdatePeriod>0){

while(mCblk->server>=mNewPosition){

/*

进度通知,但它不是以时间为基准的,而是以帧数为基准的。

例如设置每500帧通知一次,假设消费者一次就读了1500帧,那么这个循环会连续通知3次。

*/

mCbf(EVENT_NEW_POS,mUserData,(void*)&mNewPosition);

mNewPosition+=mUpdatePeriod;

}

}

if(mSharedBuffer!=0){

frames=0;

}else{

frames=mRemainingFrames;

}

do{

audioBuffer.frameCount=frames;

//得到一块可写的缓冲

status_t err=obtainBuffer(&audioBuffer,1);

……

//从用户那pull数据

mCbf(EVENT_MORE_DATA,mUserData,&audioBuffer);

writtenSize=audioBuffer.size;

……

if(writtenSize>reqSize)writtenSize=reqSize;

//PCM8数据转PCM16

……

audioBuffer.size=writtenSize;

audioBuffer.frameCount=writtenSize/mCblk->frameSize;

frames-=audioBuffer.frameCount;

releaseBuffer(&audioBuffer);//写完毕,释放这块缓冲

}

while(frames);

……

return true;

}


关于obtainBuffer和releaseBuffer,后面再分析。这里有一个问题值得思考:

用例会调用write函数写数据,AudioTrackThread的回调函数也让我们提供数据。难道我们同时在使用Push和Pull模式?

这太奇怪了!来查看这个回调函数的实现,了解一下究竟是怎么回事。该回调函数是通过set调用传入的,对应的函数是audioCallback。代码如下所示:


[—>android_media_AudioTrack.cpp]

static void audioCallback(int event,voiduser,voidinfo){

if(event==AudioTrack:EVENT_MORE_DATA){

//很好,没有提供数据,也就是说,虽然AudioTrackThread通知了EVENT_MORE_DATA,

//但是我们并没有提供数据给它。

AudioTrack:BufferpBuff=(AudioTrack:Buffer)info;

pBuff->size=0;

}

……


悬着的心终于放下来了,还是老老实实地看Push模式下的数据输入吧。

2.write输入数据

write函数涉及Audio系统中最重要的问题,即数据是如何传输的,在分析它的时候,不妨先思考一下它会怎么做。回顾一下我们已了解的信息:

有一块共享内存。

有一个控制结构,里面有一些支持跨进程的同步变量。

有了这些东西,write的工作方式就非常简单了:

通过共享内存传递数据。

通过控制结构协调生产者和消费者的步调。

重点强调 带着问题和思考来分析代码相当于“智取”,它比一上来就直接扎入源码的“强攻”要高明得多。希望我们能掌握这种思路和方法。

好了,现在开始分析write,看看它的实现是不是如我们所想的那样。


[—>AudioTrack.cpp]

ssize_t AudioTrack:write(const void*buffer,size_t userSize)

{

if(mSharedBuffer!=0)return INVALID_OPERATION;

if(ssize_t(userSize)<0){

return BAD_VALUE;

}

ssize_t written=0;

const int8_tsrc=(const int8_t)buffer;

Buffer audioBuffer;//Buffer是一个辅助性的结构。

do{

//以帧为单位

audioBuffer.frameCount=userSize/frameSize();

//obtainBuffer从共享内存中得到一块空闲的数据块。

status_t err=obtainBuffer(&audioBuffer,-1);

……

size_t toWrite;

if(mFormat==AudioSystem:PCM_8_BIT&&

!(mFlags&AudioSystem:OUTPUT_FLAG_DIRECT)){

//PCM8数据转成PCM16。

}else{

//空闲数据缓冲的大小是audioBuffer.size。

//地址在audioBuffer.i8中,数据传递通过memcpy完成。

toWrite=audioBuffer.size;

memcpy(audioBuffer.i8,src,toWrite);

src+=toWrite;

}

userSize-=toWrite;

written+=toWrite;

//releaseBuffer更新写位置,同时会触发消费者。

releaseBuffer(&audioBuffer);

}while(userSize);

return written;

}


通过write函数,会发现数据的传递其实是很简单的memcpy,但消费者和生产者的协调,则是通过obtainBuffer与releaseBuffer来完成的。现在来看这两个函数。

3.obtainBuffer和releaseBuffer

这两个函数展示了作为生产者的AT和CB对象的交互方法。先简单看看,然后把它们之间交互的流程记录下来,以后在CB对象的单独分析部分,我们再来做详细介绍。


[—>AudioTrack.cpp]

status_t AudioTrack:obtainBuffer(Buffer*audioBuffer,int32_t waitCount)

{

int active;

status_t result;

audio_track_cblk_t*cblk=mCblk;

……

//①调用framesAvailable,得到当前可写的空间大小。

uint32_t framesAvail=cblk->framesAvailable();

if(framesAvail==0){

……

//如果没有可写空间,则要等待一段时间。

result=cblk->cv.waitRelative(cblk->lock,milliseconds(waitTimeMs));

……

}

cblk->waitTimeMs=0;

if(framesReq>framesAvail){

framesReq=framesAvail;

}

//user为可写空间的起始地址。

uint32_t u=cblk->user;

uint32_t bufferEnd=cblk->userBase+cblk->frameCount;

if(u+framesReq>bufferEnd){

framesReq=bufferEnd-u;

}

……

//②调用buffer,得到可写空间的首地址。

audioBuffer->raw=(int8_t*)cblk->buffer(u);

active=mActive;

return active?status_t(NO_ERROR):status_t(STOPPED);

}


obtainBuffer的功能,就是从CB管理的数据缓冲中得到一块可写空间,而releaseBuffer,则是在使用完这块空间后更新写指针的位置。


[—>AudioTrack.cpp]

void AudioTrack:releaseBuffer(Buffer*audioBuffer)

{

audio_track_cblk_t*cblk=mCblk;

cblk->stepUser(audioBuffer->frameCount);//③调用stepUser更新写位置。

}


obtainBuffer和releaseBuffer与CB交互,一共会有三个函数调用,如下所示:

framesAvailable判断是否有可写空间。

buffer得到写空间的起始地址。

stepUser更新写位置。

请记住这些流程,以后在分析CB时会发现它们有重要作用。

4.delete AudioTrack

到这里,AudioTrack的使命就进入倒计时阶段了。来看它在生命的最后还会做一些什么工作。代码如下所示:


[—>AudioTrack.cpp]

AudioTrack:~AudioTrack()

{

if(mStatus==NO_ERROR){

stop();//调用stop。

if(mAudioTrackThread!=0){

//通知AudioTrackThread退出。

mAudioTrackThread->requestExitAndWait();

mAudioTrackThread.clear();

}

mAudioTrack.clear();

//将残留在IPCThreadState发送缓冲区的信息发送出去。

IPCThreadState:self()->flushCommands();

}

}


如果不调用stop,析构函数也会先调用stop,这个做法很周到。代码如下所示:


[—>AudioTrack.cpp]

void AudioTrack:stop()

{

sp<AudioTrackThread>t=mAudioTrackThread;

if(t!=0){

t->mLock.lock();

}

if(android_atomic_and(~1,&mActive)==1){

mCblk->cv.signal();

/*

mAudioTrack是IAudioTrack类型,其stop的最终处理在AudioFlinger端。

*/

mAudioTrack->stop();

//清空循环播放设置。

setLoop(0,0,0);

mMarkerReached=false;

if(mSharedBuffer!=0){

flush();

}

if(t!=0){

t->requestExit();//请求退出AudioTrackThread。

}else{

setpriority(PRIO_PROCESS,0,ANDROID_PRIORITY_NORMAL);

}

}

if(t!=0){

t->mLock.unlock();

}

}


stop的工作比较简单,就是调用IAudioTrack的stop,并且还要求退出回调线程。要重点关注IAudioTrack的stop函数,这个将作为AT和AF交互流程中的一个步骤来分析。