cocos2dx音效的实现分析:
上面那个图是cocos2dx音效部分的实现结构图,总体的思想是:
使用同一个SimpleAudioEngine.h头文件,然后在不同平台下对应不同
的实现文件,不同平台编译不同的实现文件,这就要求这个类的函数定义
成员在各个平台下统一。其实这也可以理解为一种跨平台的实现方式。
这里分析android部分:
1、预加载背景音乐 void SimpleAudioEngine::preloadBackgroundMusic(const char* pszFilePath) { std::string fullPath = getFullPathWithoutAssetsPrefix(pszFilePath); preloadBackgroundMusicJNI(fullPath.c_str()); } -->> //得到音乐文件的路径,如果是包里的文件,则去掉assets/前缀 static std::string getFullPathWithoutAssetsPrefix(const char* pszFilename) { // Changing file path to full path //获取文件的路径,以后有时间会分析下CCFileUtils这个类的实现 std::string fullPath = CCFileUtils::sharedFileUtils()->fullPathForFilename(pszFilename); // Removing `assets` since it isn't needed for the API of playing sound. size_t pos = fullPath.find("assets/"); if (pos == 0) { fullPath = fullPath.substr(strlen("assets/")); } return fullPath; } -->> preloadBackgroundMusicJNI: //这里其实就是调用jni的方法: void preloadBackgroundMusicJNI(const char *path) { // void playBackgroundMusic(String,boolean) JniMethodInfo methodInfo; if (! getStaticMethodInfo(methodInfo,"preloadBackgroundMusic","(Ljava/lang/String;)V")) { return; } jstring stringArg = methodInfo.env->NewStringUTF(path); methodInfo.env->CallStaticVoidMethod(methodInfo.classID,methodInfo.methodID,stringArg); methodInfo.env->DeleteLocalRef(stringArg); methodInfo.env->DeleteLocalRef(methodInfo.classID); } -->>java端的代码: public static void preloadBackgroundMusic(final String pPath) { Cocos2dxHelper.sCocos2dMusic.preloadBackgroundMusic(pPath); } -->> public void preloadBackgroundMusic(final String pPath) { if ((this.mCurrentPath == null) || (!this.mCurrentPath.equals(pPath))) { // preload new background music // release old resource and create a new one if (this.mBackgroundMediaPlayer != null) { this.mBackgroundMediaPlayer.release(); } this.mBackgroundMediaPlayer = this.createMediaplayer(pPath); // record the path this.mCurrentPath = pPath; } } -->> private MediaPlayer createMediaplayer(final String pPath) { //其实就是创建了一个android下的MediaPlayer媒体播放实例,后面的播放,暂停之类 //的,都是这个实例去控制。 MediaPlayer mediaPlayer = new MediaPlayer(); try { if (pPath.startsWith("/")) { final FileInputStream fis = new FileInputStream(pPath); mediaPlayer.setDataSource(fis.getFD()); fis.close(); } else { final AssetFileDescriptor assetFileDescritor = this.mContext.getAssets().openFd(pPath); mediaPlayer.setDataSource(assetFileDescritor.getFileDescriptor(),assetFileDescritor.getStartOffset(),assetFileDescritor.getLength()); } mediaPlayer.prepare(); mediaPlayer.setVolume(this.mLeftVolume,this.mRightVolume); } catch (final Exception e) { mediaPlayer = null; Log.e(Cocos2dxMusic.TAG,"error: " + e.getMessage(),e); } return mediaPlayer; } -->>播放背景音乐,这里可以不用预先加载,就直接播放。 void SimpleAudioEngine::playBackgroundMusic(const char* pszFilePath,bool bLoop) { std::string fullPath = getFullPathWithoutAssetsPrefix(pszFilePath); playBackgroundMusicJNI(fullPath.c_str(),bLoop); }
我在(1)已经分析了一些东西,这里接着分析,这一篇我们主要分析背景音乐文件的播放, 还是基于android平台: 1、 这里只是背景音乐的预加载,为什么要进行预加载呢? 主要是加载音乐文件是比较耗时的,如果我们没有预加载就直接播放也是可以的, 但是会有一定的延时,因为如果没有预加载,就直接播放,也是会先进行加载音乐文件, 然后进行播放。 void SimpleAudioEngine::preloadBackgroundMusic(const char* pszFilePath) { std::string fullPath = getFullPathWithoutAssetsPrefix(pszFilePath); preloadBackgroundMusicJNI(fullPath.c_str()); } 其实加载背景音乐最终调用android端: public void preloadBackgroundMusic(final String pPath) { if ((this.mCurrentPath == null) || (!this.mCurrentPath.equals(pPath))) { // preload new background music // release old resource and create a new one // 如果我们播放的是一个新的背景音乐文件,那么我们需要先释放旧的播放器,然后创建一个新的 // Releases resources associated with this MediaPlayer object. if (this.mBackgroundMediaPlayer != null) { this.mBackgroundMediaPlayer.release(); } //创建一个播放器即MediaPlayer类的实例 this.mBackgroundMediaPlayer = this.createMediaplayer(pPath); // record the path // 记录当前播放的背景音乐文件,因为下次如果播放的是同一个音乐 // 文件,那么我们就可以直接进行播放了,不用再重新创建MediaPlayer类的实例 this.mCurrentPath = pPath; } } ----->>> /** * create mediaplayer for music * * @param pPath * the pPath relative to assets * @return */ private MediaPlayer createMediaplayer(final String pPath) { MediaPlayer mediaPlayer = new MediaPlayer(); try { //对绝对路径和包里的路径进行区分处理,当最终的目的就是设置播放源 if (pPath.startsWith("/")) { final FileInputStream fis = new FileInputStream(pPath); mediaPlayer.setDataSource(fis.getFD()); fis.close(); } else { final AssetFileDescriptor assetFileDescritor = this.mContext.getAssets().openFd(pPath); mediaPlayer.setDataSource(assetFileDescritor.getFileDescriptor(),assetFileDescritor.getLength()); } //播放器前需要做些准备工作,这个只是android的api,不明白的话,查下文档。 /** * Prepares the player for playback,synchronously. * * After setting the datasource and the display surface,you need to either * call prepare() or prepareAsync(). For files,it is OK to call prepare(),* which blocks until MediaPlayer is ready for playback. * * @throws IllegalStateException if it is called in an invalid state */ mediaPlayer.prepare(); //设置声音音量 mediaPlayer.setVolume(this.mLeftVolume,e); } return mediaPlayer; } 2、 音乐播放函数 //pszFilePath: 音乐文件名 //bLoop: 是否循环播放,音乐文件我们一般设置为循环播放,看具体情况 void SimpleAudioEngine::playBackgroundMusic(const char* pszFilePath,bLoop); } --->>> 最终都会调用到android端的playBackgroundMusic函数,并把文件路径,是否循环播放传进来 public void playBackgroundMusic(final String path,final boolean isLoop) { if (mCurrentPath == null) { // it is the first time to play background music or end() was called // 如果以前没有播放过音乐文件,那么重新创建一个,上面的英文注释很清楚 mBackgroundMediaPlayer = createMediaplayer(path); mCurrentPath = path; } else { if (!mCurrentPath.equals(path)) { // play new background music //如果这次播放的音乐文件和上次的不同,即是一个新的音乐文件, //那么就需要先释放掉旧的,然后创建一个新的。 // release old resource and create a new one if (mBackgroundMediaPlayer != null) { mBackgroundMediaPlayer.release(); } mBackgroundMediaPlayer = createMediaplayer(path); // record the path mCurrentPath = path; } } if (mBackgroundMediaPlayer == null) { Log.e(Cocos2dxMusic.TAG,"playBackgroundMusic: background media player is null"); } else { try { // if the music is playing or paused,stop it // 对playing or paused,stop三种情况进行分别处理 if (mPaused) { //如果是暂停状态,那么就把播放进度设置到0,然后开始。 //这就意味着,如果我们调用了暂停,然后又调用play,那么 //音乐将会从头开始播放,而不是从暂停的地方接着播放。 /** * Seeks to specified time position. * * @param msec the offset in milliseconds from the start to seek to * @throws IllegalStateException if the internal player engine has not been * initialized */ mBackgroundMediaPlayer.seekTo(0); /** * Starts(开始) or resumes playback(恢复播放). If playback had prevIoUsly been paused,* playback will continue from where it was paused. If playback had * been stopped,or never started before,playback will start at the * beginning. * start函数两个功能,一个是开始播放,一个是恢复播放 * 1、如果stopped或者never started before(第一次开始),那么就从头开始播放 * 2、如果paused即暂停,那么将会从暂停的地方接着播放。 */ mBackgroundMediaPlayer.start(); } else if (mBackgroundMediaPlayer.isPlaying()) { //如果处于播放状态,则回到开始,从头播放 mBackgroundMediaPlayer.seekTo(0); } else { //如果处于stop状态,则从新播放,上面已经说明了start函数的两个作用 mBackgroundMediaPlayer.start(); } /* 总结:其实对上面三种情况分别处理,最终达到的效果都是一样的, 那就是从头开始播放背景音乐文件。 */ //设置是否循环播放 mBackgroundMediaPlayer.setLooping(isLoop); //mPaused 表示设为false,表示不处于暂停状态 mPaused = false; //是否循环播放记录 mIsLoop = isLoop; } catch (final Exception e) { Log.e(Cocos2dxMusic.TAG,"playBackgroundMusic: error state"); } } } 3、总结: 从上面的分析我们可以知道,如果预先进行加载即先创建一个MediaPlayer, 那么我们播放时可以直接进行播放,如果我们我们没有提前进行预加载, 而是直接调用playBackgroundMusic函数,也可以进行播放,只不过会有一个创建 MediaPlayer的过程,会有一些时间上的延时。
我在(2)已经分析了背景音乐文件的预加载preloadBackgroundMusic和播放playBackgroundMusic两个函数, 这里接着分析,还是基于android平台: 1、 //暂停函数,用于音乐的暂停 void SimpleAudioEngine::pauseBackgroundMusic() { //在SimpleAudioEngineJni.cpp源文件中定义 pauseBackgroundMusicJNI(); } //--pauseBackgroundMusicJNI--->>> void pauseBackgroundMusicJNI() { // void pauseBackgroundMusic() JniMethodInfo methodInfo; if (! getStaticMethodInfo(methodInfo,"pauseBackgroundMusic","()V")) { return; } //通过jni调用java端的函数,调用的是Cocos2dxHelper类中的 /* public static void pauseBackgroundMusic() { Cocos2dxHelper.sCocos2dMusic.pauseBackgroundMusic(); } */ methodInfo.env->CallStaticVoidMethod(methodInfo.classID,methodInfo.methodID); methodInfo.env->DeleteLocalRef(methodInfo.classID); } 最终调用的是Cocos2dxMusic类中的 public void pauseBackgroundMusic() { //mBackgroundMediaPlayer在(2)中有分析过,创建的MediaPlayer实例 if (this.mBackgroundMediaPlayer != null && this.mBackgroundMediaPlayer.isPlaying()) { this.mBackgroundMediaPlayer.pause(); this.mPaused = true; //是否暂停标志 } } 2、 恢复播放 void SimpleAudioEngine::resumeBackgroundMusic() { resumeBackgroundMusicJNI(); } 其实和上面暂停的调用过程是一样的,就不分析了,直接进入java端看最终调用的函数,public void resumeBackgroundMusic() { //这里只有处于暂停状态时即mPaused变量为true时,才会接着 //上次播放的位置开始播放 if (this.mBackgroundMediaPlayer != null && this.mPaused) { this.mBackgroundMediaPlayer.start(); this.mPaused = false; //把暂停标志位设置false } } 3、 从头开始播放音乐文件 void rewindBackgroundMusicJNI() { // void rewindBackgroundMusic() JniMethodInfo methodInfo; if (! getStaticMethodInfo(methodInfo,"rewindBackgroundMusic","()V")) { return; } methodInfo.env->CallStaticVoidMethod(methodInfo.classID,methodInfo.methodID); methodInfo.env->DeleteLocalRef(methodInfo.classID); } --->>//java端函数 //这个函数最终也是会调用playBackgroundMusic函数,但是和playBackgroundMusic有一点不同, //playBackgroundMusic需要传入音乐文件名,可以播放和上一次一样的音乐也可以和是上一次不一样的 //音乐,但是rewindBackgroundMusic函数只有在mBackgroundMediaPlayer不为null时才执行, //也就是必须播放过音乐,且播放的是上次播放的音乐,只不过这次是从头开始播放 public void rewindBackgroundMusic() { if (this.mBackgroundMediaPlayer != null) { playBackgroundMusic(mCurrentPath,mIsLoop); } } 4、停止播放音乐文件 void stopBackgroundMusicJNI() { // void stopBackgroundMusic() JniMethodInfo methodInfo; if (! getStaticMethodInfo(methodInfo,"stopBackgroundMusic",methodInfo.methodID); methodInfo.env->DeleteLocalRef(methodInfo.classID); } --->>>//java端函数: public void stopBackgroundMusic() { if (this.mBackgroundMediaPlayer != null) { mBackgroundMediaPlayer.release(); //不太明白这里为什么有从新创建了MediaPlayer实例 //可能是一些特殊情况下会出现问题? mBackgroundMediaPlayer = createMediaplayer(mCurrentPath); // should set the state,if not,the following sequence will be error // play -> pause -> stop -> resume //为什么设置mPaused标志,直接看上面的英文注释 this.mPaused = false; } } 5、 返回是否处于播放状态 bool isBackgroundMusicPlayingJNI() { // boolean rewindBackgroundMusic() JniMethodInfo methodInfo; jboolean ret = false; if (! getStaticMethodInfo(methodInfo,"isBackgroundMusicPlaying","()Z")) { return ret; } ret = methodInfo.env->CallStaticBooleanMethod(methodInfo.classID,methodInfo.methodID); methodInfo.env->DeleteLocalRef(methodInfo.classID); return ret; } --->>>//java端函数,没什么东西 public boolean isBackgroundMusicPlaying() { boolean ret = false; if (this.mBackgroundMediaPlayer == null) { ret = false; } else { ret = this.mBackgroundMediaPlayer.isPlaying(); } return ret; } 6、获取播放声音音量值 float getBackgroundMusicVolumeJNI() { // float getBackgroundMusicVolume() JniMethodInfo methodInfo; jfloat ret = -1.0; if (! getStaticMethodInfo(methodInfo,"getBackgroundMusicVolume","()F")) { return ret; } ret = methodInfo.env->CallStaticFloatMethod(methodInfo.classID,methodInfo.methodID); methodInfo.env->DeleteLocalRef(methodInfo.classID); return ret; } ------->>>//java public float getBackgroundVolume() { if (this.mBackgroundMediaPlayer != null) { return (this.mLeftVolume + this.mRightVolume) / 2; } else { return 0.0f; } } 7、设置声音音量值 void setBackgroundMusicVolumeJNI(float volume) { // void setBackgroundMusicVolume() JniMethodInfo methodInfo; if (! getStaticMethodInfo(methodInfo,"setBackgroundMusicVolume","(F)V")) { return ; } methodInfo.env->CallStaticVoidMethod(methodInfo.classID,volume); methodInfo.env->DeleteLocalRef(methodInfo.classID); } ---->>>java public void setBackgroundVolume(float pVolume) { if (pVolume < 0.0f) { pVolume = 0.0f; } if (pVolume > 1.0f) { pVolume = 1.0f; } this.mLeftVolume = this.mRightVolume = pVolume; if (this.mBackgroundMediaPlayer != null) { this.mBackgroundMediaPlayer.setVolume(this.mLeftVolume,this.mRightVolume); } } 8、 end函数,这个一般在退出游戏是调用,关掉所有的音乐和音效。 void endJNI() { // void end() JniMethodInfo methodInfo; if (! getStaticMethodInfo(methodInfo,"end","()V")) { return ; } methodInfo.env->CallStaticVoidMethod(methodInfo.classID,methodInfo.methodID); methodInfo.env->DeleteLocalRef(methodInfo.classID); } --->>>//java端函数 public static void end() { Cocos2dxHelper.sCocos2dMusic.end(); //背景音乐文件的处理 Cocos2dxHelper.sCocos2dSound.end(); } ---->>>>/////背景音乐文件的处理 public void end() { if (this.mBackgroundMediaPlayer != null) { this.mBackgroundMediaPlayer.release(); } //把所有的变量恢复初始值 /* private void initData() { this.mLeftVolume = 0.5f; this.mRightVolume = 0.5f; this.mBackgroundMediaPlayer = null; this.mPaused = false; this.mCurrentPath = null; } */ this.initData(); }