Cocos2d-x中SimpleAudioEngine播放多個背景音樂時被覆蓋的問題

前段時間,做小示例的時候偶然發(fā)現(xiàn)了個問題(也許是我沒仔細了解過SimpleAudioEngine):

  1. 使用SimpleAudioEngine來播放背景音樂A
  • 然后音樂開始播放了,使用preloadBackgroundMusic加載音樂B
  • 然后使用isBackgroundMusicPlaying來查詢時,返回值為false
  • 然后使用stopBackgroundMusic來關(guān)閉背景音樂時,發(fā)現(xiàn)根本關(guān)不掉

從操作可以看出來,出問題的關(guān)鍵就在于,播放背景音樂時,進行了另外一個背景音樂的預(yù)加載,然而SimpleAudioEngine中,背景不能同時存在多個。


既然出現(xiàn)了問題,那就來看看源碼是怎么寫的(cocos2d-x 3.10)。

背景音樂預(yù)加載的代碼:

//audio/ios/SimpleAudioEngine.mm
void SimpleAudioEngine::preloadBackgroundMusic(const char* pszFilePath)
{
    // Changing file path to full path
    std::string fullPath = FileUtils::getInstance()->fullPathForFilename(pszFilePath);
    static_preloadBackgroundMusic(fullPath.c_str());
}

這個函數(shù)中,重點是第二句,調(diào)用了一個靜態(tài)方法static_preloadBackgroundMusic來加載背景音樂文件。這個方法的源碼:

//audio/ios/SimpleAudioEngine.mm
static void static_preloadBackgroundMusic(const char* pszFilePath)
{
    [[SimpleAudioEngine sharedEngine] preloadBackgroundMusic: [NSString stringWithUTF8String: pszFilePath]];
}

看來是調(diào)用了ObjC封裝的接口,繼續(xù)往下看:

//audio/ios/SimpleAudioEngine_objc.m
-(void) preloadBackgroundMusic:(NSString*) filePath {
    [am preloadBackgroundMusic:filePath];
}

這里看到,在Objc的接口中調(diào)用到了一個對象am的preloadBackgroundMusic方法。這個am,我們在這個.m文件的頭部可以看到,它是一個CDAudioManager對象。所以這個對象里面的預(yù)加載是如何實現(xiàn)的呢:

//audio/ios/CDAudioManager.m
//Load background music ready for playing
-(void) preloadBackgroundMusic:(NSString*) filePath
{
    [self.backgroundMusic load:filePath];    
} 

我們在這個文件中能夠找到很多backgroundMusic,不能確定這里用到的是哪一個backgroundMusic。所以往上找,能看到它是屬于
CDAudioManager,然后在頭文件中可以看到:

@interface CDAudioManager : NSObject <CDLongAudioSourceDelegate, CDAudioInterruptProtocol, AVAudioSessionDelegate> {
    CDSoundEngine        *soundEngine;
    CDLongAudioSource    *backgroundMusic;
    NSMutableArray        *audioSourceChannels;
//....后面省略

其實,在這里我們就能看到問題了,這個CDAudioManager所持有的CDLongAudioSource引用只有一個,也就是說,表示我們的BackgroundMusic的資源對象只有一個。繼續(xù)看這個CDLongAudioSource的load函數(shù)是怎么實現(xiàn)的:

-(void) load:(NSString*) filePath {
    //We have already loaded a file previously, check if we are being asked to load the same file
    if (state == kLAS_Init || ![filePath isEqualToString:audioSourceFilePath]) {
        CDLOGINFO(@"Denshion::CDLongAudioSource - Loading new audio source %@",filePath);
        //New file
        if (state != kLAS_Init) {
            [audioSourceFilePath release];//Release old file path
            [audioSourcePlayer release];//Release old AVAudioPlayer, they can't be reused
        }
        audioSourceFilePath = [filePath copy];
        NSError *error = nil;
        NSString *path = [CDUtilities fullPathFromRelativePath:audioSourceFilePath];
        audioSourcePlayer = [(AVAudioPlayer*)[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:&error];
        if (error == nil) {
            [audioSourcePlayer prepareToPlay];
            audioSourcePlayer.delegate = self;
            if (delegate && [delegate respondsToSelector:@selector(cdAudioSourceFileDidChange:)]) {
                //Tell our delegate the file has changed
                [delegate cdAudioSourceFileDidChange:self];
            }    
        } else {
            CDLOG(@"Denshion::CDLongAudioSource - Error initialising audio player: %@",error);
        }    
    } else {
        //Same file - just return it to a consistent state
        [self pause];
        [self rewind];
    }
    audioSourcePlayer.volume = volume;
    audioSourcePlayer.numberOfLoops = numberOfLoops;
    state = kLAS_Loaded;
}    

在load函數(shù)中,如果新的Path與本對象中的屬性audioSourceFilePath不同,而且之前已經(jīng)加載過一個背景音樂,state不再是kLAS_Init,此時會釋放并覆蓋舊的audioSourcePlayer和audioSourceFilePath。

即使你先加載了音樂A,再加載音樂B,然后再重新加載一次音樂A,雖然Path與之前的A相同,但是控制的并不再是同一個對象,因為“舊”的引用總是會被丟棄。

到這里并沒有結(jié)束,我們還需要確定一下,播放背景音樂的時候和這個audioSource有關(guān)。看看playBackgroundMusic的代碼:

//audio/ios/SimpleAudioEngine.mm
void SimpleAudioEngine::playBackgroundMusic(const char* pszFilePath, bool bLoop)
{
    // Changing file path to full path
    std::string fullPath = FileUtils::getInstance()->fullPathForFilename(pszFilePath);
    static_playBackgroundMusic(fullPath.c_str(), bLoop);
}

同樣,去找static_playBackgroundMusic方法:

static void static_playBackgroundMusic(const char* pszFilePath, bool bLoop)
{
    [[SimpleAudioEngine sharedEngine] playBackgroundMusic: [NSString stringWithUTF8String: pszFilePath] loop: bLoop];
}

然后找到Obj-c的接口:

-(void) playBackgroundMusic:(NSString*) filePath
{
    [am playBackgroundMusic:filePath loop:TRUE];
}
-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop
{
    [am playBackgroundMusic:filePath loop:loop];
}

這里可以看到進行了一個重載,playBackgroundMusic方法的默認loop屬性為true。然后繼續(xù)看am中的代碼:

-(void) playBackgroundMusic:(NSString*) filePath loop:(BOOL) loop
{
    [self.backgroundMusic load:filePath];

 if (loop) {
  [self.backgroundMusic setNumberOfLoops:-1];
 } else {
  [self.backgroundMusic setNumberOfLoops:0];
 }

 if (!willPlayBackgroundMusic || _mute) {
  CDLOGINFO(@"Denshion::CDAudioManager - play bgm aborted because audio is not exclusive or sound is muted");
  return;
 }

 [self.backgroundMusic play];
}

這里第一句就是調(diào)用CDLongAudioSource的load方法,也就是之前我們看到的preloadBakcgroundMusic函數(shù)中所做的操作。中間一段是進行l(wèi)oop設(shè)置,以及錯誤處理。最后調(diào)用了self.backgroundMusic(CDLongAudioSource)的play方法。


結(jié)論:
至始至終,SimpleAudioEngine中只持有了一個關(guān)于背景音樂的引用。所以,進行連續(xù)的preloadBackgroundMusic或者playBackgroundMusic,我們看到的現(xiàn)象都是丟失了對原來的背景音樂的控制(引用)。

使用SimpleAudioEngine播放背景音樂時,如果游戲中有多個場景,每個場景有不同的背景音樂,我們沒有必要去一次preload所有的BackgroundMusic文件,注意每次播放背景音樂前進行判斷,確保舊的背景音樂被停止

關(guān)于SimpleAudioEngine的測試代碼,從我的Github中可以看到。如有錯誤與不完整指出,還請小伙伴指出!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容