diff --git a/media/jni/soundpool/Android.bp b/media/jni/soundpool/Android.bp index 35b7b01e207ca..e1945dddaa0b2 100644 --- a/media/jni/soundpool/Android.bp +++ b/media/jni/soundpool/Android.bp @@ -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", diff --git a/media/jni/soundpool/Sound.cpp b/media/jni/soundpool/Sound.cpp new file mode 100644 index 0000000000000..0bbc3e46b0440 --- /dev/null +++ b/media/jni/soundpool/Sound.cpp @@ -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 + +#include "Sound.h" + +#include +#include +#include + +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 heap, + size_t *sizeInBytes) { + ALOGV("%s(fd=%d, offset=%lld, length=%lld, ...)", + __func__, fd, (long long)offset, (long long)length); + std::unique_ptr 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 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 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(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 diff --git a/media/jni/soundpool/Sound.h b/media/jni/soundpool/Sound.h new file mode 100644 index 0000000000000..efe940a7bd680 --- /dev/null +++ b/media/jni/soundpool/Sound.h @@ -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 +#include +#include +#include + +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(mData->unsecurePointer()); } + sp 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 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 mData; + sp mHeap; +}; + +} // namespace android::soundpool diff --git a/media/jni/soundpool/SoundDecoder.cpp b/media/jni/soundpool/SoundDecoder.cpp new file mode 100644 index 0000000000000..12200ef03aadc --- /dev/null +++ b/media/jni/soundpool/SoundDecoder.cpp @@ -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( + 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(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 = 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 diff --git a/media/jni/soundpool/SoundDecoder.h b/media/jni/soundpool/SoundDecoder.h new file mode 100644 index 0000000000000..1288943e86e3b --- /dev/null +++ b/media/jni/soundpool/SoundDecoder.h @@ -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 +#include + +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 mThreadPool; // set in constructor, has own lock + + std::mutex mLock; + std::condition_variable mQueueSpaceAvailable; + std::condition_variable mQueueDataAvailable; + + std::deque mSoundIDs; // GUARDED_BY(mLock); + bool mQuit = false; // GUARDED_BY(mLock); +}; + +} // end namespace android::soundpool + diff --git a/media/jni/soundpool/SoundManager.cpp b/media/jni/soundpool/SoundManager.cpp new file mode 100644 index 0000000000000..3c625bf3bb7f5 --- /dev/null +++ b/media/jni/soundpool/SoundManager.cpp @@ -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 + +#include "SoundManager.h" + +#include + +#include "SoundDecoder.h" + +namespace android::soundpool { + +static const size_t kDecoderThreads = std::thread::hardware_concurrency() >= 4 ? 2 : 1; + +SoundManager::SoundManager() + : mDecoder{std::make_unique(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(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 SoundManager::findSound(int32_t soundID) const +{ + std::lock_guard lock(mSoundManagerLock); + return findSound_l(soundID); +} + +std::shared_ptr 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 diff --git a/media/jni/soundpool/SoundManager.h b/media/jni/soundpool/SoundManager.h new file mode 100644 index 0000000000000..9201e78132f43 --- /dev/null +++ b/media/jni/soundpool/SoundManager.h @@ -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 +#include + +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 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 lock(mCallbackLock); + mSoundPool = soundPool; + mCallback = callback; + mUserData = userData; + } + void notify(SoundPoolEvent event) const + { + std::lock_guard 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 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 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 mDecoder; // has its own lock + + mutable std::mutex mSoundManagerLock; + std::unordered_map> mSounds; // GUARDED_BY(mSoundManagerLock) + int32_t mNextSoundID = 0; // GUARDED_BY(mSoundManagerLock) +}; + +} // namespace android::soundpool diff --git a/media/jni/soundpool/SoundPool.cpp b/media/jni/soundpool/SoundPool.cpp index 102bbf0e59316..ac44843859f6d 100644 --- a/media/jni/soundpool/SoundPool.cpp +++ b/media/jni/soundpool/SoundPool.cpp @@ -16,1124 +16,230 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "SoundPool" - -#include -#include -#include #include -#define USE_SHARED_MEM_BUFFER +#include +#include -#include #include "SoundPool.h" -#include "SoundPoolThread.h" -#include -#include -#include namespace android { -int kDefaultBufferCount = 4; -uint32_t kMaxSampleRate = 48000; -uint32_t kDefaultSampleRate = 44100; -uint32_t kDefaultFrameCount = 1200; -size_t kDefaultHeapSize = 1024 * 1024; // 1MB +// kManagerThreads = 1 historically. +// Not really necessary to have more than one, but it does speed things up by about +// 25% having 2 threads instead of 1 when playing many sounds. Having many threads +// could starve other AudioFlinger clients with SoundPool activity. It may also cause +// issues with app loading, e.g. Camera. +static const size_t kStreamManagerThreads = std::thread::hardware_concurrency() >= 4 ? 2 : 1; +// kUseApiLock = true prior to R. +// Set to true to prevent multiple users access internal to the SoundPool API. +// Set to false to make the SoundPool methods weakly consistent. When set to false, +// only AutoPause and AutoResume are locked, which are the only two methods that +// require API level locking for consistency. +static constexpr bool kUseApiLock = false; -SoundPool::SoundPool(int maxChannels, const audio_attributes_t* pAttributes) +namespace { +// Check input arguments to SoundPool - return "true" to reject request. + +bool checkVolume(float *leftVolume, float *rightVolume) { - ALOGV("SoundPool constructor: maxChannels=%d, attr.usage=%d, attr.flags=0x%x, attr.tags=%s", - maxChannels, pAttributes->usage, pAttributes->flags, pAttributes->tags); - - // check limits - mMaxChannels = maxChannels; - if (mMaxChannels < 1) { - mMaxChannels = 1; + if (*leftVolume != std::clamp(*leftVolume, 0.f, 1.f) || + *rightVolume != std::clamp(*rightVolume, 0.f, 1.f)) { + ALOGI("volume l=%f r=%f out of (0.f, 1.f) bounds, using 1.f", *leftVolume, *rightVolume); + // for backward compatibility use 1.f. + *leftVolume = *rightVolume = 1.f; } - else if (mMaxChannels > 32) { - mMaxChannels = 32; + return false; +} + +bool checkRate(float *rate) +{ + if (*rate != std::clamp(*rate, 0.125f, 8.f)) { + ALOGI("rate %f out of (0.125f, 8.f) bounds, clamping", *rate); + // for backward compatibility just clamp + *rate = std::clamp(*rate, 0.125f, 8.f); } - ALOGW_IF(maxChannels != mMaxChannels, "App requested %d channels", maxChannels); + return false; +} - mQuit = false; - mMuted = false; - mDecodeThread = 0; - memcpy(&mAttributes, pAttributes, sizeof(audio_attributes_t)); - mAllocated = 0; - mNextSampleID = 0; - mNextChannelID = 0; - - mCallback = 0; - mUserData = 0; - - mChannelPool = new SoundChannel[mMaxChannels]; - for (int i = 0; i < mMaxChannels; ++i) { - mChannelPool[i].init(this); - mChannels.push_back(&mChannelPool[i]); +bool checkPriority(int32_t *priority) +{ + if (*priority < 0) { + ALOGI("negative priority %d, should be >= 0.", *priority); + // for backward compatibility, ignore. } + return false; +} - // start decode thread - startThreads(); +bool checkLoop(int32_t *loop) +{ + if (*loop < -1) { + ALOGI("loop %d, should be >= -1", *loop); + *loop = -1; + } + return false; +} + +} // namespace + +SoundPool::SoundPool(int32_t maxStreams, const audio_attributes_t* attributes) + : mStreamManager(maxStreams, kStreamManagerThreads, attributes) +{ + ALOGV("%s(maxStreams=%d, attr={ content_type=%d, usage=%d, flags=0x%x, tags=%s })", + __func__, maxStreams, + attributes->content_type, attributes->usage, attributes->flags, attributes->tags); } SoundPool::~SoundPool() { - ALOGV("SoundPool destructor"); - mDecodeThread->quit(); - quit(); - - Mutex::Autolock lock(&mLock); - - mChannels.clear(); - if (mChannelPool) - delete [] mChannelPool; - // clean up samples - ALOGV("clear samples"); - mSamples.clear(); - - if (mDecodeThread) - delete mDecodeThread; + ALOGV("%s()", __func__); } -void SoundPool::addToRestartList(SoundChannel* channel) +int32_t SoundPool::load(int fd, int64_t offset, int64_t length, int32_t priority) { - Mutex::Autolock lock(&mRestartLock); - if (!mQuit) { - mRestart.push_back(channel); - mCondition.signal(); - } + ALOGV("%s(fd=%d, offset=%lld, length=%lld, priority=%d)", + __func__, fd, (long long)offset, (long long)length, priority); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + return mSoundManager.load(fd, offset, length, priority); } -void SoundPool::addToStopList(SoundChannel* channel) +bool SoundPool::unload(int32_t soundID) { - Mutex::Autolock lock(&mRestartLock); - if (!mQuit) { - mStop.push_back(channel); - mCondition.signal(); - } + ALOGV("%s(%d)", __func__, soundID); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + return mSoundManager.unload(soundID); } -int SoundPool::beginThread(void* arg) +int32_t SoundPool::play(int32_t soundID, float leftVolume, float rightVolume, + int32_t priority, int32_t loop, float rate) { - SoundPool* p = (SoundPool*)arg; - return p->run(); -} + ALOGV("%s(soundID=%d, leftVolume=%f, rightVolume=%f, priority=%d, loop=%d, rate=%f)", + __func__, soundID, leftVolume, rightVolume, priority, loop, rate); -int SoundPool::run() -{ - mRestartLock.lock(); - while (!mQuit) { - mCondition.wait(mRestartLock); - ALOGV("awake"); - if (mQuit) break; + // New for R: check arguments to ensure track can be created. + // If SoundPool defers the creation of the AudioTrack to the StreamManager thread, + // the failure to create may not be visible to the caller, so this precheck is needed. + if (checkVolume(&leftVolume, &rightVolume) + || checkPriority(&priority) + || checkLoop(&loop) + || checkRate(&rate)) return 0; - while (!mStop.empty()) { - SoundChannel* channel; - ALOGV("Getting channel from stop list"); - List::iterator iter = mStop.begin(); - channel = *iter; - mStop.erase(iter); - mRestartLock.unlock(); - if (channel != 0) { - Mutex::Autolock lock(&mLock); - channel->stop(); - } - mRestartLock.lock(); - if (mQuit) break; - } - - while (!mRestart.empty()) { - SoundChannel* channel; - ALOGV("Getting channel from list"); - List::iterator iter = mRestart.begin(); - channel = *iter; - mRestart.erase(iter); - mRestartLock.unlock(); - if (channel != 0) { - Mutex::Autolock lock(&mLock); - channel->nextEvent(); - } - mRestartLock.lock(); - if (mQuit) break; - } - } - - mStop.clear(); - mRestart.clear(); - mCondition.signal(); - mRestartLock.unlock(); - ALOGV("goodbye"); - return 0; -} - -void SoundPool::quit() -{ - mRestartLock.lock(); - mQuit = true; - mCondition.signal(); - mCondition.wait(mRestartLock); - ALOGV("return from quit"); - mRestartLock.unlock(); -} - -bool SoundPool::startThreads() -{ - createThreadEtc(beginThread, this, "SoundPool"); - if (mDecodeThread == NULL) - mDecodeThread = new SoundPoolThread(this); - return mDecodeThread != NULL; -} - -sp SoundPool::findSample(int sampleID) -{ - Mutex::Autolock lock(&mLock); - return findSample_l(sampleID); -} - -sp SoundPool::findSample_l(int sampleID) -{ - return mSamples.valueFor(sampleID); -} - -SoundChannel* SoundPool::findChannel(int channelID) -{ - for (int i = 0; i < mMaxChannels; ++i) { - if (mChannelPool[i].channelID() == channelID) { - return &mChannelPool[i]; - } - } - return NULL; -} - -SoundChannel* SoundPool::findNextChannel(int channelID) -{ - for (int i = 0; i < mMaxChannels; ++i) { - if (mChannelPool[i].nextChannelID() == channelID) { - return &mChannelPool[i]; - } - } - return NULL; -} - -int SoundPool::load(int fd, int64_t offset, int64_t length, int priority __unused) -{ - ALOGV("load: fd=%d, offset=%" PRId64 ", length=%" PRId64 ", priority=%d", - fd, offset, length, priority); - int sampleID; - { - Mutex::Autolock lock(&mLock); - sampleID = ++mNextSampleID; - sp sample = new Sample(sampleID, fd, offset, length); - mSamples.add(sampleID, sample); - sample->startLoad(); - } - // mDecodeThread->loadSample() must be called outside of mLock. - // mDecodeThread->loadSample() may block on mDecodeThread message queue space; - // the message queue emptying may block on SoundPool::findSample(). - // - // It theoretically possible that sample loads might decode out-of-order. - mDecodeThread->loadSample(sampleID); - return sampleID; -} - -bool SoundPool::unload(int sampleID) -{ - ALOGV("unload: sampleID=%d", sampleID); - Mutex::Autolock lock(&mLock); - return mSamples.removeItem(sampleID) >= 0; // removeItem() returns index or BAD_VALUE -} - -int SoundPool::play(int sampleID, float leftVolume, float rightVolume, - int priority, int loop, float rate) -{ - ALOGV("play sampleID=%d, leftVolume=%f, rightVolume=%f, priority=%d, loop=%d, rate=%f", - sampleID, leftVolume, rightVolume, priority, loop, rate); - SoundChannel* channel; - int channelID; - - Mutex::Autolock lock(&mLock); - - if (mQuit) { - return 0; - } - // is sample ready? - sp sample(findSample_l(sampleID)); - if ((sample == 0) || (sample->state() != Sample::READY)) { - ALOGW(" sample %d not READY", sampleID); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + const std::shared_ptr sound = mSoundManager.findSound(soundID); + if (sound == nullptr || sound->getState() != soundpool::Sound::READY) { + ALOGW("%s soundID %d not READY", __func__, soundID); return 0; } - dump(); - - // allocate a channel - channel = allocateChannel_l(priority, sampleID); - - // no channel allocated - return 0 - if (!channel) { - ALOGV("No channel allocated"); - return 0; - } - - channelID = ++mNextChannelID; - - ALOGV("play channel %p state = %d", channel, channel->state()); - channel->play(sample, channelID, leftVolume, rightVolume, priority, loop, rate); - return channelID; -} - -SoundChannel* SoundPool::allocateChannel_l(int priority, int sampleID) -{ - List::iterator iter; - SoundChannel* channel = NULL; - - // check if channel for given sampleID still available - if (!mChannels.empty()) { - for (iter = mChannels.begin(); iter != mChannels.end(); ++iter) { - if (sampleID == (*iter)->getPrevSampleID() && (*iter)->state() == SoundChannel::IDLE) { - channel = *iter; - mChannels.erase(iter); - ALOGV("Allocated recycled channel for same sampleID"); - break; - } - } - } - - // allocate any channel - if (!channel && !mChannels.empty()) { - iter = mChannels.begin(); - if (priority >= (*iter)->priority()) { - channel = *iter; - mChannels.erase(iter); - ALOGV("Allocated active channel"); - } - } - - // update priority and put it back in the list - if (channel) { - channel->setPriority(priority); - for (iter = mChannels.begin(); iter != mChannels.end(); ++iter) { - if (priority < (*iter)->priority()) { - break; - } - } - mChannels.insert(iter, channel); - } - return channel; -} - -// move a channel from its current position to the front of the list -void SoundPool::moveToFront_l(SoundChannel* channel) -{ - for (List::iterator iter = mChannels.begin(); iter != mChannels.end(); ++iter) { - if (*iter == channel) { - mChannels.erase(iter); - mChannels.push_front(channel); - break; - } - } -} - -void SoundPool::pause(int channelID) -{ - ALOGV("pause(%d)", channelID); - Mutex::Autolock lock(&mLock); - SoundChannel* channel = findChannel(channelID); - if (channel) { - channel->pause(); - } + const int32_t streamID = mStreamManager.queueForPlay( + sound, soundID, leftVolume, rightVolume, priority, loop, rate); + ALOGV("%s returned %d", __func__, streamID); + return streamID; } void SoundPool::autoPause() { - ALOGV("autoPause()"); - Mutex::Autolock lock(&mLock); - for (int i = 0; i < mMaxChannels; ++i) { - SoundChannel* channel = &mChannelPool[i]; - channel->autoPause(); - } -} - -void SoundPool::resume(int channelID) -{ - ALOGV("resume(%d)", channelID); - Mutex::Autolock lock(&mLock); - SoundChannel* channel = findChannel(channelID); - if (channel) { - channel->resume(); - } -} - -void SoundPool::mute(bool muting) -{ - ALOGV("mute(%d)", muting); - Mutex::Autolock lock(&mLock); - mMuted = muting; - if (!mChannels.empty()) { - for (List::iterator iter = mChannels.begin(); - iter != mChannels.end(); ++iter) { - (*iter)->mute(muting); - } - } + ALOGV("%s()", __func__); + auto apiLock = std::make_unique>(mApiLock); + mStreamManager.forEach([](soundpool::Stream *stream) { stream->autoPause(); }); } void SoundPool::autoResume() { - ALOGV("autoResume()"); - Mutex::Autolock lock(&mLock); - for (int i = 0; i < mMaxChannels; ++i) { - SoundChannel* channel = &mChannelPool[i]; - channel->autoResume(); + ALOGV("%s()", __func__); + auto apiLock = std::make_unique>(mApiLock); + mStreamManager.forEach([](soundpool::Stream *stream) { stream->autoResume(); }); +} + +void SoundPool::mute(bool muting) +{ + ALOGV("%s(%d)", __func__, muting); + auto apiLock = std::make_unique>(mApiLock); + mStreamManager.forEach([=](soundpool::Stream *stream) { stream->mute(muting); }); +} + +void SoundPool::pause(int32_t streamID) +{ + ALOGV("%s(%d)", __func__, streamID); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + if (soundpool::Stream* stream = mStreamManager.findStream(streamID)) { + stream->pause(streamID); } } -void SoundPool::stop(int channelID) +void SoundPool::resume(int32_t streamID) { - ALOGV("stop(%d)", channelID); - Mutex::Autolock lock(&mLock); - SoundChannel* channel = findChannel(channelID); - if (channel) { - channel->stop(); - } else { - channel = findNextChannel(channelID); - if (channel) - channel->clearNextEvent(); + ALOGV("%s(%d)", __func__, streamID); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + if (soundpool::Stream* stream = mStreamManager.findStream(streamID)) { + stream->resume(streamID); } } -void SoundPool::setVolume(int channelID, float leftVolume, float rightVolume) +void SoundPool::stop(int32_t streamID) { - Mutex::Autolock lock(&mLock); - SoundChannel* channel = findChannel(channelID); - if (channel) { - channel->setVolume(leftVolume, rightVolume); + ALOGV("%s(%d)", __func__, streamID); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + soundpool::Stream* stream = mStreamManager.findStream(streamID); + if (stream != nullptr && stream->requestStop(streamID)) { + mStreamManager.moveToRestartQueue(stream); } } -void SoundPool::setPriority(int channelID, int priority) +void SoundPool::setVolume(int32_t streamID, float leftVolume, float rightVolume) { - ALOGV("setPriority(%d, %d)", channelID, priority); - Mutex::Autolock lock(&mLock); - SoundChannel* channel = findChannel(channelID); - if (channel) { - channel->setPriority(priority); + ALOGV("%s(%d, %f %f)", __func__, streamID, leftVolume, rightVolume); + if (checkVolume(&leftVolume, &rightVolume)) return; + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + if (soundpool::Stream* stream = mStreamManager.findStream(streamID)) { + stream->setVolume(streamID, leftVolume, rightVolume); } } -void SoundPool::setLoop(int channelID, int loop) +void SoundPool::setPriority(int32_t streamID, int32_t priority) { - ALOGV("setLoop(%d, %d)", channelID, loop); - Mutex::Autolock lock(&mLock); - SoundChannel* channel = findChannel(channelID); - if (channel) { - channel->setLoop(loop); + ALOGV("%s(%d, %d)", __func__, streamID, priority); + if (checkPriority(&priority)) return; + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + if (soundpool::Stream* stream = mStreamManager.findStream(streamID)) { + stream->setPriority(streamID, priority); } } -void SoundPool::setRate(int channelID, float rate) +void SoundPool::setLoop(int32_t streamID, int32_t loop) { - ALOGV("setRate(%d, %f)", channelID, rate); - Mutex::Autolock lock(&mLock); - SoundChannel* channel = findChannel(channelID); - if (channel) { - channel->setRate(rate); + ALOGV("%s(%d, %d)", __func__, streamID, loop); + if (checkLoop(&loop)) return; + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + if (soundpool::Stream* stream = mStreamManager.findStream(streamID)) { + stream->setLoop(streamID, loop); } } -// call with lock held -void SoundPool::done_l(SoundChannel* channel) +void SoundPool::setRate(int32_t streamID, float rate) { - ALOGV("done_l(%d)", channel->channelID()); - // if "stolen", play next event - if (channel->nextChannelID() != 0) { - ALOGV("add to restart list"); - addToRestartList(channel); - } - - // return to idle state - else { - ALOGV("move to front"); - moveToFront_l(channel); + ALOGV("%s(%d, %f)", __func__, streamID, rate); + if (checkRate(&rate)) return; + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + if (soundpool::Stream* stream = mStreamManager.findStream(streamID)) { + stream->setRate(streamID, rate); } } void SoundPool::setCallback(SoundPoolCallback* callback, void* user) { - Mutex::Autolock lock(&mCallbackLock); - mCallback = callback; - mUserData = user; + ALOGV("%s(%p, %p)", __func__, callback, user); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + mSoundManager.setCallback(this, callback, user); } -void SoundPool::notify(SoundPoolEvent event) +void* SoundPool::getUserData() const { - Mutex::Autolock lock(&mCallbackLock); - if (mCallback != NULL) { - mCallback(event, this, mUserData); - } -} - -void SoundPool::dump() -{ - for (int i = 0; i < mMaxChannels; ++i) { - mChannelPool[i].dump(); - } -} - - -Sample::Sample(int sampleID, int fd, int64_t offset, int64_t length) -{ - init(); - mSampleID = sampleID; - mFd = dup(fd); - mOffset = offset; - mLength = length; - ALOGV("create sampleID=%d, fd=%d, offset=%" PRId64 " length=%" PRId64, - mSampleID, mFd, mLength, mOffset); -} - -void Sample::init() -{ - mSize = 0; - mRefCount = 0; - mSampleID = 0; - mState = UNLOADED; - mFd = -1; - mOffset = 0; - mLength = 0; -} - -Sample::~Sample() -{ - ALOGV("Sample::destructor sampleID=%d, fd=%d", mSampleID, mFd); - if (mFd > 0) { - ALOGV("close(%d)", mFd); - ::close(mFd); - } -} - -static status_t decode(int fd, int64_t offset, int64_t length, - uint32_t *rate, int *numChannels, audio_format_t *audioFormat, - audio_channel_mask_t *channelMask, sp heap, - size_t *memsize) { - - ALOGV("fd %d, offset %" PRId64 ", size %" PRId64, fd, offset, length); - AMediaExtractor *ex = AMediaExtractor_new(); - status_t err = AMediaExtractor_setDataSourceFd(ex, fd, offset, length); - - if (err != AMEDIA_OK) { - AMediaExtractor_delete(ex); - return err; - } - - *audioFormat = AUDIO_FORMAT_PCM_16_BIT; - - size_t numTracks = AMediaExtractor_getTrackCount(ex); - for (size_t i = 0; i < numTracks; i++) { - AMediaFormat *format = AMediaExtractor_getTrackFormat(ex, i); - const char *mime; - if (!AMediaFormat_getString(format, AMEDIAFORMAT_KEY_MIME, &mime)) { - AMediaExtractor_delete(ex); - AMediaFormat_delete(format); - return UNKNOWN_ERROR; - } - if (strncmp(mime, "audio/", 6) == 0) { - - AMediaCodec *codec = AMediaCodec_createDecoderByType(mime); - if (codec == NULL - || AMediaCodec_configure(codec, format, - NULL /* window */, NULL /* drm */, 0 /* flags */) != AMEDIA_OK - || AMediaCodec_start(codec) != AMEDIA_OK - || AMediaExtractor_selectTrack(ex, i) != AMEDIA_OK) { - AMediaExtractor_delete(ex); - AMediaCodec_delete(codec); - AMediaFormat_delete(format); - return UNKNOWN_ERROR; - } - - bool sawInputEOS = false; - bool sawOutputEOS = false; - uint8_t* writePos = static_cast(heap->getBase()); - size_t available = heap->getSize(); - size_t written = 0; - - AMediaFormat_delete(format); - format = AMediaCodec_getOutputFormat(codec); - - while (!sawOutputEOS) { - if (!sawInputEOS) { - ssize_t bufidx = AMediaCodec_dequeueInputBuffer(codec, 5000); - ALOGV("input buffer %zd", bufidx); - if (bufidx >= 0) { - size_t bufsize; - uint8_t *buf = AMediaCodec_getInputBuffer(codec, bufidx, &bufsize); - if (buf == nullptr) { - ALOGE("AMediaCodec_getInputBuffer returned nullptr, short decode"); - break; - } - int sampleSize = AMediaExtractor_readSampleData(ex, buf, bufsize); - ALOGV("read %d", sampleSize); - if (sampleSize < 0) { - sampleSize = 0; - sawInputEOS = true; - ALOGV("EOS"); - } - int64_t presentationTimeUs = AMediaExtractor_getSampleTime(ex); - - media_status_t mstatus = AMediaCodec_queueInputBuffer(codec, bufidx, - 0 /* offset */, sampleSize, presentationTimeUs, - sawInputEOS ? AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM : 0); - if (mstatus != AMEDIA_OK) { - // AMEDIA_ERROR_UNKNOWN == { -ERANGE -EINVAL -EACCES } - ALOGE("AMediaCodec_queueInputBuffer returned status %d, short decode", - (int)mstatus); - break; - } - (void)AMediaExtractor_advance(ex); - } - } - - AMediaCodecBufferInfo info; - int status = AMediaCodec_dequeueOutputBuffer(codec, &info, 1); - ALOGV("dequeueoutput returned: %d", status); - if (status >= 0) { - if (info.flags & AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM) { - ALOGV("output EOS"); - sawOutputEOS = true; - } - ALOGV("got decoded buffer size %d", info.size); - - uint8_t *buf = AMediaCodec_getOutputBuffer(codec, status, NULL /* out_size */); - if (buf == nullptr) { - ALOGE("AMediaCodec_getOutputBuffer returned nullptr, short decode"); - break; - } - size_t dataSize = info.size; - if (dataSize > available) { - dataSize = available; - } - memcpy(writePos, buf + info.offset, dataSize); - writePos += dataSize; - written += dataSize; - available -= dataSize; - media_status_t mstatus = AMediaCodec_releaseOutputBuffer( - codec, status, false /* render */); - if (mstatus != AMEDIA_OK) { - // AMEDIA_ERROR_UNKNOWN == { -ERANGE -EINVAL -EACCES } - ALOGE("AMediaCodec_releaseOutputBuffer returned status %d, short decode", - (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("output buffers changed"); - } else if (status == AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED) { - AMediaFormat_delete(format); - format = AMediaCodec_getOutputFormat(codec); - ALOGV("format changed to: %s", AMediaFormat_toString(format)); - } else if (status == AMEDIACODEC_INFO_TRY_AGAIN_LATER) { - ALOGV("no output buffer right now"); - } else if (status <= AMEDIA_ERROR_BASE) { - ALOGE("decode error: %d", status); - break; - } else { - ALOGV("unexpected info code: %d", status); - } - } - - (void)AMediaCodec_stop(codec); - (void)AMediaCodec_delete(codec); - (void)AMediaExtractor_delete(ex); - if (!AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, (int32_t*) rate) || - !AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, numChannels)) { - (void)AMediaFormat_delete(format); - return UNKNOWN_ERROR; - } - if (!AMediaFormat_getInt32(format, AMEDIAFORMAT_KEY_CHANNEL_MASK, - (int32_t*) channelMask)) { - *channelMask = AUDIO_CHANNEL_NONE; - } - (void)AMediaFormat_delete(format); - *memsize = written; - return OK; - } - (void)AMediaFormat_delete(format); - } - (void)AMediaExtractor_delete(ex); - return UNKNOWN_ERROR; -} - -status_t Sample::doLoad() -{ - uint32_t sampleRate; - int numChannels; - audio_format_t format; - audio_channel_mask_t channelMask; - status_t status; - mHeap = new MemoryHeapBase(kDefaultHeapSize); - - ALOGV("Start decode"); - status = decode(mFd, mOffset, mLength, &sampleRate, &numChannels, &format, - &channelMask, mHeap, &mSize); - ALOGV("close(%d)", mFd); - ::close(mFd); - mFd = -1; - if (status != NO_ERROR) { - ALOGE("Unable to load sample"); - goto error; - } - ALOGV("pointer = %p, size = %zu, sampleRate = %u, numChannels = %d", - mHeap->getBase(), mSize, sampleRate, numChannels); - - if (sampleRate > kMaxSampleRate) { - ALOGE("Sample rate (%u) out of range", sampleRate); - status = BAD_VALUE; - goto error; - } - - if ((numChannels < 1) || (numChannels > FCC_8)) { - ALOGE("Sample channel count (%d) out of range", numChannels); - status = BAD_VALUE; - goto error; - } - - mData = new MemoryBase(mHeap, 0, mSize); - mSampleRate = sampleRate; - mNumChannels = numChannels; - mFormat = format; - mChannelMask = channelMask; - mState = READY; - return NO_ERROR; - -error: - mHeap.clear(); - return status; -} - - -void SoundChannel::init(SoundPool* soundPool) -{ - mSoundPool = soundPool; - mPrevSampleID = -1; -} - -// call with sound pool lock held -void SoundChannel::play(const sp& sample, int nextChannelID, float leftVolume, - float rightVolume, int priority, int loop, float rate) -{ - sp oldTrack; - sp newTrack; - status_t status = NO_ERROR; - - { // scope for the lock - Mutex::Autolock lock(&mLock); - - ALOGV("SoundChannel::play %p: sampleID=%d, channelID=%d, leftVolume=%f, rightVolume=%f," - " priority=%d, loop=%d, rate=%f", - this, sample->sampleID(), nextChannelID, leftVolume, rightVolume, - priority, loop, rate); - - // if not idle, this voice is being stolen - if (mState != IDLE) { - ALOGV("channel %d stolen - event queued for channel %d", channelID(), nextChannelID); - mNextEvent.set(sample, nextChannelID, leftVolume, rightVolume, priority, loop, rate); - stop_l(); - return; - } - - // initialize track - size_t afFrameCount; - uint32_t afSampleRate; - audio_stream_type_t streamType = - AudioSystem::attributesToStreamType(*mSoundPool->attributes()); - if (AudioSystem::getOutputFrameCount(&afFrameCount, streamType) != NO_ERROR) { - afFrameCount = kDefaultFrameCount; - } - if (AudioSystem::getOutputSamplingRate(&afSampleRate, streamType) != NO_ERROR) { - afSampleRate = kDefaultSampleRate; - } - int numChannels = sample->numChannels(); - uint32_t sampleRate = uint32_t(float(sample->sampleRate()) * rate + 0.5); - size_t frameCount = 0; - - if (loop) { - const audio_format_t format = sample->format(); - const size_t frameSize = audio_is_linear_pcm(format) - ? numChannels * audio_bytes_per_sample(format) : 1; - frameCount = sample->size() / frameSize; - } - -#ifndef USE_SHARED_MEM_BUFFER - uint32_t totalFrames = (kDefaultBufferCount * afFrameCount * sampleRate) / afSampleRate; - // Ensure minimum audio buffer size in case of short looped sample - if(frameCount < totalFrames) { - frameCount = totalFrames; - } -#endif - - // check if the existing track has the same sample id. - if (mAudioTrack != 0 && mPrevSampleID == sample->sampleID()) { - // the sample rate may fail to change if the audio track is a fast track. - if (mAudioTrack->setSampleRate(sampleRate) == NO_ERROR) { - newTrack = mAudioTrack; - ALOGV("reusing track %p for sample %d", mAudioTrack.get(), sample->sampleID()); - } - } - if (newTrack == 0) { - // mToggle toggles each time a track is started on a given channel. - // The toggle is concatenated with the SoundChannel 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) - unsigned long toggle = mToggle ^ 1; - void *userData = (void *)((unsigned long)this | toggle); - audio_channel_mask_t sampleChannelMask = sample->channelMask(); - // When sample contains a not none channel mask, use it as is. - // Otherwise, use channel count to calculate channel mask. - audio_channel_mask_t channelMask = sampleChannelMask != AUDIO_CHANNEL_NONE - ? sampleChannelMask : audio_channel_out_mask_from_count(numChannels); - - // do not create a new audio track if current track is compatible with sample parameters - #ifdef USE_SHARED_MEM_BUFFER - newTrack = new AudioTrack(streamType, sampleRate, sample->format(), - channelMask, sample->getIMemory(), AUDIO_OUTPUT_FLAG_FAST, callback, userData, - 0 /*default notification frames*/, AUDIO_SESSION_ALLOCATE, - AudioTrack::TRANSFER_DEFAULT, - NULL /*offloadInfo*/, -1 /*uid*/, -1 /*pid*/, mSoundPool->attributes()); - #else - uint32_t bufferFrames = (totalFrames + (kDefaultBufferCount - 1)) / kDefaultBufferCount; - newTrack = new AudioTrack(streamType, sampleRate, sample->format(), - channelMask, frameCount, AUDIO_OUTPUT_FLAG_FAST, callback, userData, - bufferFrames, AUDIO_SESSION_ALLOCATE, AudioTrack::TRANSFER_DEFAULT, - NULL /*offloadInfo*/, -1 /*uid*/, -1 /*pid*/, mSoundPool->attributes()); - #endif - oldTrack = mAudioTrack; - status = newTrack->initCheck(); - if (status != NO_ERROR) { - ALOGE("Error creating AudioTrack"); - // 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("using new track %p for sample %d", newTrack.get(), sample->sampleID()); - } - if (mMuted) { - newTrack->setVolume(0.0f, 0.0f); - } else { - newTrack->setVolume(leftVolume, rightVolume); - } - newTrack->setLoop(0, frameCount, loop); - mPos = 0; - mSample = sample; - mChannelID = nextChannelID; - mPriority = priority; - mLoop = loop; - mLeftVolume = leftVolume; - mRightVolume = rightVolume; - mNumChannels = numChannels; - mRate = rate; - clearNextEvent(); - mState = PLAYING; - mAudioTrack->start(); - mAudioBufferSize = newTrack->frameCount()*newTrack->frameSize(); - } - -exit: - ALOGV("delete oldTrack %p", oldTrack.get()); - if (status != NO_ERROR) { - mAudioTrack.clear(); - } -} - -void SoundChannel::nextEvent() -{ - sp sample; - int nextChannelID; - float leftVolume; - float rightVolume; - int priority; - int loop; - float rate; - - // check for valid event - { - Mutex::Autolock lock(&mLock); - nextChannelID = mNextEvent.channelID(); - if (nextChannelID == 0) { - ALOGV("stolen channel has no event"); - return; - } - - sample = mNextEvent.sample(); - leftVolume = mNextEvent.leftVolume(); - rightVolume = mNextEvent.rightVolume(); - priority = mNextEvent.priority(); - loop = mNextEvent.loop(); - rate = mNextEvent.rate(); - } - - ALOGV("Starting stolen channel %d -> %d", channelID(), nextChannelID); - play(sample, nextChannelID, leftVolume, rightVolume, priority, loop, rate); -} - -void SoundChannel::callback(int event, void* user, void *info) -{ - SoundChannel* channel = static_cast((void *)((unsigned long)user & ~1)); - - channel->process(event, info, (unsigned long)user & 1); -} - -void SoundChannel::process(int event, void *info, unsigned long toggle) -{ - //ALOGV("process(%d)", mChannelID); - - Mutex::Autolock lock(&mLock); - - AudioTrack::Buffer* b = NULL; - if (event == AudioTrack::EVENT_MORE_DATA) { - b = static_cast(info); - } - - if (mToggle != toggle) { - ALOGV("process wrong toggle %p channel %d", this, mChannelID); - if (b != NULL) { - b->size = 0; - } - return; - } - - sp sample = mSample; - -// ALOGV("SoundChannel::process event %d", event); - - if (event == AudioTrack::EVENT_MORE_DATA) { - - // check for stop state - if (b->size == 0) return; - - if (mState == IDLE) { - b->size = 0; - return; - } - - if (sample != 0) { - // fill buffer - uint8_t* q = (uint8_t*) b->i8; - size_t count = 0; - - if (mPos < (int)sample->size()) { - uint8_t* p = sample->data() + mPos; - count = sample->size() - mPos; - if (count > b->size) { - count = b->size; - } - memcpy(q, p, count); -// ALOGV("fill: q=%p, p=%p, mPos=%u, b->size=%u, count=%d", q, p, mPos, b->size, -// count); - } else if (mPos < mAudioBufferSize) { - count = mAudioBufferSize - mPos; - if (count > b->size) { - count = b->size; - } - memset(q, 0, count); -// ALOGV("fill extra: q=%p, mPos=%u, b->size=%u, count=%d", q, mPos, b->size, count); - } - - mPos += count; - b->size = count; - //ALOGV("buffer=%p, [0]=%d", b->i16, b->i16[0]); - } - } else if (event == AudioTrack::EVENT_UNDERRUN || event == AudioTrack::EVENT_BUFFER_END) { - ALOGV("process %p channel %d event %s", - this, mChannelID, (event == AudioTrack::EVENT_UNDERRUN) ? "UNDERRUN" : - "BUFFER_END"); - // Only BUFFER_END should happen as we use static tracks. - setVolume_l(0.f, 0.f); // set volume to 0 to indicate no need to ramp volume down. - mSoundPool->addToStopList(this); - } else if (event == AudioTrack::EVENT_LOOP_END) { - ALOGV("End loop %p channel %d", this, mChannelID); - } else if (event == AudioTrack::EVENT_NEW_IAUDIOTRACK) { - ALOGV("process %p channel %d NEW_IAUDIOTRACK", this, mChannelID); - } else { - ALOGW("SoundChannel::process unexpected event %d", event); - } -} - - -// call with lock held -bool SoundChannel::doStop_l() -{ - if (mState != IDLE) { - ALOGV("stop"); - if (mLeftVolume != 0.f || mRightVolume != 0.f) { - setVolume_l(0.f, 0.f); - if (mSoundPool->attributes()->usage != AUDIO_USAGE_GAME) { - // Since we're forcibly halting the previously playing content, - // we sleep here to ensure the volume is ramped down before we stop the track. - // Ideally the sleep time is the mixer period, or an approximation thereof - // (Fast vs Normal tracks are different). - ALOGV("sleeping: ChannelID:%d SampleID:%d", mChannelID, mSample->sampleID()); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - } - } - mAudioTrack->stop(); - mPrevSampleID = mSample->sampleID(); - mSample.clear(); - mState = IDLE; - mPriority = IDLE_PRIORITY; - return true; - } - return false; -} - -// call with lock held and sound pool lock held -void SoundChannel::stop_l() -{ - if (doStop_l()) { - mSoundPool->done_l(this); - } -} - -// call with sound pool lock held -void SoundChannel::stop() -{ - bool stopped; - { - Mutex::Autolock lock(&mLock); - stopped = doStop_l(); - } - - if (stopped) { - mSoundPool->done_l(this); - } -} - -//FIXME: Pause is a little broken right now -void SoundChannel::pause() -{ - Mutex::Autolock lock(&mLock); - if (mState == PLAYING) { - ALOGV("pause track"); - mState = PAUSED; - mAudioTrack->pause(); - } -} - -void SoundChannel::autoPause() -{ - Mutex::Autolock lock(&mLock); - if (mState == PLAYING) { - ALOGV("pause track"); - mState = PAUSED; - mAutoPaused = true; - mAudioTrack->pause(); - } -} - -void SoundChannel::resume() -{ - Mutex::Autolock lock(&mLock); - if (mState == PAUSED) { - ALOGV("resume track"); - mState = PLAYING; - mAutoPaused = false; - mAudioTrack->start(); - } -} - -void SoundChannel::autoResume() -{ - Mutex::Autolock lock(&mLock); - if (mAutoPaused && (mState == PAUSED)) { - ALOGV("resume track"); - mState = PLAYING; - mAutoPaused = false; - mAudioTrack->start(); - } -} - -void SoundChannel::setRate(float rate) -{ - Mutex::Autolock lock(&mLock); - if (mAudioTrack != NULL && mSample != 0) { - uint32_t sampleRate = uint32_t(float(mSample->sampleRate()) * rate + 0.5); - mAudioTrack->setSampleRate(sampleRate); - mRate = rate; - } -} - -// call with lock held -void SoundChannel::setVolume_l(float leftVolume, float rightVolume) -{ - mLeftVolume = leftVolume; - mRightVolume = rightVolume; - if (mAudioTrack != NULL && !mMuted) - mAudioTrack->setVolume(leftVolume, rightVolume); -} - -void SoundChannel::setVolume(float leftVolume, float rightVolume) -{ - Mutex::Autolock lock(&mLock); - setVolume_l(leftVolume, rightVolume); -} - -void SoundChannel::mute(bool muting) -{ - Mutex::Autolock lock(&mLock); - mMuted = muting; - if (mAudioTrack != NULL) { - if (mMuted) { - mAudioTrack->setVolume(0.0f, 0.0f); - } else { - mAudioTrack->setVolume(mLeftVolume, mRightVolume); - } - } -} - -void SoundChannel::setLoop(int loop) -{ - Mutex::Autolock lock(&mLock); - if (mAudioTrack != NULL && mSample != 0) { - uint32_t loopEnd = mSample->size()/mNumChannels/ - ((mSample->format() == AUDIO_FORMAT_PCM_16_BIT) ? sizeof(int16_t) : sizeof(uint8_t)); - mAudioTrack->setLoop(0, loopEnd, loop); - mLoop = loop; - } -} - -SoundChannel::~SoundChannel() -{ - ALOGV("SoundChannel destructor %p", this); - { - Mutex::Autolock lock(&mLock); - clearNextEvent(); - doStop_l(); - } - // do not call AudioTrack destructor with mLock held as it will wait for the AudioTrack - // callback thread to exit which may need to execute process() and acquire the mLock. - mAudioTrack.clear(); -} - -void SoundChannel::dump() -{ - ALOGV("mState = %d mChannelID=%d, mNumChannels=%d, mPos = %d, mPriority=%d, mLoop=%d", - mState, mChannelID, mNumChannels, mPos, mPriority, mLoop); -} - -void SoundEvent::set(const sp& sample, int channelID, float leftVolume, - float rightVolume, int priority, int loop, float rate) -{ - mSample = sample; - mChannelID = channelID; - mLeftVolume = leftVolume; - mRightVolume = rightVolume; - mPriority = priority; - mLoop = loop; - mRate =rate; + ALOGV("%s()", __func__); + auto apiLock = kUseApiLock ? std::make_unique>(mApiLock) : nullptr; + return mSoundManager.getUserData(); } } // end namespace android diff --git a/media/jni/soundpool/SoundPool.h b/media/jni/soundpool/SoundPool.h index 01e4faae6f6cf..d5b16ef629cda 100644 --- a/media/jni/soundpool/SoundPool.h +++ b/media/jni/soundpool/SoundPool.h @@ -14,227 +14,59 @@ * limitations under the License. */ -#ifndef SOUNDPOOL_H_ -#define SOUNDPOOL_H_ +#pragma once -#include -#include -#include -#include -#include -#include -#include +#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(mData->unsecurePointer()); } - status_t doLoad(); - void startLoad() { mState = LOADING; } - sp 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 mData; - sp 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, int channelID, float leftVolume, - float rightVolume, int priority, int loop, float rate); - sp 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 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, 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 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 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 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 mChannels; - List mRestart; - List mStop; - DefaultKeyedVector< int, sp > 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_*/ diff --git a/media/jni/soundpool/SoundPoolThread.cpp b/media/jni/soundpool/SoundPoolThread.cpp deleted file mode 100644 index ba3b482935dd6..0000000000000 --- a/media/jni/soundpool/SoundPoolThread.cpp +++ /dev/null @@ -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 = mSoundPool->findSample(sampleID); - status_t status = -1; - if (sample != 0) { - status = sample->doLoad(); - } - mSoundPool->notify(SoundPoolEvent(SoundPoolEvent::SAMPLE_LOADED, sampleID, status)); -} - -} // end namespace android diff --git a/media/jni/soundpool/SoundPoolThread.h b/media/jni/soundpool/SoundPoolThread.h deleted file mode 100644 index 7b3e1dda0a23b..0000000000000 --- a/media/jni/soundpool/SoundPoolThread.h +++ /dev/null @@ -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 -#include -#include - -#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 mMsgQueue; - SoundPool* mSoundPool; - bool mRunning; -}; - -} // end namespace android - -#endif /*SOUNDPOOLTHREAD_H_*/ diff --git a/media/jni/soundpool/Stream.cpp b/media/jni/soundpool/Stream.cpp new file mode 100644 index 0000000000000..e7d4d9093bb8e --- /dev/null +++ b/media/jni/soundpool/Stream.cpp @@ -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 + +#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, 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 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, int32_t nextStreamID, + float leftVolume, float rightVolume, int32_t priority, int32_t loop, float rate, + sp releaseTracks[2]) +{ + // These tracks are released without the lock. + sp &oldTrack = releaseTracks[0]; + sp &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(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 diff --git a/media/jni/soundpool/Stream.h b/media/jni/soundpool/Stream.h new file mode 100644 index 0000000000000..82d2690e2965c --- /dev/null +++ b/media/jni/soundpool/Stream.h @@ -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 +#include + +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, 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, int streamID, + float leftVolume, float rightVolume, int priority, int loop, float rate, + sp 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 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 mAudioTrack; + int mToggle = 0; + int64_t mStopTimeNs = 0; // if nonzero, time to wait for stop. +}; + +} // namespace android::soundpool diff --git a/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp new file mode 100644 index 0000000000000..8928c47cf4bb2 --- /dev/null +++ b/media/jni/soundpool/StreamManager.cpp @@ -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 + +#include "StreamManager.h" + +#include +#include + +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>(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( + 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, + 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(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 diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h new file mode 100644 index 0000000000000..8c98ac992f75f --- /dev/null +++ b/media/jni/soundpool/StreamManager.h @@ -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 +#include +#include +#include +#include +#include +#include +#include + +#include + +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 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(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 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 mPromise; + std::future 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> 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 f) { + std::list> 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( + [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> 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 +class PerfectHash { +public: + PerfectHash(size_t hashCapacity) + : mHashCapacity(hashCapacity) + , mK2V{new std::atomic[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 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::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 &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[]> 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::functionf) const { + for (size_t i = 0; i < mStreamPoolSize; ++i) { + f(&mStreamPool[i]); + } + } + + void forEach(std::functionf) { + 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 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> 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, + 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 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 + 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 mActiveStreams; // GUARDED_BY(mStreamManagerLock) + + // 3) mAvailableStreams: Streams that are inactive. + // The paired stream will also be inactive. + // No particular order. + std::unordered_set 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 mProcessingStreams; // GUARDED_BY(mStreamManagerLock) +}; + +} // namespace android::soundpool