好吧,我们今天来聊聊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.cpp static jint android_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 int android_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.java public native final int play(int soundID, float leftVolume,float rightVolume, intpriority, int loop, float rate); frameworks/base/media/jni/soundpool/android_media_SoundPool.cpp android_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.cpp int 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; }
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; }
// call with sound pool lock held void 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; } }
// 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; }
// call with lock held and sound pool lock held void SoundChannel::stop_l() { if (doStop_l()) { mSoundPool->done_l(this); } } // call with lock held bool SoundChannel::doStop_l() { if (mState !=IDLE) { setVolume_l(0,0); mAudioTrack->stop(); return true; } return false; }
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); }
int nextChannelID() { return mNextEvent.channelID(); }
void SoundPool::addToRestartList(SoundChannel* channel) { mRestart.push_back(channel); mCondition.signal(); }
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(); }
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); }