Android SoundPool 的使用以及原理分析

来源:互联网 发布:英文软件界面翻译工具 编辑:程序博客网 时间:2024/06/05 22:36

好吧,我们今天来聊聊SoundPool这东西。

据说这个东西是冰激凌(Android4.0)里才引入的一个新东西。按照官方的意思大多数情况下是给游戏开发用的,比如一个游戏10关,它能在游戏开始前一次加载所有10关的背景音乐,这其中也包括了解码操作,当真正要播放时候就直接把音频数据写设备了,大家自己琢磨下到底有什么好处,我自己觉得除了预先解码之外真的没发现特别大的好处。


这里我就以拍照音的播放来做切入点一步步分析它是怎么工作的。(当然你自己也可以搜一下SoundPool来找入口点。但话说这玩样儿确实被google造了出来,但是用的却很少。)

这里我说明下,正常拍照音的声音其实不是用SoundPool的,这里只是照相功能另一部分功能BurstCapture,即连拍。因为连拍时播放时间的间隔很短,所以必须省略解码过程。

代码路径:

packages/apps/Camera/src/com/android/camera/SoundClips.java        publicSoundPoolPlayer(Context context) {            mContext =context;            mSoundPool= new SoundPool(NUM_SOUND_STREAMS, audioType, 0);            mSoundIDs= new int[SOUND_RES.length];            mSoundIDReady = new boolean[SOUND_RES.length];            for (int i= 0; i < SOUND_RES.length; i++) {               mSoundIDs[i] = mSoundPool.load(mContext, SOUND_RES[i], 1);               mSoundIDReady[i] = false;            }        }


首先我们会看到初始化了一个新的SoundPool类,参数为stream数量,音频类型等。

我们先看看SoundPool构造函数做了什么。

frameworks/base/media/java/android/media/SoundPool.java    public SoundPool(int maxStreams, int streamType, int srcQuality) {        if(native_setup(new WeakReference(this), maxStreams, streamType, srcQuality) !=0) {            throw newRuntimeException("Native setup failed");        }frameworks/base/media/jni/soundpool/android_media_SoundPool.cppstatic jintandroid_media_SoundPool_native_setup(JNIEnv *env, jobjectthiz, jobject weakRef, jint maxChannels, jint streamType, jint srcQuality){   ALOGV("android_media_SoundPool_native_setup");SoundPool *ap = newSoundPool(maxChannels, (audio_stream_type_t) streamType, srcQuality);


这里我直接写上了native层的函数,我们再继续看这里SoundPool类的构造函数,传入的参数名称也容易辨别了(频道数,stream类型,质量)

frameworks/av/media/libmedia/SoundPool.cpp SoundPool::SoundPool(int maxChannels, audio_stream_type_tstreamType, int srcQuality){    // check limits    mMaxChannels =maxChannels;    if (mMaxChannels< 1) {        mMaxChannels =1;    }    else if(mMaxChannels > 32) {        mMaxChannels =32;    }    mChannelPool = newSoundChannel[mMaxChannels];    for (int i = 0; i< mMaxChannels; ++i) {       mChannelPool[i].init(this);       mChannels.push_back(&mChannelPool[i]);    }     // start decodethread    startThreads();}


从函数定义我们可以发现,这里的频道数必须在1到32个之间,即最大支持32路播放。然后又初始化了mChannelPool数组,即多个SoundChannel类,并且对每一个调用其init函数,放入mChannels指针列表。我们这里继续看下SoundChannel类的init函数。

void SoundChannel::init(SoundPool* soundPool){    mSoundPool =soundPool;}


 这个函数只是简单的保存了调用类SoundPool的指针到mSoundPool变量里。回到刚才的函数,我们继续看startThreads函数。

bool SoundPool::startThreads(){   createThreadEtc(beginThread, this, "SoundPool");    if (mDecodeThread== NULL)        mDecodeThread= new SoundPoolThread(this);    returnmDecodeThread != NULL;}


从这个函数可以看出每个SoundPool都有一个decode线程(只能有一个),好了,这里只是新建的一个SoundPoolThread,然后就返回了。这样SoundPool在native的初始化就完成了,我们有了一些SoundChannel,还有一个decode线程。

 

我们回到刚才的SoundClips里

            mSoundIDs= new int[SOUND_RES.length];           mSoundIDReady = new boolean[SOUND_RES.length];            for (int i= 0; i < SOUND_RES.length; i++) {                mSoundIDs[i] =mSoundPool.load(mContext, SOUND_RES[i], 1);               mSoundIDReady[i] = false;            }


这里会初始化一个int数组,然后调用SoundPool的load函数,我们在看看它做了神马。

在SoundPool中这个load函数有多个重载对应不同的资源类型,比如内置资源就是用资源id,而外置资源就是用文件路径,或者asset文件路径,或者文件打开后的FD标识。当然这些个玩样儿最终还是调用到native层,我们这里就看下打开文件的那个函数。

    /**     * Load the soundfrom the specified path.     *     * @param path thepath to the audio file     * @param prioritythe priority of the sound. Currently has no effect. Use     *                 a value of 1 for futurecompatibility.     * @return a soundID. This value can be used to play or unload the sound.     */    public intload(String path, int priority)    {        // passnetwork streams to player        if(path.startsWith("http:"))            return_load(path, priority);         // try localpath        int id = 0;        try {            File f =new File(path);           ParcelFileDescriptor fd = ParcelFileDescriptor.open(f,ParcelFileDescriptor.MODE_READ_ONLY);            if (fd !=null) {                id =_load(fd.getFileDescriptor(), 0, f.length(), priority);               fd.close();            }        } catch(java.io.IOException e) {            Log.e(TAG,"error loading " + path);        }        return id;    }static intandroid_media_SoundPool_load_FD(JNIEnv *env, jobject thiz,jobject fileDescriptor,        jlong offset,jlong length, jint priority){   ALOGV("android_media_SoundPool_load_FD");    SoundPool *ap =MusterSoundPool(env, thiz);    if (ap == NULL)return 0;    returnap->load(jniGetFDFromFileDescriptor(env, fileDescriptor),           int64_t(offset), int64_t(length), int(priority));}



函数MusterSoundPool用来获取之前新建的SoundPool类,并且调用其load函数

 

int SoundPool::load(const char* path, int priority){    sp<Sample>sample = new Sample(++mNextSampleID, path);   mSamples.add(sample->sampleID(), sample);    doLoad(sample);    returnsample->sampleID();}


在load函数中,首先会新建一个名叫sample的类且跟上两个参数,sample的id号,以及加载文件路径,并且将这个新建的类保存在mapping列表mSamples中,即每一个需加载的文件有一个Sample类对应,索引号即为其id号。接下来就是加载的过程了,具体看doLoad函数。

void SoundPool::doLoad(sp<Sample>& sample){   sample->startLoad();   mDecodeThread->loadSample(sample->sampleID());}void startLoad() { mState = LOADING; }void SoundPoolThread::loadSample(int sampleID) {   write(SoundPoolMsg(SoundPoolMsg::LOAD_SAMPLE, sampleID));}void SoundPoolThread::write(SoundPoolMsg msg) {    Mutex::Autolocklock(&mLock);    while(mMsgQueue.size() >= maxMessages) {       mCondition.wait(mLock);    }     // if thread isquitting, don't add to queue    if (mRunning) {       mMsgQueue.push(msg);        mCondition.signal();    }}int SoundPoolThread::run() {   ALOGV("run");    for (;;) {        SoundPoolMsgmsg = read();        switch(msg.mMessageType) {        caseSoundPoolMsg::LOAD_SAMPLE:           doLoadSample(msg.mData);            break;        default:           ALOGW("run: Unrecognized message %d\n",                   msg.mMessageType);            break;        }    }}


首先调用Sample类中的startLoad函数来设置当前sample的状态,这里即LOADING状态。这里我省略了些不是非常重要的代码。在loadSample函数中会将当前的sampleid号打包成一个消息并调用write函数写到消息队列中,如果消息队列满了会稍微等等,如果还没满则会加入队列并通知取消息的线程(这里的线程就是我们之前创建的mDecodeThread,如果大家不记得了,可以搜索下之前的内容)。在这个线程中会读取消息的类型,这里为LOAD_SAMPLE,并调用doLoadSample函数,参数即为sampleid号。我们看下这个函数。

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));}


函数会先根据id号找到相对应的Sample类并调用doLoad函数(说实话,我觉得这里转了一大圈才开始加载数据只是为了调度来平衡磁盘操作,大家也可以自己琢磨下。)

status_t Sample::doLoad(){     ALOGV("Startdecode");    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;    }    mData = p;mSize = p->size();


这个函数有点长,但是也没做很多事。首先他会进行decode即解码文件成波形文件,可以理解为wav文件纯数据。(预加载以备播放,SoundPool的好处之一)。这里还做了些判断,比如采样率不能大于48000,声道数不能小于1或者大于2,然后就保存了这个加载数据。加载完数据后还会通过调用notify函数来调用之前设置的回调函数通知上层,具体实现用户可以自己实现。

我们好像已经走得很远了,让我们回到相机SoundClip里。这时我们已经完成了load函数并且有一份加载好的数据,并且有一个对应其Sample的id号。然后我们就要开始播放了。

mSoundPool.play(mSoundIDs[index], 1f, 1f, 0, 0, 1f);


其实播放也很简单,只需要知道Sampleid号,然后直接调用SoundPool的play函数即可。我们看下play做了些什么。

frameworks/base/media/java/android/media/SoundPool.javapublic native final int play(int soundID, float leftVolume,float rightVolume,            intpriority, int loop, float rate);frameworks/base/media/jni/soundpool/android_media_SoundPool.cppandroid_media_SoundPool_play(JNIEnv *env, jobject thiz, jintsampleID,        jfloatleftVolume, jfloat rightVolume, jint priority, jint loop,        jfloat rate){    SoundPool *ap =MusterSoundPool(env, thiz);return ap->play(sampleID,leftVolume, rightVolume, priority, loop, rate);frameworks/av/media/libmedia/SoundPool.cppint SoundPool::play(int sampleID, float leftVolume, floatrightVolume,        int priority,int loop, float rate){     // is sampleready?    sample =findSample(sampleID);     // allocate achannel    channel =allocateChannel_l(priority);     // no channelallocated - return 0    if (!channel) {        ALOGV("Nochannel allocated");        return 0;    }     channelID =++mNextChannelID;    channel->play(sample, channelID, leftVolume, rightVolume, priority,loop, rate);    return channelID;}



我们一路从java层调用到c++,中间的native只是打打酱油,我们具体看c++层的play函数。这里我们会看到一个新东西Channel,这个东西你可以暂时理解只是为播放用的,我同事形象的比作他是大炮,sample就是炮弹,想听声音就打炮。

 

首先函数会找炮弹,即用sampleid号找sample,找到了sample就意味着我们有了音频数据。之后会调用allocateChannel_l并有个权限的参数。

SoundChannel* SoundPool::allocateChannel_l(int priority){   List<SoundChannel*>::iterator iter;    SoundChannel* channel= NULL;     // allocate achannel    if(!mChannels.empty()) {        iter =mChannels.begin();        if (priority>= (*iter)->priority()) {            channel =*iter;           mChannels.erase(iter);           ALOGV("Allocated active channel");        }    }     // update priorityand put it back in the list    if (channel) {       channel->setPriority(priority);        for (iter =mChannels.begin(); iter != mChannels.end(); ++iter) {            if(priority < (*iter)->priority()) {                break;            }        }       mChannels.insert(iter, channel);    }    return channel;}




这个函数其实就是一个算法用来更新channel的,它首先会判断我们传进来的权限值,并找到最接近的那个channel(小于等于)作为返回值,然后再找到最接近的另一个channel(大于等于),并插在它后面。这里的mChannels是我们创建SoundPool时一并初始化的列表(可以理解为我们有几门打炮)。找到了适合弹药口径的打炮,就需要开炮了。让我们看下channel的play函数。
// call with sound pool lock heldvoid SoundChannel::play(const sp<Sample>& sample,int nextChannelID, float leftVolume,        floatrightVolume, int priority, int loop, float rate){    AudioTrack*oldTrack;    AudioTrack*newTrack;    status_t status;     { // scope for thelock       Mutex::Autolock lock(&mLock);         // if not idle,this voice is being stolen        if (mState !=IDLE) {           ALOGV("channel %d stolen - event queued for channel %d",channelID(), nextChannelID);           mNextEvent.set(sample, nextChannelID, leftVolume, rightVolume, priority,loop, rate);            stop_l();            return;        }         intnumChannels = sample->numChannels();         // do notcreate a new audio track if current track is compatible with sample parameters#ifdef USE_SHARED_MEM_BUFFER        newTrack = newAudioTrack(streamType, sampleRate, sample->format(),               channels, sample->getIMemory(), AUDIO_OUTPUT_FLAG_NONE, callback,userData);#else        newTrack = newAudioTrack(streamType, sampleRate, sample->format(),                channels, frameCount,AUDIO_OUTPUT_FLAG_FAST, callback, userData,               bufferFrames);#endif        oldTrack =mAudioTrack;        mAudioTrack->start();     } exit:    ALOGV("deleteoldTrack %p", oldTrack);    delete oldTrack;    if (status !=NO_ERROR) {        deletenewTrack;        mAudioTrack =NULL;    }}



这个函数也有点长,我们一步步分析。首先会判断这个channel是不是正在播放(这种情况存在于当所有channel都在开炮的时候,我们之后再讨论这个),假设这时候channel还没工作,然后我们就会新建一个AudioTrack(有一定安卓audio开发经验的同学都知道他是播放声音最原始的单元,有了它就能给设配写数据等)。这里我们可以看到两端初始化代码并且使用宏来隔开,区别在于一个是共享内存的AudioTrack,另一个使用FastTrack,即低延时的机制。(这个东西我也不是特别明白,似乎是4.1新加的,大家可以百度学习学习),这里还给了一个回调函数参数用来从文件搬数据到AudioTrack,这个回调函数具体我们就不分析的,简单来说就是AudioTrack定时调用回调函数申请数据,回调函数只管读取数据返回给它。在初始化完AudioTrack之后就调用start函数让它工作了,这个时候声音也就出来了。最后这个play函数还不忘把老的AudioTrack给清空防止内存侧漏,哈哈。

 

话说,到这里SoundPool基本播放声音的分析就结束了,但是还有个特殊情况我们得分析下。即如果所有的channel都在播放,但应用程序又想播放新的数据会怎么处理呢?这时候就会用到之前play函数里那个判断,即是否当前状态不为空闲状态。
        // if not idle,this voice is being stolen        if (mState !=IDLE) {           ALOGV("channel %d stolen - event queued for channel %d",channelID(), nextChannelID);           mNextEvent.set(sample, nextChannelID, leftVolume, rightVolume, priority,loop, rate);            stop_l();            return;        }





如果当前channel已经在播放,又有一个新的sample想被播放,则会进入这个条件语句。这里会先设置一个新的类SoundEvent,即mNextEvent,这个类会保存想要播放的sample以及channel的id号等一些参数。然后停止当前播放的channel,这一过程称之为偷窃。这里channel的id号不是当前播放的channel的id号,正确的讲每次播放新sample时的channel的id号都不一样。然后我们看下stop_l函数。
// call with lock held and sound pool lock heldvoid SoundChannel::stop_l(){    if (doStop_l()) {       mSoundPool->done_l(this);    }}// call with lock heldbool SoundChannel::doStop_l(){    if (mState !=IDLE) {        setVolume_l(0,0);       mAudioTrack->stop();        return true;    }    return false;}




这里会先调用doStop_l来停止当前的播放。然后继续调用SoundPool的done_l函数。
void SoundPool::done_l(SoundChannel* channel){   ALOGV("done_l(%d)", channel->channelID());    // if"stolen", play next event    if(channel->nextChannelID() != 0) {       ALOGV("add to restart list");       addToRestartList(channel);}



在这个函数中,会先判断是否这个channel是否被偷窃了,即调用nextChannelID函数来判断。

int nextChannelID() { return mNextEvent.channelID(); }



我们可以看到其实就是判断是否mNextEvent是否有id值保存,之前我们已经保存了一份,所以这里的判断就满足了。然后会把当前的channel放到restart列表中。
void SoundPool::addToRestartList(SoundChannel* channel){       mRestart.push_back(channel);       mCondition.signal();}


添加完channel到mRestart之后,又会向mCondition发出信号,另一个线程肯定已经等了很久了。这个线程我之前没有提到,当新建SoundPool的时候,会调用createThreadEtc函数来创建一条SoundPool自己的线程并执行,这时候它就知道要开干了。
int SoundPool::run(){    while (!mQuit) {       mCondition.wait(mRestartLock);         while(!mRestart.empty()) {           SoundChannel* channel;           List<SoundChannel*>::iterator iter = mRestart.begin();            channel =*iter;           mRestart.erase(iter);            if (channel != 0) {               Mutex::Autolock lock(&mLock);               channel->nextEvent();            }


这个线程里会从mRestart列表中一个个拿出channel并调用nextEvent函数。
void SoundChannel::nextEvent(){         nextChannelID= mNextEvent.channelID();        if(nextChannelID  == 0) {            ALOGV("stolen channel has noevent");            return;        }         sample =mNextEvent.sample();     play(sample,nextChannelID, leftVolume, rightVolume, priority, loop, rate);}



这个函数首先会读取channel的mNextEvent,我们在之前已经设置过一些参数了,包括了sample已经channel的id号等。然后直接调用了之前我们分析的play函数,这样作为play函数来说这就算是一个新的sample需要播放了,然后么,声音就又出来了。

 

当然什么事都有始有终,有播放就有停止暂停神马的,这些功能实现比较简单,我这里就简单说下,所有sample在播放时都有个channel会返回给应用层,所以停止暂时基本上就是调用那个channel的stop和pause函数,接着对AudioTrack进行函数调用操作。但是如果是被偷窃的channel呢?这个也不必担心,在停止暂停的时候如果当前的channel的id不存在,它会继续找它的mNextEvent里channel的id号,最终还是能达到目的的。

 

至此,对SoundPool的介绍就到此结束了,最后附上用XMind画的示意图。


原创粉丝点击