| /* |
| * Copyright 2017 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 ANDROID_VOLUME_SHAPER_H |
| #define ANDROID_VOLUME_SHAPER_H |
| |
| #include <list> |
| #include <math.h> |
| #include <sstream> |
| |
| #include <binder/Parcel.h> |
| #include <media/Interpolator.h> |
| #include <utils/Mutex.h> |
| #include <utils/RefBase.h> |
| |
| #pragma push_macro("LOG_TAG") |
| #undef LOG_TAG |
| #define LOG_TAG "VolumeShaper" |
| |
| // turn on VolumeShaper logging |
| #if 0 |
| #define VS_LOG ALOGD |
| #else |
| #define VS_LOG(...) |
| #endif |
| |
| namespace android { |
| |
| // The native VolumeShaper class mirrors the java VolumeShaper class; |
| // in addition, the native class contains implementation for actual operation. |
| // |
| // VolumeShaper methods are not safe for multiple thread access. |
| // Use VolumeHandler for thread-safe encapsulation of multiple VolumeShapers. |
| // |
| // Classes below written are to avoid naked pointers so there are no |
| // explicit destructors required. |
| |
| class VolumeShaper { |
| public: |
| using S = float; |
| using T = float; |
| |
| static const int kSystemIdMax = 16; |
| |
| // VolumeShaper::Status is equivalent to status_t if negative |
| // but if non-negative represents the id operated on. |
| // It must be expressible as an int32_t for binder purposes. |
| using Status = status_t; |
| |
| class Configuration : public Interpolator<S, T>, public RefBase { |
| public: |
| /* VolumeShaper.Configuration derives from the Interpolator class and adds |
| * parameters relating to the volume shape. |
| */ |
| |
| // TODO document as per VolumeShaper.java flags. |
| |
| // must match with VolumeShaper.java in frameworks/base |
| enum Type : int32_t { |
| TYPE_ID, |
| TYPE_SCALE, |
| }; |
| |
| // must match with VolumeShaper.java in frameworks/base |
| enum OptionFlag : int32_t { |
| OPTION_FLAG_NONE = 0, |
| OPTION_FLAG_VOLUME_IN_DBFS = (1 << 0), |
| OPTION_FLAG_CLOCK_TIME = (1 << 1), |
| |
| OPTION_FLAG_ALL = (OPTION_FLAG_VOLUME_IN_DBFS | OPTION_FLAG_CLOCK_TIME), |
| }; |
| |
| // bring to derived class; must match with VolumeShaper.java in frameworks/base |
| using InterpolatorType = Interpolator<S, T>::InterpolatorType; |
| |
| Configuration() |
| : Interpolator<S, T>() |
| , mType(TYPE_SCALE) |
| , mOptionFlags(OPTION_FLAG_NONE) |
| , mDurationMs(1000.) |
| , mId(-1) { |
| } |
| |
| Type getType() const { |
| return mType; |
| } |
| |
| status_t setType(Type type) { |
| switch (type) { |
| case TYPE_ID: |
| case TYPE_SCALE: |
| mType = type; |
| return NO_ERROR; |
| default: |
| ALOGE("invalid Type: %d", type); |
| return BAD_VALUE; |
| } |
| } |
| |
| OptionFlag getOptionFlags() const { |
| return mOptionFlags; |
| } |
| |
| status_t setOptionFlags(OptionFlag optionFlags) { |
| if ((optionFlags & ~OPTION_FLAG_ALL) != 0) { |
| ALOGE("optionFlags has invalid bits: %#x", optionFlags); |
| return BAD_VALUE; |
| } |
| mOptionFlags = optionFlags; |
| return NO_ERROR; |
| } |
| |
| double getDurationMs() const { |
| return mDurationMs; |
| } |
| |
| void setDurationMs(double durationMs) { |
| mDurationMs = durationMs; |
| } |
| |
| int32_t getId() const { |
| return mId; |
| } |
| |
| void setId(int32_t id) { |
| mId = id; |
| } |
| |
| T adjustVolume(T volume) const { |
| if ((getOptionFlags() & OPTION_FLAG_VOLUME_IN_DBFS) != 0) { |
| const T out = powf(10.f, volume / 10.); |
| VS_LOG("in: %f out: %f", volume, out); |
| volume = out; |
| } |
| // clamp |
| if (volume < 0.f) { |
| volume = 0.f; |
| } else if (volume > 1.f) { |
| volume = 1.f; |
| } |
| return volume; |
| } |
| |
| status_t checkCurve() { |
| if (mType == TYPE_ID) return NO_ERROR; |
| if (this->size() < 2) { |
| ALOGE("curve must have at least 2 points"); |
| return BAD_VALUE; |
| } |
| if (first().first != 0.f || last().first != 1.f) { |
| ALOGE("curve must start at 0.f and end at 1.f"); |
| return BAD_VALUE; |
| } |
| if ((getOptionFlags() & OPTION_FLAG_VOLUME_IN_DBFS) != 0) { |
| for (const auto &pt : *this) { |
| if (!(pt.second <= 0.f) /* handle nan */) { |
| ALOGE("positive volume dbFS"); |
| return BAD_VALUE; |
| } |
| } |
| } else { |
| for (const auto &pt : *this) { |
| if (!(pt.second >= 0.f) || !(pt.second <= 1.f) /* handle nan */) { |
| ALOGE("volume < 0.f or > 1.f"); |
| return BAD_VALUE; |
| } |
| } |
| } |
| return NO_ERROR; |
| } |
| |
| void clampVolume() { |
| if ((mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0) { |
| for (auto it = this->begin(); it != this->end(); ++it) { |
| if (!(it->second <= 0.f) /* handle nan */) { |
| it->second = 0.f; |
| } |
| } |
| } else { |
| for (auto it = this->begin(); it != this->end(); ++it) { |
| if (!(it->second >= 0.f) /* handle nan */) { |
| it->second = 0.f; |
| } else if (!(it->second <= 1.f)) { |
| it->second = 1.f; |
| } |
| } |
| } |
| } |
| |
| /* scaleToStartVolume() is used to set the start volume of a |
| * new VolumeShaper curve, when replacing one VolumeShaper |
| * with another using the "join" (volume match) option. |
| * |
| * It works best for monotonic volume ramps or ducks. |
| */ |
| void scaleToStartVolume(T volume) { |
| if (this->size() < 2) { |
| return; |
| } |
| const T startVolume = first().second; |
| const T endVolume = last().second; |
| if (endVolume == startVolume) { |
| // match with linear ramp |
| const T offset = volume - startVolume; |
| for (auto it = this->begin(); it != this->end(); ++it) { |
| it->second = it->second + offset * (1.f - it->first); |
| } |
| } else { |
| const T scale = (volume - endVolume) / (startVolume - endVolume); |
| for (auto it = this->begin(); it != this->end(); ++it) { |
| it->second = scale * (it->second - endVolume) + endVolume; |
| } |
| } |
| clampVolume(); |
| } |
| |
| status_t writeToParcel(Parcel *parcel) const { |
| if (parcel == nullptr) return BAD_VALUE; |
| return parcel->writeInt32((int32_t)mType) |
| ?: parcel->writeInt32(mId) |
| ?: mType == TYPE_ID |
| ? NO_ERROR |
| : parcel->writeInt32((int32_t)mOptionFlags) |
| ?: parcel->writeDouble(mDurationMs) |
| ?: Interpolator<S, T>::writeToParcel(parcel); |
| } |
| |
| status_t readFromParcel(const Parcel &parcel) { |
| int32_t type, optionFlags; |
| return parcel.readInt32(&type) |
| ?: setType((Type)type) |
| ?: parcel.readInt32(&mId) |
| ?: mType == TYPE_ID |
| ? NO_ERROR |
| : parcel.readInt32(&optionFlags) |
| ?: setOptionFlags((OptionFlag)optionFlags) |
| ?: parcel.readDouble(&mDurationMs) |
| ?: Interpolator<S, T>::readFromParcel(parcel) |
| ?: checkCurve(); |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "mType: " << mType << std::endl; |
| ss << "mId: " << mId << std::endl; |
| if (mType != TYPE_ID) { |
| ss << "mOptionFlags: " << mOptionFlags << std::endl; |
| ss << "mDurationMs: " << mDurationMs << std::endl; |
| ss << Interpolator<S, T>::toString().c_str(); |
| } |
| return ss.str(); |
| } |
| |
| private: |
| Type mType; |
| int32_t mId; |
| OptionFlag mOptionFlags; |
| double mDurationMs; |
| }; // Configuration |
| |
| // must match with VolumeShaper.java in frameworks/base |
| // TODO document per VolumeShaper.java flags. |
| class Operation : public RefBase { |
| public: |
| enum Flag : int32_t { |
| FLAG_NONE = 0, |
| FLAG_REVERSE = (1 << 0), |
| FLAG_TERMINATE = (1 << 1), |
| FLAG_JOIN = (1 << 2), |
| FLAG_DELAY = (1 << 3), |
| |
| FLAG_ALL = (FLAG_REVERSE | FLAG_TERMINATE | FLAG_JOIN | FLAG_DELAY), |
| }; |
| |
| Operation() |
| : mFlags(FLAG_NONE) |
| , mReplaceId(-1) { |
| } |
| |
| explicit Operation(Flag flags, int replaceId) |
| : mFlags(flags) |
| , mReplaceId(replaceId) { |
| } |
| |
| int32_t getReplaceId() const { |
| return mReplaceId; |
| } |
| |
| void setReplaceId(int32_t replaceId) { |
| mReplaceId = replaceId; |
| } |
| |
| Flag getFlags() const { |
| return mFlags; |
| } |
| |
| status_t setFlags(Flag flags) { |
| if ((flags & ~FLAG_ALL) != 0) { |
| ALOGE("flags has invalid bits: %#x", flags); |
| return BAD_VALUE; |
| } |
| mFlags = flags; |
| return NO_ERROR; |
| } |
| |
| status_t writeToParcel(Parcel *parcel) const { |
| if (parcel == nullptr) return BAD_VALUE; |
| return parcel->writeInt32((int32_t)mFlags) |
| ?: parcel->writeInt32(mReplaceId); |
| } |
| |
| status_t readFromParcel(const Parcel &parcel) { |
| int32_t flags; |
| return parcel.readInt32(&flags) |
| ?: parcel.readInt32(&mReplaceId) |
| ?: setFlags((Flag)flags); |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "mFlags: " << mFlags << std::endl; |
| ss << "mReplaceId: " << mReplaceId << std::endl; |
| return ss.str(); |
| } |
| |
| private: |
| Flag mFlags; |
| int32_t mReplaceId; |
| }; // Operation |
| |
| // must match with VolumeShaper.java in frameworks/base |
| class State : public RefBase { |
| public: |
| explicit State(T volume, S xOffset) |
| : mVolume(volume) |
| , mXOffset(xOffset) { |
| } |
| |
| State() |
| : State(-1.f, -1.f) { } |
| |
| T getVolume() const { |
| return mVolume; |
| } |
| |
| void setVolume(T volume) { |
| mVolume = volume; |
| } |
| |
| S getXOffset() const { |
| return mXOffset; |
| } |
| |
| void setXOffset(S xOffset) { |
| mXOffset = xOffset; |
| } |
| |
| status_t writeToParcel(Parcel *parcel) const { |
| if (parcel == nullptr) return BAD_VALUE; |
| return parcel->writeFloat(mVolume) |
| ?: parcel->writeFloat(mXOffset); |
| } |
| |
| status_t readFromParcel(const Parcel &parcel) { |
| return parcel.readFloat(&mVolume) |
| ?: parcel.readFloat(&mXOffset); |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "mVolume: " << mVolume << std::endl; |
| ss << "mXOffset: " << mXOffset << std::endl; |
| return ss.str(); |
| } |
| |
| private: |
| T mVolume; |
| S mXOffset; |
| }; // State |
| |
| template <typename R> |
| class Translate { |
| public: |
| Translate() |
| : mOffset(0) |
| , mScale(1) { |
| } |
| |
| R getOffset() const { |
| return mOffset; |
| } |
| |
| void setOffset(R offset) { |
| mOffset = offset; |
| } |
| |
| R getScale() const { |
| return mScale; |
| } |
| |
| void setScale(R scale) { |
| mScale = scale; |
| } |
| |
| R operator()(R in) const { |
| return mScale * (in - mOffset); |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "mOffset: " << mOffset << std::endl; |
| ss << "mScale: " << mScale << std::endl; |
| return ss.str(); |
| } |
| |
| private: |
| R mOffset; |
| R mScale; |
| }; // Translate |
| |
| static int64_t convertTimespecToUs(const struct timespec &tv) |
| { |
| return tv.tv_sec * 1000000ll + tv.tv_nsec / 1000; |
| } |
| |
| // current monotonic time in microseconds. |
| static int64_t getNowUs() |
| { |
| struct timespec tv; |
| if (clock_gettime(CLOCK_MONOTONIC, &tv) != 0) { |
| return 0; // system is really sick, just return 0 for consistency. |
| } |
| return convertTimespecToUs(tv); |
| } |
| |
| Translate<S> mXTranslate; |
| Translate<T> mYTranslate; |
| sp<VolumeShaper::Configuration> mConfiguration; |
| sp<VolumeShaper::Operation> mOperation; |
| int64_t mStartFrame; |
| T mLastVolume; |
| S mXOffset; |
| |
| // TODO: Since we pass configuration and operation as shared pointers |
| // there is a potential risk that the caller may modify these after |
| // delivery. Currently, we don't require copies made here. |
| explicit VolumeShaper( |
| const sp<VolumeShaper::Configuration> &configuration, |
| const sp<VolumeShaper::Operation> &operation) |
| : mConfiguration(configuration) // we do not make a copy |
| , mOperation(operation) // ditto |
| , mStartFrame(-1) |
| , mLastVolume(T(1)) |
| , mXOffset(0.f) { |
| if (configuration.get() != nullptr |
| && (getFlags() & VolumeShaper::Operation::FLAG_DELAY) == 0) { |
| mLastVolume = configuration->first().second; |
| } |
| } |
| |
| void updatePosition(int64_t startFrame, double sampleRate) { |
| double scale = (mConfiguration->last().first - mConfiguration->first().first) |
| / (mConfiguration->getDurationMs() * 0.001 * sampleRate); |
| const double minScale = 1. / INT64_MAX; |
| scale = std::max(scale, minScale); |
| VS_LOG("update position: scale %lf frameCount:%lld, sampleRate:%lf", |
| scale, (long long) startFrame, sampleRate); |
| mXTranslate.setOffset(startFrame - mConfiguration->first().first / scale); |
| mXTranslate.setScale(scale); |
| VS_LOG("translate: %s", mXTranslate.toString().c_str()); |
| } |
| |
| // We allow a null operation here, though VolumeHandler always provides one. |
| VolumeShaper::Operation::Flag getFlags() const { |
| return mOperation == nullptr |
| ? VolumeShaper::Operation::FLAG_NONE :mOperation->getFlags(); |
| } |
| |
| sp<VolumeShaper::State> getState() const { |
| return new VolumeShaper::State(mLastVolume, mXOffset); |
| } |
| |
| std::pair<T, bool> getVolume(int64_t trackFrameCount, double trackSampleRate) { |
| if (mConfiguration.get() == nullptr || mConfiguration->empty()) { |
| ALOGE("nonexistent VolumeShaper, removing"); |
| mLastVolume = T(1); |
| mXOffset = 0.f; |
| return std::make_pair(T(1), true); |
| } |
| if ((getFlags() & VolumeShaper::Operation::FLAG_DELAY) != 0) { |
| VS_LOG("delayed VolumeShaper, ignoring"); |
| mLastVolume = T(1); |
| mXOffset = 0.; |
| return std::make_pair(T(1), false); |
| } |
| const bool clockTime = (mConfiguration->getOptionFlags() |
| & VolumeShaper::Configuration::OPTION_FLAG_CLOCK_TIME) != 0; |
| const int64_t frameCount = clockTime ? getNowUs() : trackFrameCount; |
| const double sampleRate = clockTime ? 1000000 : trackSampleRate; |
| |
| if (mStartFrame < 0) { |
| updatePosition(frameCount, sampleRate); |
| mStartFrame = frameCount; |
| } |
| VS_LOG("frameCount: %lld", (long long)frameCount); |
| S x = mXTranslate((T)frameCount); |
| VS_LOG("translation: %f", x); |
| |
| // handle reversal of position |
| if (getFlags() & VolumeShaper::Operation::FLAG_REVERSE) { |
| x = 1.f - x; |
| VS_LOG("reversing to %f", x); |
| if (x < mConfiguration->first().first) { |
| mXOffset = 1.f; |
| const T volume = mConfiguration->adjustVolume( |
| mConfiguration->first().second); // persist last value |
| VS_LOG("persisting volume %f", volume); |
| mLastVolume = volume; |
| return std::make_pair(volume, false); |
| } |
| if (x > mConfiguration->last().first) { |
| mXOffset = 0.f; |
| mLastVolume = 1.f; |
| return std::make_pair(T(1), false); // too early |
| } |
| } else { |
| if (x < mConfiguration->first().first) { |
| mXOffset = 0.f; |
| mLastVolume = 1.f; |
| return std::make_pair(T(1), false); // too early |
| } |
| if (x > mConfiguration->last().first) { |
| mXOffset = 1.f; |
| const T volume = mConfiguration->adjustVolume( |
| mConfiguration->last().second); // persist last value |
| VS_LOG("persisting volume %f", volume); |
| mLastVolume = volume; |
| return std::make_pair(volume, false); |
| } |
| } |
| mXOffset = x; |
| // x contains the location on the volume curve to use. |
| const T unscaledVolume = mConfiguration->findY(x); |
| const T volumeChange = mYTranslate(unscaledVolume); |
| const T volume = mConfiguration->adjustVolume(volumeChange); |
| VS_LOG("volume: %f unscaled: %f", volume, unscaledVolume); |
| mLastVolume = volume; |
| return std::make_pair(volume, false); |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "StartFrame: " << mStartFrame << std::endl; |
| ss << mXTranslate.toString().c_str(); |
| ss << mYTranslate.toString().c_str(); |
| if (mConfiguration.get() == nullptr) { |
| ss << "VolumeShaper::Configuration: nullptr" << std::endl; |
| } else { |
| ss << "VolumeShaper::Configuration:" << std::endl; |
| ss << mConfiguration->toString().c_str(); |
| } |
| if (mOperation.get() == nullptr) { |
| ss << "VolumeShaper::Operation: nullptr" << std::endl; |
| } else { |
| ss << "VolumeShaper::Operation:" << std::endl; |
| ss << mOperation->toString().c_str(); |
| } |
| return ss.str(); |
| } |
| }; // VolumeShaper |
| |
| // VolumeHandler combines the volume factors of multiple VolumeShapers and handles |
| // multiple thread access by synchronizing all public methods. |
| class VolumeHandler : public RefBase { |
| public: |
| using S = float; |
| using T = float; |
| |
| explicit VolumeHandler(uint32_t sampleRate) |
| : mSampleRate((double)sampleRate) |
| , mLastFrame(0) { |
| } |
| |
| VolumeShaper::Status applyVolumeShaper( |
| const sp<VolumeShaper::Configuration> &configuration, |
| const sp<VolumeShaper::Operation> &operation) { |
| AutoMutex _l(mLock); |
| if (configuration == nullptr) { |
| ALOGE("null configuration"); |
| return VolumeShaper::Status(BAD_VALUE); |
| } |
| if (operation == nullptr) { |
| ALOGE("null operation"); |
| return VolumeShaper::Status(BAD_VALUE); |
| } |
| const int32_t id = configuration->getId(); |
| if (id < 0) { |
| ALOGE("negative id: %d", id); |
| return VolumeShaper::Status(BAD_VALUE); |
| } |
| VS_LOG("applyVolumeShaper id: %d", id); |
| |
| switch (configuration->getType()) { |
| case VolumeShaper::Configuration::TYPE_ID: { |
| VS_LOG("trying to find id: %d", id); |
| auto it = findId_l(id); |
| if (it == mVolumeShapers.end()) { |
| VS_LOG("couldn't find id: %d\n%s", id, this->toString().c_str()); |
| return VolumeShaper::Status(INVALID_OPERATION); |
| } |
| if ((it->getFlags() & VolumeShaper::Operation::FLAG_TERMINATE) != 0) { |
| VS_LOG("terminate id: %d", id); |
| mVolumeShapers.erase(it); |
| break; |
| } |
| if ((it->getFlags() & VolumeShaper::Operation::FLAG_REVERSE) != |
| (operation->getFlags() & VolumeShaper::Operation::FLAG_REVERSE)) { |
| const S x = it->mXTranslate((T)mLastFrame); |
| VS_LOG("translation: %f", x); |
| // reflect position |
| S target = 1.f - x; |
| if (target < it->mConfiguration->first().first) { |
| VS_LOG("clamp to start - begin immediately"); |
| target = 0.; |
| } |
| VS_LOG("target: %f", target); |
| it->mXTranslate.setOffset(it->mXTranslate.getOffset() |
| + (x - target) / it->mXTranslate.getScale()); |
| } |
| it->mOperation = operation; // replace the operation |
| } break; |
| case VolumeShaper::Configuration::TYPE_SCALE: { |
| const int replaceId = operation->getReplaceId(); |
| if (replaceId >= 0) { |
| auto replaceIt = findId_l(replaceId); |
| if (replaceIt == mVolumeShapers.end()) { |
| ALOGW("cannot find replace id: %d", replaceId); |
| } else { |
| if ((replaceIt->getFlags() & VolumeShaper::Operation::FLAG_JOIN) != 0) { |
| // For join, we scale the start volume of the current configuration |
| // to match the last-used volume of the replacing VolumeShaper. |
| auto state = replaceIt->getState(); |
| if (state->getXOffset() >= 0) { // valid |
| const T volume = state->getVolume(); |
| ALOGD("join: scaling start volume to %f", volume); |
| configuration->scaleToStartVolume(volume); |
| } |
| } |
| (void)mVolumeShapers.erase(replaceIt); |
| } |
| } |
| // check if we have another of the same id. |
| auto oldIt = findId_l(id); |
| if (oldIt != mVolumeShapers.end()) { |
| ALOGW("duplicate id, removing old %d", id); |
| (void)mVolumeShapers.erase(oldIt); |
| } |
| // create new VolumeShaper |
| mVolumeShapers.emplace_back(configuration, operation); |
| } break; |
| } |
| return VolumeShaper::Status(id); |
| } |
| |
| sp<VolumeShaper::State> getVolumeShaperState(int id) { |
| AutoMutex _l(mLock); |
| auto it = findId_l(id); |
| if (it == mVolumeShapers.end()) { |
| return nullptr; |
| } |
| return it->getState(); |
| } |
| |
| T getVolume(int64_t trackFrameCount) { |
| AutoMutex _l(mLock); |
| mLastFrame = trackFrameCount; |
| T volume(1); |
| for (auto it = mVolumeShapers.begin(); it != mVolumeShapers.end();) { |
| std::pair<T, bool> shaperVolume = |
| it->getVolume(trackFrameCount, mSampleRate); |
| volume *= shaperVolume.first; |
| if (shaperVolume.second) { |
| it = mVolumeShapers.erase(it); |
| continue; |
| } |
| ++it; |
| } |
| return volume; |
| } |
| |
| std::string toString() const { |
| AutoMutex _l(mLock); |
| std::stringstream ss; |
| ss << "mSampleRate: " << mSampleRate << std::endl; |
| ss << "mLastFrame: " << mLastFrame << std::endl; |
| for (const auto &shaper : mVolumeShapers) { |
| ss << shaper.toString().c_str(); |
| } |
| return ss.str(); |
| } |
| |
| private: |
| std::list<VolumeShaper>::iterator findId_l(int32_t id) { |
| std::list<VolumeShaper>::iterator it = mVolumeShapers.begin(); |
| for (; it != mVolumeShapers.end(); ++it) { |
| if (it->mConfiguration->getId() == id) { |
| break; |
| } |
| } |
| return it; |
| } |
| |
| mutable Mutex mLock; |
| double mSampleRate; // in samples (frames) per second |
| int64_t mLastFrame; // logging purpose only |
| std::list<VolumeShaper> mVolumeShapers; // list provides stable iterators on erase |
| }; // VolumeHandler |
| |
| } // namespace android |
| |
| #pragma pop_macro("LOG_TAG") |
| |
| #endif // ANDROID_VOLUME_SHAPER_H |