Merge "SoundPool: Refactor class"
This commit is contained in:
@@ -3,11 +3,16 @@ cc_library_shared {
|
||||
|
||||
srcs: [
|
||||
"android_media_SoundPool.cpp",
|
||||
"Sound.cpp",
|
||||
"SoundDecoder.cpp",
|
||||
"SoundManager.cpp",
|
||||
"SoundPool.cpp",
|
||||
"SoundPoolThread.cpp",
|
||||
"Stream.cpp",
|
||||
"StreamManager.cpp",
|
||||
],
|
||||
|
||||
shared_libs: [
|
||||
"libaudioutils",
|
||||
"liblog",
|
||||
"libcutils",
|
||||
"libutils",
|
||||
|
||||
241
media/jni/soundpool/Sound.cpp
Normal file
241
media/jni/soundpool/Sound.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//#define LOG_NDEBUG 0
|
||||
#define LOG_TAG "SoundPool::Sound"
|
||||
#include <utils/Log.h>
|
||||
|
||||
#include "Sound.h"
|
||||
|
||||
#include <media/NdkMediaCodec.h>
|
||||
#include <media/NdkMediaExtractor.h>
|
||||
#include <media/NdkMediaFormat.h>
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
constexpr uint32_t kMaxSampleRate = 192000;
|
||||
constexpr size_t kDefaultHeapSize = 1024 * 1024; // 1MB (compatible with low mem devices)
|
||||
|
||||
Sound::Sound(int32_t soundID, int fd, int64_t offset, int64_t length)
|
||||
: mSoundID(soundID)
|
||||
, mFd(dup(fd))
|
||||
, mOffset(offset)
|
||||
, mLength(length)
|
||||
{
|
||||
ALOGV("%s(soundID=%d, fd=%d, offset=%lld, length=%lld)",
|
||||
__func__, soundID, fd, (long long)offset, (long long)length);
|
||||
ALOGW_IF(mFd == -1, "Unable to dup descriptor %d", fd);
|
||||
}
|
||||
|
||||
Sound::~Sound()
|
||||
{
|
||||
ALOGV("%s(soundID=%d, fd=%d)", __func__, mSoundID, mFd.get());
|
||||
}
|
||||
|
||||
static status_t decode(int fd, int64_t offset, int64_t length,
|
||||
uint32_t *rate, int32_t *channelCount, audio_format_t *audioFormat,
|
||||
audio_channel_mask_t *channelMask, sp<MemoryHeapBase> heap,
|
||||
size_t *sizeInBytes) {
|
||||
ALOGV("%s(fd=%d, offset=%lld, length=%lld, ...)",
|
||||
__func__, fd, (long long)offset, (long long)length);
|
||||
std::unique_ptr<AMediaExtractor, decltype(&AMediaExtractor_delete)> ex{
|
||||
AMediaExtractor_new(), &AMediaExtractor_delete};
|
||||
status_t err = AMediaExtractor_setDataSourceFd(ex.get(), fd, offset, length);
|
||||
|
||||
if (err != AMEDIA_OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
*audioFormat = AUDIO_FORMAT_PCM_16_BIT; // default format for audio codecs.
|
||||
const size_t numTracks = AMediaExtractor_getTrackCount(ex.get());
|
||||
for (size_t i = 0; i < numTracks; i++) {
|
||||
std::unique_ptr<AMediaFormat, decltype(&AMediaFormat_delete)> format{
|
||||
AMediaExtractor_getTrackFormat(ex.get(), i), &AMediaFormat_delete};
|
||||
const char *mime;
|
||||
if (!AMediaFormat_getString(format.get(), AMEDIAFORMAT_KEY_MIME, &mime)) {
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
if (strncmp(mime, "audio/", 6) == 0) {
|
||||
std::unique_ptr<AMediaCodec, decltype(&AMediaCodec_delete)> codec{
|
||||
AMediaCodec_createDecoderByType(mime), &AMediaCodec_delete};
|
||||
if (codec == nullptr
|
||||
|| AMediaCodec_configure(codec.get(), format.get(),
|
||||
nullptr /* window */, nullptr /* drm */, 0 /* flags */) != AMEDIA_OK
|
||||
|| AMediaCodec_start(codec.get()) != AMEDIA_OK
|
||||
|| AMediaExtractor_selectTrack(ex.get(), i) != AMEDIA_OK) {
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
bool sawInputEOS = false;
|
||||
bool sawOutputEOS = false;
|
||||
uint8_t* writePos = static_cast<uint8_t*>(heap->getBase());
|
||||
size_t available = heap->getSize();
|
||||
size_t written = 0;
|
||||
format.reset(AMediaCodec_getOutputFormat(codec.get())); // update format.
|
||||
|
||||
while (!sawOutputEOS) {
|
||||
if (!sawInputEOS) {
|
||||
ssize_t bufidx = AMediaCodec_dequeueInputBuffer(codec.get(), 5000);
|
||||
ALOGV("%s: input buffer %zd", __func__, bufidx);
|
||||
if (bufidx >= 0) {
|
||||
size_t bufsize;
|
||||
uint8_t * const buf = AMediaCodec_getInputBuffer(
|
||||
codec.get(), bufidx, &bufsize);
|
||||
if (buf == nullptr) {
|
||||
ALOGE("%s: AMediaCodec_getInputBuffer returned nullptr, short decode",
|
||||
__func__);
|
||||
break;
|
||||
}
|
||||
int sampleSize = AMediaExtractor_readSampleData(ex.get(), buf, bufsize);
|
||||
ALOGV("%s: read %d", __func__, sampleSize);
|
||||
if (sampleSize < 0) {
|
||||
sampleSize = 0;
|
||||
sawInputEOS = true;
|
||||
ALOGV("%s: EOS", __func__);
|
||||
}
|
||||
const int64_t presentationTimeUs = AMediaExtractor_getSampleTime(ex.get());
|
||||
|
||||
const media_status_t mstatus = AMediaCodec_queueInputBuffer(
|
||||
codec.get(), bufidx,
|
||||
0 /* offset */, sampleSize, presentationTimeUs,
|
||||
sawInputEOS ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0);
|
||||
if (mstatus != AMEDIA_OK) {
|
||||
// AMEDIA_ERROR_UNKNOWN == { -ERANGE -EINVAL -EACCES }
|
||||
ALOGE("%s: AMediaCodec_queueInputBuffer returned status %d,"
|
||||
"short decode",
|
||||
__func__, (int)mstatus);
|
||||
break;
|
||||
}
|
||||
(void)AMediaExtractor_advance(ex.get());
|
||||
}
|
||||
}
|
||||
|
||||
AMediaCodecBufferInfo info;
|
||||
const int status = AMediaCodec_dequeueOutputBuffer(codec.get(), &info, 1);
|
||||
ALOGV("%s: dequeueoutput returned: %d", __func__, status);
|
||||
if (status >= 0) {
|
||||
if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) {
|
||||
ALOGV("%s: output EOS", __func__);
|
||||
sawOutputEOS = true;
|
||||
}
|
||||
ALOGV("%s: got decoded buffer size %d", __func__, info.size);
|
||||
|
||||
const uint8_t * const buf = AMediaCodec_getOutputBuffer(
|
||||
codec.get(), status, nullptr /* out_size */);
|
||||
if (buf == nullptr) {
|
||||
ALOGE("%s: AMediaCodec_getOutputBuffer returned nullptr, short decode",
|
||||
__func__);
|
||||
break;
|
||||
}
|
||||
const size_t dataSize = std::min((size_t)info.size, available);
|
||||
memcpy(writePos, buf + info.offset, dataSize);
|
||||
writePos += dataSize;
|
||||
written += dataSize;
|
||||
available -= dataSize;
|
||||
const media_status_t mstatus = AMediaCodec_releaseOutputBuffer(
|
||||
codec.get(), status, false /* render */);
|
||||
if (mstatus != AMEDIA_OK) {
|
||||
// AMEDIA_ERROR_UNKNOWN == { -ERANGE -EINVAL -EACCES }
|
||||
ALOGE("%s: AMediaCodec_releaseOutputBuffer"
|
||||
" returned status %d, short decode",
|
||||
__func__, (int)mstatus);
|
||||
break;
|
||||
}
|
||||
if (available == 0) {
|
||||
// there might be more data, but there's no space for it
|
||||
sawOutputEOS = true;
|
||||
}
|
||||
} else if (status == AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED) {
|
||||
ALOGV("%s: output buffers changed", __func__);
|
||||
} else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) {
|
||||
format.reset(AMediaCodec_getOutputFormat(codec.get())); // update format
|
||||
ALOGV("%s: format changed to: %s",
|
||||
__func__, AMediaFormat_toString(format.get()));
|
||||
} else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
|
||||
ALOGV("%s: no output buffer right now", __func__);
|
||||
} else if (status <= AMEDIA_ERROR_BASE) {
|
||||
ALOGE("%s: decode error: %d", __func__, status);
|
||||
break;
|
||||
} else {
|
||||
ALOGV("%s: unexpected info code: %d", __func__, status);
|
||||
}
|
||||
}
|
||||
|
||||
(void)AMediaCodec_stop(codec.get());
|
||||
if (!AMediaFormat_getInt32(
|
||||
format.get(), AMEDIAFORMAT_KEY_SAMPLE_RATE, (int32_t*) rate) ||
|
||||
!AMediaFormat_getInt32(
|
||||
format.get(), AMEDIAFORMAT_KEY_CHANNEL_COUNT, channelCount)) {
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
if (!AMediaFormat_getInt32(format.get(), AMEDIAFORMAT_KEY_CHANNEL_MASK,
|
||||
(int32_t*) channelMask)) {
|
||||
*channelMask = AUDIO_CHANNEL_NONE;
|
||||
}
|
||||
*sizeInBytes = written;
|
||||
return OK;
|
||||
}
|
||||
}
|
||||
return UNKNOWN_ERROR;
|
||||
}
|
||||
|
||||
status_t Sound::doLoad()
|
||||
{
|
||||
ALOGV("%s()", __func__);
|
||||
status_t status = NO_INIT;
|
||||
if (mFd.get() != -1) {
|
||||
mHeap = new MemoryHeapBase(kDefaultHeapSize);
|
||||
|
||||
ALOGV("%s: start decode", __func__);
|
||||
uint32_t sampleRate;
|
||||
int32_t channelCount;
|
||||
audio_format_t format;
|
||||
audio_channel_mask_t channelMask;
|
||||
status_t status = decode(mFd.get(), mOffset, mLength, &sampleRate, &channelCount, &format,
|
||||
&channelMask, mHeap, &mSizeInBytes);
|
||||
ALOGV("%s: close(%d)", __func__, mFd.get());
|
||||
mFd.reset(); // close
|
||||
|
||||
if (status != NO_ERROR) {
|
||||
ALOGE("%s: unable to load sound", __func__);
|
||||
} else if (sampleRate > kMaxSampleRate) {
|
||||
ALOGE("%s: sample rate (%u) out of range", __func__, sampleRate);
|
||||
status = BAD_VALUE;
|
||||
} else if (channelCount < 1 || channelCount > FCC_8) {
|
||||
ALOGE("%s: sample channel count (%d) out of range", __func__, channelCount);
|
||||
status = BAD_VALUE;
|
||||
} else {
|
||||
// Correctly loaded, proper parameters
|
||||
ALOGV("%s: pointer = %p, sizeInBytes = %zu, sampleRate = %u, channelCount = %d",
|
||||
__func__, mHeap->getBase(), mSizeInBytes, sampleRate, channelCount);
|
||||
mData = new MemoryBase(mHeap, 0, mSizeInBytes);
|
||||
mSampleRate = sampleRate;
|
||||
mChannelCount = channelCount;
|
||||
mFormat = format;
|
||||
mChannelMask = channelMask;
|
||||
mState = READY; // this should be last, as it is an atomic sync point
|
||||
return NO_ERROR;
|
||||
}
|
||||
} else {
|
||||
ALOGE("%s: uninitialized fd, dup failed", __func__);
|
||||
}
|
||||
// ERROR handling
|
||||
mHeap.clear();
|
||||
mState = DECODE_ERROR; // this should be last, as it is an atomic sync point
|
||||
return status;
|
||||
}
|
||||
|
||||
} // namespace android::soundpool
|
||||
93
media/jni/soundpool/Sound.h
Normal file
93
media/jni/soundpool/Sound.h
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <android-base/unique_fd.h>
|
||||
#include <binder/MemoryBase.h>
|
||||
#include <binder/MemoryHeapBase.h>
|
||||
#include <system/audio.h>
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
class SoundDecoder;
|
||||
|
||||
/**
|
||||
* Sound is a resource used by SoundPool, referenced by soundID.
|
||||
*
|
||||
* After loading, it is effectively const so no locking required.
|
||||
* However, in order to guarantee that all the values have been
|
||||
* written properly and read properly, we use the mState as an atomic synchronization
|
||||
* point. So if getState() shows READY, then all the other getters may
|
||||
* be safely read.
|
||||
*
|
||||
* Technical details:
|
||||
* We access the mState atomic value through memory_order_seq_cst
|
||||
*
|
||||
* https://en.cppreference.com/w/cpp/atomic/memory_order
|
||||
*
|
||||
* which provides memory barriers. So if the last value written by the SoundDecoder
|
||||
* is mState, then the compiler ensures no other prior writes by SoundDecoder will be
|
||||
* reordered afterwards, and memory barrier is placed (as necessary) to ensure the
|
||||
* cache is visible to other processors.
|
||||
*
|
||||
* Likewise, if the first value read by SoundPool is mState,
|
||||
* the compiler ensures no reads for that thread will be reordered before mState is read,
|
||||
* and a memory barrier is placed (as necessary) to ensure that the cache is properly
|
||||
* updated with other processor's writes before reading.
|
||||
*
|
||||
* See https://developer.android.com/training/articles/smp for discussions about
|
||||
* the variant load-acquire, store-release semantics.
|
||||
*/
|
||||
class Sound {
|
||||
friend SoundDecoder; // calls doLoad().
|
||||
|
||||
public:
|
||||
enum sound_state : int32_t { LOADING, READY, DECODE_ERROR };
|
||||
// A sound starts in the LOADING state and transitions only once
|
||||
// to either READY or DECODE_ERROR when doLoad() is called.
|
||||
|
||||
Sound(int soundID, int fd, int64_t offset, int64_t length);
|
||||
~Sound();
|
||||
|
||||
int32_t getSoundID() const { return mSoundID; }
|
||||
int32_t getChannelCount() const { return mChannelCount; }
|
||||
uint32_t getSampleRate() const { return mSampleRate; }
|
||||
audio_format_t getFormat() const { return mFormat; }
|
||||
audio_channel_mask_t getChannelMask() const { return mChannelMask; }
|
||||
size_t getSizeInBytes() const { return mSizeInBytes; }
|
||||
sound_state getState() const { return mState; }
|
||||
uint8_t* getData() const { return static_cast<uint8_t*>(mData->unsecurePointer()); }
|
||||
sp<IMemory> getIMemory() const { return mData; }
|
||||
|
||||
private:
|
||||
status_t doLoad(); // only SoundDecoder accesses this.
|
||||
|
||||
size_t mSizeInBytes = 0;
|
||||
const int32_t mSoundID;
|
||||
uint32_t mSampleRate = 0;
|
||||
std::atomic<sound_state> mState = LOADING; // used as synchronization point
|
||||
int32_t mChannelCount = 0;
|
||||
audio_format_t mFormat = AUDIO_FORMAT_INVALID;
|
||||
audio_channel_mask_t mChannelMask = AUDIO_CHANNEL_NONE;
|
||||
base::unique_fd mFd; // initialized in constructor, reset to -1 after loading
|
||||
const int64_t mOffset; // int64_t to match java long, see off64_t
|
||||
const int64_t mLength; // int64_t to match java long, see off64_t
|
||||
sp<IMemory> mData;
|
||||
sp<MemoryHeapBase> mHeap;
|
||||
};
|
||||
|
||||
} // namespace android::soundpool
|
||||
115
media/jni/soundpool/SoundDecoder.cpp
Normal file
115
media/jni/soundpool/SoundDecoder.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//#define LOG_NDEBUG 0
|
||||
#define LOG_TAG "SoundPool::SoundDecoder"
|
||||
#include "utils/Log.h"
|
||||
|
||||
#include "SoundDecoder.h"
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
// Maximum Samples that can be background decoded before we block the caller.
|
||||
static constexpr size_t kMaxQueueSize = 128;
|
||||
|
||||
// The amount of time we wait for a new Sound decode request
|
||||
// before the SoundDecoder thread closes.
|
||||
static constexpr int32_t kWaitTimeBeforeCloseMs = 1000;
|
||||
|
||||
SoundDecoder::SoundDecoder(SoundManager* soundManager, size_t threads)
|
||||
: mSoundManager(soundManager)
|
||||
{
|
||||
ALOGV("%s(%p, %zu)", __func__, soundManager, threads);
|
||||
// ThreadPool is created, but we don't launch any threads.
|
||||
mThreadPool = std::make_unique<ThreadPool>(
|
||||
std::min(threads, (size_t)std::thread::hardware_concurrency()),
|
||||
"SoundDecoder_");
|
||||
}
|
||||
|
||||
SoundDecoder::~SoundDecoder()
|
||||
{
|
||||
ALOGV("%s()", __func__);
|
||||
quit();
|
||||
}
|
||||
|
||||
void SoundDecoder::quit()
|
||||
{
|
||||
ALOGV("%s()", __func__);
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
mQuit = true;
|
||||
mQueueSpaceAvailable.notify_all(); // notify all load waiters
|
||||
mQueueDataAvailable.notify_all(); // notify all worker threads
|
||||
}
|
||||
mThreadPool->quit();
|
||||
}
|
||||
|
||||
void SoundDecoder::run(int32_t id __unused /* ALOGV only */)
|
||||
{
|
||||
ALOGV("%s(%d): entering", __func__, id);
|
||||
std::unique_lock lock(mLock);
|
||||
while (!mQuit) {
|
||||
if (mSoundIDs.size() == 0) {
|
||||
ALOGV("%s(%d): waiting", __func__, id);
|
||||
mQueueDataAvailable.wait_for(
|
||||
lock, std::chrono::duration<int32_t, std::milli>(kWaitTimeBeforeCloseMs));
|
||||
if (mSoundIDs.size() == 0) {
|
||||
break; // no new sound, exit this thread.
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const int32_t soundID = mSoundIDs.front();
|
||||
mSoundIDs.pop_front();
|
||||
mQueueSpaceAvailable.notify_one();
|
||||
ALOGV("%s(%d): processing soundID: %d size: %zu", __func__, id, soundID, mSoundIDs.size());
|
||||
lock.unlock();
|
||||
std::shared_ptr<Sound> sound = mSoundManager->findSound(soundID);
|
||||
status_t status = NO_INIT;
|
||||
if (sound.get() != nullptr) {
|
||||
status = sound->doLoad();
|
||||
}
|
||||
ALOGV("%s(%d): notifying loaded soundID:%d status:%d", __func__, id, soundID, status);
|
||||
mSoundManager->notify(SoundPoolEvent(SoundPoolEvent::SOUND_LOADED, soundID, status));
|
||||
lock.lock();
|
||||
}
|
||||
ALOGV("%s(%d): exiting", __func__, id);
|
||||
}
|
||||
|
||||
void SoundDecoder::loadSound(int32_t soundID)
|
||||
{
|
||||
ALOGV("%s(%d)", __func__, soundID);
|
||||
size_t pendingSounds;
|
||||
{
|
||||
std::unique_lock lock(mLock);
|
||||
while (mSoundIDs.size() == kMaxQueueSize) {
|
||||
if (mQuit) return;
|
||||
ALOGV("%s: waiting soundID: %d size: %zu", __func__, soundID, mSoundIDs.size());
|
||||
mQueueSpaceAvailable.wait(lock);
|
||||
}
|
||||
if (mQuit) return;
|
||||
mSoundIDs.push_back(soundID);
|
||||
mQueueDataAvailable.notify_one();
|
||||
ALOGV("%s: adding soundID: %d size: %zu", __func__, soundID, mSoundIDs.size());
|
||||
pendingSounds = mSoundIDs.size();
|
||||
}
|
||||
// Launch threads as needed. The "as needed" is weakly consistent as we release mLock.
|
||||
if (pendingSounds > mThreadPool->getActiveThreadCount()) {
|
||||
const int32_t id __unused = mThreadPool->launch([this](int32_t id) { run(id); });
|
||||
ALOGV_IF(id != 0, "%s: launched thread %d", __func__, id);
|
||||
}
|
||||
}
|
||||
|
||||
} // end namespace android::soundpool
|
||||
51
media/jni/soundpool/SoundDecoder.h
Normal file
51
media/jni/soundpool/SoundDecoder.h
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "SoundPool.h"
|
||||
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
/**
|
||||
* SoundDecoder handles background decoding tasks.
|
||||
*/
|
||||
class SoundDecoder {
|
||||
public:
|
||||
SoundDecoder(SoundManager* soundManager, size_t threads);
|
||||
~SoundDecoder();
|
||||
void loadSound(int32_t soundID);
|
||||
void quit();
|
||||
|
||||
private:
|
||||
void run(int32_t id); // The decode thread function.
|
||||
|
||||
SoundManager* const mSoundManager; // set in constructor, has own lock
|
||||
std::unique_ptr<ThreadPool> mThreadPool; // set in constructor, has own lock
|
||||
|
||||
std::mutex mLock;
|
||||
std::condition_variable mQueueSpaceAvailable;
|
||||
std::condition_variable mQueueDataAvailable;
|
||||
|
||||
std::deque<int32_t> mSoundIDs; // GUARDED_BY(mLock);
|
||||
bool mQuit = false; // GUARDED_BY(mLock);
|
||||
};
|
||||
|
||||
} // end namespace android::soundpool
|
||||
|
||||
104
media/jni/soundpool/SoundManager.cpp
Normal file
104
media/jni/soundpool/SoundManager.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//#define LOG_NDEBUG 0
|
||||
#define LOG_TAG "SoundPool::SoundManager"
|
||||
#include <utils/Log.h>
|
||||
|
||||
#include "SoundManager.h"
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include "SoundDecoder.h"
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
static const size_t kDecoderThreads = std::thread::hardware_concurrency() >= 4 ? 2 : 1;
|
||||
|
||||
SoundManager::SoundManager()
|
||||
: mDecoder{std::make_unique<SoundDecoder>(this, kDecoderThreads)}
|
||||
{
|
||||
ALOGV("%s()", __func__);
|
||||
}
|
||||
|
||||
SoundManager::~SoundManager()
|
||||
{
|
||||
ALOGV("%s()", __func__);
|
||||
mDecoder->quit();
|
||||
|
||||
std::lock_guard lock(mSoundManagerLock);
|
||||
mSounds.clear();
|
||||
}
|
||||
|
||||
int32_t SoundManager::load(int fd, int64_t offset, int64_t length, int32_t priority __unused)
|
||||
{
|
||||
ALOGV("%s(fd=%d, offset=%lld, length=%lld, priority=%d)",
|
||||
__func__, fd, (long long)offset, (long long)length, priority);
|
||||
int32_t soundID;
|
||||
{
|
||||
std::lock_guard lock(mSoundManagerLock);
|
||||
// mNextSoundID is always positive and does not "integer overflow"
|
||||
do {
|
||||
mNextSoundID = mNextSoundID == INT32_MAX ? 1 : mNextSoundID + 1;
|
||||
} while (findSound_l(mNextSoundID) != nullptr);
|
||||
soundID = mNextSoundID;
|
||||
auto sound = std::make_shared<Sound>(soundID, fd, offset, length);
|
||||
mSounds.emplace(soundID, sound);
|
||||
}
|
||||
// mDecoder->loadSound() must be called outside of mSoundManagerLock.
|
||||
// mDecoder->loadSound() may block on mDecoder message queue space;
|
||||
// the message queue emptying may block on SoundManager::findSound().
|
||||
//
|
||||
// It is theoretically possible that sound loads might decode out-of-order.
|
||||
mDecoder->loadSound(soundID);
|
||||
return soundID;
|
||||
}
|
||||
|
||||
bool SoundManager::unload(int32_t soundID)
|
||||
{
|
||||
ALOGV("%s(soundID=%d)", __func__, soundID);
|
||||
std::lock_guard lock(mSoundManagerLock);
|
||||
return mSounds.erase(soundID) > 0; // erase() returns number of sounds removed.
|
||||
}
|
||||
|
||||
std::shared_ptr<Sound> SoundManager::findSound(int32_t soundID) const
|
||||
{
|
||||
std::lock_guard lock(mSoundManagerLock);
|
||||
return findSound_l(soundID);
|
||||
}
|
||||
|
||||
std::shared_ptr<Sound> SoundManager::findSound_l(int32_t soundID) const
|
||||
{
|
||||
auto it = mSounds.find(soundID);
|
||||
return it != mSounds.end() ? it->second : nullptr;
|
||||
}
|
||||
|
||||
void SoundManager::setCallback(SoundPool *soundPool, SoundPoolCallback* callback, void* user)
|
||||
{
|
||||
mCallbackHandler.setCallback(soundPool, callback, user);
|
||||
}
|
||||
|
||||
void SoundManager::notify(SoundPoolEvent event)
|
||||
{
|
||||
mCallbackHandler.notify(event);
|
||||
}
|
||||
|
||||
void* SoundManager::getUserData() const
|
||||
{
|
||||
return mCallbackHandler.getUserData();
|
||||
}
|
||||
|
||||
} // namespace android::soundpool
|
||||
110
media/jni/soundpool/SoundManager.h
Normal file
110
media/jni/soundpool/SoundManager.h
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Sound.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace android {
|
||||
|
||||
class SoundPool;
|
||||
|
||||
// for queued events
|
||||
class SoundPoolEvent {
|
||||
public:
|
||||
explicit SoundPoolEvent(int msg, int arg1 = 0, int arg2 = 0) :
|
||||
mMsg(msg), mArg1(arg1), mArg2(arg2) {}
|
||||
const int mMsg; // MessageType
|
||||
const int mArg1; // soundID
|
||||
const int mArg2; // status
|
||||
enum MessageType { INVALID, SOUND_LOADED };
|
||||
};
|
||||
|
||||
// callback function prototype
|
||||
typedef void SoundPoolCallback(SoundPoolEvent event, SoundPool* soundPool, void* user);
|
||||
|
||||
} // namespace android
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
// This class manages Sounds for the SoundPool.
|
||||
class SoundManager {
|
||||
public:
|
||||
SoundManager();
|
||||
~SoundManager();
|
||||
|
||||
// Matches corresponding SoundPool API functions
|
||||
int32_t load(int fd, int64_t offset, int64_t length, int32_t priority);
|
||||
bool unload(int32_t soundID);
|
||||
void setCallback(SoundPool* soundPool, SoundPoolCallback* callback, void* user);
|
||||
void* getUserData() const;
|
||||
|
||||
// SoundPool and SoundDecoder access
|
||||
std::shared_ptr<Sound> findSound(int32_t soundID) const;
|
||||
|
||||
// from the SoundDecoder
|
||||
void notify(SoundPoolEvent event);
|
||||
|
||||
private:
|
||||
|
||||
// CallbackHandler is used to manage notifications back to the app when a sound
|
||||
// has been loaded. It uses a recursive lock to allow setting the callback
|
||||
// during the callback.
|
||||
class CallbackHandler {
|
||||
public:
|
||||
void setCallback(SoundPool *soundPool, SoundPoolCallback* callback, void* userData)
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mCallbackLock);
|
||||
mSoundPool = soundPool;
|
||||
mCallback = callback;
|
||||
mUserData = userData;
|
||||
}
|
||||
void notify(SoundPoolEvent event) const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mCallbackLock);
|
||||
if (mCallback != nullptr) {
|
||||
mCallback(event, mSoundPool, mUserData);
|
||||
// Note: mCallback may call setCallback().
|
||||
// so mCallback, mUserData may have changed.
|
||||
}
|
||||
}
|
||||
void* getUserData() const
|
||||
{
|
||||
std::lock_guard<std::recursive_mutex> lock(mCallbackLock);
|
||||
return mUserData;
|
||||
}
|
||||
private:
|
||||
mutable std::recursive_mutex mCallbackLock; // allow mCallback to setCallback().
|
||||
SoundPool* mSoundPool = nullptr; // GUARDED_BY(mCallbackLock)
|
||||
SoundPoolCallback* mCallback = nullptr; // GUARDED_BY(mCallbackLock)
|
||||
void* mUserData = nullptr; // GUARDED_BY(mCallbackLock)
|
||||
};
|
||||
|
||||
std::shared_ptr<Sound> findSound_l(int32_t soundID) const;
|
||||
|
||||
// The following variables are initialized in constructor and can be accessed anytime.
|
||||
CallbackHandler mCallbackHandler; // has its own lock
|
||||
const std::unique_ptr<SoundDecoder> mDecoder; // has its own lock
|
||||
|
||||
mutable std::mutex mSoundManagerLock;
|
||||
std::unordered_map<int, std::shared_ptr<Sound>> mSounds; // GUARDED_BY(mSoundManagerLock)
|
||||
int32_t mNextSoundID = 0; // GUARDED_BY(mSoundManagerLock)
|
||||
};
|
||||
|
||||
} // namespace android::soundpool
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,227 +14,59 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef SOUNDPOOL_H_
|
||||
#define SOUNDPOOL_H_
|
||||
#pragma once
|
||||
|
||||
#include <utils/threads.h>
|
||||
#include <utils/List.h>
|
||||
#include <utils/Vector.h>
|
||||
#include <utils/KeyedVector.h>
|
||||
#include <media/AudioTrack.h>
|
||||
#include <binder/MemoryHeapBase.h>
|
||||
#include <binder/MemoryBase.h>
|
||||
#include "SoundManager.h"
|
||||
#include "StreamManager.h"
|
||||
|
||||
namespace android {
|
||||
|
||||
static const int IDLE_PRIORITY = -1;
|
||||
|
||||
// forward declarations
|
||||
class SoundEvent;
|
||||
class SoundPoolThread;
|
||||
class SoundPool;
|
||||
|
||||
// for queued events
|
||||
class SoundPoolEvent {
|
||||
public:
|
||||
explicit SoundPoolEvent(int msg, int arg1=0, int arg2=0) :
|
||||
mMsg(msg), mArg1(arg1), mArg2(arg2) {}
|
||||
int mMsg;
|
||||
int mArg1;
|
||||
int mArg2;
|
||||
enum MessageType { INVALID, SAMPLE_LOADED };
|
||||
};
|
||||
|
||||
// callback function prototype
|
||||
typedef void SoundPoolCallback(SoundPoolEvent event, SoundPool* soundPool, void* user);
|
||||
|
||||
// tracks samples used by application
|
||||
class Sample : public RefBase {
|
||||
public:
|
||||
enum sample_state { UNLOADED, LOADING, READY, UNLOADING };
|
||||
Sample(int sampleID, int fd, int64_t offset, int64_t length);
|
||||
~Sample();
|
||||
int sampleID() { return mSampleID; }
|
||||
int numChannels() { return mNumChannels; }
|
||||
int sampleRate() { return mSampleRate; }
|
||||
audio_format_t format() { return mFormat; }
|
||||
audio_channel_mask_t channelMask() { return mChannelMask; }
|
||||
size_t size() { return mSize; }
|
||||
int state() { return mState; }
|
||||
uint8_t* data() { return static_cast<uint8_t*>(mData->unsecurePointer()); }
|
||||
status_t doLoad();
|
||||
void startLoad() { mState = LOADING; }
|
||||
sp<IMemory> getIMemory() { return mData; }
|
||||
|
||||
private:
|
||||
void init();
|
||||
|
||||
size_t mSize;
|
||||
volatile int32_t mRefCount;
|
||||
uint16_t mSampleID;
|
||||
uint16_t mSampleRate;
|
||||
uint8_t mState;
|
||||
uint8_t mNumChannels;
|
||||
audio_format_t mFormat;
|
||||
audio_channel_mask_t mChannelMask;
|
||||
int mFd;
|
||||
int64_t mOffset;
|
||||
int64_t mLength;
|
||||
sp<IMemory> mData;
|
||||
sp<MemoryHeapBase> mHeap;
|
||||
};
|
||||
|
||||
// stores pending events for stolen channels
|
||||
class SoundEvent
|
||||
{
|
||||
public:
|
||||
SoundEvent() : mChannelID(0), mLeftVolume(0), mRightVolume(0),
|
||||
mPriority(IDLE_PRIORITY), mLoop(0), mRate(0) {}
|
||||
void set(const sp<Sample>& sample, int channelID, float leftVolume,
|
||||
float rightVolume, int priority, int loop, float rate);
|
||||
sp<Sample> sample() { return mSample; }
|
||||
int channelID() { return mChannelID; }
|
||||
float leftVolume() { return mLeftVolume; }
|
||||
float rightVolume() { return mRightVolume; }
|
||||
int priority() { return mPriority; }
|
||||
int loop() { return mLoop; }
|
||||
float rate() { return mRate; }
|
||||
void clear() { mChannelID = 0; mSample.clear(); }
|
||||
|
||||
protected:
|
||||
sp<Sample> mSample;
|
||||
int mChannelID;
|
||||
float mLeftVolume;
|
||||
float mRightVolume;
|
||||
int mPriority;
|
||||
int mLoop;
|
||||
float mRate;
|
||||
};
|
||||
|
||||
// for channels aka AudioTracks
|
||||
class SoundChannel : public SoundEvent {
|
||||
public:
|
||||
enum state { IDLE, RESUMING, STOPPING, PAUSED, PLAYING };
|
||||
SoundChannel() : mState(IDLE), mNumChannels(1),
|
||||
mPos(0), mToggle(0), mAutoPaused(false), mMuted(false) {}
|
||||
~SoundChannel();
|
||||
void init(SoundPool* soundPool);
|
||||
void play(const sp<Sample>& sample, int channelID, float leftVolume, float rightVolume,
|
||||
int priority, int loop, float rate);
|
||||
void setVolume_l(float leftVolume, float rightVolume);
|
||||
void setVolume(float leftVolume, float rightVolume);
|
||||
void mute(bool muting);
|
||||
void stop_l();
|
||||
void stop();
|
||||
void pause();
|
||||
void autoPause();
|
||||
void resume();
|
||||
void autoResume();
|
||||
void setRate(float rate);
|
||||
int state() { return mState; }
|
||||
void setPriority(int priority) { mPriority = priority; }
|
||||
void setLoop(int loop);
|
||||
int numChannels() { return mNumChannels; }
|
||||
void clearNextEvent() { mNextEvent.clear(); }
|
||||
void nextEvent();
|
||||
int nextChannelID() { return mNextEvent.channelID(); }
|
||||
void dump();
|
||||
int getPrevSampleID(void) { return mPrevSampleID; }
|
||||
|
||||
private:
|
||||
static void callback(int event, void* user, void *info);
|
||||
void process(int event, void *info, unsigned long toggle);
|
||||
bool doStop_l();
|
||||
|
||||
SoundPool* mSoundPool;
|
||||
sp<AudioTrack> mAudioTrack;
|
||||
SoundEvent mNextEvent;
|
||||
Mutex mLock;
|
||||
int mState;
|
||||
int mNumChannels;
|
||||
int mPos;
|
||||
int mAudioBufferSize;
|
||||
unsigned long mToggle;
|
||||
bool mAutoPaused;
|
||||
int mPrevSampleID;
|
||||
bool mMuted;
|
||||
};
|
||||
|
||||
// application object for managing a pool of sounds
|
||||
/**
|
||||
* Native class for Java SoundPool, manages a pool of sounds.
|
||||
*
|
||||
* See the Android SoundPool Java documentation for description of valid values.
|
||||
* https://developer.android.com/reference/android/media/SoundPool
|
||||
*/
|
||||
class SoundPool {
|
||||
friend class SoundPoolThread;
|
||||
friend class SoundChannel;
|
||||
public:
|
||||
SoundPool(int maxChannels, const audio_attributes_t* pAttributes);
|
||||
SoundPool(int32_t maxStreams, const audio_attributes_t* attributes);
|
||||
~SoundPool();
|
||||
int load(int fd, int64_t offset, int64_t length, int priority);
|
||||
bool unload(int sampleID);
|
||||
int play(int sampleID, float leftVolume, float rightVolume, int priority,
|
||||
int loop, float rate);
|
||||
void pause(int channelID);
|
||||
void mute(bool muting);
|
||||
|
||||
// SoundPool Java API support
|
||||
int32_t load(int fd, int64_t offset, int64_t length, int32_t priority);
|
||||
bool unload(int32_t soundID);
|
||||
int32_t play(int32_t soundID, float leftVolume, float rightVolume, int32_t priority,
|
||||
int32_t loop, float rate);
|
||||
void pause(int32_t streamID);
|
||||
void autoPause();
|
||||
void resume(int channelID);
|
||||
void resume(int32_t streamID);
|
||||
void autoResume();
|
||||
void stop(int channelID);
|
||||
void setVolume(int channelID, float leftVolume, float rightVolume);
|
||||
void setPriority(int channelID, int priority);
|
||||
void setLoop(int channelID, int loop);
|
||||
void setRate(int channelID, float rate);
|
||||
const audio_attributes_t* attributes() { return &mAttributes; }
|
||||
|
||||
// called from SoundPoolThread
|
||||
void sampleLoaded(int sampleID);
|
||||
sp<Sample> findSample(int sampleID);
|
||||
|
||||
// called from AudioTrack thread
|
||||
void done_l(SoundChannel* channel);
|
||||
|
||||
// callback function
|
||||
void stop(int32_t streamID);
|
||||
void setVolume(int32_t streamID, float leftVolume, float rightVolume);
|
||||
void setPriority(int32_t streamID, int32_t priority);
|
||||
void setLoop(int32_t streamID, int32_t loop);
|
||||
void setRate(int32_t streamID, float rate);
|
||||
void setCallback(SoundPoolCallback* callback, void* user);
|
||||
void* getUserData() { return mUserData; }
|
||||
void* getUserData() const;
|
||||
|
||||
// not exposed in the public Java API, used for internal playerSetVolume() muting.
|
||||
void mute(bool muting);
|
||||
|
||||
private:
|
||||
SoundPool() {} // no default constructor
|
||||
bool startThreads();
|
||||
sp<Sample> findSample_l(int sampleID);
|
||||
SoundChannel* findChannel (int channelID);
|
||||
SoundChannel* findNextChannel (int channelID);
|
||||
SoundChannel* allocateChannel_l(int priority, int sampleID);
|
||||
void moveToFront_l(SoundChannel* channel);
|
||||
void notify(SoundPoolEvent event);
|
||||
void dump();
|
||||
|
||||
// restart thread
|
||||
void addToRestartList(SoundChannel* channel);
|
||||
void addToStopList(SoundChannel* channel);
|
||||
static int beginThread(void* arg);
|
||||
int run();
|
||||
void quit();
|
||||
// Constructor initialized variables
|
||||
// Can access without lock as they are internally locked,
|
||||
// though care needs to be taken that the final result composed of
|
||||
// individually consistent actions are consistent.
|
||||
soundpool::SoundManager mSoundManager;
|
||||
soundpool::StreamManager mStreamManager;
|
||||
|
||||
Mutex mLock;
|
||||
Mutex mRestartLock;
|
||||
Condition mCondition;
|
||||
SoundPoolThread* mDecodeThread;
|
||||
SoundChannel* mChannelPool;
|
||||
List<SoundChannel*> mChannels;
|
||||
List<SoundChannel*> mRestart;
|
||||
List<SoundChannel*> mStop;
|
||||
DefaultKeyedVector< int, sp<Sample> > mSamples;
|
||||
int mMaxChannels;
|
||||
audio_attributes_t mAttributes;
|
||||
int mAllocated;
|
||||
int mNextSampleID;
|
||||
int mNextChannelID;
|
||||
bool mQuit;
|
||||
bool mMuted;
|
||||
|
||||
// callback
|
||||
Mutex mCallbackLock;
|
||||
SoundPoolCallback* mCallback;
|
||||
void* mUserData;
|
||||
// mApiLock serializes SoundPool application calls (configurable by kUseApiLock).
|
||||
// It only locks at the SoundPool layer and not below. At this level,
|
||||
// mApiLock is only required for autoPause() and autoResume() to prevent zippering
|
||||
// of the individual pauses and resumes, and mute() for self-interaction with itself.
|
||||
// It is optional for all other apis.
|
||||
mutable std::mutex mApiLock;
|
||||
};
|
||||
|
||||
} // end namespace android
|
||||
|
||||
#endif /*SOUNDPOOL_H_*/
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//#define LOG_NDEBUG 0
|
||||
#define LOG_TAG "SoundPoolThread"
|
||||
#include "utils/Log.h"
|
||||
|
||||
#include "SoundPoolThread.h"
|
||||
|
||||
namespace android {
|
||||
|
||||
void SoundPoolThread::write(SoundPoolMsg msg) {
|
||||
Mutex::Autolock lock(&mLock);
|
||||
while (mMsgQueue.size() >= maxMessages) {
|
||||
mCondition.wait(mLock);
|
||||
}
|
||||
|
||||
// if thread is quitting, don't add to queue
|
||||
if (mRunning) {
|
||||
mMsgQueue.push(msg);
|
||||
mCondition.signal();
|
||||
}
|
||||
}
|
||||
|
||||
const SoundPoolMsg SoundPoolThread::read() {
|
||||
Mutex::Autolock lock(&mLock);
|
||||
while (mMsgQueue.size() == 0) {
|
||||
mCondition.wait(mLock);
|
||||
}
|
||||
SoundPoolMsg msg = mMsgQueue[0];
|
||||
mMsgQueue.removeAt(0);
|
||||
mCondition.signal();
|
||||
return msg;
|
||||
}
|
||||
|
||||
void SoundPoolThread::quit() {
|
||||
Mutex::Autolock lock(&mLock);
|
||||
if (mRunning) {
|
||||
mRunning = false;
|
||||
mMsgQueue.clear();
|
||||
mMsgQueue.push(SoundPoolMsg(SoundPoolMsg::KILL, 0));
|
||||
mCondition.signal();
|
||||
mCondition.wait(mLock);
|
||||
}
|
||||
ALOGV("return from quit");
|
||||
}
|
||||
|
||||
SoundPoolThread::SoundPoolThread(SoundPool* soundPool) :
|
||||
mSoundPool(soundPool)
|
||||
{
|
||||
mMsgQueue.setCapacity(maxMessages);
|
||||
if (createThreadEtc(beginThread, this, "SoundPoolThread")) {
|
||||
mRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
SoundPoolThread::~SoundPoolThread()
|
||||
{
|
||||
quit();
|
||||
}
|
||||
|
||||
int SoundPoolThread::beginThread(void* arg) {
|
||||
ALOGV("beginThread");
|
||||
SoundPoolThread* soundPoolThread = (SoundPoolThread*)arg;
|
||||
return soundPoolThread->run();
|
||||
}
|
||||
|
||||
int SoundPoolThread::run() {
|
||||
ALOGV("run");
|
||||
for (;;) {
|
||||
SoundPoolMsg msg = read();
|
||||
ALOGV("Got message m=%d, mData=%d", msg.mMessageType, msg.mData);
|
||||
switch (msg.mMessageType) {
|
||||
case SoundPoolMsg::KILL:
|
||||
ALOGV("goodbye");
|
||||
return NO_ERROR;
|
||||
case SoundPoolMsg::LOAD_SAMPLE:
|
||||
doLoadSample(msg.mData);
|
||||
break;
|
||||
default:
|
||||
ALOGW("run: Unrecognized message %d\n",
|
||||
msg.mMessageType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SoundPoolThread::loadSample(int sampleID) {
|
||||
write(SoundPoolMsg(SoundPoolMsg::LOAD_SAMPLE, sampleID));
|
||||
}
|
||||
|
||||
void SoundPoolThread::doLoadSample(int sampleID) {
|
||||
sp <Sample> sample = mSoundPool->findSample(sampleID);
|
||||
status_t status = -1;
|
||||
if (sample != 0) {
|
||||
status = sample->doLoad();
|
||||
}
|
||||
mSoundPool->notify(SoundPoolEvent(SoundPoolEvent::SAMPLE_LOADED, sampleID, status));
|
||||
}
|
||||
|
||||
} // end namespace android
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2007 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#ifndef SOUNDPOOLTHREAD_H_
|
||||
#define SOUNDPOOLTHREAD_H_
|
||||
|
||||
#include <utils/threads.h>
|
||||
#include <utils/Vector.h>
|
||||
#include <media/AudioTrack.h>
|
||||
|
||||
#include "SoundPool.h"
|
||||
|
||||
namespace android {
|
||||
|
||||
class SoundPoolMsg {
|
||||
public:
|
||||
enum MessageType { INVALID, KILL, LOAD_SAMPLE };
|
||||
SoundPoolMsg() : mMessageType(INVALID), mData(0) {}
|
||||
SoundPoolMsg(MessageType MessageType, int data) :
|
||||
mMessageType(MessageType), mData(data) {}
|
||||
uint16_t mMessageType;
|
||||
uint16_t mData;
|
||||
};
|
||||
|
||||
/*
|
||||
* This class handles background requests from the SoundPool
|
||||
*/
|
||||
class SoundPoolThread {
|
||||
public:
|
||||
explicit SoundPoolThread(SoundPool* SoundPool);
|
||||
~SoundPoolThread();
|
||||
void loadSample(int sampleID);
|
||||
void quit();
|
||||
void write(SoundPoolMsg msg);
|
||||
|
||||
private:
|
||||
static const size_t maxMessages = 128;
|
||||
|
||||
static int beginThread(void* arg);
|
||||
int run();
|
||||
void doLoadSample(int sampleID);
|
||||
const SoundPoolMsg read();
|
||||
|
||||
Mutex mLock;
|
||||
Condition mCondition;
|
||||
Vector<SoundPoolMsg> mMsgQueue;
|
||||
SoundPool* mSoundPool;
|
||||
bool mRunning;
|
||||
};
|
||||
|
||||
} // end namespace android
|
||||
|
||||
#endif /*SOUNDPOOLTHREAD_H_*/
|
||||
448
media/jni/soundpool/Stream.cpp
Normal file
448
media/jni/soundpool/Stream.cpp
Normal file
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//#define LOG_NDEBUG 0
|
||||
#define LOG_TAG "SoundPool::Stream"
|
||||
#include <utils/Log.h>
|
||||
|
||||
#include "Stream.h"
|
||||
|
||||
#include "StreamManager.h"
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
Stream::~Stream()
|
||||
{
|
||||
ALOGV("%s(%p)", __func__, this);
|
||||
}
|
||||
|
||||
void Stream::autoPause()
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (mState == PLAYING) {
|
||||
ALOGV("%s: track streamID: %d", __func__, (int)mStreamID);
|
||||
mState = PAUSED;
|
||||
mAutoPaused = true;
|
||||
if (mAudioTrack != nullptr) {
|
||||
mAudioTrack->pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::autoResume()
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (mAutoPaused) {
|
||||
if (mState == PAUSED) {
|
||||
ALOGV("%s: track streamID: %d", __func__, (int)mStreamID);
|
||||
mState = PLAYING;
|
||||
if (mAudioTrack != nullptr) {
|
||||
mAudioTrack->start();
|
||||
}
|
||||
}
|
||||
mAutoPaused = false; // New for R: always reset autopause (consistent with API spec).
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::mute(bool muting)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
mMuted = muting;
|
||||
if (mAudioTrack != nullptr) {
|
||||
if (mMuted) {
|
||||
mAudioTrack->setVolume(0.0f, 0.0f);
|
||||
} else {
|
||||
mAudioTrack->setVolume(mLeftVolume, mRightVolume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::pause(int32_t streamID)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (streamID == mStreamID) {
|
||||
if (mState == PLAYING) {
|
||||
ALOGV("%s: track streamID: %d", __func__, streamID);
|
||||
mState = PAUSED;
|
||||
if (mAudioTrack != nullptr) {
|
||||
mAudioTrack->pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::resume(int32_t streamID)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (streamID == mStreamID) {
|
||||
if (mState == PAUSED) {
|
||||
ALOGV("%s: track streamID: %d", __func__, streamID);
|
||||
mState = PLAYING;
|
||||
if (mAudioTrack != nullptr) {
|
||||
mAudioTrack->start();
|
||||
}
|
||||
mAutoPaused = false; // TODO: is this right? (ambiguous per spec), move outside?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::setRate(int32_t streamID, float rate)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (streamID == mStreamID) {
|
||||
mRate = rate;
|
||||
if (mAudioTrack != nullptr && mSound != nullptr) {
|
||||
const uint32_t sampleRate = uint32_t(float(mSound->getSampleRate()) * rate + 0.5);
|
||||
mAudioTrack->setSampleRate(sampleRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::setVolume_l(float leftVolume, float rightVolume)
|
||||
{
|
||||
mLeftVolume = leftVolume;
|
||||
mRightVolume = rightVolume;
|
||||
if (mAudioTrack != nullptr && !mMuted) {
|
||||
mAudioTrack->setVolume(leftVolume, rightVolume);
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::setVolume(int32_t streamID, float leftVolume, float rightVolume)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (streamID == mStreamID) {
|
||||
setVolume_l(leftVolume, rightVolume);
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::setPriority(int32_t streamID, int32_t priority)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (streamID == mStreamID) {
|
||||
mPriority = priority;
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::setLoop(int32_t streamID, int32_t loop)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (streamID == mStreamID) {
|
||||
if (mAudioTrack != nullptr && mSound != nullptr) {
|
||||
const uint32_t loopEnd = mSound->getSizeInBytes() / mSound->getChannelCount() /
|
||||
(mSound->getFormat() == AUDIO_FORMAT_PCM_16_BIT
|
||||
? sizeof(int16_t) : sizeof(uint8_t));
|
||||
mAudioTrack->setLoop(0, loopEnd, loop);
|
||||
}
|
||||
mLoop = loop;
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::setPlay(
|
||||
int32_t streamID, const std::shared_ptr<Sound> &sound, int32_t soundID,
|
||||
float leftVolume, float rightVolume, int32_t priority, int32_t loop, float rate)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
// We must be idle, or we must be repurposing a pending Stream.
|
||||
LOG_ALWAYS_FATAL_IF(mState != IDLE && mAudioTrack != nullptr, "State %d must be IDLE", mState);
|
||||
mSound = sound;
|
||||
mSoundID = soundID;
|
||||
mLeftVolume = leftVolume;
|
||||
mRightVolume = rightVolume;
|
||||
mPriority = priority;
|
||||
mLoop = loop;
|
||||
mRate = rate;
|
||||
mState = PLAYING;
|
||||
mAutoPaused = false; // New for R (consistent with Java API spec).
|
||||
mStreamID = streamID; // prefer this to be the last, as it is an atomic sync point
|
||||
}
|
||||
|
||||
void Stream::setStopTimeNs(int64_t stopTimeNs)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
mStopTimeNs = stopTimeNs;
|
||||
}
|
||||
|
||||
bool Stream::requestStop(int32_t streamID)
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
if (streamID == mStreamID) {
|
||||
if (mAudioTrack != nullptr) {
|
||||
if (mState == PLAYING && !mMuted && (mLeftVolume != 0.f || mRightVolume != 0.f)) {
|
||||
setVolume_l(0.f, 0.f);
|
||||
mStopTimeNs = systemTime() + kStopWaitTimeNs;
|
||||
} else {
|
||||
mStopTimeNs = systemTime();
|
||||
}
|
||||
return true; // must be queued on the restart list.
|
||||
}
|
||||
stop_l();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Stream::stop()
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
stop_l();
|
||||
}
|
||||
|
||||
void Stream::stop_l()
|
||||
{
|
||||
if (mState != IDLE) {
|
||||
if (mAudioTrack != nullptr) {
|
||||
mAudioTrack->stop();
|
||||
}
|
||||
mSound.reset();
|
||||
mState = IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::clearAudioTrack()
|
||||
{
|
||||
// This will invoke the destructor which waits for the AudioTrack thread to join,
|
||||
// and is currently the only safe way to ensure there are no callbacks afterwards.
|
||||
mAudioTrack.clear();
|
||||
}
|
||||
|
||||
Stream* Stream::getPairStream() const
|
||||
{
|
||||
return mStreamManager->getPairStream(this);
|
||||
}
|
||||
|
||||
Stream* Stream::playPairStream() {
|
||||
Stream* pairStream = getPairStream();
|
||||
LOG_ALWAYS_FATAL_IF(pairStream == nullptr, "No pair stream!");
|
||||
sp<AudioTrack> releaseTracks[2];
|
||||
{
|
||||
// TODO: Do we really want to force a simultaneous synchronization between
|
||||
// the stream and its pair?
|
||||
|
||||
// note locking order - the paired stream is obtained before the queued stream.
|
||||
// we can invert the locking order, but it is slightly more optimal to do it this way.
|
||||
std::lock_guard lockp(pairStream->mLock);
|
||||
if (pairStream->mSound == nullptr) {
|
||||
return nullptr; // no pair sound
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(mLock);
|
||||
LOG_ALWAYS_FATAL_IF(mState != IDLE, "State: %d must be IDLE", mState);
|
||||
// TODO: do we want a specific set() here?
|
||||
pairStream->mAudioTrack = mAudioTrack;
|
||||
pairStream->mSoundID = mSoundID; // optimization to reuse AudioTrack.
|
||||
pairStream->mToggle = mToggle;
|
||||
pairStream->mAutoPaused = mAutoPaused; // save autopause state
|
||||
pairStream->mMuted = mMuted;
|
||||
mAudioTrack.clear(); // the pair owns the audiotrack.
|
||||
mSound.reset();
|
||||
mSoundID = 0;
|
||||
}
|
||||
// TODO: do we need a specific play_l() anymore?
|
||||
const int pairState = pairStream->mState;
|
||||
pairStream->play_l(pairStream->mSound, pairStream->mStreamID,
|
||||
pairStream->mLeftVolume, pairStream->mRightVolume, pairStream->mPriority,
|
||||
pairStream->mLoop, pairStream->mRate, releaseTracks);
|
||||
if (pairStream->mState == IDLE) {
|
||||
return nullptr; // AudioTrack error
|
||||
}
|
||||
if (pairState == PAUSED) { // reestablish pause
|
||||
pairStream->mState = PAUSED;
|
||||
pairStream->mAudioTrack->pause();
|
||||
}
|
||||
}
|
||||
// release tracks outside of Stream lock
|
||||
return pairStream;
|
||||
}
|
||||
|
||||
void Stream::play_l(const std::shared_ptr<Sound>& sound, int32_t nextStreamID,
|
||||
float leftVolume, float rightVolume, int32_t priority, int32_t loop, float rate,
|
||||
sp<AudioTrack> releaseTracks[2])
|
||||
{
|
||||
// These tracks are released without the lock.
|
||||
sp<AudioTrack> &oldTrack = releaseTracks[0];
|
||||
sp<AudioTrack> &newTrack = releaseTracks[1];
|
||||
status_t status = NO_ERROR;
|
||||
|
||||
{
|
||||
ALOGV("%s(%p)(soundID=%d, streamID=%d, leftVolume=%f, rightVolume=%f,"
|
||||
" priority=%d, loop=%d, rate=%f)",
|
||||
__func__, this, sound->getSoundID(), nextStreamID, leftVolume, rightVolume,
|
||||
priority, loop, rate);
|
||||
|
||||
// initialize track
|
||||
const audio_stream_type_t streamType =
|
||||
AudioSystem::attributesToStreamType(*mStreamManager->getAttributes());
|
||||
const int32_t channelCount = sound->getChannelCount();
|
||||
const uint32_t sampleRate = uint32_t(float(sound->getSampleRate()) * rate + 0.5);
|
||||
size_t frameCount = 0;
|
||||
|
||||
if (loop) {
|
||||
const audio_format_t format = sound->getFormat();
|
||||
const size_t frameSize = audio_is_linear_pcm(format)
|
||||
? channelCount * audio_bytes_per_sample(format) : 1;
|
||||
frameCount = sound->getSizeInBytes() / frameSize;
|
||||
}
|
||||
|
||||
// check if the existing track has the same sound id.
|
||||
if (mAudioTrack != nullptr && mSoundID == sound->getSoundID()) {
|
||||
// the sample rate may fail to change if the audio track is a fast track.
|
||||
if (mAudioTrack->setSampleRate(sampleRate) == NO_ERROR) {
|
||||
newTrack = mAudioTrack;
|
||||
ALOGV("%s: reusing track %p for sound %d",
|
||||
__func__, mAudioTrack.get(), sound->getSoundID());
|
||||
}
|
||||
}
|
||||
if (newTrack == 0) {
|
||||
// mToggle toggles each time a track is started on a given stream.
|
||||
// The toggle is concatenated with the Stream address and passed to AudioTrack
|
||||
// as callback user data. This enables the detection of callbacks received from the old
|
||||
// audio track while the new one is being started and avoids processing them with
|
||||
// wrong audio audio buffer size (mAudioBufferSize)
|
||||
auto toggle = mToggle ^ 1;
|
||||
void* userData = (void*)((uintptr_t)this | toggle);
|
||||
audio_channel_mask_t soundChannelMask = sound->getChannelMask();
|
||||
// When sound contains a valid channel mask, use it as is.
|
||||
// Otherwise, use stream count to calculate channel mask.
|
||||
audio_channel_mask_t channelMask = soundChannelMask != AUDIO_CHANNEL_NONE
|
||||
? soundChannelMask : audio_channel_out_mask_from_count(channelCount);
|
||||
|
||||
// do not create a new audio track if current track is compatible with sound parameters
|
||||
|
||||
newTrack = new AudioTrack(streamType, sampleRate, sound->getFormat(),
|
||||
channelMask, sound->getIMemory(), AUDIO_OUTPUT_FLAG_FAST,
|
||||
staticCallback, userData,
|
||||
0 /*default notification frames*/, AUDIO_SESSION_ALLOCATE,
|
||||
AudioTrack::TRANSFER_DEFAULT,
|
||||
nullptr /*offloadInfo*/, -1 /*uid*/, -1 /*pid*/,
|
||||
mStreamManager->getAttributes());
|
||||
|
||||
oldTrack = mAudioTrack;
|
||||
status = newTrack->initCheck();
|
||||
if (status != NO_ERROR) {
|
||||
ALOGE("%s: error creating AudioTrack", __func__);
|
||||
// newTrack goes out of scope, so reference count drops to zero
|
||||
goto exit;
|
||||
}
|
||||
// From now on, AudioTrack callbacks received with previous toggle value will be ignored.
|
||||
mToggle = toggle;
|
||||
mAudioTrack = newTrack;
|
||||
ALOGV("%s: using new track %p for sound %d",
|
||||
__func__, newTrack.get(), sound->getSoundID());
|
||||
}
|
||||
if (mMuted) {
|
||||
newTrack->setVolume(0.0f, 0.0f);
|
||||
} else {
|
||||
newTrack->setVolume(leftVolume, rightVolume);
|
||||
}
|
||||
newTrack->setLoop(0, frameCount, loop);
|
||||
mAudioTrack->start();
|
||||
mSound = sound;
|
||||
mSoundID = sound->getSoundID();
|
||||
mPriority = priority;
|
||||
mLoop = loop;
|
||||
mLeftVolume = leftVolume;
|
||||
mRightVolume = rightVolume;
|
||||
mRate = rate;
|
||||
mState = PLAYING;
|
||||
mStopTimeNs = 0;
|
||||
mStreamID = nextStreamID; // prefer this to be the last, as it is an atomic sync point
|
||||
}
|
||||
|
||||
exit:
|
||||
ALOGV("%s: delete oldTrack %p", __func__, oldTrack.get());
|
||||
if (status != NO_ERROR) {
|
||||
// TODO: should we consider keeping the soundID if the old track is OK?
|
||||
// Do not attempt to restart this track (should we remove the stream id?)
|
||||
mState = IDLE;
|
||||
mSoundID = 0;
|
||||
mSound.reset();
|
||||
mAudioTrack.clear(); // actual release from releaseTracks[]
|
||||
}
|
||||
}
|
||||
|
||||
/* static */
|
||||
void Stream::staticCallback(int event, void* user, void* info)
|
||||
{
|
||||
const uintptr_t userAsInt = (uintptr_t)user;
|
||||
Stream* stream = reinterpret_cast<Stream*>(userAsInt & ~1);
|
||||
stream->callback(event, info, userAsInt & 1, 0 /* tries */);
|
||||
}
|
||||
|
||||
void Stream::callback(int event, void* info, int toggle, int tries)
|
||||
{
|
||||
ALOGV("%s streamID %d", __func__, (int)mStreamID);
|
||||
int32_t activeStreamIDToRestart = 0;
|
||||
{
|
||||
std::unique_lock lock(mLock);
|
||||
|
||||
if (mAudioTrack == nullptr) {
|
||||
// The AudioTrack is either with this stream or its pair.
|
||||
// if this swaps a few times, the toggle is bound to be wrong, so we fail then.
|
||||
//
|
||||
// TODO: Modify AudioTrack callbacks to avoid the hacky toggle and retry
|
||||
// logic here.
|
||||
if (tries < 3) {
|
||||
lock.unlock();
|
||||
getPairStream()->callback(event, info, toggle, tries + 1);
|
||||
} else {
|
||||
ALOGW("%s streamID %d cannot find track", __func__, (int)mStreamID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mToggle != toggle) {
|
||||
ALOGD("%s streamID %d wrong toggle", __func__, (int)mStreamID);
|
||||
return;
|
||||
}
|
||||
switch (event) {
|
||||
case AudioTrack::EVENT_MORE_DATA:
|
||||
ALOGW("%s streamID %d Invalid EVENT_MORE_DATA for static track",
|
||||
__func__, (int)mStreamID);
|
||||
break;
|
||||
case AudioTrack::EVENT_UNDERRUN:
|
||||
ALOGW("%s streamID %d Invalid EVENT_UNDERRUN for static track",
|
||||
__func__, (int)mStreamID);
|
||||
break;
|
||||
case AudioTrack::EVENT_BUFFER_END:
|
||||
ALOGV("%s streamID %d EVENT_BUFFER_END", __func__, (int)mStreamID);
|
||||
if (mState != IDLE) {
|
||||
activeStreamIDToRestart = mStreamID;
|
||||
mStopTimeNs = systemTime();
|
||||
}
|
||||
break;
|
||||
case AudioTrack::EVENT_LOOP_END:
|
||||
ALOGV("%s streamID %d EVENT_LOOP_END", __func__, (int)mStreamID);
|
||||
break;
|
||||
case AudioTrack::EVENT_NEW_IAUDIOTRACK:
|
||||
ALOGV("%s streamID %d NEW_IAUDIOTRACK", __func__, (int)mStreamID);
|
||||
break;
|
||||
default:
|
||||
ALOGW("%s streamID %d Invalid event %d", __func__, (int)mStreamID, event);
|
||||
break;
|
||||
}
|
||||
} // lock ends here. This is on the callback thread, no need to be precise.
|
||||
if (activeStreamIDToRestart > 0) {
|
||||
// Restart only if a particular streamID is still current and active.
|
||||
ALOGV("%s: moveToRestartQueue %d", __func__, activeStreamIDToRestart);
|
||||
mStreamManager->moveToRestartQueue(this, activeStreamIDToRestart);
|
||||
}
|
||||
}
|
||||
|
||||
void Stream::dump() const
|
||||
{
|
||||
ALOGV("mPairStream=%p, mState=%d, mStreamID=%d, mSoundID=%d, mPriority=%d, mLoop=%d",
|
||||
getPairStream(), mState, (int)mStreamID, mSoundID, mPriority, mLoop);
|
||||
}
|
||||
|
||||
} // namespace android::soundpool
|
||||
150
media/jni/soundpool/Stream.h
Normal file
150
media/jni/soundpool/Stream.h
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Sound.h"
|
||||
|
||||
#include <audio_utils/clock.h>
|
||||
#include <media/AudioTrack.h>
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
// This is the amount of time to wait after stop is called when stealing an
|
||||
// AudioTrack to allow the sound to ramp down. If this is 0, glitches
|
||||
// may occur when stealing an AudioTrack.
|
||||
inline constexpr int64_t kStopWaitTimeNs = 20 * NANOS_PER_MILLISECOND;
|
||||
|
||||
inline constexpr size_t kCacheLineSize = 64; /* std::hardware_constructive_interference_size */
|
||||
|
||||
class StreamManager; // forward decl
|
||||
|
||||
/**
|
||||
* A Stream is associated with a StreamID exposed to the app to play a Sound.
|
||||
*
|
||||
* The Stream uses monitor locking strategy on mLock.
|
||||
* https://en.wikipedia.org/wiki/Monitor_(synchronization)
|
||||
*
|
||||
* where public methods are guarded by a lock (as needed)
|
||||
*
|
||||
* For Java equivalent APIs, see
|
||||
* https://developer.android.com/reference/android/media/SoundPool
|
||||
*
|
||||
* Streams are paired by the StreamManager, so one stream in the pair may be "stopping"
|
||||
* while the other stream of the pair has been prepared to run
|
||||
* (and the streamID returned to the app) pending its pair to be stopped.
|
||||
* The pair of a Stream may be obtained by calling getPairStream(),
|
||||
* where this->getPairStream()->getPairStream() == this; (pair is a commutative relationship).
|
||||
*
|
||||
* playPairStream() and getPairPriority() access the paired stream.
|
||||
* See also StreamManager.h for details of physical layout implications of paired streams.
|
||||
*/
|
||||
class alignas(kCacheLineSize) Stream {
|
||||
public:
|
||||
enum state { IDLE, PAUSED, PLAYING };
|
||||
// The PAUSED, PLAYING state directly corresponds to the AudioTrack state of an active Stream.
|
||||
//
|
||||
// The IDLE state indicates an inactive Stream. An IDLE Stream may have a non-nullptr
|
||||
// AudioTrack, which may be recycled for use if the SoundID matches the next Stream playback.
|
||||
//
|
||||
// PAUSED -> PLAYING through resume() (see also autoResume())
|
||||
// PLAYING -> PAUSED through pause() (see also autoPause())
|
||||
//
|
||||
// IDLE is the initial state of a Stream and also when a stream becomes inactive.
|
||||
// {PAUSED, PLAYING} -> IDLE through stop() (or if the Sound finishes playing)
|
||||
// IDLE -> PLAYING through play(). (there is no way to start a Stream in paused mode).
|
||||
|
||||
~Stream();
|
||||
void setStreamManager(StreamManager* streamManager) { // non-nullptr
|
||||
mStreamManager = streamManager; // set in StreamManager constructor, not changed
|
||||
}
|
||||
|
||||
// The following methods are monitor locked by mLock.
|
||||
//
|
||||
// For methods taking a streamID:
|
||||
// if the streamID matches the Stream's mStreamID, then method proceeds
|
||||
// else the command is ignored with no effect.
|
||||
|
||||
// returns true if the stream needs to be explicitly stopped.
|
||||
bool requestStop(int32_t streamID);
|
||||
void stop(); // explicit stop(), typically called from the worker thread.
|
||||
void clearAudioTrack();
|
||||
void pause(int32_t streamID);
|
||||
void autoPause(); // see the Java SoundPool.autoPause documentation for details.
|
||||
void resume(int32_t streamID);
|
||||
void autoResume();
|
||||
void mute(bool muting);
|
||||
void dump() const;
|
||||
|
||||
// returns the pair stream if successful, nullptr otherwise
|
||||
Stream* playPairStream();
|
||||
|
||||
// These parameters are explicitly checked in the SoundPool class
|
||||
// so never deviate from the Java API specified values.
|
||||
void setVolume(int32_t streamID, float leftVolume, float rightVolume);
|
||||
void setRate(int32_t streamID, float rate);
|
||||
void setPriority(int32_t streamID, int priority);
|
||||
void setLoop(int32_t streamID, int loop);
|
||||
void setPlay(int32_t streamID, const std::shared_ptr<Sound> &sound, int32_t soundID,
|
||||
float leftVolume, float rightVolume, int32_t priority, int32_t loop, float rate);
|
||||
void setStopTimeNs(int64_t stopTimeNs); // systemTime() clock monotonic.
|
||||
|
||||
// The following getters are not locked and have weak consistency.
|
||||
// These are considered advisory only - being stale is of nuisance.
|
||||
int32_t getPriority() const { return mPriority; }
|
||||
int32_t getPairPriority() const { return getPairStream()->getPriority(); }
|
||||
int64_t getStopTimeNs() const { return mStopTimeNs; }
|
||||
|
||||
int32_t getStreamID() const { return mStreamID; } // Can change with setPlay()
|
||||
int32_t getSoundID() const { return mSoundID; } // Can change with play_l()
|
||||
bool hasSound() const { return mSound.get() != nullptr; }
|
||||
|
||||
Stream* getPairStream() const; // this never changes. See top of header.
|
||||
|
||||
private:
|
||||
void play_l(const std::shared_ptr<Sound>& sound, int streamID,
|
||||
float leftVolume, float rightVolume, int priority, int loop, float rate,
|
||||
sp<AudioTrack> releaseTracks[2]);
|
||||
void stop_l();
|
||||
void setVolume_l(float leftVolume, float rightVolume);
|
||||
|
||||
// For use with AudioTrack callback.
|
||||
static void staticCallback(int event, void* user, void* info);
|
||||
void callback(int event, void* info, int toggle, int tries);
|
||||
|
||||
// StreamManager should be set on construction and not changed.
|
||||
// release mLock before calling into StreamManager
|
||||
StreamManager* mStreamManager = nullptr;
|
||||
|
||||
mutable std::mutex mLock;
|
||||
std::atomic_int32_t mStreamID = 0; // Note: valid streamIDs are always positive.
|
||||
int mState = IDLE;
|
||||
std::shared_ptr<Sound> mSound; // Non-null if playing.
|
||||
int32_t mSoundID = 0; // The sound ID associated with the AudioTrack.
|
||||
float mLeftVolume = 0.f;
|
||||
float mRightVolume = 0.f;
|
||||
int32_t mPriority = INT32_MIN;
|
||||
int32_t mLoop = 0;
|
||||
float mRate = 0.f;
|
||||
bool mAutoPaused = false;
|
||||
bool mMuted = false;
|
||||
|
||||
sp<AudioTrack> mAudioTrack;
|
||||
int mToggle = 0;
|
||||
int64_t mStopTimeNs = 0; // if nonzero, time to wait for stop.
|
||||
};
|
||||
|
||||
} // namespace android::soundpool
|
||||
407
media/jni/soundpool/StreamManager.cpp
Normal file
407
media/jni/soundpool/StreamManager.cpp
Normal file
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
//#define LOG_NDEBUG 0
|
||||
#define LOG_TAG "SoundPool::StreamManager"
|
||||
#include <utils/Log.h>
|
||||
|
||||
#include "StreamManager.h"
|
||||
|
||||
#include <audio_utils/clock.h>
|
||||
#include <audio_utils/roundup.h>
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
// kMaxStreams is number that should be less than the current AudioTrack max per UID of 40.
|
||||
// It is the maximum number of AudioTrack resources allowed in the SoundPool.
|
||||
// We suggest a value at least 4 or greater to allow CTS tests to pass.
|
||||
static constexpr int32_t kMaxStreams = 32;
|
||||
|
||||
// kStealActiveStream_OldestFirst = false historically (Q and earlier)
|
||||
// Changing to true could break app expectations but could change behavior beneficially.
|
||||
// In R, we change this to true, as it is the correct way per SoundPool documentation.
|
||||
static constexpr bool kStealActiveStream_OldestFirst = true;
|
||||
|
||||
// kPlayOnCallingThread = true prior to R.
|
||||
// Changing to false means calls to play() are almost instantaneous instead of taking around
|
||||
// ~10ms to launch the AudioTrack. It is perhaps 100x faster.
|
||||
static constexpr bool kPlayOnCallingThread = false;
|
||||
|
||||
// Amount of time for a StreamManager thread to wait before closing.
|
||||
static constexpr int64_t kWaitTimeBeforeCloseNs = 9 * NANOS_PER_SECOND;
|
||||
|
||||
////////////
|
||||
|
||||
StreamMap::StreamMap(int32_t streams) {
|
||||
ALOGV("%s(%d)", __func__, streams);
|
||||
if (streams > kMaxStreams) {
|
||||
ALOGW("%s: requested %d streams, clamping to %d", __func__, streams, kMaxStreams);
|
||||
streams = kMaxStreams;
|
||||
} else if (streams < 1) {
|
||||
ALOGW("%s: requested %d streams, clamping to 1", __func__, streams);
|
||||
streams = 1;
|
||||
}
|
||||
mStreamPoolSize = streams * 2;
|
||||
mStreamPool.reset(new Stream[mStreamPoolSize]);
|
||||
// we use a perfect hash table with 2x size to map StreamIDs to Stream pointers.
|
||||
mPerfectHash = std::make_unique<PerfectHash<int32_t, Stream *>>(roundup(mStreamPoolSize * 2));
|
||||
}
|
||||
|
||||
Stream* StreamMap::findStream(int32_t streamID) const
|
||||
{
|
||||
Stream *stream = lookupStreamFromId(streamID);
|
||||
return stream != nullptr && stream->getStreamID() == streamID ? stream : nullptr;
|
||||
}
|
||||
|
||||
size_t StreamMap::streamPosition(const Stream* stream) const
|
||||
{
|
||||
ptrdiff_t index = stream - mStreamPool.get();
|
||||
LOG_ALWAYS_FATAL_IF(index < 0 || index >= mStreamPoolSize,
|
||||
"%s: stream position out of range: %td", __func__, index);
|
||||
return (size_t)index;
|
||||
}
|
||||
|
||||
Stream* StreamMap::lookupStreamFromId(int32_t streamID) const
|
||||
{
|
||||
return streamID > 0 ? mPerfectHash->getValue(streamID).load() : nullptr;
|
||||
}
|
||||
|
||||
int32_t StreamMap::getNextIdForStream(Stream* stream) const {
|
||||
// even though it is const, it mutates the internal hash table.
|
||||
const int32_t id = mPerfectHash->generateKey(
|
||||
stream,
|
||||
[] (Stream *stream) {
|
||||
return stream == nullptr ? 0 : stream->getStreamID();
|
||||
}, /* getKforV() */
|
||||
stream->getStreamID() /* oldID */);
|
||||
return id;
|
||||
}
|
||||
|
||||
////////////
|
||||
|
||||
StreamManager::StreamManager(
|
||||
int32_t streams, size_t threads, const audio_attributes_t* attributes)
|
||||
: StreamMap(streams)
|
||||
, mAttributes(*attributes)
|
||||
{
|
||||
ALOGV("%s(%d, %zu, ...)", __func__, streams, threads);
|
||||
forEach([this](Stream *stream) {
|
||||
stream->setStreamManager(this);
|
||||
if ((streamPosition(stream) & 1) == 0) { // put the first stream of pair as available.
|
||||
mAvailableStreams.insert(stream);
|
||||
}
|
||||
});
|
||||
|
||||
mThreadPool = std::make_unique<ThreadPool>(
|
||||
std::min(threads, (size_t)std::thread::hardware_concurrency()),
|
||||
"SoundPool_");
|
||||
}
|
||||
|
||||
StreamManager::~StreamManager()
|
||||
{
|
||||
ALOGV("%s", __func__);
|
||||
{
|
||||
std::unique_lock lock(mStreamManagerLock);
|
||||
mQuit = true;
|
||||
mStreamManagerCondition.notify_all();
|
||||
}
|
||||
mThreadPool->quit();
|
||||
|
||||
// call stop on the stream pool
|
||||
forEach([](Stream *stream) { stream->stop(); });
|
||||
|
||||
// This invokes the destructor on the AudioTracks -
|
||||
// we do it here to ensure that AudioTrack callbacks will not occur
|
||||
// afterwards.
|
||||
forEach([](Stream *stream) { stream->clearAudioTrack(); });
|
||||
}
|
||||
|
||||
|
||||
int32_t StreamManager::queueForPlay(const std::shared_ptr<Sound> &sound,
|
||||
int32_t soundID, float leftVolume, float rightVolume,
|
||||
int32_t priority, int32_t loop, float rate)
|
||||
{
|
||||
ALOGV("%s(sound=%p, soundID=%d, leftVolume=%f, rightVolume=%f, priority=%d, loop=%d, rate=%f)",
|
||||
__func__, sound.get(), soundID, leftVolume, rightVolume, priority, loop, rate);
|
||||
bool launchThread = false;
|
||||
int32_t streamID = 0;
|
||||
|
||||
{ // for lock
|
||||
std::unique_lock lock(mStreamManagerLock);
|
||||
Stream *newStream = nullptr;
|
||||
bool fromAvailableQueue = false;
|
||||
ALOGV("%s: mStreamManagerLock lock acquired", __func__);
|
||||
|
||||
sanityCheckQueue_l();
|
||||
// find an available stream, prefer one that has matching sound id.
|
||||
if (mAvailableStreams.size() > 0) {
|
||||
newStream = *mAvailableStreams.begin();
|
||||
for (auto stream : mAvailableStreams) {
|
||||
if (stream->getSoundID() == soundID) {
|
||||
newStream = stream;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newStream != nullptr) {
|
||||
newStream->setStopTimeNs(systemTime());
|
||||
}
|
||||
fromAvailableQueue = true;
|
||||
}
|
||||
|
||||
// also look in the streams restarting (if the paired stream doesn't have a pending play)
|
||||
if (newStream == nullptr || newStream->getSoundID() != soundID) {
|
||||
for (auto [unused , stream] : mRestartStreams) {
|
||||
if (!stream->getPairStream()->hasSound()) {
|
||||
if (stream->getSoundID() == soundID) {
|
||||
newStream = stream;
|
||||
break;
|
||||
} else if (newStream == nullptr) {
|
||||
newStream = stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no available streams, look for one to steal from the active list
|
||||
if (newStream == nullptr) {
|
||||
for (auto stream : mActiveStreams) {
|
||||
if (stream->getPriority() <= priority) {
|
||||
if (newStream == nullptr
|
||||
|| newStream->getPriority() > stream->getPriority()) {
|
||||
newStream = stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newStream != nullptr) { // we need to mute as it is still playing.
|
||||
(void)newStream->requestStop(newStream->getStreamID());
|
||||
}
|
||||
}
|
||||
|
||||
// none found, look for a stream that is restarting, evict one.
|
||||
if (newStream == nullptr) {
|
||||
for (auto [unused, stream] : mRestartStreams) {
|
||||
if (stream->getPairPriority() <= priority) {
|
||||
newStream = stream;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO NOT LOOK into mProcessingStreams as those are held by the StreamManager threads.
|
||||
|
||||
if (newStream == nullptr) {
|
||||
ALOGD("%s: unable to find stream, returning 0", __func__);
|
||||
return 0; // unable to find available stream
|
||||
}
|
||||
|
||||
Stream *pairStream = newStream->getPairStream();
|
||||
streamID = getNextIdForStream(pairStream);
|
||||
pairStream->setPlay(
|
||||
streamID, sound, soundID, leftVolume, rightVolume, priority, loop, rate);
|
||||
if (fromAvailableQueue && kPlayOnCallingThread) {
|
||||
removeFromQueues_l(newStream);
|
||||
mProcessingStreams.emplace(newStream);
|
||||
lock.unlock();
|
||||
if (Stream* nextStream = newStream->playPairStream()) {
|
||||
lock.lock();
|
||||
ALOGV("%s: starting streamID:%d", __func__, nextStream->getStreamID());
|
||||
addToActiveQueue_l(nextStream);
|
||||
} else {
|
||||
lock.lock();
|
||||
mAvailableStreams.insert(newStream);
|
||||
streamID = 0;
|
||||
}
|
||||
mProcessingStreams.erase(newStream);
|
||||
} else {
|
||||
launchThread = moveToRestartQueue_l(newStream) && needMoreThreads_l();
|
||||
}
|
||||
sanityCheckQueue_l();
|
||||
ALOGV("%s: mStreamManagerLock released", __func__);
|
||||
} // lock
|
||||
|
||||
if (launchThread) {
|
||||
const int32_t id __unused = mThreadPool->launch([this](int32_t id) { run(id); });
|
||||
ALOGV_IF(id != 0, "%s: launched thread %d", __func__, id);
|
||||
}
|
||||
ALOGV("%s: returning %d", __func__, streamID);
|
||||
return streamID;
|
||||
}
|
||||
|
||||
void StreamManager::moveToRestartQueue(
|
||||
Stream* stream, int32_t activeStreamIDToMatch)
|
||||
{
|
||||
ALOGV("%s(stream(ID)=%d, activeStreamIDToMatch=%d)",
|
||||
__func__, stream->getStreamID(), activeStreamIDToMatch);
|
||||
bool restart;
|
||||
{
|
||||
std::lock_guard lock(mStreamManagerLock);
|
||||
sanityCheckQueue_l();
|
||||
if (mProcessingStreams.count(stream) > 0 ||
|
||||
mProcessingStreams.count(stream->getPairStream()) > 0) {
|
||||
ALOGD("%s: attempting to restart processing stream(%d)",
|
||||
__func__, stream->getStreamID());
|
||||
restart = false;
|
||||
} else {
|
||||
moveToRestartQueue_l(stream, activeStreamIDToMatch);
|
||||
restart = needMoreThreads_l();
|
||||
}
|
||||
sanityCheckQueue_l();
|
||||
}
|
||||
if (restart) {
|
||||
const int32_t id __unused = mThreadPool->launch([this](int32_t id) { run(id); });
|
||||
ALOGV_IF(id != 0, "%s: launched thread %d", __func__, id);
|
||||
}
|
||||
}
|
||||
|
||||
bool StreamManager::moveToRestartQueue_l(
|
||||
Stream* stream, int32_t activeStreamIDToMatch)
|
||||
{
|
||||
ALOGV("%s(stream(ID)=%d, activeStreamIDToMatch=%d)",
|
||||
__func__, stream->getStreamID(), activeStreamIDToMatch);
|
||||
if (activeStreamIDToMatch > 0 && stream->getStreamID() != activeStreamIDToMatch) {
|
||||
return false;
|
||||
}
|
||||
const ssize_t found = removeFromQueues_l(stream, activeStreamIDToMatch);
|
||||
if (found < 0) return false;
|
||||
|
||||
LOG_ALWAYS_FATAL_IF(found > 1, "stream on %zd > 1 stream lists", found);
|
||||
|
||||
addToRestartQueue_l(stream);
|
||||
mStreamManagerCondition.notify_one();
|
||||
return true;
|
||||
}
|
||||
|
||||
ssize_t StreamManager::removeFromQueues_l(
|
||||
Stream* stream, int32_t activeStreamIDToMatch) {
|
||||
size_t found = 0;
|
||||
for (auto it = mActiveStreams.begin(); it != mActiveStreams.end(); ++it) {
|
||||
if (*it == stream) {
|
||||
mActiveStreams.erase(it); // we erase the iterator and break (otherwise it not safe).
|
||||
++found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// activeStreamIDToMatch is nonzero indicates we proceed only if found.
|
||||
if (found == 0 && activeStreamIDToMatch > 0) {
|
||||
return -1; // special code: not present on active streams, ignore restart request
|
||||
}
|
||||
|
||||
for (auto it = mRestartStreams.begin(); it != mRestartStreams.end(); ++it) {
|
||||
if (it->second == stream) {
|
||||
mRestartStreams.erase(it);
|
||||
++found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
found += mAvailableStreams.erase(stream);
|
||||
|
||||
// streams on mProcessingStreams are undergoing processing by the StreamManager thread
|
||||
// and do not participate in normal stream migration.
|
||||
return found;
|
||||
}
|
||||
|
||||
void StreamManager::addToRestartQueue_l(Stream *stream) {
|
||||
mRestartStreams.emplace(stream->getStopTimeNs(), stream);
|
||||
}
|
||||
|
||||
void StreamManager::addToActiveQueue_l(Stream *stream) {
|
||||
if (kStealActiveStream_OldestFirst) {
|
||||
mActiveStreams.push_back(stream); // oldest to newest
|
||||
} else {
|
||||
mActiveStreams.push_front(stream); // newest to oldest
|
||||
}
|
||||
}
|
||||
|
||||
void StreamManager::run(int32_t id)
|
||||
{
|
||||
ALOGV("%s(%d) entering", __func__, id);
|
||||
int64_t waitTimeNs = kWaitTimeBeforeCloseNs;
|
||||
std::unique_lock lock(mStreamManagerLock);
|
||||
while (!mQuit) {
|
||||
mStreamManagerCondition.wait_for(
|
||||
lock, std::chrono::duration<int64_t, std::nano>(waitTimeNs));
|
||||
ALOGV("%s(%d) awake", __func__, id);
|
||||
|
||||
sanityCheckQueue_l();
|
||||
|
||||
if (mQuit || (mRestartStreams.empty() && waitTimeNs == kWaitTimeBeforeCloseNs)) {
|
||||
break; // end the thread
|
||||
}
|
||||
|
||||
waitTimeNs = kWaitTimeBeforeCloseNs;
|
||||
while (!mQuit && !mRestartStreams.empty()) {
|
||||
const nsecs_t nowNs = systemTime();
|
||||
auto it = mRestartStreams.begin();
|
||||
Stream* const stream = it->second;
|
||||
const int64_t diffNs = stream->getStopTimeNs() - nowNs;
|
||||
if (diffNs > 0) {
|
||||
waitTimeNs = std::min(waitTimeNs, diffNs);
|
||||
break;
|
||||
}
|
||||
mRestartStreams.erase(it);
|
||||
mProcessingStreams.emplace(stream);
|
||||
lock.unlock();
|
||||
stream->stop();
|
||||
ALOGV("%s(%d) stopping streamID:%d", __func__, id, stream->getStreamID());
|
||||
if (Stream* nextStream = stream->playPairStream()) {
|
||||
ALOGV("%s(%d) starting streamID:%d", __func__, id, nextStream->getStreamID());
|
||||
lock.lock();
|
||||
if (nextStream->getStopTimeNs() > 0) {
|
||||
// the next stream was stopped before we can move it to the active queue.
|
||||
ALOGV("%s(%d) stopping started streamID:%d",
|
||||
__func__, id, nextStream->getStreamID());
|
||||
moveToRestartQueue_l(nextStream);
|
||||
} else {
|
||||
addToActiveQueue_l(nextStream);
|
||||
}
|
||||
} else {
|
||||
lock.lock();
|
||||
mAvailableStreams.insert(stream);
|
||||
}
|
||||
mProcessingStreams.erase(stream);
|
||||
sanityCheckQueue_l();
|
||||
}
|
||||
}
|
||||
ALOGV("%s(%d) exiting", __func__, id);
|
||||
}
|
||||
|
||||
void StreamManager::dump() const
|
||||
{
|
||||
forEach([](const Stream *stream) { stream->dump(); });
|
||||
}
|
||||
|
||||
void StreamManager::sanityCheckQueue_l() const
|
||||
{
|
||||
// We want to preserve the invariant that each stream pair is exactly on one of the queues.
|
||||
const size_t availableStreams = mAvailableStreams.size();
|
||||
const size_t restartStreams = mRestartStreams.size();
|
||||
const size_t activeStreams = mActiveStreams.size();
|
||||
const size_t processingStreams = mProcessingStreams.size();
|
||||
const size_t managedStreams = availableStreams + restartStreams + activeStreams
|
||||
+ processingStreams;
|
||||
const size_t totalStreams = getStreamMapSize() >> 1;
|
||||
LOG_ALWAYS_FATAL_IF(managedStreams != totalStreams,
|
||||
"%s: mAvailableStreams:%zu + mRestartStreams:%zu + "
|
||||
"mActiveStreams:%zu + mProcessingStreams:%zu = %zu != total streams %zu",
|
||||
__func__, availableStreams, restartStreams, activeStreams, processingStreams,
|
||||
managedStreams, totalStreams);
|
||||
ALOGV("%s: mAvailableStreams:%zu + mRestartStreams:%zu + "
|
||||
"mActiveStreams:%zu + mProcessingStreams:%zu = %zu (total streams: %zu)",
|
||||
__func__, availableStreams, restartStreams, activeStreams, processingStreams,
|
||||
managedStreams, totalStreams);
|
||||
}
|
||||
|
||||
} // namespace android::soundpool
|
||||
467
media/jni/soundpool/StreamManager.h
Normal file
467
media/jni/soundpool/StreamManager.h
Normal file
@@ -0,0 +1,467 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Stream.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <future>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <utils/AndroidThreads.h>
|
||||
|
||||
namespace android::soundpool {
|
||||
|
||||
// TODO: Move helper classes to a utility file, with separate test.
|
||||
|
||||
/**
|
||||
* JavaThread is used like std::thread but for threads that may call the JVM.
|
||||
*
|
||||
* std::thread does not easily attach to the JVM. We need JVM capable threads
|
||||
* from createThreadEtc() since android binder call optimization may attempt to
|
||||
* call back into Java if the SoundPool runs in system server.
|
||||
*
|
||||
*
|
||||
* No locking is required - the member variables are inherently thread-safe.
|
||||
*/
|
||||
class JavaThread {
|
||||
public:
|
||||
JavaThread(std::function<void()> f, const char *name)
|
||||
: mF{std::move(f)} {
|
||||
createThreadEtc(staticFunction, this, name);
|
||||
}
|
||||
|
||||
JavaThread(JavaThread &&) = delete; // uses "this" ptr, not moveable.
|
||||
|
||||
void join() const {
|
||||
mFuture.wait();
|
||||
}
|
||||
|
||||
bool isClosed() const {
|
||||
return mIsClosed;
|
||||
}
|
||||
|
||||
private:
|
||||
static int staticFunction(void *data) {
|
||||
JavaThread *jt = static_cast<JavaThread *>(data);
|
||||
jt->mF();
|
||||
jt->mIsClosed = true;
|
||||
jt->mPromise.set_value();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// No locking is provided as these variables are initialized in the constructor
|
||||
// and the members referenced are thread-safe objects.
|
||||
// (mFuture.wait() can block multiple threads.)
|
||||
// Note the order of member variables is reversed for destructor.
|
||||
const std::function<void()> mF;
|
||||
// Used in join() to block until the thread completes.
|
||||
// See https://en.cppreference.com/w/cpp/thread/promise for the void specialization of
|
||||
// promise.
|
||||
std::promise<void> mPromise;
|
||||
std::future<void> mFuture{mPromise.get_future()};
|
||||
std::atomic_bool mIsClosed = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* The ThreadPool manages thread lifetimes of SoundPool worker threads.
|
||||
*
|
||||
* TODO: the (eventual) goal of ThreadPool is to transparently and cooperatively
|
||||
* maximize CPU utilization while avoiding starvation of other applications.
|
||||
* Some possibilities:
|
||||
*
|
||||
* We should create worker threads when we have SoundPool work and the system is idle.
|
||||
* CPU cycles are "use-it-or-lose-it" when the system is idle.
|
||||
*
|
||||
* We should adjust the priority of worker threads so that the second (and subsequent) worker
|
||||
* threads have lower priority (should we try to promote priority also?).
|
||||
*
|
||||
* We should throttle the spawning of new worker threads, spacing over time, to avoid
|
||||
* creating too many new threads all at once, on initialization.
|
||||
*/
|
||||
class ThreadPool {
|
||||
public:
|
||||
ThreadPool(size_t maxThreadCount, std::string name)
|
||||
: mMaxThreadCount(maxThreadCount)
|
||||
, mName{std::move(name)} { }
|
||||
|
||||
~ThreadPool() { quit(); }
|
||||
|
||||
size_t getActiveThreadCount() const { return mActiveThreadCount; }
|
||||
size_t getMaxThreadCount() const { return mMaxThreadCount; }
|
||||
|
||||
void quit() {
|
||||
std::list<std::unique_ptr<JavaThread>> threads;
|
||||
{
|
||||
std::lock_guard lock(mThreadLock);
|
||||
if (mQuit) return; // already joined.
|
||||
mQuit = true;
|
||||
threads = std::move(mThreads);
|
||||
mThreads.clear();
|
||||
}
|
||||
// mQuit set under lock, no more threads will be created.
|
||||
for (auto &thread : threads) {
|
||||
thread->join();
|
||||
thread.reset();
|
||||
}
|
||||
LOG_ALWAYS_FATAL_IF(mActiveThreadCount != 0,
|
||||
"Invalid Active Threads: %zu", (size_t)mActiveThreadCount);
|
||||
}
|
||||
|
||||
// returns a non-zero id if successful, the id is to help logging messages.
|
||||
int32_t launch(std::function<void(int32_t /* id */)> f) {
|
||||
std::list<std::unique_ptr<JavaThread>> threadsToRelease; // release outside of lock.
|
||||
std::lock_guard lock(mThreadLock);
|
||||
if (mQuit) return 0; // ignore if we have quit
|
||||
|
||||
// clean up threads.
|
||||
for (auto it = mThreads.begin(); it != mThreads.end(); ) {
|
||||
if ((*it)->isClosed()) {
|
||||
threadsToRelease.emplace_back(std::move(*it));
|
||||
it = mThreads.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t threadCount = mThreads.size();
|
||||
if (threadCount < mMaxThreadCount) {
|
||||
// if the id wraps, we don't care about collisions. it's just for logging.
|
||||
mNextThreadId = mNextThreadId == INT32_MAX ? 1 : ++mNextThreadId;
|
||||
const int32_t id = mNextThreadId;
|
||||
mThreads.emplace_back(std::make_unique<JavaThread>(
|
||||
[this, id, mf = std::move(f)] { mf(id); --mActiveThreadCount; },
|
||||
(mName + std::to_string(id)).c_str()));
|
||||
++mActiveThreadCount;
|
||||
return id;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// TODO: launch only if load average is low.
|
||||
// This gets the load average
|
||||
// See also std::thread::hardware_concurrency() for the concurrent capability.
|
||||
static double getLoadAvg() {
|
||||
double loadAvg[1];
|
||||
if (getloadavg(loadAvg, std::size(loadAvg)) > 0) {
|
||||
return loadAvg[0];
|
||||
}
|
||||
return -1.;
|
||||
}
|
||||
|
||||
private:
|
||||
const size_t mMaxThreadCount;
|
||||
const std::string mName;
|
||||
|
||||
std::atomic_size_t mActiveThreadCount = 0;
|
||||
|
||||
std::mutex mThreadLock;
|
||||
bool mQuit = false; // GUARDED_BY(mThreadLock)
|
||||
int32_t mNextThreadId = 0; // GUARDED_BY(mThreadLock)
|
||||
std::list<std::unique_ptr<JavaThread>> mThreads; // GUARDED_BY(mThreadLock)
|
||||
};
|
||||
|
||||
/**
|
||||
* A Perfect HashTable for IDs (key) to pointers (value).
|
||||
*
|
||||
* There are no collisions. Why? because we generate the IDs for you to look up :-).
|
||||
*
|
||||
* The goal of this hash table is to map an integer ID handle > 0 to a pointer.
|
||||
* We give these IDs in monotonic order (though we may skip if it were to cause a collision).
|
||||
*
|
||||
* The size of the hashtable must be large enough to accommodate the max number of keys.
|
||||
* We suggest 2x.
|
||||
*
|
||||
* Readers are lockless
|
||||
* Single writer could be lockless, but we allow multiple writers through an internal lock.
|
||||
*
|
||||
* For the Key type K, valid keys generated are > 0 (signed or unsigned)
|
||||
* For the Value type V, values are pointers - nullptr means empty.
|
||||
*/
|
||||
template <typename K, typename V>
|
||||
class PerfectHash {
|
||||
public:
|
||||
PerfectHash(size_t hashCapacity)
|
||||
: mHashCapacity(hashCapacity)
|
||||
, mK2V{new std::atomic<V>[hashCapacity]()} {
|
||||
}
|
||||
|
||||
// Generate a key for a value V.
|
||||
// There is a testing function getKforV() which checks what the value reports as its key.
|
||||
//
|
||||
// Calls back into getKforV under lock.
|
||||
//
|
||||
// We expect that the hashCapacity is 2x the number of stored keys in order
|
||||
// to have one or two tries to find an empty slot
|
||||
K generateKey(V value, std::function<K(V)> getKforV, K oldKey = 0) {
|
||||
std::lock_guard lock(mHashLock);
|
||||
// try to remove the old key.
|
||||
if (oldKey > 0) { // key valid
|
||||
const V v = getValue(oldKey);
|
||||
if (v != nullptr) { // value still valid
|
||||
const K atPosition = getKforV(v);
|
||||
if (atPosition < 0 || // invalid value
|
||||
atPosition == oldKey || // value's key still valid and matches old key
|
||||
((atPosition ^ oldKey) & (mHashCapacity - 1)) != 0) { // stale key entry
|
||||
getValue(oldKey) = nullptr; // invalidate
|
||||
}
|
||||
} // else if value is invalid, no need to invalidate.
|
||||
}
|
||||
// check if we are invalidating only.
|
||||
if (value == nullptr) return 0;
|
||||
// now insert the new value and return the key.
|
||||
size_t tries = 0;
|
||||
for (; tries < mHashCapacity; ++tries) {
|
||||
mNextKey = mNextKey == std::numeric_limits<K>::max() ? 1 : mNextKey + 1;
|
||||
const V v = getValue(mNextKey);
|
||||
//ALOGD("tries: %zu, key:%d value:%p", tries, (int)mNextKey, v);
|
||||
if (v == nullptr) break; // empty
|
||||
const K atPosition = getKforV(v);
|
||||
//ALOGD("tries: %zu key atPosition:%d", tries, (int)atPosition);
|
||||
if (atPosition < 0 || // invalid value
|
||||
((atPosition ^ mNextKey) & (mHashCapacity - 1)) != 0) { // stale key entry
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG_ALWAYS_FATAL_IF(tries == mHashCapacity, "hash table overflow!");
|
||||
//ALOGD("%s: found after %zu tries", __func__, tries);
|
||||
getValue(mNextKey) = value;
|
||||
return mNextKey;
|
||||
}
|
||||
|
||||
std::atomic<V> &getValue(K key) { return mK2V[key & (mHashCapacity - 1)]; }
|
||||
const std::atomic_int32_t &getValue(K key) const { return mK2V[key & (mHashCapacity - 1)]; }
|
||||
|
||||
private:
|
||||
mutable std::mutex mHashLock;
|
||||
const size_t mHashCapacity; // size of mK2V no lock needed.
|
||||
std::unique_ptr<std::atomic<V>[]> mK2V; // no lock needed for read access.
|
||||
K mNextKey{}; // GUARDED_BY(mHashLock)
|
||||
};
|
||||
|
||||
/**
|
||||
* StreamMap contains the all the valid streams available to SoundPool.
|
||||
*
|
||||
* There is no Lock required for this class because the streams are
|
||||
* allocated in the constructor, the lookup is lockless, and the Streams
|
||||
* returned are locked internally.
|
||||
*
|
||||
* The lookup uses a perfect hash.
|
||||
* It is possible to use a lockless hash table or to use a stripe-locked concurrent
|
||||
* hashmap for essentially lock-free lookup.
|
||||
*
|
||||
* This follows Map-Reduce parallelism model.
|
||||
* https://en.wikipedia.org/wiki/MapReduce
|
||||
*
|
||||
* Conceivably the forEach could be parallelized using std::for_each with a
|
||||
* std::execution::par policy.
|
||||
*
|
||||
* https://en.cppreference.com/w/cpp/algorithm/for_each
|
||||
*/
|
||||
class StreamMap {
|
||||
public:
|
||||
explicit StreamMap(int32_t streams);
|
||||
|
||||
// Returns the stream associated with streamID or nullptr if not found.
|
||||
// This need not be locked.
|
||||
// The stream ID will never migrate to another Stream, but it may change
|
||||
// underneath you. The Stream operations that take a streamID will confirm
|
||||
// that the streamID matches under the Stream lock before executing otherwise
|
||||
// it ignores the command as stale.
|
||||
Stream* findStream(int32_t streamID) const;
|
||||
|
||||
// Iterates through the stream pool applying the function f.
|
||||
// Since this enumerates over every single stream, it is unlocked.
|
||||
//
|
||||
// See related: https://en.cppreference.com/w/cpp/algorithm/for_each
|
||||
void forEach(std::function<void(const Stream *)>f) const {
|
||||
for (size_t i = 0; i < mStreamPoolSize; ++i) {
|
||||
f(&mStreamPool[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void forEach(std::function<void(Stream *)>f) {
|
||||
for (size_t i = 0; i < mStreamPoolSize; ++i) {
|
||||
f(&mStreamPool[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the pair stream for a given Stream.
|
||||
// This need not be locked as it is a property of the pointer address.
|
||||
Stream* getPairStream(const Stream* stream) const {
|
||||
const size_t index = streamPosition(stream);
|
||||
return &mStreamPool[index ^ 1];
|
||||
}
|
||||
|
||||
// find the position of the stream in mStreamPool array.
|
||||
size_t streamPosition(const Stream* stream) const; // no lock needed
|
||||
|
||||
size_t getStreamMapSize() const {
|
||||
return mStreamPoolSize;
|
||||
}
|
||||
|
||||
// find the next valid ID for a stream and store in hash table.
|
||||
int32_t getNextIdForStream(Stream* stream) const;
|
||||
|
||||
private:
|
||||
|
||||
// use the hash table to attempt to find the stream.
|
||||
// nullptr is returned if the lookup fails.
|
||||
Stream* lookupStreamFromId(int32_t streamID) const;
|
||||
|
||||
// The stream pool is initialized in the constructor, effectively const.
|
||||
// no locking required for access.
|
||||
//
|
||||
// The constructor parameter "streams" results in streams pairs of streams.
|
||||
// We have twice as many streams because we wish to return a streamID "handle"
|
||||
// back to the app immediately, while we may be stopping the other stream in the
|
||||
// pair to get its AudioTrack :-).
|
||||
//
|
||||
// Of the stream pair, only one of the streams may have an AudioTrack.
|
||||
// The fixed association of a stream pair allows callbacks from the AudioTrack
|
||||
// to be associated properly to either one or the other of the stream pair.
|
||||
//
|
||||
// TODO: The stream pair arrangement can be removed if we have better AudioTrack
|
||||
// callback handling (being able to remove and change the callback after construction).
|
||||
//
|
||||
// Streams may be accessed anytime off of the stream pool
|
||||
// as there is internal locking on each stream.
|
||||
std::unique_ptr<Stream[]> mStreamPool; // no lock needed for access.
|
||||
size_t mStreamPoolSize; // no lock needed for access.
|
||||
|
||||
// In order to find the Stream from a StreamID, we could do a linear lookup in mStreamPool.
|
||||
// As an alternative, one could use stripe-locked or lock-free concurrent hashtables.
|
||||
//
|
||||
// When considering linear search vs hashmap, verify the typical use-case size.
|
||||
// Linear search is faster than std::unordered_map (circa 2018) for less than 40 elements.
|
||||
// [ Skarupke, M. (2018), "You Can Do Better than std::unordered_map: New and Recent
|
||||
// Improvements to Hash Table Performance." C++Now 2018. cppnow.org, see
|
||||
// https://www.youtube.com/watch?v=M2fKMP47slQ ]
|
||||
//
|
||||
// Here, we use a PerfectHash of Id to Stream *, since we can control the
|
||||
// StreamID returned to the user. This allows O(1) read access to mStreamPool lock-free.
|
||||
//
|
||||
// We prefer that the next stream ID is monotonic for aesthetic reasons
|
||||
// (if we didn't care about monotonicity, a simple method is to apply a generation count
|
||||
// to each stream in the unused upper bits of its index in mStreamPool for the id).
|
||||
//
|
||||
std::unique_ptr<PerfectHash<int32_t, Stream *>> mPerfectHash;
|
||||
};
|
||||
|
||||
/**
|
||||
* StreamManager is used to manage the streams (accessed by StreamID from Java).
|
||||
*
|
||||
* Locking order (proceeds from application to component).
|
||||
* SoundPool mApiLock (if needed) -> StreamManager mStreamManagerLock
|
||||
* -> pair Stream mLock -> queued Stream mLock
|
||||
*/
|
||||
class StreamManager : public StreamMap {
|
||||
public:
|
||||
// Note: the SoundPool pointer is only used for stream initialization.
|
||||
// It is not stored in StreamManager.
|
||||
StreamManager(int32_t streams, size_t threads, const audio_attributes_t* attributes);
|
||||
~StreamManager();
|
||||
|
||||
// Returns positive streamID on success, 0 on failure. This is locked.
|
||||
int32_t queueForPlay(const std::shared_ptr<Sound> &sound,
|
||||
int32_t soundID, float leftVolume, float rightVolume,
|
||||
int32_t priority, int32_t loop, float rate);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// Called from soundpool::Stream
|
||||
|
||||
const audio_attributes_t* getAttributes() const { return &mAttributes; }
|
||||
|
||||
// Moves the stream to the restart queue (called upon BUFFER_END of the static track)
|
||||
// this is locked internally.
|
||||
// If activeStreamIDToMatch is nonzero, it will only move to the restart queue
|
||||
// if the streamIDToMatch is found on the active queue.
|
||||
void moveToRestartQueue(Stream* stream, int32_t activeStreamIDToMatch = 0);
|
||||
|
||||
private:
|
||||
|
||||
void run(int32_t id); // worker thread, takes lock internally.
|
||||
void dump() const; // no lock needed
|
||||
|
||||
// returns true if more worker threads are needed.
|
||||
bool needMoreThreads_l() {
|
||||
return mRestartStreams.size() > 0 &&
|
||||
(mThreadPool->getActiveThreadCount() == 0
|
||||
|| std::distance(mRestartStreams.begin(),
|
||||
mRestartStreams.upper_bound(systemTime()))
|
||||
> (ptrdiff_t)mThreadPool->getActiveThreadCount());
|
||||
}
|
||||
|
||||
// returns true if the stream was added.
|
||||
bool moveToRestartQueue_l(Stream* stream, int32_t activeStreamIDToMatch = 0);
|
||||
// returns number of queues the stream was removed from (should be 0 or 1);
|
||||
// a special code of -1 is returned if activeStreamIDToMatch is > 0 and
|
||||
// the stream wasn't found on the active queue.
|
||||
ssize_t removeFromQueues_l(Stream* stream, int32_t activeStreamIDToMatch = 0);
|
||||
void addToRestartQueue_l(Stream *stream);
|
||||
void addToActiveQueue_l(Stream *stream);
|
||||
void sanityCheckQueue_l() const;
|
||||
|
||||
const audio_attributes_t mAttributes;
|
||||
std::unique_ptr<ThreadPool> mThreadPool; // locked internally
|
||||
|
||||
// mStreamManagerLock is used to lock access for transitions between the
|
||||
// 4 stream queues by the Manager Thread or by the user initiated play().
|
||||
// A stream pair has exactly one stream on exactly one of the queues.
|
||||
std::mutex mStreamManagerLock;
|
||||
std::condition_variable mStreamManagerCondition;
|
||||
|
||||
bool mQuit = false; // GUARDED_BY(mStreamManagerLock)
|
||||
|
||||
// There are constructor arg "streams" pairs of streams, only one of each
|
||||
// pair on the 4 stream queues below. The other stream in the pair serves as
|
||||
// placeholder to accumulate user changes, pending actual availability of the
|
||||
// AudioTrack, as it may be in use, requiring stop-then-restart.
|
||||
//
|
||||
// The 4 queues are implemented in the appropriate STL container based on perceived
|
||||
// optimality.
|
||||
|
||||
// 1) mRestartStreams: Streams awaiting stop.
|
||||
// The paired stream may be active (but with no AudioTrack), and will be restarted
|
||||
// with an active AudioTrack when the current stream is stopped.
|
||||
std::multimap<int64_t /* stopTimeNs */, Stream*>
|
||||
mRestartStreams; // GUARDED_BY(mStreamManagerLock)
|
||||
|
||||
// 2) mActiveStreams: Streams that are active.
|
||||
// The paired stream will be inactive.
|
||||
// This is in order of specified by kStealActiveStream_OldestFirst
|
||||
std::list<Stream*> mActiveStreams; // GUARDED_BY(mStreamManagerLock)
|
||||
|
||||
// 3) mAvailableStreams: Streams that are inactive.
|
||||
// The paired stream will also be inactive.
|
||||
// No particular order.
|
||||
std::unordered_set<Stream*> mAvailableStreams; // GUARDED_BY(mStreamManagerLock)
|
||||
|
||||
// 4) mProcessingStreams: Streams that are being processed by the ManagerThreads
|
||||
// When on this queue, the stream and its pair are not available for stealing.
|
||||
// Each ManagerThread will have at most one stream on the mProcessingStreams queue.
|
||||
// The paired stream may be active or restarting.
|
||||
// No particular order.
|
||||
std::unordered_set<Stream*> mProcessingStreams; // GUARDED_BY(mStreamManagerLock)
|
||||
};
|
||||
|
||||
} // namespace android::soundpool
|
||||
Reference in New Issue
Block a user