Android音频系统之AudioTrack起播线与underrun问题研究(Android 5.1)

一、引言:
使用audiotrack进行pcm数据播放的时候,复杂场景下会遇到underrun问题,所谓underrun,是指生产者提供数据的速度跟不上消费者使用数据的速度。因为网上讲述这类问题的博客很少,所以这里分享一下自己的研究心得,但是由于underrun和audiotrack的buffer机制非常复杂,所以也会遗留一些问题,欢迎大神补充指正。

二、代码分析:
1.添加活跃track:
首先,我们回顾下活跃track的添加,前几篇博文已经分析过,thread起来之后并不是立马不停地去处理数据,这样对功耗和CPU而言是一种浪费,当上层调用audiotrack的start接口之后,线程会添加当前track为活跃track,并且发送广播,threadloop就开始转起来处理数据了,先看一下addTrack_l函数:

AudioFlinger::PlaybackThread::addTrack_l@Threads.cpp
status_t AudioFlinger::PlaybackThread::addTrack_l(const sp<Track>& track)
{
	...
    // set retry count for buffer fill
    track->mRetryCount = kMaxTrackStartupRetries;
    ...
    mActiveTracks.add(track);
    ...
}

注意看,这个函数进来先对track设置了一个最大开始尝试次数,但是我从代码中来看,这个值并没有使用处,反倒是和它一起定义的kMaxTrackRetries 很有用,这个值会跟underrun扯上关系,假如thread在kMaxTrackRetries 的次数内持续underrun,那么,系统就认为这个track已经“死了”,会将其从mActiveTracks中移除,我们看一下系统设置的这个值大小:

// retry counts for buffer fill timeout
// 50 * ~20msecs = 1 second
static const int8_t kMaxTrackRetries = 50;
static const int8_t kMaxTrackStartupRetries = 50;

注意注释,告诉我们threadloop每执行一次为20ms;

2.prepareTracks_l:
接下来我们去threadloop里面,首先关注的是肯定是prepareTracks_l,因为这个函数会去遍历所有的活跃track,计算buffer中的数据等操作,然后给后续的mixer使用,这里先抛出一个结果,track动起来之后,threadloop每执行一次的值确实是20ms,这个值的计算很复杂,是由threadloop来控制的(不是prepareTracks_l做的),在代码中我没有明显的看到这个值的得出结论。下面简化一下prepareTracks_l的代码:

AudioFlinger::PlaybackThread::mixer_state AudioFlinger::MixerThread::prepareTracks_l(
        Vector< sp<Track> > *tracksToRemove)
{
	/* 先将mixer状态设置为IDLE */
	mixer_state mixerStatus = MIXER_IDLE;	
	...
	/* fasttrack下,设置try值为50 */
	case TrackBase::ACTIVE:
    if (recentFull > 0 || recentPartial > 0) {
        // track has provided at least some frames recently: reset retry count
        track->mRetryCount = kMaxTrackRetries;
    }
    ...
    audio_track_cblk_t* cblk = track->cblk();
    ...
    size_t desiredFrames;
    uint32_t sr = track->sampleRate();
    if (sr == mSampleRate) {
    	desiredFrames = mNormalFrameCount;
	} else
	{
		...
	}
	minFrames = desiredFrames;
	/* 获取共享buffer中的帧数 */
	size_t framesReady = track->framesReady();
	/* 如果目前帧数大于minFrames */
    if ((framesReady >= minFrames) && track->isReady() &&
        
            !track->isPaused() && !track->isTerminated())
    {	
    	...
		/* 进行软音量相关的计算 */
		...
		/* 使能mixer,让mixer准备开工 */
		mAudioMixer->enable(name);
		...
        /* 重置underrun的值 */
        track->mRetryCount = kMaxTrackRetries;
        ...
        /* 设置mixerStatus  */
        mixerStatus = MIXER_TRACKS_READY;
	}
	/* 如果buffer中的数据不到minFrames,系统认为underrun了 */
	else
	{
		/* 记录underrun的帧数,实际上是一个固定值,在dumpsys中可以看到 */
		if (framesReady < desiredFrames && !track->isStopped() && !track->isPaused()) {
			track->mAudioTrackServerProxy->tallyUnderrunFrames(desiredFrames);
		}
			...
	   /* 如果连续underrun50次 */
	   if (--(track->mRetryCount) <= 0) {
	 	    /* 移除出活跃track,此处存疑 */
	 	    tracksToRemove->add(track);
	    }
	   	...
	   	/* mixer不用干活了 */
	   	mAudioMixer->disable(name);
	}
	
track_is_ready: ;
	mixerStatus = MIXER_TRACKS_READY;
	...
	return mixerStatus;
}

prepareTracks_l代码真的是“又臭又长”,大家耐心点,我去掉了很多不必要的代码,下面我们来分析一下,可以看到,代码进来之后,是将mixerStatus设置为IDLE的,最终为READY,这个地方我想吐槽一下,不知道设置来有何意义,只要进函数了,出来必定是READY的状态,这里的kMaxTrackRetries可以把它理解为记录underrun的次数,从代码上看,如果超过了50次,那么系统会将其从活跃track中移除,但是,实际调试中,我通过dumpsys发现track还是一直存在的, 而代码确实在不停地移除,非常奇怪,不知道是Android 5.1的bug还是怎么回事(如果有哪位大侠知道此处原因,请告知),既然这个prepareTracks_l会判断是否处于underrun,那么underrun的依据是什么呢?我们需要注意代码中的mNormalFrameCount和desiredFrames两个变量 ,mNormalFrameCount是指当前系统hal层正常播放支持的帧数(跟采样率,位宽,声道数相关),而desiredFrames 指当前track配置下,hal层正常播放支持的帧数,很绕,对不对,什么叫当前track配置,就是你apk在创建track的时候,设置的track参数,比如我的配置如下:

    public void trackConfig(){
        mSampleRate = SAMPLERATE;		//48000
        mChannelCount = CHANNEL_CONFIG;	//2channel
        mBitwidth = AUDIO_FORMAT;		//16bit位宽
    }

所以,desiredFrames 看来是跟mNormalFrameCount有关了,如果hal层的采样率跟你当前track的一致,那么两值相等,如果不一样,那么desiredFrames 需要传入mNormalFrameCount并依据一定的算法算出来(上图中未贴出),mNormalFrameCount值到底是从哪里来的?
通过分析代码,系统启动APS的服务时,会去构造PlaybackThread,其构造函数就会去调用readOutputParameters_l,这个是用于获取hal层的硬件数据的,这个函数只会在系统启机加载服务或者策略选择的时候才会去调用(策略选择比如你走USB或者蓝牙,加载的hal层库肯定是不一样的,那么,这个函数肯定会被重新调用),贴一下readOutputParameters_l代码:

void AudioFlinger::PlaybackThread::readOutputParameters_l()
{
    // unfortunately we have no way of recovering from errors here, hence the LOG_ALWAYS_FATAL
    mSampleRate = mOutput->stream->common.get_sample_rate(&mOutput->stream->common);
    mChannelMask = mOutput->stream->common.get_channels(&mOutput->stream->common);
    ...
    mNormalFrameCount = multiplier * mFrameCount;
    ...
}

计算原理是调用hal层函数获取采样率,声道数等,经过一系列的算法,最终计算出来的公式如上,以我的hal配置为例(48k,两声道,16bit位宽),算出mNormalFrameCount 的值为960帧;
好了,回到prepareTracks_l,为了配合测试,我的track配置和hal层一样,这样保证了desiredFrames 也等于960帧,underrun的最终判断就是看当前共享buffer中的数据量是否大于960帧,当前buffer中的数据由track->framesReady()获得,两者一对比,如果少于960,好吧,underrun了,但是Android系统也简单粗暴,也不去算到底underrun了多少帧,直接就把desiredFrames 贴上去了,不信你看tallyUnderrunFrames函数:

tallyUnderrunFrames@AudioTrackShared.cpp
/* 传入的就是desiredFrames  */
void AudioTrackServerProxy::tallyUnderrunFrames(uint32_t frameCount)
{
    audio_track_cblk_t* cblk = mCblk;
    cblk->u.mStreaming.mUnderrunFrames += frameCount;

    // FIXME also wake futex so that underrun is noticed more quickly
    (void) android_atomic_or(CBLK_UNDERRUN, &cblk->mFlags);
}

所以,我们在dumpsys中看到的underrun帧数,就是这个desiredFrames 的固定值N倍,可别指望Android给你认真计算:
underrun示例图:当前活跃trackunderrun的帧数为1052 * 960,即1009920
知道了underrun的判断依据之后,我们就会发现,如果在某次播放中,连续underrun了50次,那么从代码看是会被移除出活跃track的(实际没有,真的是bug么?再唠叨一遍),如果偶尔的underrun,那么会去重置这个数,是否underrun的区别是为了什么?决定了接下来调用threadLoop_mix函数函数是hook调用的函数指针是哪一个mix处理函数。
画一张四不像的图来表达一下逻辑:
在这里插入图片描述
3.threadLoop_mix:
这个函数我们知道目的就是将数据进行混音处理,前面prepareTracks_l在这里面会进行一个比较复杂的函数选择过程,而且选择不同的混音函数,其数据处理算法相差很大,也这是为什么在underrun情况下dump下面hal层的数据时,会有各种杂音的现象,因为在没有充足数据的情况下,做混音时系统会填充静音数据。

4.threadLoop_write:
这个函数最终会将混音完后的数据填充到hal层,我们跟下流程:

ssize_t AudioFlinger::PlaybackThread::threadLoop_write()
{
	...
	if (mNormalSink != 0) {
		ssize_t framesWritten = mNormalSink->write((char *)mSinkBuffer + offset, count);
	}
	else
	{
		...
		bytesWritten = mOutput->stream->write(mOutput->stream,
                                                   (char *)mSinkBuffer + offset, mBytesRemaining);	
        ...
	}
	...
}

这里会去选择if分支进入,下面else也有一个是直接写到hal层中的语句,这里if和else的选择我不是很清楚,从描述来看的话,如果是混音场景,是会走到if的,我们就按照这个if分支里分析,这里的mNormalSink又是什么?在mixer的构造函数中,函数最后有一个如下语句:

AudioFlinger::MixerThread::MixerThread@Threads.cpp

    switch (kUseFastMixer) {
    case FastMixer_Never:
    case FastMixer_Dynamic:
        mNormalSink = mOutputSink;
        break;
    case FastMixer_Always:
        mNormalSink = mPipeSink;
        break;
    case FastMixer_Static:
        mNormalSink = initFastMixer ? mPipeSink : mOutputSink;
        break;
    }

如果使用kUseFastMixer,那么mNormalSink 就是mOutputSink,这个mOutputSink又是哪里来的,答案还是在这个函数中,往前看一下,有如下语句:

mOutputSink = new AudioStreamOutSink(output->stream);

AudioStreamOutSink构造函数所在文件:frameworks\av\media\libnbaio\AudioStreamOutSink.cpp
找一下write,看下做了什么:

ssize_t AudioStreamOutSink::write(const void *buffer, size_t count)
{
    if (!mNegotiated) {
        return NEGOTIATE;
    }
    ALOG_ASSERT(Format_isValid(mFormat));
    ssize_t ret = mStream->write(mStream, buffer, count * mFrameSize);
    if (ret > 0) {
        ret /= mFrameSize;
        mFramesWritten += ret;
    } else {
        // FIXME verify HAL implementations are returning the correct error codes e.g. WOULD_BLOCK
    }
    return ret;
}

终于是找到了写入hal层的地方了,可以看出来,mNormalSink 实际上相当于framework层与hal最后的中转站,framework层最终的数据就是从这里写入到hal层去的。
回顾这个threadloop函数,似乎所有针对underrun的处理都是Android系统自己做了,且对于开发人员,如果想要修改系统级underrun的难度是很巨大的,所以,开发人员想要最大程度的避免underrun,那么,对于自己的hal层设计,一定要合理,对于hal层buffer的大小,数据的copy速度都有很大的关系,也许,普通的2channel音频不会导致underrun的出现,但是,如果是针对高码率,高音质的音频,比如杜比5.1和7.1以及无损解码格式等等,数据量是非常庞大的,稍有不合理,就会出现underrun。

三、验证性试验:
1.如何调试underrun:

首先我们要学会如何调试使得track出现underrun,在实际的码流播放过程中,除了pcm数据以外,都是经过解码然后输出这样一个流程,解码器的速度可能是不确定的,因此,产生的数据大小可能也不一样,underrun的直观感受是声音卡顿或者杂音,我们可以通过dumpsys去看media.audio_flinger的服务确认是否存在underrun,因为在前面的分析中,我们已经知道,存在活跃track的情况下,每20ms会从sharebuffer中读取数据,抓住这个点,我们让上层每次写数据的时间大于20ms,这只是一个满足点,还需要注意每次APk写入sharebuffer中的数据量,不然,一次性写大了,也不会出现underrun,还是前面的分析,按照我环境的hal层及audiotrack的配置,desiredFrames 为960帧,所以我将apk的设置改为每隔21ms写入480帧:
因为我的apk中每次往下写的buffer大小是由AudioTrack.getMinBufferSize查询而来的,所以我在这里限制了往下写的数据大小:

    public void trackCreate(){
        /* 1.获取最小buffersize */
        mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRate,
                mChannelCount, mBitwidth);
        /* 2.创建audiotrack */
        mTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
                mSampleRate, mChannelCount, mBitwidth,
                mMinBufferSize, AudioTrack.MODE_STREAM);
        /* 修改mMinBufferSize */
        mMinBufferSize = 4 * 480;//mMinBufferSize本来的值为10584,这里改为1960,帧数为480帧,一次write不能达到起播线;
        /* 3.创建数据读取线程 */
        mThread = new PlayThread(mTrack, mMinBufferSize);
    }

在线程中设置一个睡眠来阻塞:

    public void writeData(){
        try {
            while (mFileInputStream.available() > 0) {
            	/* 设置睡眠时间 */
                Thread.sleep(21);
                int readCount = mFileInputStream.read(mTempBuffer);
                if (readCount == AudioTrack.ERROR_INVALID_OPERATION ||
                        readCount == AudioTrack.ERROR_BAD_VALUE) {
                    continue;
                }

                if (readCount != 0 && readCount != -1) {
                    if (mTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING){
                        mTrack.write(mTempBuffer, 0, readCount);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

万事具备,我们dumpsys看下media.audio_flinger服务:
在这里插入图片描述
声音卡顿很明显,不停地敲这个指令,我们看到当前活跃的track只有一个,后面的UndFrmCnt一直在不停增加;
2.如何持续underrun,hal层数据是怎样的?
首先,我将上层应用进行修改,每21ms仅仅往下写180帧的数据,然后,我在hal层中添加了dump数据的代码,播放码流,抓取了数据,使用软件打开,听起来完全是杂音,数据显示也很凌乱:
在这里插入图片描述
但是这个数据具体是哪个threadLoop_mix方法处理的,我不清楚,不过可以确定的是,underrun情况下,处理数据的方法肯定是不一样的,假如你在处理实际问题时,如果听到卡顿伴随杂音,请注意是否是underrun导致的。

3.正常情况下的dumpsys和log:
我们来看一下正常情况下是什么样的,首先,是apk层的修改,每19ms写入AudioTrack.getMinBufferSize获取的帧(1920帧),然后开始播放,dumpsys服务如下:
在这里插入图片描述
一直dumpsys,你会发现underrun数据恒定为1920,之所以会underrun一次,那是因为threadloop第一次去查询sharebuffer中的数据时,此时即便是有数据,但是因为指针还没有更新,所以就显示这一次。
在这里插入图片描述
这是log中的截图,我在prepareTracks_l中加了两处打印,一处是track->framesReady()中的打印,反馈目前sharebuffer中上层已经写入了多少帧的数据,另一处则是确认mNormalFrameCount和desiredFrames的值(虽然前面的分析我们已经将两值设置为一样),你可以发现,共享buffer中最多拥有的帧数就是2880帧,并且,每次在buffer充足的情况下,写入hal层中的数据正是960帧,那么, 这2880帧是不是就是audioflinger中申请的sharebuffer的总值呢?经过打印确认,我发现并不是,这也是我想抛出的另一个疑问的地方,audioflinger的client端一共申请了4M(正常是1M,我的环境对应的芯片公司改为了4M)总buffer,该进程所有的track都在这个4Mbuffer中去申请,而测试显示,当前活跃track申请的buffer可容纳4512帧,但实际上每次到达2880帧(大约占比58%)时,就不再往里面填写数据了,还剩下那么多空间干嘛?(audio_track_cblk_t占用空间很少)这让我觉得很奇怪,如果有大神知道,请告知。另外,顺便再提一个结论,AudioTrack.getMinBufferSize返回的值并不是该track在audioflinger中申请的值,该返回值可播出的持续时间为60ms,按照每20ms写入960帧计算,3 * 960 = 2880。所以,起播线可以理解为desiredFrames所代表的值。

四、总结:
underrun问题出现的原因很多,本着生产者提供数据的速度跟不上消费者使用数据的速度这一根本原因,从上到下都需要考虑周全,最简单的是修改上层应用,尽量减少往track中写数据的的时间间隔或者增加往track中单次写入的数据量,保证源头充足的情况下,剩下的事情就可以由下层来处理了。hal层的设计是最关重要的,hal层会决定起播线,buffer如果设计的太小,会造成audioflinger中共享buffer出现overrun,hal层如果太大,会很快将audioflinger中的数据给“吸空”。Android5.1系统hal层默认是走tinyalsa的,这套成熟的框架对buffer的处理使用的很好,但在更高的Android版本中,芯片公司会根据需求的多样化实现自己的hal层,这个时候,对buffer的设计就需要更加慎重了。当然,更厉害的公司,会从audioflinger开始入手,比如修改track在audioflinger中申请的buffer大小,调整threadloop的循环时间,添加活跃track之后对buffer进行一定数据的积攒等等,这些都要求对audioflinger和audiotrack非常深入的了解才能办到。
如果有更好的解决办法和心得体会,欢迎大家共同分享。

  • 15
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值