no message
This commit is contained in:
339
cocos/audio/oalsoft/AudioCache.cpp
Normal file
339
cocos/audio/oalsoft/AudioCache.cpp
Normal file
@@ -0,0 +1,339 @@
|
||||
/****************************************************************************
|
||||
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.
|
||||
****************************************************************************/
|
||||
|
||||
#define LOG_TAG "AudioCache"
|
||||
|
||||
#include "audio/oalsoft/AudioCache.h"
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "audio/common/decoder/AudioDecoder.h"
|
||||
#include "audio/common/decoder/AudioDecoderManager.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define VERY_VERY_VERBOSE_LOGGING
|
||||
#ifdef VERY_VERY_VERBOSE_LOGGING
|
||||
#define ALOGVV ALOGV
|
||||
#else
|
||||
#define ALOGVV(...) \
|
||||
do { \
|
||||
} while (false)
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
unsigned int gIdIndex = 0;
|
||||
}
|
||||
|
||||
#define PCMDATA_CACHEMAXSIZE 1048576
|
||||
|
||||
using namespace cc; //NOLINT
|
||||
|
||||
AudioCache::AudioCache()
|
||||
: _isDestroyed(std::make_shared<bool>(false)), _id(++gIdIndex) {
|
||||
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();
|
||||
_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);
|
||||
}
|
||||
|
||||
free(_pcmData);
|
||||
}
|
||||
|
||||
if (_queBufferFrames > 0) {
|
||||
for (auto &buffer : _queBuffers) {
|
||||
free(buffer);
|
||||
}
|
||||
}
|
||||
ALOGVV("~AudioCache() %p, id=%u, end", this, _id);
|
||||
}
|
||||
|
||||
void AudioCache::readDataTask(unsigned int selfId) {
|
||||
//Note: It's in sub thread
|
||||
ALOGVV("readDataTask begin, cache id=%u", selfId);
|
||||
|
||||
_readDataTaskMutex.lock();
|
||||
_state = State::LOADING;
|
||||
|
||||
AudioDecoder *decoder = AudioDecoderManager::createDecoder(_fileFullPath.c_str());
|
||||
do {
|
||||
if (decoder == nullptr || !decoder->open(_fileFullPath.c_str())) {
|
||||
break;
|
||||
}
|
||||
|
||||
const uint32_t originalTotalFrames = decoder->getTotalFrames();
|
||||
_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 = static_cast<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));
|
||||
|
||||
ccstd::vector<char> adjustFrameBuf;
|
||||
|
||||
if (decoder->seek(totalFrames)) {
|
||||
auto *tmpBuf = static_cast<char *>(malloc(framesToReadOnce * _bytesPerFrame));
|
||||
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 = static_cast<char *>(malloc(dataSize));
|
||||
|
||||
CC_ASSERT(_pcmData);
|
||||
memset(_pcmData, 0x00, dataSize);
|
||||
|
||||
if (adjustFrames > 0) {
|
||||
memcpy(_pcmData + (dataSize - adjustFrameBuf.size()), adjustFrameBuf.data(), adjustFrameBuf.size());
|
||||
}
|
||||
|
||||
alGenBuffers(1, &_alBufferId);
|
||||
auto alError = alGetError();
|
||||
if (alError != AL_NO_ERROR) {
|
||||
ALOGE("%s: attaching audio to buffer fail: %x", __FUNCTION__, alError);
|
||||
break;
|
||||
}
|
||||
|
||||
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 (*_isDestroyed) {
|
||||
break;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
alBufferData(_alBufferId, _format, _pcmData, static_cast<ALsizei>(dataSize), static_cast<ALsizei>(sampleRate));
|
||||
|
||||
_state = State::READY;
|
||||
} else {
|
||||
_isStreaming = true;
|
||||
_queBufferFrames = static_cast<uint32_t>(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] = static_cast<char *>(malloc(queBufferBytes));
|
||||
_queBufferSize[index] = queBufferBytes;
|
||||
|
||||
decoder->readFixedFrames(_queBufferFrames, _queBuffers[index]);
|
||||
}
|
||||
|
||||
_state = State::READY;
|
||||
}
|
||||
|
||||
} while (false);
|
||||
|
||||
if (decoder != nullptr) {
|
||||
decoder->close();
|
||||
}
|
||||
|
||||
AudioDecoderManager::destroyDecoder(decoder);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
//IDEA: Why to invoke play callback first? Should it be after 'load' callback?
|
||||
invokingPlayCallbacks();
|
||||
invokingLoadCallbacks();
|
||||
|
||||
_isLoadingFinished = true;
|
||||
_readDataTaskMutex.unlock();
|
||||
ALOGVV("readDataTask end, cache id=%u", selfId);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
BaseEngine::SchedulerPtr scheduler =
|
||||
CC_CURRENT_APPLICATION() ? CC_CURRENT_APPLICATION()->getEngine()->getScheduler() : nullptr;
|
||||
if (!scheduler) {
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
121
cocos/audio/oalsoft/AudioCache.h
Normal file
121
cocos/audio/oalsoft/AudioCache.h
Normal file
@@ -0,0 +1,121 @@
|
||||
/****************************************************************************
|
||||
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 <sys/types.h>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include "base/std/container/string.h"
|
||||
#if defined(OPENAL_PLAIN_INCLUDES)
|
||||
#include <al.h>
|
||||
#elif CC_PLATFORM == CC_PLATFORM_WINDOWS
|
||||
#include <OpenalSoft/al.h>
|
||||
#elif CC_PLATFORM == CC_PLATFORM_OHOS
|
||||
#include <AL/al.h>
|
||||
#elif CC_PLATFORM == CC_PLATFORM_LINUX || CC_PLATFORM == CC_PLATFORM_QNX
|
||||
#include <AL/al.h>
|
||||
#endif
|
||||
#include "audio/include/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 CC_DLL 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);
|
||||
|
||||
uint32_t getChannelCount() const { return _channelCount; }
|
||||
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{-1};
|
||||
float _duration{0.0F};
|
||||
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
|
||||
645
cocos/audio/oalsoft/AudioEngine-soft.cpp
Normal file
645
cocos/audio/oalsoft/AudioEngine-soft.cpp
Normal file
@@ -0,0 +1,645 @@
|
||||
/****************************************************************************
|
||||
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.
|
||||
****************************************************************************/
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include "audio/common/decoder/AudioDecoder.h"
|
||||
#include "base/Log.h"
|
||||
#include "base/Utils.h"
|
||||
#include "base/std/container/vector.h"
|
||||
#define LOG_TAG "AudioEngine-OALSOFT"
|
||||
|
||||
#include "audio/oalsoft/AudioEngine-soft.h"
|
||||
|
||||
#ifdef OPENAL_PLAIN_INCLUDES
|
||||
#include "alc.h"
|
||||
#include "alext.h"
|
||||
#elif CC_PLATFORM == CC_PLATFORM_WINDOWS
|
||||
#include "OpenalSoft/alc.h"
|
||||
#include "OpenalSoft/alext.h"
|
||||
#elif CC_PLATFORM == CC_PLATFORM_OHOS
|
||||
#include "AL/alc.h"
|
||||
#include "AL/alext.h"
|
||||
#elif CC_PLATFORM == CC_PLATFORM_LINUX || CC_PLATFORM == CC_PLATFORM_QNX
|
||||
#include "AL/alc.h"
|
||||
#include "AL/alext.h"
|
||||
#endif
|
||||
#include "application/ApplicationManager.h"
|
||||
#include "audio/common/decoder/AudioDecoderManager.h"
|
||||
#include "audio/include/AudioEngine.h"
|
||||
#include "base/Scheduler.h"
|
||||
#include "base/memory/Memory.h"
|
||||
#include "platform/FileUtils.h"
|
||||
|
||||
#if CC_PLATFORM == CC_PLATFORM_WINDOWS
|
||||
#include <windows.h>
|
||||
|
||||
// log, CC_LOG_DEBUG aren't threadsafe, since we uses sub threads for parsing pcm data, threadsafe log output
|
||||
// is needed. Define the following macros (ALOGV, ALOGD, ALOGI, ALOGW, ALOGE) for threadsafe log output.
|
||||
|
||||
//IDEA: Move _winLog, winLog to a separated file
|
||||
static void _winLog(const char *format, va_list args) {
|
||||
static const int MAX_LOG_LENGTH = 16 * 1024;
|
||||
int bufferSize = MAX_LOG_LENGTH;
|
||||
char *buf = nullptr;
|
||||
|
||||
do {
|
||||
buf = ccnew char[bufferSize];
|
||||
if (buf == nullptr)
|
||||
return; // not enough memory
|
||||
|
||||
int ret = vsnprintf(buf, bufferSize - 3, format, args);
|
||||
if (ret < 0) {
|
||||
bufferSize *= 2;
|
||||
|
||||
delete[] buf;
|
||||
} else
|
||||
break;
|
||||
|
||||
} while (true);
|
||||
|
||||
strcat(buf, "\n");
|
||||
|
||||
int pos = 0;
|
||||
auto len = static_cast<int>(strlen(buf));
|
||||
char tempBuf[MAX_LOG_LENGTH + 1] = {0};
|
||||
WCHAR wszBuf[MAX_LOG_LENGTH + 1] = {0};
|
||||
|
||||
do {
|
||||
std::copy(buf + pos, buf + pos + MAX_LOG_LENGTH, tempBuf);
|
||||
|
||||
tempBuf[MAX_LOG_LENGTH] = 0;
|
||||
|
||||
MultiByteToWideChar(CP_UTF8, 0, tempBuf, -1, wszBuf, sizeof(wszBuf));
|
||||
OutputDebugStringW(wszBuf);
|
||||
|
||||
pos += MAX_LOG_LENGTH;
|
||||
|
||||
} while (pos < len);
|
||||
|
||||
delete[] buf;
|
||||
}
|
||||
|
||||
#ifndef audioLog
|
||||
void audioLog(const char *format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
_winLog(format, args);
|
||||
va_end(args);
|
||||
}
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#define audioLog(...) CC_LOG_DEBUG(__VA_ARGS__)
|
||||
|
||||
#endif
|
||||
|
||||
using namespace cc; //NOLINT
|
||||
|
||||
static ALCdevice *sALDevice = nullptr;
|
||||
static ALCcontext *sALContext = nullptr;
|
||||
|
||||
AudioEngineImpl::AudioEngineImpl()
|
||||
: _lazyInitLoop(true),
|
||||
_currentAudioID(0) {
|
||||
}
|
||||
|
||||
AudioEngineImpl::~AudioEngineImpl() {
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->unschedule("AudioEngine", this);
|
||||
}
|
||||
|
||||
if (sALContext) {
|
||||
alDeleteSources(MAX_AUDIOINSTANCES, _alSources);
|
||||
|
||||
_audioCaches.clear();
|
||||
|
||||
alcMakeContextCurrent(nullptr);
|
||||
alcDestroyContext(sALContext);
|
||||
sALContext = nullptr;
|
||||
}
|
||||
|
||||
if (sALDevice) {
|
||||
alcCloseDevice(sALDevice);
|
||||
sALDevice = nullptr;
|
||||
}
|
||||
|
||||
AudioDecoderManager::destroy();
|
||||
}
|
||||
|
||||
bool AudioEngineImpl::init() {
|
||||
bool ret = false;
|
||||
do {
|
||||
sALDevice = alcOpenDevice(nullptr);
|
||||
|
||||
if (sALDevice) {
|
||||
alGetError();
|
||||
sALContext = alcCreateContext(sALDevice, nullptr);
|
||||
alcMakeContextCurrent(sALContext);
|
||||
|
||||
alGenSources(MAX_AUDIOINSTANCES, _alSources);
|
||||
auto alError = alGetError();
|
||||
if (alError != AL_NO_ERROR) {
|
||||
CC_LOG_ERROR("%s:generating sources failed! error = %x\n", __FUNCTION__, alError);
|
||||
break;
|
||||
}
|
||||
|
||||
for (unsigned int src : _alSources) {
|
||||
_alSourceUsed[src] = false;
|
||||
}
|
||||
|
||||
_scheduler = CC_CURRENT_ENGINE()->getScheduler();
|
||||
ret = AudioDecoderManager::init();
|
||||
CC_LOG_DEBUG("OpenAL was initialized successfully!");
|
||||
}
|
||||
} while (false);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
AudioCache *AudioEngineImpl::preload(const ccstd::string &filePath, const 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 (sALDevice == nullptr) {
|
||||
return AudioEngine::INVALID_AUDIO_ID;
|
||||
}
|
||||
|
||||
bool sourceFlag = false;
|
||||
ALuint alSource = 0;
|
||||
for (unsigned int src : _alSources) {
|
||||
alSource = src;
|
||||
|
||||
if (!_alSourceUsed[alSource]) {
|
||||
sourceFlag = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!sourceFlag) {
|
||||
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();
|
||||
|
||||
_alSourceUsed[alSource] = true;
|
||||
|
||||
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 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()) {
|
||||
if (auto sche = _scheduler.lock()) {
|
||||
sche->performFunctionInCocosThread([audioID]() {
|
||||
if (AudioEngine::sAudioIDInfoMap.find(audioID) != AudioEngine::sAudioIDInfoMap.end()) {
|
||||
AudioEngine::sAudioIDInfoMap[audioID].state = AudioEngine::AudioState::PLAYING;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
_threadMutex.unlock();
|
||||
} else {
|
||||
CC_LOG_DEBUG("AudioEngineImpl::play2dImpl, 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) {
|
||||
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", __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", __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\n", __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\n", __FUNCTION__, audioID, error);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AudioEngineImpl::stop(int audioID) {
|
||||
if (!checkAudioIdValid(audioID)) {
|
||||
return;
|
||||
}
|
||||
auto player = _audioPlayers[audioID];
|
||||
player->destroy();
|
||||
//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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
//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;
|
||||
// }
|
||||
|
||||
// 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;
|
||||
}
|
||||
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", __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;
|
||||
}
|
||||
|
||||
if (player->_audioCache->_framesRead != player->_audioCache->_totalFrames &&
|
||||
(time * player->_audioCache->_sampleRate) > player->_audioCache->_framesRead) {
|
||||
ALOGE("%s: audio id = %d", __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", __FUNCTION__, audioID, error);
|
||||
}
|
||||
ret = true;
|
||||
|
||||
} while (false);
|
||||
|
||||
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;
|
||||
_alSourceUsed[alSource] = false;
|
||||
} 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 (player->_finishCallbak) {
|
||||
player->_finishCallbak(audioID, filePath); //IDEA: callback will delay 50ms
|
||||
}
|
||||
delete player;
|
||||
_alSourceUsed[alSource] = false;
|
||||
} 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.empty()) {
|
||||
CC_LOG_DEBUG("file %s does not exist or failed to load", url);
|
||||
return header;
|
||||
}
|
||||
|
||||
AudioDecoder *decoder = AudioDecoderManager::createDecoder(fileFullPath.c_str());
|
||||
if (decoder == nullptr) {
|
||||
CC_LOG_DEBUG("decode %s failed, the file formate might not support", url);
|
||||
return header;
|
||||
}
|
||||
// Ready to decode
|
||||
do {
|
||||
if (!decoder->open(fileFullPath.c_str())) {
|
||||
CC_LOG_ERROR("[Audio Decoder] File open failed %s", url);
|
||||
break;
|
||||
}
|
||||
header = decoder->getPCMHeader();
|
||||
} while (false);
|
||||
|
||||
AudioDecoderManager::destroyDecoder(decoder);
|
||||
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 = AudioDecoderManager::createDecoder(fileFullPath.c_str());
|
||||
if (decoder == nullptr) {
|
||||
CC_LOG_DEBUG("decode %s failed, the file formate might not support", url);
|
||||
return pcmData;
|
||||
}
|
||||
do {
|
||||
if (!decoder->open(fileFullPath.c_str())) {
|
||||
CC_LOG_ERROR("[Audio Decoder] File open failed %s", url);
|
||||
break;
|
||||
}
|
||||
auto audioInfo = decoder->getPCMHeader();
|
||||
const uint32_t bytesPerChannelInFrame = audioInfo.bytesPerFrame / audioInfo.channelCount;
|
||||
if (channelID >= audioInfo.channelCount) {
|
||||
CC_LOG_ERROR("channelID invalid, total channel count is %d but %d is required", audioInfo.channelCount, channelID);
|
||||
break;
|
||||
}
|
||||
uint32_t totalFrames = decoder->getTotalFrames();
|
||||
uint32_t remainingFrames = totalFrames;
|
||||
uint32_t framesRead = 0;
|
||||
uint32_t framesToReadOnce = std::min(remainingFrames, static_cast<uint32_t>(decoder->getSampleRate() * QUEUEBUFFER_TIME_STEP * QUEUEBUFFER_NUM));
|
||||
AudioDataFormat type = audioInfo.dataFormat;
|
||||
char *tmpBuf = static_cast<char *>(malloc(framesToReadOnce * audioInfo.bytesPerFrame));
|
||||
pcmData.resize(bytesPerChannelInFrame * audioInfo.totalFrames);
|
||||
uint8_t *p = pcmData.data();
|
||||
while (remainingFrames > 0) {
|
||||
framesToReadOnce = std::min(framesToReadOnce, remainingFrames);
|
||||
framesRead = decoder->read(framesToReadOnce, tmpBuf);
|
||||
for (int itr = 0; itr < framesToReadOnce; itr++) {
|
||||
memcpy(p, tmpBuf + itr * audioInfo.bytesPerFrame + channelID * bytesPerChannelInFrame, bytesPerChannelInFrame);
|
||||
|
||||
p += bytesPerChannelInFrame;
|
||||
}
|
||||
remainingFrames -= framesToReadOnce;
|
||||
};
|
||||
free(tmpBuf);
|
||||
} while (false);
|
||||
AudioDecoderManager::destroyDecoder(decoder);
|
||||
return pcmData;
|
||||
}
|
||||
89
cocos/audio/oalsoft/AudioEngine-soft.h
Normal file
89
cocos/audio/oalsoft/AudioEngine-soft.h
Normal file
@@ -0,0 +1,89 @@
|
||||
/****************************************************************************
|
||||
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 <stdint.h>
|
||||
#include "audio/include/AudioDef.h"
|
||||
#include "audio/oalsoft/AudioCache.h"
|
||||
#include "audio/oalsoft/AudioPlayer.h"
|
||||
#include "base/std/container/unordered_map.h"
|
||||
#include "cocos/base/RefCounted.h"
|
||||
#include "cocos/base/std/any.h"
|
||||
|
||||
namespace cc {
|
||||
|
||||
class Scheduler;
|
||||
|
||||
#define MAX_AUDIOINSTANCES 32
|
||||
|
||||
class CC_DLL AudioEngineImpl : public RefCounted {
|
||||
public:
|
||||
AudioEngineImpl();
|
||||
~AudioEngineImpl() override;
|
||||
|
||||
bool init();
|
||||
int play2d(const ccstd::string &filePath, 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 &filePath);
|
||||
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, const std::function<void(bool)> &callback);
|
||||
void update(float dt);
|
||||
PCMHeader getPCMHeader(const char *url);
|
||||
ccstd::vector<uint8_t> getOriginalPCMBuffer(const char *url, uint32_t channelID);
|
||||
|
||||
private:
|
||||
bool checkAudioIdValid(int audioID);
|
||||
void play2dImpl(AudioCache *cache, int audioID);
|
||||
|
||||
ALuint _alSources[MAX_AUDIOINSTANCES];
|
||||
|
||||
//source,used
|
||||
ccstd::unordered_map<ALuint, bool> _alSourceUsed;
|
||||
|
||||
//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
|
||||
318
cocos/audio/oalsoft/AudioPlayer.cpp
Normal file
318
cocos/audio/oalsoft/AudioPlayer.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
/****************************************************************************
|
||||
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.
|
||||
****************************************************************************/
|
||||
|
||||
#define LOG_TAG "AudioPlayer"
|
||||
|
||||
#include "audio/oalsoft/AudioPlayer.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include "audio/common/decoder/AudioDecoder.h"
|
||||
#include "audio/common/decoder/AudioDecoderManager.h"
|
||||
#include "audio/oalsoft/AudioCache.h"
|
||||
#include "base/Log.h"
|
||||
#include "base/memory/Memory.h"
|
||||
|
||||
using namespace cc; // NOLINT
|
||||
|
||||
namespace {
|
||||
unsigned int gIdIndex = 0;
|
||||
}
|
||||
|
||||
AudioPlayer::AudioPlayer()
|
||||
: _audioCache(nullptr),
|
||||
_finishCallbak(nullptr),
|
||||
_isDestroyed(false),
|
||||
_removeByAudioEngine(false),
|
||||
_ready(false),
|
||||
_currTime(0.0F),
|
||||
_streamingSource(false),
|
||||
_rotateBufferThread(nullptr),
|
||||
_timeDirty(false),
|
||||
_isRotateThreadExited(false),
|
||||
_id(++gIdIndex) {
|
||||
memset(_bufferIds, 0, sizeof(_bufferIds));
|
||||
}
|
||||
|
||||
AudioPlayer::~AudioPlayer() {
|
||||
// CC_LOG_DEBUG("~AudioPlayer() (%p), id=%u", this, _id);
|
||||
destroy();
|
||||
|
||||
if (_streamingSource) {
|
||||
alDeleteBuffers(3, _bufferIds);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioPlayer::destroy() {
|
||||
if (_isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// CC_LOG_DEBUG("AudioPlayer::destroy begin, id=%u", _id);
|
||||
|
||||
_isDestroyed = true;
|
||||
|
||||
do {
|
||||
if (_audioCache != nullptr) {
|
||||
if (_audioCache->_state == AudioCache::State::INITIAL) {
|
||||
CC_LOG_INFO("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;
|
||||
CC_LOG_DEBUG("rotateBufferThread exited!");
|
||||
}
|
||||
}
|
||||
} while (false);
|
||||
|
||||
// CC_LOG_DEBUG("Before alSourceStop");
|
||||
alSourceStop(_alSource);
|
||||
CHECK_AL_ERROR_DEBUG();
|
||||
// CC_LOG_DEBUG("Before alSourcei");
|
||||
alSourcei(_alSource, AL_BUFFER, 0);
|
||||
CHECK_AL_ERROR_DEBUG();
|
||||
|
||||
_removeByAudioEngine = true;
|
||||
|
||||
_ready = false;
|
||||
// CC_LOG_DEBUG("AudioPlayer::destroy end, id=%u", _id);
|
||||
}
|
||||
|
||||
void AudioPlayer::setCache(AudioCache *cache) {
|
||||
_audioCache = cache;
|
||||
}
|
||||
|
||||
bool AudioPlayer::play2d() {
|
||||
_play2dMutex.lock();
|
||||
//CC_LOG_INFO("AudioPlayer::play2d, _alSource: %u, player id=%u", _alSource, _id);
|
||||
|
||||
/*********************************************************************/
|
||||
/* Note that it may be in sub thread or in main thread. **/
|
||||
/*********************************************************************/
|
||||
bool ret = false;
|
||||
do {
|
||||
if (_audioCache->_state != AudioCache::State::READY) {
|
||||
CC_LOG_ERROR("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(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", __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 = 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", __FUNCTION__, alError);
|
||||
break;
|
||||
}
|
||||
|
||||
ALint state;
|
||||
alGetSourcei(_alSource, AL_SOURCE_STATE, &state);
|
||||
if (state != AL_PLAYING) {
|
||||
ALOGE("state isn't playing, %d, %s, cache id=%u, player id=%u", state, _audioCache->_fileFullPath.c_str(),
|
||||
_audioCache->_id, _id);
|
||||
// abort playing if the state is incorrect
|
||||
break;
|
||||
}
|
||||
_ready = true;
|
||||
ret = true;
|
||||
} while (false);
|
||||
|
||||
if (!ret) {
|
||||
_removeByAudioEngine = true;
|
||||
}
|
||||
|
||||
_play2dMutex.unlock();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void AudioPlayer::rotateBufferThread(int offsetFrame) {
|
||||
char *tmpBuffer = nullptr;
|
||||
AudioDecoder *decoder = AudioDecoderManager::createDecoder(_audioCache->_fileFullPath.c_str());
|
||||
do {
|
||||
BREAK_IF(decoder == nullptr || !decoder->open(_audioCache->_fileFullPath.c_str()));
|
||||
|
||||
uint32_t framesRead = 0;
|
||||
const uint32_t framesToRead = _audioCache->_queBufferFrames;
|
||||
const uint32_t bufferSize = framesToRead * decoder->getBytesPerFrame();
|
||||
tmpBuffer = static_cast<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);
|
||||
if (sourceState == AL_PLAYING) {
|
||||
alGetSourcei(_alSource, AL_BUFFERS_PROCESSED, &bufferProcessed);
|
||||
while (bufferProcessed > 0) {
|
||||
bufferProcessed--;
|
||||
if (_timeDirty) {
|
||||
_timeDirty = false;
|
||||
offsetFrame = static_cast<int>(_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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_sleepCondition.wait_for(lk, std::chrono::milliseconds(75));
|
||||
}
|
||||
|
||||
} while (false);
|
||||
|
||||
CC_LOG_INFO("Exit rotate buffer thread ...");
|
||||
if (decoder != nullptr) {
|
||||
decoder->close();
|
||||
}
|
||||
AudioDecoderManager::destroyDecoder(decoder);
|
||||
free(tmpBuffer);
|
||||
_isRotateThreadExited = true;
|
||||
CC_LOG_INFO("%s exited.\n", __FUNCTION__);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
94
cocos/audio/oalsoft/AudioPlayer.h
Normal file
94
cocos/audio/oalsoft/AudioPlayer.h
Normal file
@@ -0,0 +1,94 @@
|
||||
/****************************************************************************
|
||||
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 <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include "base/std/container/string.h"
|
||||
#ifdef OPENAL_PLAIN_INCLUDES
|
||||
#include <al.h>
|
||||
#elif CC_PLATFORM == CC_PLATFORM_WINDOWS
|
||||
#include <OpenalSoft/al.h>
|
||||
#elif CC_PLATFORM == CC_PLATFORM_OHOS
|
||||
#include <AL/al.h>
|
||||
#elif CC_PLATFORM == CC_PLATFORM_LINUX || CC_PLATFORM == CC_PLATFORM_QNX
|
||||
#include <AL/al.h>
|
||||
#endif
|
||||
#include "base/Macros.h"
|
||||
|
||||
namespace cc {
|
||||
|
||||
class AudioCache;
|
||||
class AudioEngineImpl;
|
||||
|
||||
class CC_DLL 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();
|
||||
|
||||
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[3];
|
||||
std::thread *_rotateBufferThread;
|
||||
std::condition_variable _sleepCondition;
|
||||
std::mutex _sleepMutex;
|
||||
bool _timeDirty;
|
||||
bool _isRotateThreadExited;
|
||||
|
||||
std::mutex _play2dMutex;
|
||||
|
||||
unsigned int _id;
|
||||
|
||||
friend class AudioEngineImpl;
|
||||
};
|
||||
|
||||
} // namespace cc
|
||||
Reference in New Issue
Block a user