Android音頻系統探究——從SoundPool到AudioHardware

    對音頻系統的探索起源於工作中遇到的一個bug。平時都是力求快速解決問題,不問原因。這次時間比較寬裕,正好藉着解決問題的機會,把Android的音頻系統瞭解一下。既然由bug引發,那就從bug開始說。


一. bug現象


    Android的照相機在拍照的時候會播放一個按鍵音。最近的一個MID項目(基於RK3188,Android 4.2)中,測試部門反饋,拍照時按鍵音播放異常情況如下:

    (1)進入應用程序以後,第一次拍照,沒有按鍵音

    (2)連續拍照,有按鍵音

    (3)停止連拍,等待幾秒鐘後,再次拍照,又沒有按鍵音



二. 問題簡化


    看CameraApp代碼可以知道,播放按鍵音使用了SoundPool類。做一個使用SoundPool播放聲音的應用程序,界面上只有一個Button,點擊後播放聲音。這樣就能確定這單純是聲音播放問題還是複合性問題。代碼很簡單:

protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_test_sound_pool);
		mSoundPool = new SoundPool(10, AudioManager.STREAM_SYSTEM, 5);
		mSoundId = mSoundPool.load(this, R.raw.camera_click, 1);   //這裏R.raw.camera_click是ogg格式的音頻資源
		
		vBtnShut = (Button) findViewById(R.id.btn_click);
		vBtnShut.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				mSoundPool.play(mSoundId, 1, 1, 0, 0, 1);
			}
		});
	}
	
    結果表明,BUG現象仍然是一樣的。我們將BUG現象做一次簡化:

    idle-->play failed-->idle-->play failed-->play success-->play success-->idle-->play failed-->...

    可以總結爲,每間隔幾秒鐘後,第一次播放音頻無聲音輸出。 




三. 初步分析

 

   理清了現象,簡化了環境,我們可以開始分析問題了:

    顯而易見的是,BUG非常規律,只有相隔幾秒鐘後的第一次播放纔出現問題,與軟件邏輯密切相關,可以排除硬件問題。本質上來講,無論使用什麼軟件系統,聲音播放的流程一般都是——用戶指定要播放的聲音數據,可能是文件,可能是Buffer;Audio系統對聲音數據解碼,可能採用軟解碼,也可能採用硬解碼;將解碼出來的數字音頻信號傳給功放設備,經過D/A轉換後送到揚聲器,聲音就播放出來了。可以說,這個流程中的第一部分,是應用程序的行爲;第二部分,是Android系統的職責;第三部分,是kernel中驅動的工作。應用程序的問題可以排除,現在要解決的疑問是,是解碼程序出了問題,還是驅動程序出了問題?出現了什麼情況,導致了idle後播放不出來?

    


四.  代碼研究


1. Android Audio框架

    首先網絡上找找資料,要搞清楚Android音頻的框架層次結構,才容易定位問題。用圖說明——

    有了大致的概念,開始以SoundPool爲入口,摸清播放流程。其中在每個層次中要了解兩點:數據如何傳遞,播放的動作如何執行。 也就是沿着SoundPool.load()和Sound.play()順藤摸瓜。


2. SoundPool和AudioFlinger

    SoundPool.java基本是個空殼,直接使用了Native接口,代碼沒什麼可看的。不過可以先看下這個類的介紹,就在SoundPool.java的開頭,整一頁的英文註釋。幸運的是,很快就找到了我們需要看的資料:

/**
 * The SoundPool class manages and plays audio resources for applications.
 *
 * <p>A SoundPool is a collection of samples that can be loaded into memory
 * from a resource inside the APK or from a file in the file system. The
 * SoundPool library uses the MediaPlayer service to decode the audio
 * into a raw 16-bit PCM mono or stereo stream. This allows applications
 * to ship with compressed streams without having to suffer the CPU load
 * and latency of decompressing during playback.</p>
... ...
... ...
*/

     挑重要的說,SoundPool是Sample的集合,能把APK裏的資源或者文件系統中的文件加載到內存中,使用MediaPlayer服務把音頻解碼成原始的16位PCM單聲道或立體聲數據流。好嘛,原來解碼在這裏就做了。還是看看代碼實現吧,免得心裏不踏實。


     不去理會Jni那些手續,直接看SoundPool.cpp。上面那個測試APK的代碼,調用了SoundPool的load,play兩個接口,就把聲音播放出來了。load一次後,可多次播放,這兩個接口之所以要分開,應該就是load做了解碼。先看load的實現,爲滿足不同音頻資源的需要,load被重載了,看其中一個就行了。

int SoundPool::load(int fd, int64_t offset, int64_t length, int priority)
{
    ALOGV("load: fd=%d, offset=%lld, length=%lld, priority=%d",
            fd, offset, length, priority);
    Mutex::Autolock lock(&mLock);
    sp<Sample> sample = new Sample(++mNextSampleID, fd, offset, length);
    mSamples.add(sample->sampleID(), sample);   //將sample對象加入管理
    doLoad(sample);   //load所在
    return sample->sampleID();
}

    數據處理角度來說,真正的load在doLoad中:

void SoundPool::doLoad(sp<Sample>& sample)
{
    ALOGV("doLoad: loading sample sampleID=%d", sample->sampleID());
    sample->startLoad();   //只是改變了狀態
    mDecodeThread->loadSample(sample->sampleID());  //真正加載的地方
}

    看到了mDecodeThread,眼前一亮,很可能這裏就是將ogg解碼成PCM的地方了。所以進入loadSample看一看:

void SoundPoolThread::loadSample(int sampleID) {
    write(SoundPoolMsg(SoundPoolMsg::LOAD_SAMPLE, sampleID));
}

    只是消息傳遞而已,找到LOAD_SAMPLE消息處理的地方:

int SoundPoolThread::run() {
    ALOGV("run");
    for (;;) {
        SoundPoolMsg msg = read();
        ALOGV("Got message m=%d, mData=%d", msg.mMessageType, msg.mData);
        switch (msg.mMessageType) {
        case SoundPoolMsg::KILL:
            ALOGV("goodbye");
            return NO_ERROR;
        case SoundPoolMsg::LOAD_SAMPLE:   //在這裏處理LOAD_SAMPLE
            doLoadSample(msg.mData);
            break;
        default:
            ALOGW("run: Unrecognized message %d\n",
                    msg.mMessageType);
            break;
        }
    }
}
void SoundPoolThread::doLoadSample(int sampleID) {
    sp <Sample> sample = mSoundPool->findSample(sampleID);
    status_t status = -1;
    if (sample != 0) {
        status = sample->doLoad();
    }
    mSoundPool->notify(SoundPoolEvent(SoundPoolEvent::SAMPLE_LOADED, sampleID, status));
}

    看來最後是在sample->doLoad()中做的處理。進去看看,頗有驚喜:

status_t Sample::doLoad()
{
    uint32_t sampleRate;
    int numChannels;
    audio_format_t format;
    sp<IMemory> p;
    ALOGV("Start decode");
    if (mUrl) {
        p = MediaPlayer::decode(mUrl, &sampleRate, &numChannels, &format);
    } else {
        p = MediaPlayer::decode(mFd, mOffset, mLength, &sampleRate, &numChannels, &format);
        ALOGV("close(%d)", mFd);
        ::close(mFd);
        mFd = -1;
    }
    if (p == 0) {
        ALOGE("Unable to load sample: %s", mUrl);
        return -1;
    }
    ALOGV("pointer = %p, size = %u, sampleRate = %u, numChannels = %d",
            p->pointer(), p->size(), sampleRate, numChannels);

    if (sampleRate > kMaxSampleRate) {
       ALOGE("Sample rate (%u) out of range", sampleRate);
       return - 1;
    }

    if ((numChannels < 1) || (numChannels > 2)) {
        ALOGE("Sample channel count (%d) out of range", numChannels);
        return - 1;
    }

    //_dumpBuffer(p->pointer(), p->size());
    uint8_t* q = static_cast<uint8_t*>(p->pointer()) + p->size() - 10;
    //_dumpBuffer(q, 10, 10, false);

    mData = p;
    mSize = p->size();
    mSampleRate = sampleRate;
    mNumChannels = numChannels;
    mFormat = format;
    mState = READY;
    return 0;
}


    原來Sample請來了MediaPlayer幫其解碼,並計算出了採樣率和幀數。到這裏數據已經準備好了。接下來我們就要看Framework能否把數據正確的傳遞給HAL,至於MediaPlayer是如何解碼的我們先不研究。

    弄清楚SoundPool的Play做了什麼,也就能找到HAL的代碼了。下面看只看play中的關鍵代碼:

int SoundPool::play(int sampleID, float leftVolume, float rightVolume,
        int priority, int loop, float rate)
{
	//...
	channel = allocateChannel_l(priority);
	//...
	channel->play(sample, channelID, leftVolume, rightVolume, priority, loop, rate);
	//...
}
    調用了SoundChannel的play.好讀書而不求甚解,先把代碼一路追下去,不作細究。

void SoundChannel::play(const sp<Sample>& sample, int nextChannelID, float leftVolume,
        float rightVolume, int priority, int loop, float rate)
{
	AudioTrack* newTrack;
	//....
	newTrack = new AudioTrack(streamType, sampleRate, sample->format(),
                channels, frameCount, AUDIO_OUTPUT_FLAG_FAST, callback, userData,                bufferFrames);
	//...
	mState = PLAYING;
	mAudioTrack->start();
	//...
}

    SoundChannel::play創建了一個AudioTrack對象,在AudioTrack的構造函數中,調用了set,set又調用了createTrack_l。createTrack_I中,通過IAudioFlinger創建了一個IAudioTrack。關於AudioTrack和AudioFlinger是爲何物,兩者如何交換音頻數據,就說來話長了。而且有很多大大分析得很詳細,就不贅述了。有幾篇寫得很好——


    閱讀這些資料我們可以知道,Android Framework的音頻子系統中,每一個音頻流對應着一個AudioTrack類的一個實例,每個AudioTrack會在創建時註冊到AudioFlinger中,由AudioFlinger把所有的AudioTrack進行混合(Mixer),然後輸送到AudioHardware中進行播放。換言之,AudioFlinger是Audio系統的核心服務之一,起到了承上啓下的銜接作用。

    我們現在已經讓SoundPool牽線,抓到AudioFlinger這條大魚。下面着重來看AudioFlinger如何向下調用AudioHardware的。



3. AudioFlinger與AudioHardware


    這裏需要一點基礎知識,先要了解Android的硬件抽象接口機制,才能理解AudioFlinger如何調用到AudioHardware,相關資料:

http://blog.csdn.net/myarrow/article/details/7175204

    因爲對Audio系統一無所知,所以很慚愧用了反相的代碼搜索,在hardware/xxx/audio目錄下查找HAL_MODULE_INFO_SYM,然後反過來到framework找HAL_MODULE_INFO_SYM的id "AUDIO_HARDWARE_MODULE_ID",過程非常笨拙,不足爲道。他山之石可以攻玉,看到一篇好文,藉助其中的一段分析來完成對AudioFlinger和AudioHardware關聯的分析。原文地址:http://blog.csdn.net/xuesen_lin/article/details/8805108  

    當AudioPolicyService構造時創建了一個AudioPolicyDevice(mpAudioPolicyDev)並由此打開一個AudioPolicy(mpAudioPolicy)——這個Policy默認情況下的實現是legacy_audio_policy::policy(數據類型audio_policy)。同時legacy_audio_policy還包含了一個AudioPolicyInterface成員變量,它會被初始化爲一個AudioPolicyManagerDefault。AudioPolicyManagerDefault的父類,即AudioPolicyManagerBase,它的構造函數中調用了mpClientInterface->loadHwModule()。

AudioPolicyManagerBase::AudioPolicyManagerBase(AudioPolicyClientInterface*clientInterface)…
{   
	//......
    for (size_t i = 0; i < mHwModules.size();i++) {
       mHwModules[i]->mHandle = mpClientInterface->loadHwModule(mHwModules[i]->mName);
        if(mHwModules[i]->mHandle == 0) {
            continue;
        }
	//......
}

    很明顯的mpClientInterface這個變量在AudioPolicyManagerBase構造函數中做了初始化,再回溯追蹤,可以發現它的根源在AudioPolicyService的構造函數中,對應的代碼語句如下:

rc =mpAudioPolicyDev->create_audio_policy(mpAudioPolicyDev, &aps_ops, this, &mpAudioPolicy);      

    在這個場景下,函數create_audio_policy對應的是create_legacy_ap,並將傳入的aps_ops組裝到一個AudioPolicyCompatClient對象中,也就是mpClientInterface所指向的那個對象。
    換句話說,mpClientInterface->loadHwModule實際上調用的就是aps_ops->loadHwModule,即:

static audio_module_handle_t  aps_load_hw_module(void*service,const char *name)
{
    sp<IAudioFlinger> af= AudioSystem::get_audio_flinger();
    …
    return af->loadHwModule(name);
}

    AudioFlinger終於出現了,同樣的情況也適用於mpClientInterface->openOutput,代碼如下:
static audio_io_handle_t  aps_open_output(…)
{
    sp<IAudioFlinger> af= AudioSystem::get_audio_flinger();
    …
    return  af->openOutput((audio_module_handle_t)0,pDevices, pSamplingRate, pFormat, pChannelMask,
                         pLatencyMs, flags);
}


   現在前方就是AudioHardware了,終於打開了從APK到HAL的通路。



4. AudioHardware


    AudioHardware有兩個內部類,AudioStreamOutALSA和AudioStreamInALSA,我們要解決的是聲音播放的問題,看AudioStreamOutALSA即可。 AudioStreamOutALSA代碼很清晰,很快找到了我們需要的代碼,寫PCM數據用的函數:

AudioHardware::AudioStreamOutALSA::AudioStreamOutALSA() :
    mHardware(0), mPcm(0), mMixer(0), mRouteCtl(0),
    mStandby(true), mDevices(0), mChannels(AUDIO_HW_OUT_CHANNELS),
    mSampleRate(AUDIO_HW_OUT_SAMPLERATE), mBufferSize(AUDIO_HW_OUT_PERIOD_BYTES),
    mDriverOp(DRV_NONE), mStandbyCnt(0)
{
#ifdef DEBUG_ALSA_OUT
	if(alsa_out_fp== NULL)
		alsa_out_fp = fopen("/data/data/out.pcm","a+");
	if(alsa_out_fp)
		ALOGI("------------>openfile success");         
#endif                                                         
}
ssize_t AudioHardware::AudioStreamOutALSA::write(const void* buffer, size_t bytes)
{
	//...
#ifdef DEBUG_ALSA_OUT
	if(alsa_out_fp)
		fwrite(buffer,1,bytes,alsa_out_fp);
#endif 
	//...
	if (mStandby) {
		open_l();   //重新open音頻設備
	   mStandby = false;
	}
	//...
	ret = pcm_write(mPcm,(void*) p, bytes);
	//...
}

    這裏提供了一個很容易驗證PCM數據是否正確的方法,打開DEBUG_ALSA_OUT開關後,可以將PCM流保存到“/data/data/out.pcm”文件中。到了驗證數據是否正確的時候了,打開這個編譯開關,得到out.pcm,將它pull到PC上,用coolEdit打開播放,發現正常播放正常。好了,我們現在可以知道,問題並不出在解碼程序上了。那又是什麼原因導致的呢,我們從write函數開始,研究播放的流程。

    首先,AudioStreamOutALSA的構造函數中將mStandby初始化爲true。這個變量顯然是作爲記錄音頻設備待機狀態用的。當mStandby==true時,每次調用write,都會調用open_l()重新開啓一次音頻設備,然後再做pcm_write。

    再看看open_l():

status_t AudioHardware::AudioStreamOutALSA::open_l()
{
 	//...
    mPcm = mHardware->openPcmOut_l();
    if (mPcm == NULL) {
        return NO_INIT;
    }
	//...
}
struct pcm *AudioHardware::openPcmOut_l()
{
		//...
        mPcm = pcm_open(flags);
		//...
        if (!pcm_ready(mPcm)) {
            pcm_close(mPcm);
            //...
        }
    }
    return mPcm;
}
open_l中調用了AudioHardware::openPcmOut_l,AudioHardware::openPcmOut_l中調用了pcm_open。再到pcm_open 中去看看:

struct pcm *pcm_open(unsigned flags)
{
	//... ...

    if (flags & PCM_IN) {
        dname = "/dev/snd/pcmC0D0c";
		channalFlags = -1;
		startCheckCount = 0;
    } else {
#ifdef SUPPORT_USB
        dname = "/dev/snd/pcmC1D0p";
#else
		dname = "/dev/snd/pcmC0D0p";
#endif
    }

    pcm->fd = open(dname, O_RDWR);
    if (pcm->fd < 0) {
        oops(pcm, errno, "cannot open device '%s'", dname);
        return pcm;
    }

    if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_INFO, &info)) {
        oops(pcm, errno, "cannot get info - %s", dname);
        goto fail;
    }
    
    if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_HW_PARAMS, &params)) {
        oops(pcm, errno, "cannot set hw params");
        goto fail;
    }
    if (ioctl(pcm->fd, SNDRV_PCM_IOCTL_SW_PARAMS, &sparams)) {
        oops(pcm, errno, "cannot set sw params");
        goto fail;
    }

fail:
    close(pcm->fd);
    pcm->fd = -1;
    return pcm;
}


    果然,這裏就是操作設備節點的地方了。我們先在AudioStreamOutALSA的write中加打印信息,看看第一次播放和後續播放究竟有何不同。測試結果發現,每次播放不出聲音的情況,都發生mStandby==true之後,這個時候做了一次打開音頻設備的動作,但此時PCM數據是正確的。我們先來看看什麼時候會導致mStandby==true。

status_t AudioHardware::AudioStreamOutALSA::standby()
{
        doStandby_l();
}

void AudioHardware::AudioStreamOutALSA::doStandby_l()
{

	if(!mStandby)
		mStandby = true;
    close_l();
}

void AudioHardware::AudioStreamOutALSA::close_l()
{
    if (mPcm) {
        mHardware->closePcmOut_l();
        mPcm = NULL;
    }
}


    好了,現在我們可以確定,mStandby是在調用standby的時候被設置生true了。如果不總是重新打開音頻設備,會不會變正常?做了一個實驗,把standby函數體的代碼都註釋掉。這樣修改後,果然開機只有一次聲音播放不出來,那就是第一次。每隔一段時間,聲音就播不出來的問題不見了。

     其實到現在,問題已經定位出來了。這個問題屬於kernel問題,不再屬於Framework了。但是還是想弄清楚,standby爲什麼隔一段時間被調用一次,是被誰調用的。經過一系列反查,找到了standby的真正調用處,AudioFlinger的播放線程中。具體怎麼查的,還是要參考HAL知識去,就不重複記載了。


void AudioFlinger::PlaybackThread::threadLoop_standby()
{
    ALOGV("Audio hardware entering standby, mixer %p, suspend count %d", this, mSuspended);
    mOutput->stream->common.standby(&mOutput->stream->common);
}

bool AudioFlinger::PlaybackThread::threadLoop()
{
	// ... ...
	while (!exitPending())
	{
if (CC_UNLIKELY((!mActiveTracks.size() && systemTime() > standbyTime) || isSuspended())) { if (!mStandby) { threadLoop_standby(); mStandby = true; }//... ...}//... ...standbyTime = systemTime() + standbyDelay;//... ...}// ... ...}

    這裏我們看到了,standby是由AudioFlinger控制的,一旦滿足以下條件後,沒有AudioTrack處於活動狀態並且已經到達了standbyTime這個時間就進入Standby模式。那麼standbyTime=systemTime() + standbyDelay,也就是過了standbyDelay這段時間後,音頻系統將進入待機,關閉音頻設備。最後找到standbyDelay的值是多少。

   AudioFlinger::PlaybackThread構造函數中,將standbyDelay初始化,standbyDelay(AudioFlinger::mStandbyTimeInNsecs), 

   AudioFlinger這個類第一次被引用時,就對成員變量mStandbyTimeInNsecs 進行了初始化

void AudioFlinger::onFirstRef()
{
    //... ...
    /* TODO: move all this work into an Init() function */
    char val_str[PROPERTY_VALUE_MAX] = { 0 };
    if (property_get("ro.audio.flinger_standbytime_ms", val_str, NULL) >= 0) {
        uint32_t int_val;
        if (1 == sscanf(val_str, "%u", &int_val)) {
            mStandbyTimeInNsecs = milliseconds(int_val);
            ALOGI("Using %u mSec as standby time.", int_val);
        } else {
            mStandbyTimeInNsecs = kDefaultStandbyTimeInNsecs;
            ALOGI("Using default %u mSec as standby time.",
                    (uint32_t)(mStandbyTimeInNsecs / 1000000));
        }
    }

    mMode = AUDIO_MODE_NORMAL;
}

    如果有ro.audio.flinger_standbytime_ms這個屬性,就按這個屬性值設定stand by的idle time(很可能是OEM代碼),如果沒有,取kDefaultStandbyTimeInNsecs的值。kDefaultStandbyTimeInNsecs是個常量,3s:

static const nsecs_t kDefaultStandbyTimeInNsecs = seconds(3);



五. 結論及收穫


    通過分析研究Android系統代碼,我們雖然最終沒有解決問題,但是已經定位出了問題所在的層次,確定這是一個驅動的BUG。Framework工程師的任務至此完成了。問題交付給驅動工程師,經過排查發現,是PA沒有打開造成的問題。


    經驗可以帶來技巧,如果下次遇到類似問題,我們可以直接在AudioHardware中截獲PCM,通過判斷解碼出的PCM流是否正確,較快速的定位到問題所在——是MediaPlayer Codec、AudioSystem、還是Driver。






發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章