no message
This commit is contained in:
110
cocos/audio/apple/AudioCache.h
Normal file
110
cocos/audio/apple/AudioCache.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2014-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <OpenAL/al.h>
|
||||
|
||||
#include <mutex>
|
||||
#include "base/std/container/string.h"
|
||||
|
||||
#include "audio/apple/AudioMacros.h"
|
||||
#include "base/Macros.h"
|
||||
#include "base/std/container/vector.h"
|
||||
|
||||
#define INVALID_AL_BUFFER_ID 0xFFFFFFFF
|
||||
|
||||
namespace cc {
|
||||
class AudioEngineImpl;
|
||||
class AudioPlayer;
|
||||
|
||||
class AudioCache {
|
||||
public:
|
||||
enum class State {
|
||||
INITIAL,
|
||||
LOADING,
|
||||
READY,
|
||||
FAILED
|
||||
};
|
||||
|
||||
AudioCache();
|
||||
~AudioCache();
|
||||
|
||||
void addPlayCallback(const std::function<void()> &callback);
|
||||
|
||||
void addLoadCallback(const std::function<void(bool)> &callback);
|
||||
inline bool isStreaming() const { return _isStreaming; }
|
||||
|
||||
protected:
|
||||
void setSkipReadDataTask(bool isSkip) { _isSkipReadDataTask = isSkip; };
|
||||
void readDataTask(unsigned int selfId);
|
||||
|
||||
void invokingPlayCallbacks();
|
||||
|
||||
void invokingLoadCallbacks();
|
||||
|
||||
//pcm data related stuff
|
||||
ALenum _format{-1};
|
||||
ALsizei _sampleRate{0};
|
||||
float _duration{0};
|
||||
uint32_t _totalFrames{0};
|
||||
uint32_t _framesRead{0};
|
||||
uint32_t _bytesPerFrame{0};
|
||||
bool _isStreaming{false};
|
||||
uint32_t _channelCount{1};
|
||||
/*Cache related stuff;
|
||||
* Cache pcm data when sizeInBytes less than PCMDATA_CACHEMAXSIZE
|
||||
*/
|
||||
ALuint _alBufferId{INVALID_AL_BUFFER_ID};
|
||||
char *_pcmData{nullptr};
|
||||
|
||||
/*Queue buffer related stuff
|
||||
* Streaming in openal when sizeInBytes greater then PCMDATA_CACHEMAXSIZE
|
||||
*/
|
||||
char *_queBuffers[QUEUEBUFFER_NUM];
|
||||
ALsizei _queBufferSize[QUEUEBUFFER_NUM];
|
||||
uint32_t _queBufferFrames{0};
|
||||
|
||||
std::mutex _playCallbackMutex;
|
||||
ccstd::vector<std::function<void()>> _playCallbacks;
|
||||
|
||||
// loadCallbacks doesn't need mutex since it's invoked only in Cocos thread.
|
||||
ccstd::vector<std::function<void(bool)>> _loadCallbacks;
|
||||
|
||||
std::mutex _readDataTaskMutex;
|
||||
|
||||
State _state{State::INITIAL};
|
||||
|
||||
std::shared_ptr<bool> _isDestroyed;
|
||||
ccstd::string _fileFullPath;
|
||||
unsigned int _id;
|
||||
bool _isLoadingFinished{false};
|
||||
bool _isSkipReadDataTask{false};
|
||||
|
||||
friend class AudioEngineImpl;
|
||||
friend class AudioPlayer;
|
||||
};
|
||||
|
||||
} // namespace cc
|
||||
354
cocos/audio/apple/AudioCache.mm
Normal file
354
cocos/audio/apple/AudioCache.mm
Normal file
@@ -0,0 +1,354 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2014-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2022 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated engine source code (the "Software"), a limited,
|
||||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||||
not use Cocos Creator software for developing other software or tools that's
|
||||
used for developing games. You are not granted to publish, distribute,
|
||||
sublicense, and/or sell copies of Cocos Creator.
|
||||
|
||||
The software or tools in this License Agreement are licensed, not sold.
|
||||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#define LOG_TAG "AudioCache"
|
||||
|
||||
#include "audio/apple/AudioCache.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <OpenAL/alc.h>
|
||||
#include <thread>
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Scheduler.h"
|
||||
#include "base/memory/Memory.h"
|
||||
|
||||
#include "audio/apple/AudioDecoder.h"
|
||||
|
||||
#ifdef VERY_VERY_VERBOSE_LOGGING
|
||||
#define ALOGVV ALOGV
|
||||
#else
|
||||
#define ALOGVV(...) \
|
||||
do { \
|
||||
} while (false)
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
unsigned int __idIndex = 0;
|
||||
}
|
||||
|
||||
#define PCMDATA_CACHEMAXSIZE 1048576
|
||||
|
||||
@interface NSTimerWrapper : NSObject {
|
||||
std::function<void()> _timeoutCallback;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSTimerWrapper
|
||||
|
||||
- (id)initWithTimeInterval:(double)seconds callback:(const std::function<void()> &)cb {
|
||||
if (self = [super init]) {
|
||||
_timeoutCallback = cb;
|
||||
NSTimer *timer = [NSTimer timerWithTimeInterval:seconds target:self selector:@selector(onTimeoutCallback:) userInfo:nil repeats:NO];
|
||||
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)onTimeoutCallback:(NSTimer *)timer {
|
||||
if (_timeoutCallback != nullptr) {
|
||||
_timeoutCallback();
|
||||
_timeoutCallback = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
using namespace cc;
|
||||
|
||||
AudioCache::AudioCache()
|
||||
: _isDestroyed(std::make_shared<bool>(false)), _id(++__idIndex){
|
||||
ALOGVV("AudioCache() %p, id=%u", this, _id);
|
||||
for (int i = 0; i < QUEUEBUFFER_NUM; ++i) {
|
||||
_queBuffers[i] = nullptr;
|
||||
_queBufferSize[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
AudioCache::~AudioCache() {
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (_queBufferFrames > 0) {
|
||||
for (int index = 0; index < QUEUEBUFFER_NUM; ++index) {
|
||||
free(_queBuffers[index]);
|
||||
}
|
||||
}
|
||||
ALOGVV("~AudioCache() %p, id=%u, end", this, _id);
|
||||
_readDataTaskMutex.unlock();
|
||||
}
|
||||
|
||||
void AudioCache::readDataTask(unsigned int selfId) {
|
||||
//Note: It's in sub thread
|
||||
ALOGVV("readDataTask, cache id=%u", selfId);
|
||||
|
||||
_readDataTaskMutex.lock();
|
||||
_state = State::LOADING;
|
||||
|
||||
AudioDecoder decoder;
|
||||
do {
|
||||
if (!decoder.open(_fileFullPath.c_str()))
|
||||
break;
|
||||
|
||||
const uint32_t originalTotalFrames = decoder.getTotalFrames();
|
||||
const uint32_t bytesPerFrame = decoder.getBytesPerFrame();
|
||||
const uint32_t sampleRate = decoder.getSampleRate();
|
||||
_channelCount = decoder.getChannelCount();
|
||||
|
||||
uint32_t totalFrames = originalTotalFrames;
|
||||
uint32_t dataSize = totalFrames * bytesPerFrame;
|
||||
uint32_t remainingFrames = totalFrames;
|
||||
uint32_t adjustFrames = 0;
|
||||
|
||||
_format = _channelCount > 1 ? AL_FORMAT_STEREO16 : AL_FORMAT_MONO16;
|
||||
_sampleRate = (ALsizei)sampleRate;
|
||||
_duration = 1.0f * totalFrames / sampleRate;
|
||||
_totalFrames = totalFrames;
|
||||
|
||||
if (dataSize <= PCMDATA_CACHEMAXSIZE) {
|
||||
uint32_t framesRead = 0;
|
||||
const uint32_t framesToReadOnce = std::min(totalFrames, static_cast<uint32_t>(sampleRate * QUEUEBUFFER_TIME_STEP * QUEUEBUFFER_NUM));
|
||||
|
||||
BREAK_IF_ERR_LOG(!decoder.seek(totalFrames), "AudioDecoder::seek(%u) error", totalFrames);
|
||||
|
||||
char *tmpBuf = (char *)malloc(framesToReadOnce * bytesPerFrame);
|
||||
ccstd::vector<char> adjustFrameBuf;
|
||||
adjustFrameBuf.reserve(framesToReadOnce * bytesPerFrame);
|
||||
|
||||
// Adjust total frames by setting position to the end of frames and try to read more data.
|
||||
// This is a workaround for https://github.com/cocos2d/cocos2d-x/issues/16938
|
||||
do {
|
||||
framesRead = decoder.read(framesToReadOnce, tmpBuf);
|
||||
if (framesRead > 0) {
|
||||
adjustFrames += framesRead;
|
||||
adjustFrameBuf.insert(adjustFrameBuf.end(), tmpBuf, tmpBuf + framesRead * bytesPerFrame);
|
||||
}
|
||||
|
||||
} while (framesRead > 0);
|
||||
|
||||
if (adjustFrames > 0) {
|
||||
ALOGV("Orignal total frames: %u, adjust frames: %u, current total frames: %u", totalFrames, adjustFrames, totalFrames + adjustFrames);
|
||||
totalFrames += adjustFrames;
|
||||
_totalFrames = remainingFrames = totalFrames;
|
||||
}
|
||||
|
||||
// Reset dataSize
|
||||
dataSize = totalFrames * bytesPerFrame;
|
||||
|
||||
free(tmpBuf);
|
||||
|
||||
// Reset to frame 0
|
||||
BREAK_IF_ERR_LOG(!decoder.seek(0), "AudioDecoder::seek(0) failed!");
|
||||
|
||||
_pcmData = (char *)malloc(dataSize);
|
||||
memset(_pcmData, 0x00, dataSize);
|
||||
ALOGV(" id=%u _pcmData alloc: %p", selfId, _pcmData);
|
||||
|
||||
if (adjustFrames > 0) {
|
||||
memcpy(_pcmData + (dataSize - adjustFrameBuf.size()), adjustFrameBuf.data(), adjustFrameBuf.size());
|
||||
}
|
||||
|
||||
if (*_isDestroyed)
|
||||
break;
|
||||
|
||||
framesRead = decoder.readFixedFrames(std::min(framesToReadOnce, remainingFrames), _pcmData + _framesRead * bytesPerFrame);
|
||||
_framesRead += framesRead;
|
||||
remainingFrames -= framesRead;
|
||||
|
||||
if (*_isDestroyed)
|
||||
break;
|
||||
|
||||
uint32_t frames = 0;
|
||||
while (!*_isDestroyed && _framesRead < originalTotalFrames) {
|
||||
frames = std::min(framesToReadOnce, remainingFrames);
|
||||
if (_framesRead + frames > originalTotalFrames) {
|
||||
frames = originalTotalFrames - _framesRead;
|
||||
}
|
||||
framesRead = decoder.read(frames, _pcmData + _framesRead * bytesPerFrame);
|
||||
if (framesRead == 0)
|
||||
break;
|
||||
_framesRead += framesRead;
|
||||
remainingFrames -= framesRead;
|
||||
}
|
||||
|
||||
if (_framesRead < originalTotalFrames) {
|
||||
memset(_pcmData + _framesRead * bytesPerFrame, 0x00, (totalFrames - _framesRead) * bytesPerFrame);
|
||||
}
|
||||
|
||||
ALOGV("pcm buffer was loaded successfully, total frames: %u, total read frames: %u, adjust frames: %u, remainingFrames: %u", totalFrames, _framesRead, adjustFrames, remainingFrames);
|
||||
_framesRead += adjustFrames;
|
||||
|
||||
alGenBuffers(1, &_alBufferId);
|
||||
auto alError = alGetError();
|
||||
if (alError != AL_NO_ERROR) {
|
||||
ALOGE("%s: attaching audio to buffer fail: %x", __PRETTY_FUNCTION__, alError);
|
||||
break;
|
||||
}
|
||||
ALOGV(" id=%u generated alGenBuffers: %u for _pcmData: %p", selfId, _alBufferId, _pcmData);
|
||||
ALOGV(" id=%u _pcmData alBufferData: %p", selfId, _pcmData);
|
||||
alBufferData(_alBufferId, _format, _pcmData, (ALsizei)dataSize, (ALsizei)sampleRate);
|
||||
_state = State::READY;
|
||||
invokingPlayCallbacks();
|
||||
|
||||
} else {
|
||||
_isStreaming = true;
|
||||
_queBufferFrames = sampleRate * QUEUEBUFFER_TIME_STEP;
|
||||
BREAK_IF_ERR_LOG(_queBufferFrames == 0, "_queBufferFrames == 0");
|
||||
|
||||
const uint32_t queBufferBytes = _queBufferFrames * bytesPerFrame;
|
||||
|
||||
for (int index = 0; index < QUEUEBUFFER_NUM; ++index) {
|
||||
_queBuffers[index] = (char *)malloc(queBufferBytes);
|
||||
_queBufferSize[index] = queBufferBytes;
|
||||
|
||||
decoder.readFixedFrames(_queBufferFrames, _queBuffers[index]);
|
||||
}
|
||||
|
||||
_state = State::READY;
|
||||
}
|
||||
|
||||
} while (false);
|
||||
|
||||
if (_pcmData != nullptr) {
|
||||
CC_SAFE_FREE(_pcmData);
|
||||
}
|
||||
|
||||
decoder.close();
|
||||
|
||||
//IDEA: 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(" id=%u readDataTask failed, delete buffer: %u", selfId, _alBufferId);
|
||||
alDeleteBuffers(1, &_alBufferId);
|
||||
_alBufferId = INVALID_AL_BUFFER_ID;
|
||||
}
|
||||
}
|
||||
|
||||
_readDataTaskMutex.unlock();
|
||||
}
|
||||
|
||||
void AudioCache::addPlayCallback(const std::function<void()> &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;
|
||||
}
|
||||
}
|
||||
|
||||
void AudioCache::invokingPlayCallbacks() {
|
||||
std::lock_guard<std::mutex> lk(_playCallbackMutex);
|
||||
|
||||
for (auto &&cb : _playCallbacks) {
|
||||
cb();
|
||||
}
|
||||
|
||||
_playCallbacks.clear();
|
||||
}
|
||||
|
||||
void AudioCache::addLoadCallback(const std::function<void(bool)> &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 = CC_CURRENT_ENGINE()->getScheduler();
|
||||
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();
|
||||
});
|
||||
}
|
||||
118
cocos/audio/apple/AudioDecoder.h
Normal file
118
cocos/audio/apple/AudioDecoder.h
Normal file
@@ -0,0 +1,118 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <AudioToolbox/ExtendedAudioFile.h>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace cc {
|
||||
|
||||
/**
|
||||
* @brief The class for decoding compressed audio file to PCM buffer.
|
||||
*/
|
||||
class AudioDecoder {
|
||||
public:
|
||||
static const uint32_t INVALID_FRAME_INDEX = UINT32_MAX;
|
||||
|
||||
AudioDecoder();
|
||||
~AudioDecoder();
|
||||
|
||||
/**
|
||||
* @brief Opens an audio file specified by a file path.
|
||||
* @return true if succeed, otherwise false.
|
||||
*/
|
||||
bool open(const char *path);
|
||||
|
||||
/**
|
||||
* @brief Checks whether decoder has opened file successfully.
|
||||
* @return true if succeed, otherwise false.
|
||||
*/
|
||||
bool isOpened() const;
|
||||
|
||||
/**
|
||||
* @brief Closes opened audio file.
|
||||
* @note The method will also be automatically invoked in the destructor.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/**
|
||||
* @brief Reads audio frames of PCM format.
|
||||
* @param framesToRead The number of frames excepted to be read.
|
||||
* @param pcmBuf The buffer to hold the frames to be read, its size should be >= |framesToRead| * _bytesPerFrame.
|
||||
* @return The number of frames actually read, it's probably less than 'framesToRead'. Returns 0 means reach the end of file.
|
||||
*/
|
||||
uint32_t read(uint32_t framesToRead, char *pcmBuf);
|
||||
|
||||
/**
|
||||
* @brief Reads fixed audio frames of PCM format.
|
||||
* @param framesToRead The number of frames excepted to be read.
|
||||
* @param pcmBuf The buffer to hold the frames to be read, its size should be >= |framesToRead| * _bytesPerFrame.
|
||||
* @return The number of frames actually read, it's probably less than |framesToRead|. Returns 0 means reach the end of file.
|
||||
* @note The different between |read| and |readFixedFrames| is |readFixedFrames| will do multiple reading operations if |framesToRead| frames
|
||||
* isn't filled entirely, while |read| just does reading operation once whatever |framesToRead| is or isn't filled entirely.
|
||||
* If current position reaches the end of frames, the return value may smaller than |framesToRead| and the remaining
|
||||
* buffer in |pcmBuf| will be set with silence data (0x00).
|
||||
*/
|
||||
uint32_t readFixedFrames(uint32_t framesToRead, char *pcmBuf);
|
||||
|
||||
/**
|
||||
* @brief Sets frame offest to be read.
|
||||
* @param frameOffset The frame offest to be set.
|
||||
* @return true if succeed, otherwise false
|
||||
*/
|
||||
bool seek(uint32_t frameOffset);
|
||||
|
||||
/**
|
||||
* @brief Tells the current frame offset.
|
||||
* @return The current frame offset.
|
||||
*/
|
||||
uint32_t tell() const;
|
||||
|
||||
/** Gets total frames of current audio.*/
|
||||
uint32_t getTotalFrames() const;
|
||||
|
||||
/** Gets bytes per frame of current audio.*/
|
||||
uint32_t getBytesPerFrame() const;
|
||||
|
||||
/** Gets sample rate of current audio.*/
|
||||
uint32_t getSampleRate() const;
|
||||
|
||||
/** Gets the channel count of current audio.
|
||||
* @note Currently we only support 1 or 2 channels.
|
||||
*/
|
||||
uint32_t getChannelCount() const;
|
||||
|
||||
private:
|
||||
bool _isOpened;
|
||||
ExtAudioFileRef _extRef;
|
||||
uint32_t _totalFrames;
|
||||
uint32_t _bytesPerFrame;
|
||||
uint32_t _sampleRate;
|
||||
uint32_t _channelCount;
|
||||
AudioStreamBasicDescription _outputFormat;
|
||||
};
|
||||
|
||||
} // namespace cc
|
||||
203
cocos/audio/apple/AudioDecoder.mm
Normal file
203
cocos/audio/apple/AudioDecoder.mm
Normal file
@@ -0,0 +1,203 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2022 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated engine source code (the "Software"), a limited,
|
||||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||||
not use Cocos Creator software for developing other software or tools that's
|
||||
used for developing games. You are not granted to publish, distribute,
|
||||
sublicense, and/or sell copies of Cocos Creator.
|
||||
|
||||
The software or tools in this License Agreement are licensed, not sold.
|
||||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#include "audio/apple/AudioDecoder.h"
|
||||
#include "audio/apple/AudioMacros.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#define LOG_TAG "AudioDecoder"
|
||||
|
||||
namespace cc {
|
||||
|
||||
AudioDecoder::AudioDecoder()
|
||||
: _isOpened(false), _extRef(nullptr), _totalFrames(0), _bytesPerFrame(0), _sampleRate(0), _channelCount(0) {
|
||||
memset(&_outputFormat, 0, sizeof(_outputFormat));
|
||||
}
|
||||
|
||||
AudioDecoder::~AudioDecoder() {
|
||||
close();
|
||||
}
|
||||
|
||||
bool AudioDecoder::open(const char *path) {
|
||||
bool ret = false;
|
||||
CFURLRef fileURL = nil;
|
||||
do {
|
||||
BREAK_IF_ERR_LOG(path == nullptr || strlen(path) == 0, "Invalid path!");
|
||||
|
||||
NSString *fileFullPath = [[NSString alloc] initWithCString:path encoding:NSUTF8StringEncoding];
|
||||
fileURL = (CFURLRef)[[NSURL alloc] initFileURLWithPath:fileFullPath];
|
||||
[fileFullPath release];
|
||||
BREAK_IF_ERR_LOG(fileURL == nil, "Converting path to CFURLRef failed!");
|
||||
|
||||
OSStatus status = ExtAudioFileOpenURL(fileURL, &_extRef);
|
||||
BREAK_IF_ERR_LOG(status != noErr, "ExtAudioFileOpenURL FAILED, Error = %ld", (long)ret);
|
||||
|
||||
AudioStreamBasicDescription fileFormat;
|
||||
UInt32 propertySize = sizeof(fileFormat);
|
||||
|
||||
// Get the audio data format
|
||||
ret = ExtAudioFileGetProperty(_extRef, kExtAudioFileProperty_FileDataFormat, &propertySize, &fileFormat);
|
||||
BREAK_IF_ERR_LOG(status != noErr, "ExtAudioFileGetProperty(kExtAudioFileProperty_FileDataFormat) FAILED, Error = %ld", (long)ret);
|
||||
BREAK_IF_ERR_LOG(fileFormat.mChannelsPerFrame > 2, "Unsupported Format, channel count is greater than stereo!");
|
||||
|
||||
// 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 = fileFormat.mSampleRate;
|
||||
_outputFormat.mChannelsPerFrame = fileFormat.mChannelsPerFrame;
|
||||
_outputFormat.mFormatID = kAudioFormatLinearPCM;
|
||||
_outputFormat.mFramesPerPacket = 1;
|
||||
_outputFormat.mBitsPerChannel = 16;
|
||||
_outputFormat.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger;
|
||||
_sampleRate = _outputFormat.mSampleRate;
|
||||
_channelCount = _outputFormat.mChannelsPerFrame;
|
||||
_bytesPerFrame = 2 * _outputFormat.mChannelsPerFrame;
|
||||
|
||||
_outputFormat.mBytesPerPacket = _bytesPerFrame;
|
||||
_outputFormat.mBytesPerFrame = _bytesPerFrame;
|
||||
|
||||
ret = ExtAudioFileSetProperty(_extRef, kExtAudioFileProperty_ClientDataFormat, sizeof(_outputFormat), &_outputFormat);
|
||||
BREAK_IF_ERR_LOG(status != noErr, "ExtAudioFileSetProperty FAILED, Error = %ld", (long)ret);
|
||||
|
||||
// Get the total frame count
|
||||
SInt64 totalFrames = 0;
|
||||
propertySize = sizeof(totalFrames);
|
||||
ret = ExtAudioFileGetProperty(_extRef, kExtAudioFileProperty_FileLengthFrames, &propertySize, &totalFrames);
|
||||
BREAK_IF_ERR_LOG(status != noErr, "ExtAudioFileGetProperty(kExtAudioFileProperty_FileLengthFrames) FAILED, Error = %ld", (long)ret);
|
||||
BREAK_IF_ERR_LOG(totalFrames <= 0, "Total frames is 0, it's an invalid audio file: %s", path);
|
||||
_totalFrames = static_cast<uint32_t>(totalFrames);
|
||||
_isOpened = true;
|
||||
|
||||
ret = true;
|
||||
} while (false);
|
||||
|
||||
if (fileURL != nil)
|
||||
CFRelease(fileURL);
|
||||
|
||||
if (!ret) {
|
||||
close();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AudioDecoder::close() {
|
||||
if (_extRef != nullptr) {
|
||||
ExtAudioFileDispose(_extRef);
|
||||
_extRef = nullptr;
|
||||
|
||||
_totalFrames = 0;
|
||||
_bytesPerFrame = 0;
|
||||
_sampleRate = 0;
|
||||
_channelCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t AudioDecoder::read(uint32_t framesToRead, char *pcmBuf) {
|
||||
uint32_t ret = 0;
|
||||
do {
|
||||
BREAK_IF_ERR_LOG(!isOpened(), "decoder isn't openned");
|
||||
BREAK_IF_ERR_LOG(framesToRead == INVALID_FRAME_INDEX, "frameToRead is INVALID_FRAME_INDEX");
|
||||
BREAK_IF_ERR_LOG(framesToRead == 0, "frameToRead is 0");
|
||||
BREAK_IF_ERR_LOG(pcmBuf == nullptr, "pcmBuf is nullptr");
|
||||
|
||||
AudioBufferList bufferList;
|
||||
bufferList.mNumberBuffers = 1;
|
||||
bufferList.mBuffers[0].mDataByteSize = framesToRead * _bytesPerFrame;
|
||||
bufferList.mBuffers[0].mNumberChannels = _outputFormat.mChannelsPerFrame;
|
||||
bufferList.mBuffers[0].mData = pcmBuf;
|
||||
|
||||
UInt32 frames = framesToRead;
|
||||
OSStatus status = ExtAudioFileRead(_extRef, &frames, &bufferList);
|
||||
BREAK_IF(status != noErr);
|
||||
ret = frames;
|
||||
} while (false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t AudioDecoder::readFixedFrames(uint32_t framesToRead, char *pcmBuf) {
|
||||
uint32_t framesRead = 0;
|
||||
uint32_t framesReadOnce = 0;
|
||||
do {
|
||||
framesReadOnce = read(framesToRead - framesRead, pcmBuf + framesRead * _bytesPerFrame);
|
||||
framesRead += framesReadOnce;
|
||||
} while (framesReadOnce != 0 && framesRead < framesToRead);
|
||||
|
||||
if (framesRead < framesToRead) {
|
||||
memset(pcmBuf + framesRead * _bytesPerFrame, 0x00, (framesToRead - framesRead) * _bytesPerFrame);
|
||||
}
|
||||
|
||||
return framesRead;
|
||||
}
|
||||
|
||||
bool AudioDecoder::seek(uint32_t frameOffset) {
|
||||
bool ret = false;
|
||||
do {
|
||||
BREAK_IF_ERR_LOG(!isOpened(), "decoder isn't openned");
|
||||
BREAK_IF_ERR_LOG(frameOffset == INVALID_FRAME_INDEX, "frameIndex is INVALID_FRAME_INDEX");
|
||||
|
||||
OSStatus status = ExtAudioFileSeek(_extRef, frameOffset);
|
||||
BREAK_IF(status != noErr);
|
||||
ret = true;
|
||||
} while (false);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t AudioDecoder::tell() const {
|
||||
uint32_t ret = INVALID_FRAME_INDEX;
|
||||
do {
|
||||
BREAK_IF_ERR_LOG(!isOpened(), "decoder isn't openned");
|
||||
SInt64 frameIndex = INVALID_FRAME_INDEX;
|
||||
OSStatus status = ExtAudioFileTell(_extRef, &frameIndex);
|
||||
BREAK_IF(status != noErr);
|
||||
ret = static_cast<uint32_t>(frameIndex);
|
||||
} while (false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint32_t AudioDecoder::getTotalFrames() const {
|
||||
return _totalFrames;
|
||||
}
|
||||
|
||||
uint32_t AudioDecoder::getBytesPerFrame() const {
|
||||
return _bytesPerFrame;
|
||||
}
|
||||
|
||||
uint32_t AudioDecoder::getSampleRate() const {
|
||||
return _sampleRate;
|
||||
}
|
||||
|
||||
uint32_t AudioDecoder::getChannelCount() const {
|
||||
return _channelCount;
|
||||
}
|
||||
|
||||
bool AudioDecoder::isOpened() const {
|
||||
return _isOpened;
|
||||
}
|
||||
|
||||
} // namespace cc
|
||||
91
cocos/audio/apple/AudioEngine-inl.h
Normal file
91
cocos/audio/apple/AudioEngine-inl.h
Normal file
@@ -0,0 +1,91 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2014-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/apple/AudioCache.h"
|
||||
#include "audio/apple/AudioPlayer.h"
|
||||
#include "audio/include/AudioDef.h"
|
||||
#include "base/RefCounted.h"
|
||||
#include "base/std/container/list.h"
|
||||
#include "base/std/container/unordered_map.h"
|
||||
|
||||
namespace cc {
|
||||
class Scheduler;
|
||||
|
||||
#define MAX_AUDIOINSTANCES 24
|
||||
|
||||
class AudioEngineImpl : public cc::RefCounted {
|
||||
public:
|
||||
AudioEngineImpl();
|
||||
~AudioEngineImpl();
|
||||
|
||||
bool init();
|
||||
int play2d(const ccstd::string &fileFullPath, bool loop, float volume);
|
||||
void setVolume(int audioID, float volume);
|
||||
void setLoop(int audioID, bool loop);
|
||||
bool pause(int audioID);
|
||||
bool resume(int audioID);
|
||||
void stop(int audioID);
|
||||
void stopAll();
|
||||
float getDuration(int audioID);
|
||||
float getDurationFromFile(const ccstd::string &fileFullPath);
|
||||
float getCurrentTime(int audioID);
|
||||
bool setCurrentTime(int audioID, float time);
|
||||
void setFinishCallback(int audioID, const std::function<void(int, const ccstd::string &)> &callback);
|
||||
|
||||
void uncache(const ccstd::string &filePath);
|
||||
void uncacheAll();
|
||||
AudioCache *preload(const ccstd::string &filePath, std::function<void(bool)> callback);
|
||||
void update(float dt);
|
||||
|
||||
PCMHeader getPCMHeader(const char *url);
|
||||
std::vector<uint8_t> getOriginalPCMBuffer(const char *url, uint32_t channelID);
|
||||
|
||||
private:
|
||||
bool checkAudioIdValid(int audioID);
|
||||
void play2dImpl(AudioCache *cache, int audioID);
|
||||
ALuint findValidSource();
|
||||
|
||||
static ALvoid myAlSourceNotificationCallback(ALuint sid, ALuint notificationID, ALvoid *userData);
|
||||
|
||||
ALuint _alSources[MAX_AUDIOINSTANCES];
|
||||
|
||||
//source,used
|
||||
ccstd::list<ALuint> _unusedSourcesPool;
|
||||
|
||||
//filePath,bufferInfo
|
||||
ccstd::unordered_map<ccstd::string, AudioCache> _audioCaches;
|
||||
|
||||
//audioID,AudioInfo
|
||||
ccstd::unordered_map<int, AudioPlayer *> _audioPlayers;
|
||||
std::mutex _threadMutex;
|
||||
|
||||
bool _lazyInitLoop;
|
||||
|
||||
int _currentAudioID;
|
||||
std::weak_ptr<Scheduler> _scheduler;
|
||||
};
|
||||
} // namespace cc
|
||||
789
cocos/audio/apple/AudioEngine-inl.mm
Normal file
789
cocos/audio/apple/AudioEngine-inl.mm
Normal file
@@ -0,0 +1,789 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2014-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2022 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated engine source code (the "Software"), a limited,
|
||||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||||
not use Cocos Creator software for developing other software or tools that's
|
||||
used for developing games. You are not granted to publish, distribute,
|
||||
sublicense, and/or sell copies of Cocos Creator.
|
||||
|
||||
The software or tools in this License Agreement are licensed, not sold.
|
||||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#define LOG_TAG "AudioEngine-inl.mm"
|
||||
#include "audio/apple/AudioEngine-inl.h"
|
||||
|
||||
#import <OpenAL/alc.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#if CC_PLATFORM == CC_PLATFORM_IOS
|
||||
#import <UIKit/UIApplication.h>
|
||||
#endif
|
||||
|
||||
#include "audio/include/AudioEngine.h"
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "base/Scheduler.h"
|
||||
#include "base/Utils.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "platform/FileUtils.h"
|
||||
#include "AudioDecoder.h"
|
||||
|
||||
using namespace cc;
|
||||
|
||||
static ALCdevice *s_ALDevice = nullptr;
|
||||
static ALCcontext *s_ALContext = nullptr;
|
||||
static AudioEngineImpl *s_instance = nullptr;
|
||||
|
||||
typedef ALvoid (*alSourceNotificationProc)(ALuint sid, ALuint notificationID, ALvoid *userData);
|
||||
typedef ALenum (*alSourceAddNotificationProcPtr)(ALuint sid, ALuint notificationID, alSourceNotificationProc notifyProc, ALvoid *userData);
|
||||
static ALenum alSourceAddNotificationExt(ALuint sid, ALuint notificationID, alSourceNotificationProc notifyProc, ALvoid *userData) {
|
||||
static alSourceAddNotificationProcPtr proc = nullptr;
|
||||
|
||||
if (proc == nullptr) {
|
||||
proc = (alSourceAddNotificationProcPtr)alcGetProcAddress(nullptr, "alSourceAddNotification");
|
||||
}
|
||||
|
||||
if (proc) {
|
||||
return proc(sid, notificationID, notifyProc, userData);
|
||||
}
|
||||
return AL_INVALID_VALUE;
|
||||
}
|
||||
|
||||
#if CC_PLATFORM == CC_PLATFORM_IOS
|
||||
@interface AudioEngineSessionHandler : NSObject {
|
||||
}
|
||||
|
||||
- (id)init;
|
||||
- (void)handleInterruption:(NSNotification *)notification;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AudioEngineSessionHandler
|
||||
|
||||
void AudioEngineInterruptionListenerCallback(void *user_data, UInt32 interruption_state) {
|
||||
if (kAudioSessionBeginInterruption == interruption_state) {
|
||||
alcMakeContextCurrent(nullptr);
|
||||
} else if (kAudioSessionEndInterruption == interruption_state) {
|
||||
OSStatus result = AudioSessionSetActive(true);
|
||||
if (result) NSLog(@"Error setting audio session active! %d\n", static_cast<int>(result));
|
||||
|
||||
alcMakeContextCurrent(s_ALContext);
|
||||
}
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
if (self = [super init]) {
|
||||
if ([[[UIDevice currentDevice] systemVersion] intValue] > 5) {
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:AVAudioSessionInterruptionNotification object:[AVAudioSession sharedInstance]];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleInterruption:) name:UIApplicationWillResignActiveNotification object:nil];
|
||||
}
|
||||
else {
|
||||
AudioSessionInitialize(NULL, NULL, AudioEngineInterruptionListenerCallback, self);
|
||||
}
|
||||
|
||||
BOOL success = [[AVAudioSession sharedInstance]
|
||||
setCategory:AVAudioSessionCategoryPlayback
|
||||
error:nil];
|
||||
if (!success)
|
||||
ALOGE("Fail to set audio session.");
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)handleInterruption:(NSNotification *)notification {
|
||||
static bool isAudioSessionInterrupted = false;
|
||||
static bool resumeOnBecomingActive = false;
|
||||
static bool pauseOnResignActive = false;
|
||||
|
||||
if ([notification.name isEqualToString:AVAudioSessionInterruptionNotification]) {
|
||||
NSInteger reason = [[[notification userInfo] objectForKey:AVAudioSessionInterruptionTypeKey] integerValue];
|
||||
if (reason == AVAudioSessionInterruptionTypeBegan) {
|
||||
isAudioSessionInterrupted = true;
|
||||
alcMakeContextCurrent(nullptr);
|
||||
|
||||
if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) {
|
||||
ALOGD("AVAudioSessionInterruptionTypeBegan, application == UIApplicationStateActive, pauseOnResignActive = true");
|
||||
pauseOnResignActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (reason == AVAudioSessionInterruptionTypeEnded) {
|
||||
isAudioSessionInterrupted = false;
|
||||
|
||||
NSError *error = nil;
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
alcMakeContextCurrent(s_ALContext);
|
||||
|
||||
if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive) {
|
||||
ALOGD("AVAudioSessionInterruptionTypeEnded, application != UIApplicationStateActive, resumeOnBecomingActive = true");
|
||||
resumeOnBecomingActive = true;
|
||||
}
|
||||
}
|
||||
} else if ([notification.name isEqualToString:UIApplicationWillResignActiveNotification]) {
|
||||
ALOGD("UIApplicationWillResignActiveNotification");
|
||||
if (pauseOnResignActive) {
|
||||
pauseOnResignActive = false;
|
||||
ALOGD("UIApplicationWillResignActiveNotification, alcMakeContextCurrent(nullptr)");
|
||||
alcMakeContextCurrent(nullptr);
|
||||
}
|
||||
} else if ([notification.name isEqualToString:UIApplicationDidBecomeActiveNotification]) {
|
||||
ALOGD("UIApplicationDidBecomeActiveNotification");
|
||||
if (resumeOnBecomingActive) {
|
||||
resumeOnBecomingActive = false;
|
||||
ALOGD("UIApplicationDidBecomeActiveNotification, alcMakeContextCurrent(s_ALContext)");
|
||||
NSError *error = nil;
|
||||
BOOL success = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error];
|
||||
if (!success) {
|
||||
ALOGE("Fail to set audio session.");
|
||||
return;
|
||||
}
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
alcMakeContextCurrent(s_ALContext);
|
||||
} else if (isAudioSessionInterrupted) {
|
||||
isAudioSessionInterrupted = false;
|
||||
ALOGD("UIApplicationDidBecomeActiveNotification, alcMakeContextCurrent(s_ALContext)");
|
||||
NSError *error = nil;
|
||||
BOOL success = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&error];
|
||||
if (!success) {
|
||||
ALOGE("Fail to set audio session.");
|
||||
return;
|
||||
}
|
||||
[[AVAudioSession sharedInstance] setActive:YES error:&error];
|
||||
alcMakeContextCurrent(s_ALContext);
|
||||
|
||||
// ALOGD("Audio session is still interrupted, pause director!");
|
||||
//IDEA: Director::getInstance()->pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:AVAudioSessionInterruptionNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
|
||||
|
||||
[super dealloc];
|
||||
}
|
||||
@end
|
||||
|
||||
static id s_AudioEngineSessionHandler = nullptr;
|
||||
#endif
|
||||
|
||||
ALvoid AudioEngineImpl::myAlSourceNotificationCallback(ALuint sid, ALuint notificationID, ALvoid *userData) {
|
||||
// Currently, we only care about AL_BUFFERS_PROCESSED event
|
||||
if (notificationID != AL_BUFFERS_PROCESSED)
|
||||
return;
|
||||
|
||||
AudioPlayer *player = nullptr;
|
||||
s_instance->_threadMutex.lock();
|
||||
for (const auto &e : s_instance->_audioPlayers) {
|
||||
player = e.second;
|
||||
if (player->_alSource == sid && player->_streamingSource) {
|
||||
player->wakeupRotateThread();
|
||||
}
|
||||
}
|
||||
s_instance->_threadMutex.unlock();
|
||||
}
|
||||
|
||||
AudioEngineImpl::AudioEngineImpl()
|
||||
: _lazyInitLoop(true), _currentAudioID(0) {
|
||||
s_instance = this;
|
||||
}
|
||||
|
||||
AudioEngineImpl::~AudioEngineImpl() {
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->unschedule("AudioEngine", this);
|
||||
}
|
||||
|
||||
if (s_ALContext) {
|
||||
alDeleteSources(MAX_AUDIOINSTANCES, _alSources);
|
||||
|
||||
_audioCaches.clear();
|
||||
|
||||
alcMakeContextCurrent(nullptr);
|
||||
alcDestroyContext(s_ALContext);
|
||||
}
|
||||
if (s_ALDevice) {
|
||||
alcCloseDevice(s_ALDevice);
|
||||
}
|
||||
|
||||
#if CC_PLATFORM == CC_PLATFORM_IOS
|
||||
[s_AudioEngineSessionHandler release];
|
||||
#endif
|
||||
s_instance = nullptr;
|
||||
}
|
||||
|
||||
bool AudioEngineImpl::init() {
|
||||
bool ret = false;
|
||||
do {
|
||||
#if CC_PLATFORM == CC_PLATFORM_IOS
|
||||
s_AudioEngineSessionHandler = [[AudioEngineSessionHandler alloc] init];
|
||||
#endif
|
||||
|
||||
s_ALDevice = alcOpenDevice(nullptr);
|
||||
|
||||
if (s_ALDevice) {
|
||||
s_ALContext = alcCreateContext(s_ALDevice, nullptr);
|
||||
alcMakeContextCurrent(s_ALContext);
|
||||
|
||||
alGenSources(MAX_AUDIOINSTANCES, _alSources);
|
||||
auto alError = alGetError();
|
||||
if (alError != AL_NO_ERROR) {
|
||||
ALOGE("%s:generating sources failed! error = %x", __PRETTY_FUNCTION__, alError);
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < MAX_AUDIOINSTANCES; ++i) {
|
||||
_unusedSourcesPool.push_back(_alSources[i]);
|
||||
alSourceAddNotificationExt(_alSources[i], AL_BUFFERS_PROCESSED, myAlSourceNotificationCallback, nullptr);
|
||||
}
|
||||
|
||||
// fixed #16170: Random crash in alGenBuffers(AudioCache::readDataTask) at startup
|
||||
// Please note that, as we know the OpenAL operation is atomic (threadsafe),
|
||||
// 'alGenBuffers' may be invoked by different threads. But in current implementation of 'alGenBuffers',
|
||||
// When the first time it's invoked, application may crash!!!
|
||||
// Why? OpenAL is opensource by Apple and could be found at
|
||||
// http://opensource.apple.com/source/OpenAL/OpenAL-48.7/Source/OpenAL/oalImp.cpp .
|
||||
/*
|
||||
|
||||
void InitializeBufferMap()
|
||||
{
|
||||
if (gOALBufferMap == NULL) // Position 1
|
||||
{
|
||||
gOALBufferMap = ccnew OALBufferMap (); // Position 2
|
||||
|
||||
// Position Gap
|
||||
|
||||
gBufferMapLock = ccnew CAGuard("OAL:BufferMapLock"); // Position 3
|
||||
gDeadOALBufferMap = ccnew OALBufferMap ();
|
||||
|
||||
OALBuffer *newBuffer = ccnew OALBuffer (AL_NONE);
|
||||
gOALBufferMap->Add(AL_NONE, &newBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
AL_API ALvoid AL_APIENTRY alGenBuffers(ALsizei n, ALuint *bids)
|
||||
{
|
||||
...
|
||||
|
||||
try {
|
||||
if (n < 0)
|
||||
throw ((OSStatus) AL_INVALID_VALUE);
|
||||
|
||||
InitializeBufferMap();
|
||||
if (gOALBufferMap == NULL)
|
||||
throw ((OSStatus) AL_INVALID_OPERATION);
|
||||
|
||||
CAGuard::Locker locked(*gBufferMapLock); // Position 4
|
||||
...
|
||||
...
|
||||
}
|
||||
|
||||
*/
|
||||
// 'gBufferMapLock' will be initialized in the 'InitializeBufferMap' function,
|
||||
// that's the problem. It means that 'InitializeBufferMap' may be invoked in different threads.
|
||||
// It will be very dangerous in multi-threads environment.
|
||||
// Imagine there're two threads (Thread A, Thread B), they call 'alGenBuffers' simultaneously.
|
||||
// While A goto 'Position Gap', 'gOALBufferMap' was assigned, then B goto 'Position 1' and find
|
||||
// that 'gOALBufferMap' isn't NULL, B just jump over 'InitialBufferMap' and goto 'Position 4'.
|
||||
// Meanwhile, A is still at 'Position Gap', B will crash at '*gBufferMapLock' since 'gBufferMapLock'
|
||||
// is still a null pointer. Oops, how could Apple implemented this method in this fucking way?
|
||||
|
||||
// Workaround is do an unused invocation in the mainthread right after OpenAL is initialized successfully
|
||||
// as bellow.
|
||||
// ================ Workaround begin ================ //
|
||||
|
||||
ALuint unusedAlBufferId = 0;
|
||||
alGenBuffers(1, &unusedAlBufferId);
|
||||
alDeleteBuffers(1, &unusedAlBufferId);
|
||||
|
||||
// ================ Workaround end ================ //
|
||||
|
||||
_scheduler = CC_CURRENT_ENGINE()->getScheduler();
|
||||
ret = true;
|
||||
ALOGI("OpenAL was initialized successfully!");
|
||||
}
|
||||
} while (false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
AudioCache *AudioEngineImpl::preload(const ccstd::string &filePath, std::function<void(bool)> callback) {
|
||||
AudioCache *audioCache = nullptr;
|
||||
|
||||
auto it = _audioCaches.find(filePath);
|
||||
if (it == _audioCaches.end()) {
|
||||
audioCache = &_audioCaches[filePath];
|
||||
audioCache->_fileFullPath = FileUtils::getInstance()->fullPathForFilename(filePath);
|
||||
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) {
|
||||
audioCache->addLoadCallback(callback);
|
||||
}
|
||||
return audioCache;
|
||||
}
|
||||
|
||||
int AudioEngineImpl::play2d(const ccstd::string &filePath, bool loop, float volume) {
|
||||
if (s_ALDevice == nullptr) {
|
||||
return AudioEngine::INVALID_AUDIO_ID;
|
||||
}
|
||||
|
||||
ALuint alSource = findValidSource();
|
||||
if (alSource == AL_INVALID) {
|
||||
return AudioEngine::INVALID_AUDIO_ID;
|
||||
}
|
||||
|
||||
auto *player = ccnew AudioPlayer;
|
||||
if (player == nullptr) {
|
||||
return AudioEngine::INVALID_AUDIO_ID;
|
||||
}
|
||||
|
||||
player->_alSource = alSource;
|
||||
player->_loop = loop;
|
||||
player->_volume = volume;
|
||||
|
||||
auto audioCache = preload(filePath, nullptr);
|
||||
if (audioCache == nullptr) {
|
||||
delete player;
|
||||
return AudioEngine::INVALID_AUDIO_ID;
|
||||
}
|
||||
|
||||
player->setCache(audioCache);
|
||||
_threadMutex.lock();
|
||||
_audioPlayers[_currentAudioID] = player;
|
||||
_threadMutex.unlock();
|
||||
|
||||
audioCache->addPlayCallback(std::bind(&AudioEngineImpl::play2dImpl, this, audioCache, _currentAudioID));
|
||||
|
||||
if (_lazyInitLoop) {
|
||||
_lazyInitLoop = false;
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->schedule(CC_CALLBACK_1(AudioEngineImpl::update, this), this, 0.05f, false, "AudioEngine");
|
||||
}
|
||||
}
|
||||
|
||||
return _currentAudioID++;
|
||||
}
|
||||
|
||||
void AudioEngineImpl::play2dImpl(AudioCache *cache, int audioID) {
|
||||
//Note: It may be 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()) {
|
||||
// Trust it, or assert it out.
|
||||
bool res = playerIt->second->play2d();
|
||||
CC_ASSERT(res);
|
||||
}
|
||||
_threadMutex.unlock();
|
||||
} else {
|
||||
ALOGD("AudioEngineImpl::play2dImpl, cache was destroyed or not ready!");
|
||||
auto iter = _audioPlayers.find(audioID);
|
||||
if (iter != _audioPlayers.end()) {
|
||||
iter->second->_removeByAudioEngine = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ALuint AudioEngineImpl::findValidSource() {
|
||||
ALuint sourceId = AL_INVALID;
|
||||
if (!_unusedSourcesPool.empty()) {
|
||||
sourceId = _unusedSourcesPool.front();
|
||||
_unusedSourcesPool.pop_front();
|
||||
}
|
||||
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
void AudioEngineImpl::setVolume(int audioID, float volume) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return;
|
||||
}
|
||||
auto player = _audioPlayers[audioID];
|
||||
player->_volume = volume;
|
||||
|
||||
if (player->_ready) {
|
||||
alSourcef(_audioPlayers[audioID]->_alSource, AL_GAIN, volume);
|
||||
|
||||
auto error = alGetError();
|
||||
if (error != AL_NO_ERROR) {
|
||||
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__, audioID, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngineImpl::setLoop(int audioID, bool loop) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return;
|
||||
}
|
||||
auto player = _audioPlayers[audioID];
|
||||
|
||||
if (player->_ready) {
|
||||
if (player->_streamingSource) {
|
||||
player->setLoop(loop);
|
||||
} else {
|
||||
if (loop) {
|
||||
alSourcei(player->_alSource, AL_LOOPING, AL_TRUE);
|
||||
} else {
|
||||
alSourcei(player->_alSource, AL_LOOPING, AL_FALSE);
|
||||
}
|
||||
|
||||
auto error = alGetError();
|
||||
if (error != AL_NO_ERROR) {
|
||||
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__, audioID, error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
player->_loop = loop;
|
||||
}
|
||||
}
|
||||
|
||||
bool AudioEngineImpl::pause(int audioID) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return false;
|
||||
}
|
||||
bool ret = true;
|
||||
alSourcePause(_audioPlayers[audioID]->_alSource);
|
||||
|
||||
auto error = alGetError();
|
||||
if (error != AL_NO_ERROR) {
|
||||
ret = false;
|
||||
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__, audioID, error);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AudioEngineImpl::resume(int audioID) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return false;
|
||||
}
|
||||
bool ret = true;
|
||||
alSourcePlay(_audioPlayers[audioID]->_alSource);
|
||||
|
||||
auto error = alGetError();
|
||||
if (error != AL_NO_ERROR) {
|
||||
ret = false;
|
||||
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__, audioID, error);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AudioEngineImpl::stop(int audioID) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return;
|
||||
}
|
||||
auto player = _audioPlayers[audioID];
|
||||
player->destroy();
|
||||
|
||||
// Call 'update' method to cleanup immediately since the schedule may be cancelled without any notification.
|
||||
update(0.0f);
|
||||
}
|
||||
|
||||
void AudioEngineImpl::stopAll() {
|
||||
for (auto &&player : _audioPlayers) {
|
||||
player.second->destroy();
|
||||
}
|
||||
|
||||
// Call 'update' method to cleanup immediately since the schedule may be cancelled without any notification.
|
||||
update(0.0f);
|
||||
}
|
||||
|
||||
float AudioEngineImpl::getDuration(int audioID) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return 0.0f;
|
||||
}
|
||||
auto player = _audioPlayers[audioID];
|
||||
if (player->_ready) {
|
||||
return player->_audioCache->_duration;
|
||||
} else {
|
||||
return AudioEngine::TIME_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
float AudioEngineImpl::getDurationFromFile(const ccstd::string &filePath) {
|
||||
auto it = _audioCaches.find(filePath);
|
||||
if (it == _audioCaches.end()) {
|
||||
this->preload(filePath, nullptr);
|
||||
return AudioEngine::TIME_UNKNOWN;
|
||||
}
|
||||
|
||||
return it->second._duration;
|
||||
}
|
||||
|
||||
float AudioEngineImpl::getCurrentTime(int audioID) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return 0.0f;
|
||||
}
|
||||
float ret = 0.0f;
|
||||
auto player = _audioPlayers[audioID];
|
||||
if (player->_ready) {
|
||||
if (player->_streamingSource) {
|
||||
ret = player->getTime();
|
||||
} else {
|
||||
alGetSourcef(player->_alSource, AL_SEC_OFFSET, &ret);
|
||||
|
||||
auto error = alGetError();
|
||||
if (error != AL_NO_ERROR) {
|
||||
ALOGE("%s, audio id:%d,error code:%x", __PRETTY_FUNCTION__, audioID, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool AudioEngineImpl::setCurrentTime(int audioID, float time) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return false;
|
||||
}
|
||||
bool ret = false;
|
||||
auto player = _audioPlayers[audioID];
|
||||
|
||||
do {
|
||||
if (!player->_ready) {
|
||||
std::lock_guard<std::mutex> lck(player->_play2dMutex);// To prevent the race condition
|
||||
player->_timeDirty = true;
|
||||
player->_currTime = time;
|
||||
break;
|
||||
}
|
||||
|
||||
if (player->_streamingSource) {
|
||||
ret = player->setTime(time);
|
||||
break;
|
||||
} else {
|
||||
if (player->_audioCache->_framesRead != player->_audioCache->_totalFrames &&
|
||||
(time * player->_audioCache->_sampleRate) > player->_audioCache->_framesRead) {
|
||||
ALOGE("%s: audio id = %d", __PRETTY_FUNCTION__, audioID);
|
||||
break;
|
||||
}
|
||||
|
||||
alSourcef(player->_alSource, AL_SEC_OFFSET, time);
|
||||
|
||||
auto error = alGetError();
|
||||
if (error != AL_NO_ERROR) {
|
||||
ALOGE("%s: audio id = %d, error = %x", __PRETTY_FUNCTION__, audioID, error);
|
||||
}
|
||||
ret = true;
|
||||
}
|
||||
} while (0);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AudioEngineImpl::setFinishCallback(int audioID, const std::function<void(int, const ccstd::string &)> &callback) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return;
|
||||
}
|
||||
_audioPlayers[audioID]->_finishCallbak = callback;
|
||||
}
|
||||
|
||||
void AudioEngineImpl::update(float dt) {
|
||||
ALint sourceState;
|
||||
int audioID;
|
||||
AudioPlayer *player;
|
||||
ALuint alSource;
|
||||
|
||||
// ALOGV("AudioPlayer count: %d", (int)_audioPlayers.size());
|
||||
|
||||
for (auto it = _audioPlayers.begin(); it != _audioPlayers.end();) {
|
||||
audioID = it->first;
|
||||
player = it->second;
|
||||
alSource = player->_alSource;
|
||||
alGetSourcei(alSource, AL_SOURCE_STATE, &sourceState);
|
||||
|
||||
if (player->_removeByAudioEngine) {
|
||||
AudioEngine::remove(audioID);
|
||||
_threadMutex.lock();
|
||||
it = _audioPlayers.erase(it);
|
||||
_threadMutex.unlock();
|
||||
delete player;
|
||||
_unusedSourcesPool.push_back(alSource);
|
||||
} else if (player->_ready && sourceState == AL_STOPPED) {
|
||||
ccstd::string filePath;
|
||||
if (player->_finishCallbak) {
|
||||
auto &audioInfo = AudioEngine::sAudioIDInfoMap[audioID];
|
||||
filePath = *audioInfo.filePath;
|
||||
}
|
||||
|
||||
AudioEngine::remove(audioID);
|
||||
_threadMutex.lock();
|
||||
it = _audioPlayers.erase(it);
|
||||
_threadMutex.unlock();
|
||||
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
if (player->_finishCallbak) {
|
||||
auto cb = player->_finishCallbak;
|
||||
sche->performFunctionInCocosThread([audioID, cb, filePath]() {
|
||||
cb(audioID, filePath); //IDEA: callback will delay 50ms
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
delete player;
|
||||
_unusedSourcesPool.push_back(alSource);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
if (_audioPlayers.empty()) {
|
||||
_lazyInitLoop = true;
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->unschedule("AudioEngine", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AudioEngineImpl::uncache(const ccstd::string &filePath) {
|
||||
_audioCaches.erase(filePath);
|
||||
}
|
||||
|
||||
void AudioEngineImpl::uncacheAll() {
|
||||
_audioCaches.clear();
|
||||
}
|
||||
|
||||
bool AudioEngineImpl::checkAudioIdValid(int audioID) {
|
||||
return _audioPlayers.find(audioID) != _audioPlayers.end();
|
||||
}
|
||||
|
||||
PCMHeader AudioEngineImpl::getPCMHeader(const char *url){
|
||||
PCMHeader header {};
|
||||
auto itr = _audioCaches.find(url);
|
||||
if (itr != _audioCaches.end() && itr->second._state == AudioCache::State::READY) {
|
||||
CC_LOG_DEBUG("file %s found in cache, load header directly", url);
|
||||
auto cache = &itr->second;
|
||||
header.bytesPerFrame = cache->_bytesPerFrame;
|
||||
header.channelCount = cache->_channelCount;
|
||||
header.dataFormat = AudioDataFormat::SIGNED_16;
|
||||
header.sampleRate = cache->_sampleRate;
|
||||
header.totalFrames = cache->_totalFrames;
|
||||
return header;
|
||||
}
|
||||
ccstd::string fileFullPath = FileUtils::getInstance()->fullPathForFilename(url);
|
||||
if (fileFullPath == "") {
|
||||
CC_LOG_DEBUG("file %s does not exist or failed to load", url);
|
||||
return header;
|
||||
}
|
||||
AudioDecoder decoder;
|
||||
do {
|
||||
if (!decoder.open(fileFullPath.c_str())) {
|
||||
CC_LOG_ERROR("[Audio Decoder] File open failed %s", url);
|
||||
break;
|
||||
}
|
||||
header.bytesPerFrame = decoder.getBytesPerFrame();
|
||||
header.channelCount = decoder.getChannelCount();
|
||||
header.dataFormat = AudioDataFormat::SIGNED_16;
|
||||
header.sampleRate = decoder.getSampleRate();
|
||||
header.totalFrames = decoder.getTotalFrames();
|
||||
} while (false);
|
||||
|
||||
decoder.close();
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
ccstd::vector<uint8_t> AudioEngineImpl::getOriginalPCMBuffer(const char *url, uint32_t channelID) {
|
||||
ccstd::vector<uint8_t> pcmData;
|
||||
auto itr = _audioCaches.find(url);
|
||||
if (itr != _audioCaches.end() && itr->second._state == AudioCache::State::READY) {
|
||||
auto cache = &itr->second;
|
||||
auto bytesPerChannelInFrame = cache->_bytesPerFrame / cache->_channelCount;
|
||||
pcmData.resize(bytesPerChannelInFrame * cache->_totalFrames);
|
||||
auto *p = pcmData.data();
|
||||
if (!cache->isStreaming()) { // Cache contains a fully prepared buffer.
|
||||
for (int itr = 0; itr < cache->_totalFrames; itr++) {
|
||||
memcpy(p, cache->_pcmData + itr * cache->_bytesPerFrame + channelID * bytesPerChannelInFrame, bytesPerChannelInFrame);
|
||||
p += bytesPerChannelInFrame;
|
||||
}
|
||||
return pcmData;
|
||||
}
|
||||
}
|
||||
ccstd::string fileFullPath = FileUtils::getInstance()->fullPathForFilename(url);
|
||||
if (fileFullPath.empty()) {
|
||||
CC_LOG_DEBUG("file %s does not exist or failed to load", url);
|
||||
return pcmData;
|
||||
}
|
||||
AudioDecoder decoder;
|
||||
|
||||
do {
|
||||
if (!decoder.open(fileFullPath.c_str())) {
|
||||
CC_LOG_ERROR("[Audio Decoder] File open failed %s", url);
|
||||
break;
|
||||
}
|
||||
const uint32_t bytesPerFrame = decoder.getBytesPerFrame();
|
||||
const uint32_t channelCount = decoder.getChannelCount();
|
||||
if (channelID >= channelCount) {
|
||||
CC_LOG_ERROR("channelID invalid, total channel count is %d but %d is required", channelCount, channelID);
|
||||
break;
|
||||
}
|
||||
uint32_t totalFrames = decoder.getTotalFrames();
|
||||
uint32_t remainingFrames = totalFrames;
|
||||
uint32_t framesRead = 0;
|
||||
uint32_t framesToReadOnce = std::min(totalFrames, static_cast<uint32_t>(decoder.getSampleRate() * QUEUEBUFFER_TIME_STEP * QUEUEBUFFER_NUM));
|
||||
const uint32_t bytesPerChannelInFrame = bytesPerFrame / channelCount;
|
||||
|
||||
pcmData.resize(bytesPerChannelInFrame * totalFrames);
|
||||
uint8_t *p = pcmData.data();
|
||||
|
||||
auto tmpBuf = static_cast<char *>(malloc(framesToReadOnce * bytesPerFrame));
|
||||
|
||||
while (remainingFrames > 0) {
|
||||
framesToReadOnce = std::min(framesToReadOnce, remainingFrames);
|
||||
framesRead = decoder.read(framesToReadOnce, tmpBuf);
|
||||
for (int itr = 0; itr < framesToReadOnce; itr++) {
|
||||
memcpy(p, tmpBuf + itr * bytesPerFrame + channelID * bytesPerChannelInFrame, bytesPerChannelInFrame);
|
||||
p += bytesPerChannelInFrame;
|
||||
}
|
||||
remainingFrames -= framesToReadOnce;
|
||||
|
||||
};
|
||||
free(tmpBuf);
|
||||
// Adjust total frames by setting position to the end of frames and try to read more data.
|
||||
// This is a workaround for https://github.com/cocos2d/cocos2d-x/issues/16938
|
||||
if (decoder.seek(totalFrames)) {
|
||||
tmpBuf = static_cast<char *>(malloc(bytesPerFrame * framesToReadOnce));
|
||||
do {
|
||||
framesRead = decoder.read(framesToReadOnce, tmpBuf); //read one by one to easy divide
|
||||
if (framesRead > 0) { // Adjust frames exist
|
||||
// transfer char data to float data
|
||||
for (int itr = 0; itr < framesRead; itr++) {
|
||||
memcpy(p, tmpBuf + itr * bytesPerFrame + channelID * bytesPerChannelInFrame, bytesPerChannelInFrame);
|
||||
p += bytesPerChannelInFrame;
|
||||
}
|
||||
}
|
||||
} while (framesRead > 0);
|
||||
free(tmpBuf);
|
||||
}
|
||||
BREAK_IF_ERR_LOG(!decoder.seek(0), "AudioDecoder::seek(0) failed!");
|
||||
} while (false);
|
||||
decoder.close();
|
||||
return pcmData;
|
||||
}
|
||||
67
cocos/audio/apple/AudioMacros.h
Normal file
67
cocos/audio/apple/AudioMacros.h
Normal file
@@ -0,0 +1,67 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#define QUEUEBUFFER_NUM (4)
|
||||
#define QUEUEBUFFER_TIME_STEP (0.05f)
|
||||
|
||||
#define QUOTEME_(x) #x
|
||||
#define QUOTEME(x) QUOTEME_(x)
|
||||
|
||||
#if defined(CC_DEBUG) && CC_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(CC_DEBUG) && CC_DEBUG > 0
|
||||
#define CHECK_AL_ERROR_DEBUG() \
|
||||
do { \
|
||||
ALenum __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
|
||||
|
||||
#define BREAK_IF(condition) \
|
||||
if (!!(condition)) { \
|
||||
break; \
|
||||
}
|
||||
|
||||
#define BREAK_IF_ERR_LOG(condition, fmt, ...) \
|
||||
if (!!(condition)) { \
|
||||
ALOGE("(" QUOTEME(condition) ") failed, message: " fmt, ##__VA_ARGS__); \
|
||||
break; \
|
||||
}
|
||||
88
cocos/audio/apple/AudioPlayer.h
Normal file
88
cocos/audio/apple/AudioPlayer.h
Normal file
@@ -0,0 +1,88 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2014-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2023 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "audio/apple/AudioMacros.h"
|
||||
#include "base/Macros.h"
|
||||
|
||||
#include <OpenAL/al.h>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include "base/std/container/string.h"
|
||||
|
||||
namespace cc {
|
||||
class AudioCache;
|
||||
class AudioEngineImpl;
|
||||
|
||||
class AudioPlayer {
|
||||
public:
|
||||
AudioPlayer();
|
||||
~AudioPlayer();
|
||||
|
||||
void destroy();
|
||||
|
||||
//queue buffer related stuff
|
||||
bool setTime(float time);
|
||||
float getTime() { return _currTime; }
|
||||
bool setLoop(bool loop);
|
||||
|
||||
protected:
|
||||
void setCache(AudioCache *cache);
|
||||
void rotateBufferThread(int offsetFrame);
|
||||
bool play2d();
|
||||
void wakeupRotateThread();
|
||||
|
||||
AudioCache *_audioCache;
|
||||
|
||||
float _volume;
|
||||
bool _loop;
|
||||
std::function<void(int, const ccstd::string &)> _finishCallbak;
|
||||
|
||||
bool _isDestroyed;
|
||||
bool _removeByAudioEngine;
|
||||
bool _ready;
|
||||
ALuint _alSource;
|
||||
|
||||
//play by circular buffer
|
||||
float _currTime;
|
||||
bool _streamingSource;
|
||||
ALuint _bufferIds[QUEUEBUFFER_NUM];
|
||||
std::thread *_rotateBufferThread;
|
||||
std::condition_variable _sleepCondition;
|
||||
std::mutex _sleepMutex;
|
||||
bool _timeDirty;
|
||||
bool _isRotateThreadExited;
|
||||
std::atomic_bool _needWakeupRotateThread;
|
||||
|
||||
std::mutex _play2dMutex;
|
||||
|
||||
unsigned int _id;
|
||||
|
||||
friend class AudioEngineImpl;
|
||||
};
|
||||
|
||||
} // namespace cc
|
||||
350
cocos/audio/apple/AudioPlayer.mm
Normal file
350
cocos/audio/apple/AudioPlayer.mm
Normal file
@@ -0,0 +1,350 @@
|
||||
/****************************************************************************
|
||||
Copyright (c) 2014-2016 Chukong Technologies Inc.
|
||||
Copyright (c) 2017-2022 Xiamen Yaji Software Co., Ltd.
|
||||
|
||||
http://www.cocos.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated engine source code (the "Software"), a limited,
|
||||
worldwide, royalty-free, non-assignable, revocable and non-exclusive license
|
||||
to use Cocos Creator solely to develop games on your target platforms. You shall
|
||||
not use Cocos Creator software for developing other software or tools that's
|
||||
used for developing games. You are not granted to publish, distribute,
|
||||
sublicense, and/or sell copies of Cocos Creator.
|
||||
|
||||
The software or tools in this License Agreement are licensed, not sold.
|
||||
Xiamen Yaji Software Co., Ltd. reserves all rights not expressly granted to you.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
****************************************************************************/
|
||||
|
||||
#define LOG_TAG "AudioPlayer"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#include "audio/apple/AudioCache.h"
|
||||
#include "audio/apple/AudioDecoder.h"
|
||||
#include "audio/apple/AudioPlayer.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "platform/FileUtils.h"
|
||||
|
||||
#ifdef VERY_VERY_VERBOSE_LOGGING
|
||||
#define ALOGVV ALOGV
|
||||
#else
|
||||
#define ALOGVV(...) \
|
||||
do { \
|
||||
} while (false)
|
||||
#endif
|
||||
|
||||
using namespace cc;
|
||||
|
||||
namespace {
|
||||
unsigned int __idIndex = 0;
|
||||
}
|
||||
|
||||
AudioPlayer::AudioPlayer()
|
||||
: _audioCache(nullptr), _finishCallbak(nullptr), _isDestroyed(false), _removeByAudioEngine(false), _ready(false), _currTime(0.0f), _streamingSource(false), _rotateBufferThread(nullptr), _timeDirty(false), _isRotateThreadExited(false), _needWakeupRotateThread(false), _id(++__idIndex) {
|
||||
memset(_bufferIds, 0, sizeof(_bufferIds));
|
||||
}
|
||||
|
||||
AudioPlayer::~AudioPlayer() {
|
||||
ALOGVV("~AudioPlayer() (%p), id=%u", this, _id);
|
||||
destroy();
|
||||
|
||||
if (_streamingSource) {
|
||||
alDeleteBuffers(QUEUEBUFFER_NUM, _bufferIds);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayer::destroy() {
|
||||
if (_isDestroyed)
|
||||
return;
|
||||
|
||||
ALOGVV("AudioPlayer::destroy begin, id=%u", _id);
|
||||
|
||||
_isDestroyed = true;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// 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!");
|
||||
|
||||
#if CC_TARGET_PLATFORM == CC_PLATFORM_IOS
|
||||
// some specific OpenAL implement defects existed on iOS platform
|
||||
// refer to: https://github.com/cocos2d/cocos2d-x/issues/18597
|
||||
ALint sourceState;
|
||||
ALint bufferProcessed = 0;
|
||||
alGetSourcei(_alSource, AL_SOURCE_STATE, &sourceState);
|
||||
if (sourceState == AL_PLAYING) {
|
||||
alGetSourcei(_alSource, AL_BUFFERS_PROCESSED, &bufferProcessed);
|
||||
while (bufferProcessed < QUEUEBUFFER_NUM) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
alGetSourcei(_alSource, AL_BUFFERS_PROCESSED, &bufferProcessed);
|
||||
}
|
||||
alSourceUnqueueBuffers(_alSource, QUEUEBUFFER_NUM, _bufferIds);
|
||||
CHECK_AL_ERROR_DEBUG();
|
||||
}
|
||||
ALOGVV("UnqueueBuffers Before alSourceStop");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} while (false);
|
||||
|
||||
ALOGVV("Before alSourceStop");
|
||||
alSourceStop(_alSource);
|
||||
CHECK_AL_ERROR_DEBUG();
|
||||
ALOGVV("Before alSourcei");
|
||||
alSourcei(_alSource, AL_BUFFER, 0);
|
||||
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();
|
||||
ALOGVV("AudioPlayer::play2d, _alSource: %u", _alSource);
|
||||
|
||||
/*********************************************************************/
|
||||
/* Note that it may be in sub thread or in main thread. **/
|
||||
/*********************************************************************/
|
||||
bool ret = false;
|
||||
do {
|
||||
if (_audioCache->_state != AudioCache::State::READY) {
|
||||
ALOGE("alBuffer isn't ready for play!");
|
||||
break;
|
||||
}
|
||||
|
||||
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 {
|
||||
if (_currTime > _audioCache->_duration) {
|
||||
_currTime = 0.F; // Target current start time is invalid, reset to 0.
|
||||
}
|
||||
alGenBuffers(QUEUEBUFFER_NUM, _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) {
|
||||
// To continuously stream audio from a source without interruption, buffer queuing is required.
|
||||
alSourceQueueBuffers(_alSource, QUEUEBUFFER_NUM, _bufferIds);
|
||||
CHECK_AL_ERROR_DEBUG();
|
||||
_rotateBufferThread = ccnew 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;
|
||||
}
|
||||
/** Due to the bug of OpenAL, when the second time OpenAL trying to mix audio into bus, the mRampState become kRampingComplete, and for those oalSource whose mRampState == kRampingComplete, nothing happens.
|
||||
* OALSource::Play{
|
||||
* switch(mState){
|
||||
* case kTransitionToStop:
|
||||
* case kTransitionToStop:
|
||||
* if(mRampState != kRampingComplete){..}
|
||||
* break;
|
||||
* }
|
||||
* }
|
||||
* So the assert here will trigger this bug as aolSource is reused.
|
||||
* Replace OpenAL with AVAudioEngine on V3.6 mightbe helpful
|
||||
*/
|
||||
// CC_ASSERT_EQ(state, AL_PLAYING);
|
||||
_ready = true;
|
||||
ret = true;
|
||||
} while (false);
|
||||
|
||||
if (!ret) {
|
||||
_removeByAudioEngine = true;
|
||||
}
|
||||
|
||||
_play2dMutex.unlock();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// rotateBufferThread is used to rotate alBufferData for _alSource when playing big audio file
|
||||
void AudioPlayer::rotateBufferThread(int offsetFrame) {
|
||||
char *tmpBuffer = nullptr;
|
||||
AudioDecoder decoder;
|
||||
long long rotateSleepTime = static_cast<long long>(QUEUEBUFFER_TIME_STEP * 1000) / 2;
|
||||
do {
|
||||
BREAK_IF(!decoder.open(_audioCache->_fileFullPath.c_str()));
|
||||
|
||||
uint32_t framesRead = 0;
|
||||
const uint32_t framesToRead = _audioCache->_queBufferFrames;
|
||||
const uint32_t bufferSize = framesToRead * decoder.getBytesPerFrame();
|
||||
tmpBuffer = (char *)malloc(bufferSize);
|
||||
memset(tmpBuffer, 0, bufferSize);
|
||||
|
||||
if (offsetFrame != 0) {
|
||||
decoder.seek(offsetFrame);
|
||||
}
|
||||
|
||||
ALint sourceState;
|
||||
ALint bufferProcessed = 0;
|
||||
bool needToExitThread = false;
|
||||
|
||||
while (!_isDestroyed) {
|
||||
alGetSourcei(_alSource, AL_SOURCE_STATE, &sourceState);
|
||||
/* On IOS, audio state will lie, when the system is not fully foreground,
|
||||
* openAl will process the buffer in queue, but our condition cannot make sure that the audio
|
||||
* is playing as it's too short. Interesting IOS system.
|
||||
* Solution is to load buffer even if it's paused, just make sure that there's no bufferProcessed in
|
||||
*/
|
||||
if (sourceState == AL_PLAYING || sourceState == AL_PAUSED) {
|
||||
alGetSourcei(_alSource, AL_BUFFERS_PROCESSED, &bufferProcessed);
|
||||
while (bufferProcessed > 0) {
|
||||
bufferProcessed--;
|
||||
if (_timeDirty) {
|
||||
_timeDirty = false;
|
||||
offsetFrame = _currTime * decoder.getSampleRate();
|
||||
decoder.seek(offsetFrame);
|
||||
} else {
|
||||
_currTime += QUEUEBUFFER_TIME_STEP;
|
||||
if (_currTime > _audioCache->_duration) {
|
||||
if (_loop) {
|
||||
_currTime = 0.0f;
|
||||
} else {
|
||||
_currTime = _audioCache->_duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
framesRead = decoder.readFixedFrames(framesToRead, tmpBuffer);
|
||||
|
||||
if (framesRead == 0) {
|
||||
if (_loop) {
|
||||
decoder.seek(0);
|
||||
framesRead = decoder.readFixedFrames(framesToRead, tmpBuffer);
|
||||
} else {
|
||||
needToExitThread = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/*
|
||||
While the source is playing, alSourceUnqueueBuffers can be called to remove buffers which have
|
||||
already played. Those buffers can then be filled with new data or discarded. New or refilled
|
||||
buffers can then be attached to the playing source using alSourceQueueBuffers. As long as there is
|
||||
always a new buffer to play in the queue, the source will continue to play.
|
||||
*/
|
||||
ALuint bid;
|
||||
alSourceUnqueueBuffers(_alSource, 1, &bid);
|
||||
alBufferData(bid, _audioCache->_format, tmpBuffer, framesRead * decoder.getBytesPerFrame(), decoder.getSampleRate());
|
||||
alSourceQueueBuffers(_alSource, 1, &bid);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lk(_sleepMutex);
|
||||
if (_isDestroyed || needToExitThread) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!_needWakeupRotateThread) {
|
||||
_sleepCondition.wait_for(lk, std::chrono::milliseconds(rotateSleepTime));
|
||||
}
|
||||
|
||||
_needWakeupRotateThread = false;
|
||||
}
|
||||
|
||||
} while (false);
|
||||
|
||||
ALOGVV("Exit rotate buffer thread ...");
|
||||
decoder.close();
|
||||
free(tmpBuffer);
|
||||
_isRotateThreadExited = true;
|
||||
}
|
||||
|
||||
void AudioPlayer::wakeupRotateThread() {
|
||||
_needWakeupRotateThread = true;
|
||||
_sleepCondition.notify_all();
|
||||
}
|
||||
|
||||
bool AudioPlayer::setLoop(bool loop) {
|
||||
if (!_isDestroyed) {
|
||||
_loop = loop;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AudioPlayer::setTime(float time) {
|
||||
if (!_isDestroyed && time >= 0.0f && time < _audioCache->_duration) {
|
||||
_currTime = time;
|
||||
_timeDirty = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user