[audio] Fixes some issues while stop/uncache right after play/preload on OSX/iOS platform. (#16147)

* [audio] Fixes some issues while stop/uncache right after play/preload on OSX/iOS platform.

* Better state control in AudioCache.

* Adds State enum class which contains INITIAL, LOADING, READY, FAILED states.
* AudioPlayer::destroy should check AudioCache::_isAudioLoaded flag, otherwise, app will freeze in cpp-tests/NewAudioEngineTest/6:Test invalid audio file.

* Should check whether state is INITIAL in the destructor of AudioCache.

* Adds test case for switching play state frequently.

* Skips invoking reading data callback if cache's state is INITIAL.

* Variables initialization for AudioCache class and fixes protential missing delete AL buffers.
This commit is contained in:
James Chen 2016-07-21 14:57:59 +08:00 committed by minggo
parent b8f8ff0d25
commit bb004c5e22
7 changed files with 640 additions and 282 deletions

View File

@ -22,12 +22,11 @@
THE SOFTWARE.
****************************************************************************/
#pragma once
#include "platform/CCPlatformConfig.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC
#ifndef __AUDIO_CACHE_H_
#define __AUDIO_CACHE_H_
#import <OpenAL/al.h>
#import <AudioToolbox/AudioToolbox.h>
@ -40,14 +39,49 @@
#define QUEUEBUFFER_NUM 3
#define QUEUEBUFFER_TIME_STEP 0.1
#define QUOTEME_(x) #x
#define QUOTEME(x) QUOTEME_(x)
#if defined(COCOS2D_DEBUG) && COCOS2D_DEBUG > 0
#define ALOGV(fmt, ...) printf("V/" LOG_TAG " (" QUOTEME(__LINE__) "): " fmt "\n", ##__VA_ARGS__)
#else
#define ALOGV(fmt, ...) do {} while(false)
#endif
#define ALOGD(fmt, ...) printf("D/" LOG_TAG " (" QUOTEME(__LINE__) "): " fmt "\n", ##__VA_ARGS__)
#define ALOGI(fmt, ...) printf("I/" LOG_TAG " (" QUOTEME(__LINE__) "): " fmt "\n", ##__VA_ARGS__)
#define ALOGW(fmt, ...) printf("W/" LOG_TAG " (" QUOTEME(__LINE__) "): " fmt "\n", ##__VA_ARGS__)
#define ALOGE(fmt, ...) printf("E/" LOG_TAG " (" QUOTEME(__LINE__) "): " fmt "\n", ##__VA_ARGS__)
#if defined(COCOS2D_DEBUG) && COCOS2D_DEBUG > 0
#define CHECK_AL_ERROR_DEBUG() \
do { \
GLenum __error = alGetError(); \
if (__error) { \
ALOGE("OpenAL error 0x%04X in %s %s %d\n", __error, __FILE__, __FUNCTION__, __LINE__); \
} \
} while (false)
#else
#define CHECK_AL_ERROR_DEBUG()
#endif
NS_CC_BEGIN
namespace experimental{
class AudioEngineImpl;
class AudioPlayer;
class AudioCache{
class AudioCache
{
public:
enum class State
{
INITIAL,
LOADING,
READY,
FAILED
};
AudioCache();
~AudioCache();
@ -56,7 +90,8 @@ public:
void addLoadCallback(const std::function<void(bool)>& callback);
protected:
void readDataTask();
void setSkipReadDataTask(bool isSkip) { _isSkipReadDataTask = isSkip; };
void readDataTask(unsigned int selfId);
void invokingPlayCallbacks();
@ -68,7 +103,7 @@ protected:
ALsizei _sampleRate;
float _duration;
int _bytesPerFrame;
AudioStreamBasicDescription outputFormat;
AudioStreamBasicDescription _outputFormat;
/*Cache related stuff;
* Cache pcm data when sizeInBytes less than PCMDATA_CACHEMAXSIZE
@ -85,16 +120,21 @@ protected:
UInt32 _queBufferFrames;
UInt32 _queBufferBytes;
bool _alBufferReady;
bool _loadFail;
std::mutex _callbackMutex;
std::mutex _playCallbackMutex;
std::vector< std::function<void()> > _playCallbacks;
std::vector< std::function<void()> > _callbacks;
// loadCallbacks doesn't need mutex since it's invoked only in Cocos thread.
std::vector< std::function<void(bool)> > _loadCallbacks;
std::mutex _readDataTaskMutex;
bool _exitReadDataTask;
State _state;
std::shared_ptr<bool> _isDestroyed;
std::string _fileFullPath;
unsigned int _id;
bool _isLoadingFinished;
bool _isSkipReadDataTask;
friend class AudioEngineImpl;
friend class AudioPlayer;
@ -103,6 +143,5 @@ protected:
}
NS_CC_END
#endif // __AUDIO_CACHE_H_
#endif

View File

@ -22,6 +22,8 @@
THE SOFTWARE.
****************************************************************************/
#define LOG_TAG "AudioCache"
#include "platform/CCPlatformConfig.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC
@ -34,6 +36,18 @@
#include "base/CCDirector.h"
#include "base/CCScheduler.h"
#define VERY_VERY_VERBOSE_LOGGING
#ifdef VERY_VERY_VERBOSE_LOGGING
#define ALOGVV ALOGV
#else
#define ALOGVV(...) do{} while(false)
#endif
namespace {
unsigned int __idIndex = 0;
}
#define INVALID_AL_BUFFER_ID 0xFFFFFFFF
#define PCMDATA_CACHEMAXSIZE 1048576
typedef ALvoid AL_APIENTRY (*alBufferDataStaticProcPtr) (const ALint bid, ALenum format, ALvoid* data, ALsizei size, ALsizei freq);
@ -56,225 +70,351 @@ using namespace cocos2d::experimental;
AudioCache::AudioCache()
: _dataSize(0)
, _format(-1)
, _duration(0.0f)
, _bytesPerFrame(0)
, _alBufferId(INVALID_AL_BUFFER_ID)
, _pcmData(nullptr)
, _bytesOfRead(0)
, _queBufferFrames(0)
, _queBufferBytes(0)
, _alBufferReady(false)
, _loadFail(false)
, _exitReadDataTask(false)
, _state(State::INITIAL)
, _isDestroyed(std::make_shared<bool>(false))
, _id(++__idIndex)
, _isLoadingFinished(false)
, _isSkipReadDataTask(false)
{
ALOGVV("AudioCache() %p, id=%u", this, _id);
for (int i = 0; i < QUEUEBUFFER_NUM; ++i)
{
_queBuffers[i] = nullptr;
_queBufferSize[i] = 0;
}
memset(&_outputFormat, 0, sizeof(_outputFormat));
}
AudioCache::~AudioCache()
{
_exitReadDataTask = true;
if(_pcmData){
if (_alBufferReady){
alDeleteBuffers(1, &_alBufferId);
ALOGVV("~AudioCache() %p, id=%u, begin", this, _id);
*_isDestroyed = true;
while (!_isLoadingFinished)
{
if (_isSkipReadDataTask)
{
ALOGV("id=%u, Skip read data task, don't continue to wait!", _id);
break;
}
ALOGVV("id=%u, waiting readData thread to finish ...", _id);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
//wait for the 'readDataTask' task to exit
_readDataTaskMutex.lock();
_readDataTaskMutex.unlock();
if (_pcmData)
{
if (_state == State::READY)
{
if (_alBufferId != INVALID_AL_BUFFER_ID && alIsBuffer(_alBufferId))
{
ALOGV("~AudioCache(id=%u), delete buffer: %u", _id, _alBufferId);
alDeleteBuffers(1, &_alBufferId);
_alBufferId = INVALID_AL_BUFFER_ID;
}
}
else
{
ALOGW("AudioCache (%p), id=%u, buffer isn't ready, state=%d", this, _id, _state);
}
//wait for the 'readDataTask' task to exit
_readDataTaskMutex.lock();
_readDataTaskMutex.unlock();
free(_pcmData);
}
if (_queBufferFrames > 0) {
for (int index = 0; index < QUEUEBUFFER_NUM; ++index) {
if (_queBufferFrames > 0)
{
for (int index = 0; index < QUEUEBUFFER_NUM; ++index)
{
free(_queBuffers[index]);
}
}
ALOGVV("~AudioCache() %p, id=%u, end", this, _id);
}
void AudioCache::readDataTask()
void AudioCache::readDataTask(unsigned int selfId)
{
//Note: It's in sub thread
ALOGVV("readDataTask, cache id=%u", selfId);
_readDataTaskMutex.lock();
_state = State::LOADING;
AudioStreamBasicDescription theFileFormat;
UInt32 thePropertySize = sizeof(theFileFormat);
SInt64 theFileLengthInFrames;
SInt64 readInFrames;
SInt64 dataSize;
SInt64 frames;
AudioBufferList theDataBuffer;
CFURLRef fileURL = nil;
ExtAudioFileRef extRef = nullptr;
NSString *fileFullPath = [[NSString alloc] initWithCString:_fileFullPath.c_str() encoding:NSUTF8StringEncoding];
auto fileURL = (CFURLRef)[[NSURL alloc] initFileURLWithPath:fileFullPath];
[fileFullPath release];
do
{
if (*_isDestroyed) break;
AudioStreamBasicDescription theFileFormat;
UInt32 thePropertySize = sizeof(theFileFormat);
SInt64 theFileLengthInFrames;
SInt64 readInFrames;
SInt64 dataSize;
SInt64 frames;
AudioBufferList theDataBuffer;
NSString *fileFullPath = [[NSString alloc] initWithCString:_fileFullPath.c_str() encoding:NSUTF8StringEncoding];
fileURL = (CFURLRef)[[NSURL alloc] initFileURLWithPath:fileFullPath];
[fileFullPath release];
if (*_isDestroyed) break;
auto error = ExtAudioFileOpenURL(fileURL, &extRef);
if(error) {
printf("%s: ExtAudioFileOpenURL FAILED, Error = %ld\n", __PRETTY_FUNCTION__, (long)error);
goto ExitThread;
}
// Get the audio data format
error = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileDataFormat, &thePropertySize, &theFileFormat);
if(error) {
printf("%s: ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) FAILED, Error = %ld\n", __PRETTY_FUNCTION__, (long)error);
goto ExitThread;
}
if (theFileFormat.mChannelsPerFrame > 2) {
printf("%s: Unsupported Format, channel count is greater than stereo\n",__PRETTY_FUNCTION__);
goto ExitThread;
}
// Set the client format to 16 bit signed integer (native-endian) data
// Maintain the channel count and sample rate of the original source format
outputFormat.mSampleRate = theFileFormat.mSampleRate;
outputFormat.mChannelsPerFrame = theFileFormat.mChannelsPerFrame;
_bytesPerFrame = 2 * outputFormat.mChannelsPerFrame;
outputFormat.mFormatID = kAudioFormatLinearPCM;
outputFormat.mBytesPerPacket = _bytesPerFrame;
outputFormat.mFramesPerPacket = 1;
outputFormat.mBytesPerFrame = _bytesPerFrame;
outputFormat.mBitsPerChannel = 16;
outputFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
error = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(outputFormat), &outputFormat);
if(error) {
printf("%s: ExtAudioFileSetProperty FAILED, Error = %ld\n", __PRETTY_FUNCTION__, (long)error);
goto ExitThread;
}
// Get the total frame count
thePropertySize = sizeof(theFileLengthInFrames);
error = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileLengthFrames, &thePropertySize, &theFileLengthInFrames);
if(error) {
printf("%s: ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) FAILED, Error = %ld\n", __PRETTY_FUNCTION__, (long)error);
goto ExitThread;
}
_dataSize = (ALsizei)(theFileLengthInFrames * outputFormat.mBytesPerFrame);
_format = (outputFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
_sampleRate = (ALsizei)outputFormat.mSampleRate;
_duration = 1.0f * theFileLengthInFrames / outputFormat.mSampleRate;
if (_dataSize <= PCMDATA_CACHEMAXSIZE) {
_pcmData = (char*)malloc(_dataSize);
alGenBuffers(1, &_alBufferId);
auto alError = alGetError();
if (alError != AL_NO_ERROR) {
printf("%s: attaching audio to buffer fail: %x\n", __PRETTY_FUNCTION__, alError);
goto ExitThread;
}
alBufferDataStaticProc(_alBufferId, _format, _pcmData, _dataSize, _sampleRate);
readInFrames = theFileFormat.mSampleRate * QUEUEBUFFER_TIME_STEP * QUEUEBUFFER_NUM;
dataSize = outputFormat.mBytesPerFrame * readInFrames;
if (dataSize > _dataSize) {
dataSize = _dataSize;
readInFrames = theFileLengthInFrames;
}
theDataBuffer.mNumberBuffers = 1;
theDataBuffer.mBuffers[0].mDataByteSize = (UInt32)dataSize;
theDataBuffer.mBuffers[0].mNumberChannels = outputFormat.mChannelsPerFrame;
theDataBuffer.mBuffers[0].mData = _pcmData;
frames = readInFrames;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
_alBufferReady = true;
_bytesOfRead += dataSize;
invokingPlayCallbacks();
while (!_exitReadDataTask && _bytesOfRead + dataSize < _dataSize) {
theDataBuffer.mBuffers[0].mData = _pcmData + _bytesOfRead;
frames = readInFrames;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
_bytesOfRead += dataSize;
auto error = ExtAudioFileOpenURL(fileURL, &extRef);
if (error) {
ALOGE("%s: ExtAudioFileOpenURL FAILED, Error = %ld", __PRETTY_FUNCTION__, (long)error);
break;
}
dataSize = _dataSize - _bytesOfRead;
if (!_exitReadDataTask && dataSize > 0) {
if (*_isDestroyed) break;
// Get the audio data format
error = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileDataFormat, &thePropertySize, &theFileFormat);
if(error)
{
ALOGE("%s: ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) FAILED, Error = %ld", __PRETTY_FUNCTION__, (long)error);
break;
}
if (theFileFormat.mChannelsPerFrame > 2)
{
ALOGE("%s: Unsupported Format, channel count is greater than stereo",__PRETTY_FUNCTION__);
break;
}
if (*_isDestroyed) break;
// Set the client format to 16 bit signed integer (native-endian) data
// Maintain the channel count and sample rate of the original source format
_outputFormat.mSampleRate = theFileFormat.mSampleRate;
_outputFormat.mChannelsPerFrame = theFileFormat.mChannelsPerFrame;
_bytesPerFrame = 2 * _outputFormat.mChannelsPerFrame;
_outputFormat.mFormatID = kAudioFormatLinearPCM;
_outputFormat.mBytesPerPacket = _bytesPerFrame;
_outputFormat.mFramesPerPacket = 1;
_outputFormat.mBytesPerFrame = _bytesPerFrame;
_outputFormat.mBitsPerChannel = 16;
_outputFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
error = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(_outputFormat), &_outputFormat);
if(error)
{
ALOGE("%s: ExtAudioFileSetProperty FAILED, Error = %ld", __PRETTY_FUNCTION__, (long)error);
break;
}
if (*_isDestroyed) break;
// Get the total frame count
thePropertySize = sizeof(theFileLengthInFrames);
error = ExtAudioFileGetProperty(extRef, kExtAudioFileProperty_FileLengthFrames, &thePropertySize, &theFileLengthInFrames);
if(error)
{
ALOGE("%s: ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) FAILED, Error = %ld", __PRETTY_FUNCTION__, (long)error);
break;
}
if (*_isDestroyed) break;
_dataSize = (ALsizei)(theFileLengthInFrames * _outputFormat.mBytesPerFrame);
_format = (_outputFormat.mChannelsPerFrame > 1) ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
_sampleRate = (ALsizei)_outputFormat.mSampleRate;
_duration = 1.0f * theFileLengthInFrames / _outputFormat.mSampleRate;
if (_dataSize <= PCMDATA_CACHEMAXSIZE)
{
_pcmData = (char*)malloc(_dataSize);
alGenBuffers(1, &_alBufferId);
auto alError = alGetError();
if (alError != AL_NO_ERROR) {
ALOGE("%s: attaching audio to buffer fail: %x", __PRETTY_FUNCTION__, alError);
break;
}
if (*_isDestroyed) break;
alBufferDataStaticProc(_alBufferId, _format, _pcmData, _dataSize, _sampleRate);
readInFrames = theFileFormat.mSampleRate * QUEUEBUFFER_TIME_STEP * QUEUEBUFFER_NUM;
dataSize = _outputFormat.mBytesPerFrame * readInFrames;
if (dataSize > _dataSize) {
dataSize = _dataSize;
readInFrames = theFileLengthInFrames;
}
theDataBuffer.mNumberBuffers = 1;
theDataBuffer.mBuffers[0].mDataByteSize = (UInt32)dataSize;
theDataBuffer.mBuffers[0].mData = _pcmData + _bytesOfRead;
theDataBuffer.mBuffers[0].mNumberChannels = _outputFormat.mChannelsPerFrame;
theDataBuffer.mBuffers[0].mData = _pcmData;
frames = readInFrames;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
if (*_isDestroyed) break;
_state = State::READY;
_bytesOfRead += dataSize;
invokingPlayCallbacks();
while (!*_isDestroyed && _bytesOfRead + dataSize < _dataSize) {
theDataBuffer.mBuffers[0].mData = _pcmData + _bytesOfRead;
frames = readInFrames;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
_bytesOfRead += dataSize; //FIXME: the buffer size of read frames may be fewer than dataSize.
}
dataSize = _dataSize - _bytesOfRead;
if (!*_isDestroyed && dataSize > 0) {
theDataBuffer.mBuffers[0].mDataByteSize = (UInt32)dataSize;
theDataBuffer.mBuffers[0].mData = _pcmData + _bytesOfRead;
frames = readInFrames;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
}
_bytesOfRead = _dataSize;
}
else
{
_queBufferFrames = theFileFormat.mSampleRate * QUEUEBUFFER_TIME_STEP;
if (_queBufferFrames == 0)
break;
_queBufferBytes = _queBufferFrames * _outputFormat.mBytesPerFrame;
theDataBuffer.mNumberBuffers = 1;
theDataBuffer.mBuffers[0].mNumberChannels = _outputFormat.mChannelsPerFrame;
for (int index = 0; index < QUEUEBUFFER_NUM; ++index) {
_queBuffers[index] = (char*)malloc(_queBufferBytes);
theDataBuffer.mBuffers[0].mDataByteSize = _queBufferBytes;
theDataBuffer.mBuffers[0].mData = _queBuffers[index];
frames = _queBufferFrames;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
_queBufferSize[index] = theDataBuffer.mBuffers[0].mDataByteSize;
}
_state = State::READY;
}
_bytesOfRead = _dataSize;
}
else{
_queBufferFrames = theFileFormat.mSampleRate * QUEUEBUFFER_TIME_STEP;
_queBufferBytes = _queBufferFrames * outputFormat.mBytesPerFrame;
theDataBuffer.mNumberBuffers = 1;
theDataBuffer.mBuffers[0].mNumberChannels = outputFormat.mChannelsPerFrame;
for (int index = 0; index < QUEUEBUFFER_NUM; ++index) {
_queBuffers[index] = (char*)malloc(_queBufferBytes);
theDataBuffer.mBuffers[0].mDataByteSize = _queBufferBytes;
theDataBuffer.mBuffers[0].mData = _queBuffers[index];
frames = _queBufferFrames;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
_queBufferSize[index] = theDataBuffer.mBuffers[0].mDataByteSize;
}
}
} while (false);
ExitThread:
CFRelease(fileURL);
if (extRef)
if (fileURL != nil)
CFRelease(fileURL);
if (extRef != nullptr)
ExtAudioFileDispose(extRef);
_readDataTaskMutex.unlock();
if (_queBufferFrames > 0)
_alBufferReady = true;
else
_loadFail = true;
//FIXME: Why to invoke play callback first? Should it be after 'load' callback?
invokingPlayCallbacks();
invokingLoadCallbacks();
_isLoadingFinished = true;
if (_state != State::READY)
{
_state = State::FAILED;
if (_alBufferId != INVALID_AL_BUFFER_ID && alIsBuffer(_alBufferId))
{
ALOGV("readDataTask failed, delete buffer: %u", _alBufferId);
alDeleteBuffers(1, &_alBufferId);
_alBufferId = INVALID_AL_BUFFER_ID;
}
}
_readDataTaskMutex.unlock();
}
void AudioCache::addPlayCallback(const std::function<void()>& callback)
{
_callbackMutex.lock();
if (_alBufferReady) {
callback();
} else if(!_loadFail){
_callbacks.push_back(callback);
std::lock_guard<std::mutex> lk(_playCallbackMutex);
switch (_state)
{
case State::INITIAL:
case State::LOADING:
_playCallbacks.push_back(callback);
break;
case State::READY:
// If state is failure, we still need to invoke the callback
// since the callback will set the 'AudioPlayer::_removeByAudioEngine' flag to true.
case State::FAILED:
callback();
break;
default:
ALOGE("Invalid state: %d", _state);
break;
}
_callbackMutex.unlock();
}
void AudioCache::invokingPlayCallbacks()
{
_callbackMutex.lock();
auto count = _callbacks.size();
for (size_t index = 0; index < count; ++index) {
_callbacks[index]();
std::lock_guard<std::mutex> lk(_playCallbackMutex);
for (auto&& cb : _playCallbacks)
{
cb();
}
_callbacks.clear();
_callbackMutex.unlock();
_playCallbacks.clear();
}
void AudioCache::addLoadCallback(const std::function<void(bool)>& callback)
{
if (_alBufferReady) {
callback(true);
} else if(_loadFail){
callback(false);
}
else {
_loadCallbacks.push_back(callback);
switch (_state)
{
case State::INITIAL:
case State::LOADING:
_loadCallbacks.push_back(callback);
break;
case State::READY:
callback(true);
break;
case State::FAILED:
callback(false);
break;
default:
ALOGE("Invalid state: %d", _state);
break;
}
}
void AudioCache::invokingLoadCallbacks()
{
if (*_isDestroyed)
{
ALOGV("AudioCache (%p) was destroyed, don't invoke preload callback ...", this);
return;
}
auto isDestroyed = _isDestroyed;
auto scheduler = Director::getInstance()->getScheduler();
scheduler->performFunctionInCocosThread([&](){
auto count = _loadCallbacks.size();
for (size_t index = 0; index < count; ++index) {
_loadCallbacks[index](_alBufferReady);
scheduler->performFunctionInCocosThread([&, isDestroyed](){
if (*isDestroyed)
{
ALOGV("invokingLoadCallbacks perform in cocos thread, AudioCache (%p) was destroyed!", this);
return;
}
for (auto&& cb : _loadCallbacks)
{
cb(_state == State::READY);
}
_loadCallbacks.clear();
});
}

View File

@ -22,6 +22,8 @@
THE SOFTWARE.
****************************************************************************/
#define LOG_TAG "AudioEngine-inl.mm"
#include "platform/CCPlatformConfig.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC
@ -119,7 +121,7 @@ void AudioEngineInterruptionListenerCallback(void* user_data, UInt32 interruptio
setCategory: AVAudioSessionCategoryAmbient
error: &error];
if (!success) {
printf("Fail to set audio session.\n");
ALOGE("Fail to set audio session.");
return;
}
[[AVAudioSession sharedInstance] setActive:YES error:&error];
@ -183,7 +185,7 @@ bool AudioEngineImpl::init()
auto alError = alGetError();
if(alError != AL_NO_ERROR)
{
printf("%s:generating sources fail! error = %x\n", __PRETTY_FUNCTION__, alError);
ALOGE("%s:generating sources failed! error = %x", __PRETTY_FUNCTION__, alError);
break;
}
@ -192,6 +194,7 @@ bool AudioEngineImpl::init()
}
_scheduler = Director::getInstance()->getScheduler();
ret = true;
ALOGI("OpenAL was initialized successfully!");
}
}while (false);
@ -206,14 +209,23 @@ AudioCache* AudioEngineImpl::preload(const std::string& filePath, std::function<
if (it == _audioCaches.end()) {
audioCache = &_audioCaches[filePath];
audioCache->_fileFullPath = FileUtils::getInstance()->fullPathForFilename(filePath);
AudioEngine::addTask(std::bind(&AudioCache::readDataTask, audioCache));
unsigned int cacheId = audioCache->_id;
auto isCacheDestroyed = audioCache->_isDestroyed;
AudioEngine::addTask([audioCache, cacheId, isCacheDestroyed](){
if (*isCacheDestroyed)
{
ALOGV("AudioCache (id=%u) was destroyed, no need to launch readDataTask.", cacheId);
audioCache->setSkipReadDataTask(true);
return;
}
audioCache->readDataTask(cacheId);
});
}
else {
audioCache = &it->second;
}
if(audioCache && callback)
if (audioCache && callback)
{
audioCache->addLoadCallback(callback);
}
@ -244,6 +256,7 @@ int AudioEngineImpl::play2d(const std::string &filePath ,bool loop ,float volume
if (player == nullptr) {
return AudioEngine::INVALID_AUDIO_ID;
}
player->_alSource = alSource;
player->_loop = loop;
player->_volume = volume;
@ -254,14 +267,15 @@ int AudioEngineImpl::play2d(const std::string &filePath ,bool loop ,float volume
return AudioEngine::INVALID_AUDIO_ID;
}
player->setCache(audioCache);
_threadMutex.lock();
_audioPlayers[_currentAudioID] = player;
_threadMutex.unlock();
audioCache->addPlayCallback(std::bind(&AudioEngineImpl::_play2d,this,audioCache,_currentAudioID));
_alSourceUsed[alSource] = true;
audioCache->addPlayCallback(std::bind(&AudioEngineImpl::_play2d,this,audioCache,_currentAudioID));
if (_lazyInitLoop) {
_lazyInitLoop = false;
_scheduler->schedule(CC_SCHEDULE_SELECTOR(AudioEngineImpl::update), this, 0.05f, false);
@ -272,11 +286,14 @@ int AudioEngineImpl::play2d(const std::string &filePath ,bool loop ,float volume
void AudioEngineImpl::_play2d(AudioCache *cache, int audioID)
{
if(cache->_alBufferReady){
//Note: It may bn in sub thread or main thread :(
if (!*cache->_isDestroyed && cache->_state == AudioCache::State::READY)
{
_threadMutex.lock();
auto playerIt = _audioPlayers.find(audioID);
if (playerIt != _audioPlayers.end() && playerIt->second->play2d(cache)) {
if (playerIt != _audioPlayers.end() && playerIt->second->play2d()) {
_scheduler->performFunctionInCocosThread([audioID](){
if (AudioEngine::_audioIDInfoMap.find(audioID) != AudioEngine::_audioIDInfoMap.end()) {
AudioEngine::_audioIDInfoMap[audioID].state = AudioEngine::AudioState::PLAYING;
}
@ -284,6 +301,15 @@ void AudioEngineImpl::_play2d(AudioCache *cache, int audioID)
}
_threadMutex.unlock();
}
else
{
ALOGD("AudioEngineImpl::_play2d, cache was destroyed or not ready!");
auto iter = _audioPlayers.find(audioID);
if (iter != _audioPlayers.end())
{
iter->second->_removeByAudioEngine = true;
}
}
}
void AudioEngineImpl::setVolume(int audioID,float volume)
@ -296,7 +322,7 @@ void AudioEngineImpl::setVolume(int audioID,float volume)
auto error = alGetError();
if (error != AL_NO_ERROR) {
printf("%s: audio id = %d, error = %x\n", __PRETTY_FUNCTION__,audioID,error);
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__,audioID,error);
}
}
}
@ -317,7 +343,7 @@ void AudioEngineImpl::setLoop(int audioID, bool loop)
auto error = alGetError();
if (error != AL_NO_ERROR) {
printf("%s: audio id = %d, error = %x\n", __PRETTY_FUNCTION__,audioID,error);
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__,audioID,error);
}
}
}
@ -334,7 +360,7 @@ bool AudioEngineImpl::pause(int audioID)
auto error = alGetError();
if (error != AL_NO_ERROR) {
ret = false;
printf("%s: audio id = %d, error = %x\n", __PRETTY_FUNCTION__,audioID,error);
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__,audioID,error);
}
return ret;
@ -348,7 +374,7 @@ bool AudioEngineImpl::resume(int audioID)
auto error = alGetError();
if (error != AL_NO_ERROR) {
ret = false;
printf("%s: audio id = %d, error = %x\n", __PRETTY_FUNCTION__,audioID,error);
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__,audioID,error);
}
return ret;
@ -358,7 +384,9 @@ void AudioEngineImpl::stop(int audioID)
{
auto player = _audioPlayers[audioID];
player->destroy();
_alSourceUsed[player->_alSource] = false;
//Note: Don't set the flag to false here, it should be set in 'update' function.
// Otherwise, the state got from alSourceState may be wrong
// _alSourceUsed[player->_alSource] = false;
}
void AudioEngineImpl::stopAll()
@ -367,10 +395,12 @@ void AudioEngineImpl::stopAll()
{
player.second->destroy();
}
for(int index = 0; index < MAX_AUDIOINSTANCES; ++index)
{
_alSourceUsed[_alSources[index]] = false;
}
//Note: Don't set the flag to false here, it should be set in 'update' function.
// Otherwise, the state got from alSourceState may be wrong
// for(int index = 0; index < MAX_AUDIOINSTANCES; ++index)
// {
// _alSourceUsed[_alSources[index]] = false;
// }
}
float AudioEngineImpl::getDuration(int audioID)
@ -395,7 +425,7 @@ float AudioEngineImpl::getCurrentTime(int audioID)
auto error = alGetError();
if (error != AL_NO_ERROR) {
printf("%s, audio id:%d,error code:%x", __PRETTY_FUNCTION__,audioID,error);
ALOGE("%s, audio id:%d,error code:%x", __PRETTY_FUNCTION__,audioID,error);
}
}
}
@ -420,7 +450,7 @@ bool AudioEngineImpl::setCurrentTime(int audioID, float time)
else {
if (player->_audioCache->_bytesOfRead != player->_audioCache->_dataSize &&
(time * player->_audioCache->_sampleRate * player->_audioCache->_bytesPerFrame) > player->_audioCache->_bytesOfRead) {
printf("%s: audio id = %d\n", __PRETTY_FUNCTION__,audioID);
ALOGE("%s: audio id = %d", __PRETTY_FUNCTION__,audioID);
break;
}
@ -428,7 +458,7 @@ bool AudioEngineImpl::setCurrentTime(int audioID, float time)
auto error = alGetError();
if (error != AL_NO_ERROR) {
printf("%s: audio id = %d, error = %x\n", __PRETTY_FUNCTION__,audioID,error);
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__,audioID,error);
}
ret = true;
}
@ -448,13 +478,17 @@ void AudioEngineImpl::update(float dt)
int audioID;
AudioPlayer* player;
// ALOGV("AudioPlayer count: %d", (int)_audioPlayers.size());
for (auto it = _audioPlayers.begin(); it != _audioPlayers.end(); ) {
audioID = it->first;
player = it->second;
alGetSourcei(player->_alSource, AL_SOURCE_STATE, &sourceState);
if(player->_removeByAudioEngine)
if (player->_removeByAudioEngine)
{
_alSourceUsed[player->_alSource] = false;
AudioEngine::remove(audioID);
_threadMutex.lock();
it = _audioPlayers.erase(it);
@ -462,10 +496,11 @@ void AudioEngineImpl::update(float dt)
delete player;
}
else if (player->_ready && sourceState == AL_STOPPED) {
_alSourceUsed[player->_alSource] = false;
if (player->_finishCallbak) {
auto& audioInfo = AudioEngine::_audioIDInfoMap[audioID];
player->_finishCallbak(audioID, *audioInfo.filePath);
player->_finishCallbak(audioID, *audioInfo.filePath); //FIXME: callback will delay 50ms
}
AudioEngine::remove(audioID);

View File

@ -22,12 +22,11 @@
THE SOFTWARE.
****************************************************************************/
#pragma once
#include "platform/CCPlatformConfig.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC
#ifndef __AUDIO_PLAYER_H_
#define __AUDIO_PLAYER_H_
#include <condition_variable>
#include <mutex>
#include <string>
@ -55,8 +54,9 @@ public:
bool setLoop(bool loop);
protected:
void setCache(AudioCache* cache);
void rotateBufferThread(int offsetFrame);
bool play2d(AudioCache* cache);
bool play2d();
AudioCache* _audioCache;
@ -64,7 +64,7 @@ protected:
bool _loop;
std::function<void (int, const std::string &)> _finishCallbak;
bool _beDestroy;
bool _isDestroyed;
bool _removeByAudioEngine;
bool _ready;
ALuint _alSource;
@ -73,16 +73,21 @@ protected:
float _currTime;
bool _streamingSource;
ALuint _bufferIds[3];
std::thread _rotateBufferThread;
std::thread* _rotateBufferThread;
std::condition_variable _sleepCondition;
std::mutex _sleepMutex;
bool _timeDirty;
bool _isRotateThreadExited;
std::mutex _play2dMutex;
unsigned int _id;
friend class AudioEngineImpl;
};
}
NS_CC_END
#endif // __AUDIO_PLAYER_H_
#endif

View File

@ -22,6 +22,8 @@
THE SOFTWARE.
****************************************************************************/
#define LOG_TAG "AudioPlayer"
#include "platform/CCPlatformConfig.h"
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS || CC_TARGET_PLATFORM == CC_PLATFORM_MAC
@ -32,111 +34,204 @@
#include "platform/CCFileUtils.h"
#import <AudioToolbox/ExtendedAudioFile.h>
#define VERY_VERY_VERBOSE_LOGGING
#ifdef VERY_VERY_VERBOSE_LOGGING
#define ALOGVV ALOGV
#else
#define ALOGVV(...) do{} while(false)
#endif
using namespace cocos2d;
using namespace cocos2d::experimental;
namespace {
unsigned int __idIndex = 0;
}
AudioPlayer::AudioPlayer()
: _audioCache(nullptr)
, _finishCallbak(nullptr)
, _beDestroy(false)
, _isDestroyed(false)
, _removeByAudioEngine(false)
, _ready(false)
, _currTime(0.0f)
, _streamingSource(false)
, _rotateBufferThread(nullptr)
, _timeDirty(false)
{
, _isRotateThreadExited(false)
, _id(++__idIndex)
{
memset(_bufferIds, 0, sizeof(_bufferIds));
}
AudioPlayer::~AudioPlayer()
{
if (_streamingSource) {
_beDestroy = true;
_sleepCondition.notify_one();
if (_rotateBufferThread.joinable()) {
_rotateBufferThread.join();
}
ALOGVV("~AudioPlayer() (%p), id=%u", this, _id);
destroy();
if (_streamingSource)
{
alDeleteBuffers(3, _bufferIds);
}
}
void AudioPlayer::destroy()
{
alSourceStop(_alSource);
alSourcei(_alSource, AL_BUFFER, NULL);
if (_isDestroyed)
return;
std::unique_lock<std::mutex> lk(_sleepMutex);
_beDestroy = true;
if (_streamingSource) {
_sleepCondition.notify_one();
}
else if (_ready) {
_removeByAudioEngine = true;
}
_ready = false;
}
ALOGVV("AudioPlayer::destroy begin, id=%u", _id);
bool AudioPlayer::play2d(AudioCache* cache)
{
if (!cache->_alBufferReady) {
_removeByAudioEngine = true;
return false;
}
_audioCache = cache;
_isDestroyed = true;
alSourcei(_alSource, AL_BUFFER, 0);
alSourcef(_alSource, AL_PITCH, 1.0f);
alSourcef(_alSource, AL_GAIN, _volume);
alSourcei(_alSource, AL_LOOPING, AL_FALSE);
if (_audioCache->_queBufferFrames == 0) {
if (_loop) {
alSourcei(_alSource, AL_LOOPING, AL_TRUE);
}
}
else {
alGetError();
alGenBuffers(3, _bufferIds);
auto alError = alGetError();
if (alError == AL_NO_ERROR) {
for (int index = 0; index < QUEUEBUFFER_NUM; ++index) {
alBufferData(_bufferIds[index], _audioCache->_format, _audioCache->_queBuffers[index], _audioCache->_queBufferSize[index], _audioCache->_sampleRate);
do
{
if (_audioCache != nullptr)
{
if (_audioCache->_state == AudioCache::State::INITIAL)
{
ALOGV("AudioPlayer::destroy, id=%u, cache isn't ready!", _id);
break;
}
while (!_audioCache->_isLoadingFinished)
{
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
}
else {
printf("%s:alGenBuffers error code:%x", __PRETTY_FUNCTION__,alError);
_removeByAudioEngine = true;
return false;
// Wait for play2d to be finished.
_play2dMutex.lock();
_play2dMutex.unlock();
if (_streamingSource)
{
if (_rotateBufferThread != nullptr)
{
while (!_isRotateThreadExited)
{
_sleepCondition.notify_one();
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
if (_rotateBufferThread->joinable()) {
_rotateBufferThread->join();
}
delete _rotateBufferThread;
_rotateBufferThread = nullptr;
ALOGVV("rotateBufferThread exited!");
}
}
_streamingSource = true;
}
} while(false);
ALOGVV("Before alSourceStop");
alSourceStop(_alSource); CHECK_AL_ERROR_DEBUG();
ALOGVV("Before alSourcei");
alSourcei(_alSource, AL_BUFFER, NULL); CHECK_AL_ERROR_DEBUG();
_removeByAudioEngine = true;
_ready = false;
ALOGVV("AudioPlayer::destroy end, id=%u", _id);
}
void AudioPlayer::setCache(AudioCache* cache)
{
_audioCache = cache;
}
bool AudioPlayer::play2d()
{
_play2dMutex.lock();
ALOGV("AudioPlayer::play2d, _alSource: %u", _alSource);
/*********************************************************************/
/* Note that it may be in sub thread or in main thread. **/
/*********************************************************************/
bool ret = false;
do
{
std::unique_lock<std::mutex> lk(_sleepMutex);
if (_beDestroy) {
_removeByAudioEngine = true;
return false;
}
if (_streamingSource) {
alSourceQueueBuffers(_alSource, QUEUEBUFFER_NUM, _bufferIds);
_rotateBufferThread = std::thread(&AudioPlayer::rotateBufferThread,this, _audioCache->_queBufferFrames * QUEUEBUFFER_NUM + 1);
}
else {
alSourcei(_alSource, AL_BUFFER, _audioCache->_alBufferId);
if (_audioCache->_state != AudioCache::State::READY)
{
ALOGE("alBuffer isn't ready for play!");
break;
}
alGetError();
alSourcePlay(_alSource);
}
alSourcei(_alSource, AL_BUFFER, 0);CHECK_AL_ERROR_DEBUG();
alSourcef(_alSource, AL_PITCH, 1.0f);CHECK_AL_ERROR_DEBUG();
alSourcef(_alSource, AL_GAIN, _volume);CHECK_AL_ERROR_DEBUG();
alSourcei(_alSource, AL_LOOPING, AL_FALSE);CHECK_AL_ERROR_DEBUG();
if (_audioCache->_queBufferFrames == 0)
{
if (_loop) {
alSourcei(_alSource, AL_LOOPING, AL_TRUE);
CHECK_AL_ERROR_DEBUG();
}
}
else
{
alGenBuffers(3, _bufferIds);
auto alError = alGetError();
if (alError == AL_NO_ERROR)
{
for (int index = 0; index < QUEUEBUFFER_NUM; ++index)
{
alBufferData(_bufferIds[index], _audioCache->_format, _audioCache->_queBuffers[index], _audioCache->_queBufferSize[index], _audioCache->_sampleRate);
}
CHECK_AL_ERROR_DEBUG();
}
else
{
ALOGE("%s:alGenBuffers error code:%x", __PRETTY_FUNCTION__,alError);
break;
}
_streamingSource = true;
}
{
std::unique_lock<std::mutex> lk(_sleepMutex);
if (_isDestroyed)
break;
if (_streamingSource)
{
alSourceQueueBuffers(_alSource, QUEUEBUFFER_NUM, _bufferIds);
CHECK_AL_ERROR_DEBUG();
_rotateBufferThread = new std::thread(&AudioPlayer::rotateBufferThread, this, _audioCache->_queBufferFrames * QUEUEBUFFER_NUM + 1);
}
else
{
alSourcei(_alSource, AL_BUFFER, _audioCache->_alBufferId);
CHECK_AL_ERROR_DEBUG();
}
alSourcePlay(_alSource);
}
auto alError = alGetError();
if (alError != AL_NO_ERROR)
{
ALOGE("%s:alSourcePlay error code:%x", __PRETTY_FUNCTION__,alError);
break;
}
ALint state;
alGetSourcei(_alSource, AL_SOURCE_STATE, &state);
assert(state == AL_PLAYING);
_ready = true;
ret = true;
} while (false);
auto alError = alGetError();
if (alError != AL_NO_ERROR) {
printf("%s:alSourcePlay error code:%x\n", __PRETTY_FUNCTION__,alError);
if (!ret)
{
_removeByAudioEngine = true;
return false;
}
_ready = true;
return true;
_play2dMutex.unlock();
return ret;
}
void AudioPlayer::rotateBufferThread(int offsetFrame)
@ -153,22 +248,22 @@ void AudioPlayer::rotateBufferThread(int offsetFrame)
auto error = ExtAudioFileOpenURL(fileURL, &extRef);
if(error) {
printf("%s: ExtAudioFileOpenURL FAILED, Error = %ld\n", __PRETTY_FUNCTION__,(long) error);
ALOGE("%s: ExtAudioFileOpenURL FAILED, Error = %ld", __PRETTY_FUNCTION__,(long) error);
goto ExitBufferThread;
}
error = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(_audioCache->outputFormat), &_audioCache->outputFormat);
error = ExtAudioFileSetProperty(extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(_audioCache->_outputFormat), &_audioCache->_outputFormat);
AudioBufferList theDataBuffer;
theDataBuffer.mNumberBuffers = 1;
theDataBuffer.mBuffers[0].mData = tmpBuffer;
theDataBuffer.mBuffers[0].mDataByteSize = _audioCache->_queBufferBytes;
theDataBuffer.mBuffers[0].mNumberChannels = _audioCache->outputFormat.mChannelsPerFrame;
theDataBuffer.mBuffers[0].mNumberChannels = _audioCache->_outputFormat.mChannelsPerFrame;
if (offsetFrame != 0) {
ExtAudioFileSeek(extRef, offsetFrame);
}
while (!_beDestroy) {
while (!_isDestroyed) {
alGetSourcei(_alSource, AL_SOURCE_STATE, &sourceState);
if (sourceState == AL_PLAYING) {
alGetSourcei(_alSource, AL_BUFFERS_PROCESSED, &bufferProcessed);
@ -176,7 +271,7 @@ void AudioPlayer::rotateBufferThread(int offsetFrame)
bufferProcessed--;
if (_timeDirty) {
_timeDirty = false;
offsetFrame = _currTime * _audioCache->outputFormat.mSampleRate;
offsetFrame = _currTime * _audioCache->_outputFormat.mSampleRate;
ExtAudioFileSeek(extRef, offsetFrame);
}
else {
@ -199,20 +294,20 @@ void AudioPlayer::rotateBufferThread(int offsetFrame)
theDataBuffer.mBuffers[0].mDataByteSize = _audioCache->_queBufferBytes;
ExtAudioFileRead(extRef, (UInt32*)&frames, &theDataBuffer);
} else {
_beDestroy = true;
_isDestroyed = true;
break;
}
}
ALuint bid;
alSourceUnqueueBuffers(_alSource, 1, &bid);
alBufferData(bid, _audioCache->_format, tmpBuffer, frames * _audioCache->outputFormat.mBytesPerFrame, _audioCache->_sampleRate);
alBufferData(bid, _audioCache->_format, tmpBuffer, frames * _audioCache->_outputFormat.mBytesPerFrame, _audioCache->_sampleRate);
alSourceQueueBuffers(_alSource, 1, &bid);
}
}
std::unique_lock<std::mutex> lk(_sleepMutex);
if (_beDestroy) {
if (_isDestroyed) {
break;
}
@ -220,17 +315,19 @@ void AudioPlayer::rotateBufferThread(int offsetFrame)
}
ExitBufferThread:
ALOGV("Exit rotate buffer thread ...");
CFRelease(fileURL);
// Dispose the ExtAudioFileRef, it is no longer needed
if (extRef){
ExtAudioFileDispose(extRef);
}
free(tmpBuffer);
_isRotateThreadExited = true;
}
bool AudioPlayer::setLoop(bool loop)
{
if (!_beDestroy ) {
if (!_isDestroyed ) {
_loop = loop;
return true;
}
@ -240,7 +337,7 @@ bool AudioPlayer::setLoop(bool loop)
bool AudioPlayer::setTime(float time)
{
if (!_beDestroy && time >= 0.0f && time < _audioCache->_duration) {
if (!_isDestroyed && time >= 0.0f && time < _audioCache->_duration) {
_currTime = time;
_timeDirty = true;

View File

@ -40,6 +40,7 @@ AudioEngineTests::AudioEngineTests()
ADD_TEST_CASE(InvalidAudioFileTest);
ADD_TEST_CASE(LargeAudioFileTest);
ADD_TEST_CASE(AudioPerformanceTest);
ADD_TEST_CASE(AudioSwitchStateTest);
}
namespace {
@ -748,3 +749,33 @@ std::string AudioPerformanceTest::subtitle() const
return "Please see console for the result";
}
/////////////////////////////////////////////////////////////////////////
bool AudioSwitchStateTest::init()
{
if (AudioEngineTestDemo::init())
{
schedule([](float dt){
AudioEngine::uncacheAll();
AudioEngine::preload("audio/SoundEffectsFX009/FX081.mp3");
AudioEngine::play2d("audio/SoundEffectsFX009/FX082.mp3");
AudioEngine::play2d("audio/LuckyDay.mp3");
}, 0.1f, "AudioSwitchStateTest");
return true;
}
return false;
}
std::string AudioSwitchStateTest::title() const
{
return "play, preload, stop switch test";
}
std::string AudioSwitchStateTest::subtitle() const
{
return "Should not crash";
}

View File

@ -177,4 +177,15 @@ public:
virtual std::string subtitle() const override;
};
class AudioSwitchStateTest : public AudioEngineTestDemo
{
public:
CREATE_FUNC(AudioSwitchStateTest);
virtual bool init() override;
virtual std::string title() const override;
virtual std::string subtitle() const override;
};
#endif /* defined(__NEWAUDIOENGINE_TEST_H_) */