前段時間,做小示例的時候偶然發(fā)現(xiàn)了個問題(也許是我沒仔細了解過SimpleAudioEngine):
- 使用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中可以看到。如有錯誤與不完整指出,還請小伙伴指出!