| /* |
| * 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 <cmath> |
| #include <list> |
| #include <math.h> |
| #include <sstream> |
| |
| #include <android/media/VolumeShaperConfiguration.h> |
| #include <android/media/VolumeShaperConfigurationOptionFlag.h> |
| #include <android/media/VolumeShaperOperation.h> |
| #include <android/media/VolumeShaperOperationFlag.h> |
| #include <android/media/VolumeShaperState.h> |
| #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 |
| #define VS_LOGGING 0 |
| #define VS_LOG(...) ALOGD_IF(VS_LOGGING, __VA_ARGS__) |
| |
| namespace android { |
| |
| namespace media { |
| |
| // 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: |
| // S and T are like template typenames (matching the Interpolator<S, T>) |
| using S = float; // time type |
| using T = float; // volume type |
| |
| // Curve and dimension information |
| // TODO: member static const or constexpr float initialization not permitted in C++11 |
| #define MIN_CURVE_TIME 0.f // type S: start of VolumeShaper curve (normalized) |
| #define MAX_CURVE_TIME 1.f // type S: end of VolumeShaper curve (normalized) |
| #define MIN_LINEAR_VOLUME 0.f // type T: silence / mute audio |
| #define MAX_LINEAR_VOLUME 1.f // type T: max volume, unity gain |
| #define MAX_LOG_VOLUME 0.f // type T: max volume, unity gain in dBFS |
| |
| /* kSystemVolumeShapersMax is the maximum number of system VolumeShapers. |
| * Each system VolumeShapers has a predefined Id, which ranges from 0 |
| * to kSystemVolumeShapersMax - 1 and is unique for its usage. |
| * |
| * "1" is reserved for system ducking. |
| */ |
| static const int kSystemVolumeShapersMax = 16; |
| |
| /* kUserVolumeShapersMax is the maximum number of application |
| * VolumeShapers for a player/track. Application VolumeShapers are |
| * assigned on creation by the client, and have Ids ranging |
| * from kSystemVolumeShapersMax to INT32_MAX. |
| * |
| * The number of user/application volume shapers is independent to the |
| * system volume shapers. If an application tries to create more than |
| * kUserVolumeShapersMax to a player, then the apply() will fail. |
| * This prevents exhausting server side resources by a potentially malicious |
| * application. |
| */ |
| static const int kUserVolumeShapersMax = 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; |
| |
| // Local definition for clamp as std::clamp is included in C++17 only. |
| // TODO: use the std::clamp version when Android build uses C++17. |
| template<typename R> |
| static constexpr const R &clamp(const R &v, const R &lo, const R &hi) { |
| return (v < lo) ? lo : (hi < v) ? hi : v; |
| } |
| |
| /* VolumeShaper.Configuration derives from the Interpolator class and adds |
| * parameters relating to the volume shape. |
| * |
| * This parallels the Java implementation and the enums must match. |
| * See "frameworks/base/media/java/android/media/VolumeShaper.java" for |
| * details on the Java implementation. |
| */ |
| class Configuration : public Interpolator<S, T>, public RefBase, public Parcelable { |
| public: |
| // 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 from base class; must match with VolumeShaper.java in frameworks/base. |
| using InterpolatorType = Interpolator<S, T>::InterpolatorType; |
| |
| Configuration() |
| : Interpolator<S, T>() |
| , RefBase() |
| , mType(TYPE_SCALE) |
| , mId(-1) |
| , mOptionFlags(OPTION_FLAG_NONE) |
| , mDurationMs(1000.) { |
| } |
| |
| Configuration(const Configuration &configuration) |
| : Interpolator<S, T>(*static_cast<const Interpolator<S, T> *>(&configuration)) |
| , RefBase() |
| , mType(configuration.mType) |
| , mId(configuration.mId) |
| , mOptionFlags(configuration.mOptionFlags) |
| , mDurationMs(configuration.mDurationMs) { |
| } |
| |
| 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; |
| } |
| |
| status_t setDurationMs(double durationMs) { |
| if (durationMs > 0.) { |
| mDurationMs = durationMs; |
| return NO_ERROR; |
| } |
| // zero, negative, or nan. These values not possible from Java. |
| return BAD_VALUE; |
| } |
| |
| int32_t getId() const { |
| return mId; |
| } |
| |
| void setId(int32_t id) { |
| // We permit a negative id here (representing invalid). |
| mId = id; |
| } |
| |
| /* Adjust the volume to be in linear range from MIN_LINEAR_VOLUME to MAX_LINEAR_VOLUME |
| * and compensate for log dbFS volume as needed. |
| */ |
| T adjustVolume(T volume) const { |
| if ((getOptionFlags() & OPTION_FLAG_VOLUME_IN_DBFS) != 0) { |
| const T out = powf(10.f, volume / 10.f); |
| VS_LOG("in: %f out: %f", volume, out); |
| volume = out; |
| } |
| return clamp(volume, MIN_LINEAR_VOLUME /* lo */, MAX_LINEAR_VOLUME /* hi */); |
| } |
| |
| /* Check if the existing curve is valid. |
| */ |
| status_t checkCurve() const { |
| 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 != MIN_CURVE_TIME || last().first != MAX_CURVE_TIME) { |
| ALOGE("curve must start at MIN_CURVE_TIME and end at MAX_CURVE_TIME"); |
| return BAD_VALUE; |
| } |
| if ((getOptionFlags() & OPTION_FLAG_VOLUME_IN_DBFS) != 0) { |
| for (const auto &pt : *this) { |
| if (!(pt.second <= MAX_LOG_VOLUME) /* handle nan */) { |
| ALOGE("positive volume dbFS"); |
| return BAD_VALUE; |
| } |
| } |
| } else { |
| for (const auto &pt : *this) { |
| if (!(pt.second >= MIN_LINEAR_VOLUME) |
| || !(pt.second <= MAX_LINEAR_VOLUME) /* handle nan */) { |
| ALOGE("volume < MIN_LINEAR_VOLUME or > MAX_LINEAR_VOLUME"); |
| return BAD_VALUE; |
| } |
| } |
| } |
| return NO_ERROR; |
| } |
| |
| /* Clamps the volume curve in the configuration to |
| * the valid range for log or linear scale. |
| */ |
| void clampVolume() { |
| if ((mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0) { |
| for (auto it = this->begin(); it != this->end(); ++it) { |
| if (!(it->second <= MAX_LOG_VOLUME) /* handle nan */) { |
| it->second = MAX_LOG_VOLUME; |
| } |
| } |
| } else { |
| for (auto it = this->begin(); it != this->end(); ++it) { |
| if (!(it->second >= MIN_LINEAR_VOLUME) /* handle nan */) { |
| it->second = MIN_LINEAR_VOLUME; |
| } else if (!(it->second <= MAX_LINEAR_VOLUME)) { |
| it->second = MAX_LINEAR_VOLUME; |
| } |
| } |
| } |
| } |
| |
| /* 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; |
| static const T scale = 1.f / (MAX_CURVE_TIME - MIN_CURVE_TIME); // nominally 1.f |
| for (auto it = this->begin(); it != this->end(); ++it) { |
| it->second = it->second + offset * (MAX_CURVE_TIME - it->first) * scale; |
| } |
| } 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 override { |
| VolumeShaperConfiguration parcelable; |
| writeToParcelable(&parcelable); |
| return parcelable.writeToParcel(parcel); |
| } |
| |
| void writeToParcelable(VolumeShaperConfiguration *parcelable) const { |
| parcelable->id = getId(); |
| parcelable->type = getTypeAsAidl(); |
| parcelable->optionFlags = 0; |
| if (mType != TYPE_ID) { |
| parcelable->optionFlags = getOptionFlagsAsAidl(); |
| parcelable->durationMs = getDurationMs(); |
| parcelable->interpolatorConfig.emplace(); // create value in std::optional |
| Interpolator<S, T>::writeToConfig(&*parcelable->interpolatorConfig); |
| } |
| } |
| |
| status_t readFromParcel(const Parcel* parcel) override { |
| VolumeShaperConfiguration data; |
| return data.readFromParcel(parcel) |
| ?: readFromParcelable(data); |
| } |
| |
| status_t readFromParcelable(const VolumeShaperConfiguration& parcelable) { |
| setId(parcelable.id); |
| return setTypeFromAidl(parcelable.type) |
| ?: mType == TYPE_ID |
| ? NO_ERROR |
| : setOptionFlagsFromAidl(parcelable.optionFlags) |
| ?: setDurationMs(parcelable.durationMs) |
| ?: !parcelable.interpolatorConfig // check std::optional for value |
| ? BAD_VALUE // must be nonnull. |
| : Interpolator<S, T>::readFromConfig(*parcelable.interpolatorConfig) |
| ?: checkCurve(); |
| } |
| |
| // Returns a string for debug printing. |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "VolumeShaper::Configuration{mType=" << static_cast<int32_t>(mType); |
| ss << ", mId=" << mId; |
| if (mType != TYPE_ID) { |
| ss << ", mOptionFlags=" << static_cast<int32_t>(mOptionFlags); |
| ss << ", mDurationMs=" << mDurationMs; |
| ss << ", " << Interpolator<S, T>::toString().c_str(); |
| } |
| ss << "}"; |
| return ss.str(); |
| } |
| |
| private: |
| Type mType; // type of configuration |
| int32_t mId; // A valid id is >= 0. |
| OptionFlag mOptionFlags; // option flags for the configuration. |
| double mDurationMs; // duration, must be > 0; default is 1000 ms. |
| |
| int32_t getOptionFlagsAsAidl() const { |
| int32_t result = 0; |
| if (getOptionFlags() & OPTION_FLAG_VOLUME_IN_DBFS) { |
| result |= |
| 1 << static_cast<int>(VolumeShaperConfigurationOptionFlag::VOLUME_IN_DBFS); |
| } |
| if (getOptionFlags() & OPTION_FLAG_CLOCK_TIME) { |
| result |= 1 << static_cast<int>(VolumeShaperConfigurationOptionFlag::CLOCK_TIME); |
| } |
| return result; |
| } |
| |
| status_t setOptionFlagsFromAidl(int32_t aidl) { |
| std::underlying_type_t<OptionFlag> options = 0; |
| if (aidl & (1 << static_cast<int>(VolumeShaperConfigurationOptionFlag::VOLUME_IN_DBFS))) { |
| options |= OPTION_FLAG_VOLUME_IN_DBFS; |
| } |
| if (aidl & (1 << static_cast<int>(VolumeShaperConfigurationOptionFlag::CLOCK_TIME))) { |
| options |= OPTION_FLAG_CLOCK_TIME; |
| } |
| return setOptionFlags(static_cast<OptionFlag>(options)); |
| } |
| |
| status_t setTypeFromAidl(VolumeShaperConfigurationType aidl) { |
| switch (aidl) { |
| case VolumeShaperConfigurationType::ID: |
| return setType(TYPE_ID); |
| case VolumeShaperConfigurationType::SCALE: |
| return setType(TYPE_SCALE); |
| default: |
| return BAD_VALUE; |
| } |
| } |
| |
| VolumeShaperConfigurationType getTypeAsAidl() const { |
| switch (getType()) { |
| case TYPE_ID: |
| return VolumeShaperConfigurationType::ID; |
| case TYPE_SCALE: |
| return VolumeShaperConfigurationType::SCALE; |
| default: |
| LOG_ALWAYS_FATAL("Shouldn't get here"); |
| } |
| } |
| }; // Configuration |
| |
| /* VolumeShaper::Operation expresses an operation to perform on the |
| * configuration (either explicitly specified or an id). |
| * |
| * This parallels the Java implementation and the enums must match. |
| * See "frameworks/base/media/java/android/media/VolumeShaper.java" for |
| * details on the Java implementation. |
| */ |
| class Operation : public RefBase, public Parcelable { |
| public: |
| // Must match with VolumeShaper.java. |
| enum Flag : int32_t { |
| FLAG_NONE = 0, |
| FLAG_REVERSE = (1 << 0), // the absence of this indicates "play" |
| FLAG_TERMINATE = (1 << 1), |
| FLAG_JOIN = (1 << 2), |
| FLAG_DELAY = (1 << 3), |
| FLAG_CREATE_IF_NECESSARY = (1 << 4), |
| |
| FLAG_ALL = (FLAG_REVERSE | FLAG_TERMINATE | FLAG_JOIN | FLAG_DELAY |
| | FLAG_CREATE_IF_NECESSARY), |
| }; |
| |
| Operation() |
| : Operation(FLAG_NONE, -1 /* replaceId */) { |
| } |
| |
| Operation(Flag flags, int replaceId) |
| : Operation(flags, replaceId, std::numeric_limits<S>::quiet_NaN() /* xOffset */) { |
| } |
| |
| Operation(const Operation &operation) |
| : Operation(operation.mFlags, operation.mReplaceId, operation.mXOffset) { |
| } |
| |
| explicit Operation(const sp<Operation> &operation) |
| : Operation(*operation.get()) { |
| } |
| |
| Operation(Flag flags, int replaceId, S xOffset) |
| : mFlags(flags) |
| , mReplaceId(replaceId) |
| , mXOffset(xOffset) { |
| } |
| |
| int32_t getReplaceId() const { |
| return mReplaceId; |
| } |
| |
| void setReplaceId(int32_t replaceId) { |
| mReplaceId = replaceId; |
| } |
| |
| S getXOffset() const { |
| return mXOffset; |
| } |
| |
| void setXOffset(S xOffset) { |
| mXOffset = clamp(xOffset, MIN_CURVE_TIME /* lo */, MAX_CURVE_TIME /* hi */); |
| } |
| |
| Flag getFlags() const { |
| return mFlags; |
| } |
| |
| /* xOffset is the position on the volume curve and may go backwards |
| * if you are in reverse mode. This must be in the range from |
| * [MIN_CURVE_TIME, MAX_CURVE_TIME]. |
| * |
| * normalizedTime always increases as time or framecount increases. |
| * normalizedTime is nominally from MIN_CURVE_TIME to MAX_CURVE_TIME when |
| * running through the curve, but could be outside this range afterwards. |
| * If you are reversing, this means the position on the curve, or xOffset, |
| * is computed as MAX_CURVE_TIME - normalizedTime, clamped to |
| * [MIN_CURVE_TIME, MAX_CURVE_TIME]. |
| */ |
| void setNormalizedTime(S normalizedTime) { |
| setXOffset((mFlags & FLAG_REVERSE) != 0 |
| ? MAX_CURVE_TIME - normalizedTime : normalizedTime); |
| } |
| |
| 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 override { |
| if (parcel == nullptr) return BAD_VALUE; |
| VolumeShaperOperation op; |
| writeToParcelable(&op); |
| return op.writeToParcel(parcel); |
| } |
| |
| void writeToParcelable(VolumeShaperOperation* op) const { |
| op->flags = getFlagsAsAidl(); |
| op->replaceId = mReplaceId; |
| op->xOffset = mXOffset; |
| } |
| |
| status_t readFromParcel(const Parcel* parcel) override { |
| VolumeShaperOperation op; |
| return op.readFromParcel(parcel) |
| ?: readFromParcelable(op); |
| } |
| |
| status_t readFromParcelable(const VolumeShaperOperation& op) { |
| mReplaceId = op.replaceId; |
| mXOffset = op.xOffset; |
| return setFlagsFromAidl(op.flags); |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "VolumeShaper::Operation{mFlags=" << static_cast<int32_t>(mFlags) ; |
| ss << ", mReplaceId=" << mReplaceId; |
| ss << ", mXOffset=" << mXOffset; |
| ss << "}"; |
| return ss.str(); |
| } |
| |
| private: |
| status_t setFlagsFromAidl(int32_t aidl) { |
| std::underlying_type_t<Flag> flags = 0; |
| if (aidl & (1 << static_cast<int>(VolumeShaperOperationFlag::REVERSE))) { |
| flags |= FLAG_REVERSE; |
| } |
| if (aidl & (1 << static_cast<int>(VolumeShaperOperationFlag::TERMINATE))) { |
| flags |= FLAG_TERMINATE; |
| } |
| if (aidl & (1 << static_cast<int>(VolumeShaperOperationFlag::JOIN))) { |
| flags |= FLAG_JOIN; |
| } |
| if (aidl & (1 << static_cast<int>(VolumeShaperOperationFlag::DELAY))) { |
| flags |= FLAG_DELAY; |
| } |
| if (aidl & (1 << static_cast<int>(VolumeShaperOperationFlag::CREATE_IF_NECESSARY))) { |
| flags |= FLAG_CREATE_IF_NECESSARY; |
| } |
| return setFlags(static_cast<Flag>(flags)); |
| } |
| |
| int32_t getFlagsAsAidl() const { |
| int32_t aidl = 0; |
| std::underlying_type_t<Flag> flags = getFlags(); |
| if (flags & FLAG_REVERSE) { |
| aidl |= (1 << static_cast<int>(VolumeShaperOperationFlag::REVERSE)); |
| } |
| if (flags & FLAG_TERMINATE) { |
| aidl |= (1 << static_cast<int>(VolumeShaperOperationFlag::TERMINATE)); |
| } |
| if (flags & FLAG_JOIN) { |
| aidl |= (1 << static_cast<int>(VolumeShaperOperationFlag::JOIN)); |
| } |
| if (flags & FLAG_DELAY) { |
| aidl |= (1 << static_cast<int>(VolumeShaperOperationFlag::DELAY)); |
| } |
| if (flags & FLAG_CREATE_IF_NECESSARY) { |
| aidl |= (1 << static_cast<int>(VolumeShaperOperationFlag::CREATE_IF_NECESSARY)); |
| } |
| return aidl; |
| } |
| |
| private: |
| Flag mFlags; // operation to do |
| int32_t mReplaceId; // if >= 0 the id to remove in a replace operation. |
| S mXOffset; // position in the curve to set if a valid number (not nan) |
| }; // Operation |
| |
| /* VolumeShaper.State is returned when requesting the last |
| * state of the VolumeShaper. |
| * |
| * This parallels the Java implementation. |
| * See "frameworks/base/media/java/android/media/VolumeShaper.java" for |
| * details on the Java implementation. |
| */ |
| class State : public RefBase, public Parcelable { |
| public: |
| State(T volume, S xOffset) |
| : mVolume(volume) |
| , mXOffset(xOffset) { |
| } |
| |
| State() |
| : State(NAN, NAN) { } |
| |
| 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 override { |
| if (parcel == nullptr) return BAD_VALUE; |
| VolumeShaperState state; |
| writeToParcelable(&state); |
| return state.writeToParcel(parcel); |
| } |
| |
| void writeToParcelable(VolumeShaperState* parcelable) const { |
| parcelable->volume = mVolume; |
| parcelable->xOffset = mXOffset; |
| } |
| |
| status_t readFromParcel(const Parcel* parcel) override { |
| VolumeShaperState state; |
| return state.readFromParcel(parcel) |
| ?: readFromParcelable(state); |
| } |
| |
| status_t readFromParcelable(const VolumeShaperState& parcelable) { |
| mVolume = parcelable.volume; |
| mXOffset = parcelable.xOffset; |
| return OK; |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "VolumeShaper::State{mVolume=" << mVolume; |
| ss << ", mXOffset=" << mXOffset; |
| ss << "}"; |
| return ss.str(); |
| } |
| |
| private: |
| T mVolume; // linear volume in the range MIN_LINEAR_VOLUME to MAX_LINEAR_VOLUME |
| S mXOffset; // position on curve expressed from MIN_CURVE_TIME to MAX_CURVE_TIME |
| }; // State |
| |
| // Internal helper class to do an affine transform for time and amplitude scaling. |
| 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 << "VolumeShaper::Translate{mOffset=" << mOffset; |
| ss << ", mScale=" << mScale; |
| ss << "}"; |
| 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); |
| } |
| |
| /* Native implementation of VolumeShaper. This is NOT mirrored |
| * on the Java side, so we don't need to mimic Java side layout |
| * and data; furthermore, this isn't refcounted as a "RefBase" object. |
| * |
| * Since we pass configuration and operation as shared pointers (like |
| * Java) there is a potential risk that the caller may modify |
| * these after delivery. |
| */ |
| 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)) |
| , mLastXOffset(MIN_CURVE_TIME) |
| , mDelayXOffset(MIN_CURVE_TIME) { |
| if (configuration.get() != nullptr |
| && (getFlags() & VolumeShaper::Operation::FLAG_DELAY) == 0) { |
| mLastVolume = configuration->first().second; |
| } |
| } |
| |
| // 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(); |
| } |
| |
| /* Returns the last volume and xoffset reported to the AudioFlinger. |
| * If the VolumeShaper has not been started, compute what the volume |
| * should be based on the initial offset specified. |
| */ |
| sp<VolumeShaper::State> getState() const { |
| if (!isStarted()) { |
| const T volume = computeVolumeFromXOffset(mDelayXOffset); |
| VS_LOG("delayed VolumeShaper, using cached offset:%f for volume:%f", |
| mDelayXOffset, volume); |
| return new VolumeShaper::State(volume, mDelayXOffset); |
| } else { |
| return new VolumeShaper::State(mLastVolume, mLastXOffset); |
| } |
| } |
| |
| S getDelayXOffset() const { |
| return mDelayXOffset; |
| } |
| |
| void setDelayXOffset(S xOffset) { |
| mDelayXOffset = clamp(xOffset, MIN_CURVE_TIME /* lo */, MAX_CURVE_TIME /* hi */); |
| } |
| |
| bool isStarted() const { |
| return mStartFrame >= 0; |
| } |
| |
| /* getVolume() updates the last volume/xoffset state so it is not |
| * const, even though logically it may be viewed as const. |
| */ |
| std::pair<T /* volume */, bool /* active */> getVolume( |
| int64_t trackFrameCount, double trackSampleRate) { |
| if ((getFlags() & VolumeShaper::Operation::FLAG_DELAY) != 0) { |
| // We haven't had PLAY called yet, so just return the value |
| // as if PLAY were called just now. |
| VS_LOG("delayed VolumeShaper, using cached offset %f", mDelayXOffset); |
| const T volume = computeVolumeFromXOffset(mDelayXOffset); |
| return std::make_pair(volume, 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, mDelayXOffset); |
| mStartFrame = frameCount; |
| } |
| VS_LOG("frameCount: %lld", (long long)frameCount); |
| const S x = mXTranslate((T)frameCount); |
| VS_LOG("translation to normalized time: %f", x); |
| |
| std::tuple<T /* volume */, S /* position */, bool /* active */> vt = |
| computeStateFromNormalizedTime(x); |
| |
| mLastVolume = std::get<0>(vt); |
| mLastXOffset = std::get<1>(vt); |
| const bool active = std::get<2>(vt); |
| VS_LOG("rescaled time:%f volume:%f xOffset:%f active:%s", |
| x, mLastVolume, mLastXOffset, active ? "true" : "false"); |
| return std::make_pair(mLastVolume, active); |
| } |
| |
| std::string toString() const { |
| std::stringstream ss; |
| ss << "VolumeShaper{mStartFrame=" << mStartFrame; |
| ss << ", mXTranslate=" << mXTranslate.toString().c_str(); |
| ss << ", mConfiguration=" << |
| (mConfiguration.get() == nullptr |
| ? "nullptr" : mConfiguration->toString().c_str()); |
| ss << ", mOperation=" << |
| (mOperation.get() == nullptr |
| ? "nullptr" : mOperation->toString().c_str()); |
| ss << "}"; |
| return ss.str(); |
| } |
| |
| Translate<S> mXTranslate; // translation from frames (usec for clock time) to normalized time. |
| sp<VolumeShaper::Configuration> mConfiguration; |
| sp<VolumeShaper::Operation> mOperation; |
| |
| private: |
| int64_t mStartFrame; // starting frame, non-negative when started (in usec for clock time) |
| T mLastVolume; // last computed interpolated volume (y-axis) |
| S mLastXOffset; // last computed interpolated xOffset/time (x-axis) |
| S mDelayXOffset; // xOffset to use for first invocation of VolumeShaper. |
| |
| // Called internally to adjust mXTranslate for first time start. |
| void updatePosition(int64_t startFrame, double sampleRate, S xOffset) { |
| double scale = (mConfiguration->last().first - mConfiguration->first().first) |
| / (mConfiguration->getDurationMs() * 0.001 * sampleRate); |
| const double minScale = 1. / static_cast<double>(INT64_MAX); |
| scale = std::max(scale, minScale); |
| VS_LOG("update position: scale %lf frameCount:%lld, sampleRate:%lf, xOffset:%f", |
| scale, (long long) startFrame, sampleRate, xOffset); |
| |
| S normalizedTime = (getFlags() & VolumeShaper::Operation::FLAG_REVERSE) != 0 ? |
| MAX_CURVE_TIME - xOffset : xOffset; |
| mXTranslate.setOffset(static_cast<float>(static_cast<double>(startFrame) |
| - static_cast<double>(normalizedTime) / scale)); |
| mXTranslate.setScale(static_cast<float>(scale)); |
| VS_LOG("translate: %s", mXTranslate.toString().c_str()); |
| } |
| |
| T computeVolumeFromXOffset(S xOffset) const { |
| const T unscaledVolume = mConfiguration->findY(xOffset); |
| const T volume = mConfiguration->adjustVolume(unscaledVolume); // handle log scale |
| VS_LOG("computeVolumeFromXOffset %f -> %f -> %f", xOffset, unscaledVolume, volume); |
| return volume; |
| } |
| |
| std::tuple<T /* volume */, S /* position */, bool /* active */> |
| computeStateFromNormalizedTime(S x) const { |
| bool active = true; |
| // handle reversal of position |
| if (getFlags() & VolumeShaper::Operation::FLAG_REVERSE) { |
| x = MAX_CURVE_TIME - x; |
| VS_LOG("reversing to %f", x); |
| if (x < MIN_CURVE_TIME) { |
| x = MIN_CURVE_TIME; |
| active = false; // at the end |
| } else if (x > MAX_CURVE_TIME) { |
| x = MAX_CURVE_TIME; //early |
| } |
| } else { |
| if (x < MIN_CURVE_TIME) { |
| x = MIN_CURVE_TIME; // early |
| } else if (x > MAX_CURVE_TIME) { |
| x = MAX_CURVE_TIME; |
| active = false; // at end |
| } |
| } |
| const S xOffset = x; |
| const T volume = computeVolumeFromXOffset(xOffset); |
| return std::make_tuple(volume, xOffset, active); |
| } |
| }; // VolumeShaper |
| |
| /* VolumeHandler combines the volume factors of multiple VolumeShapers associated |
| * with a player. It is thread safe by synchronizing all public methods. |
| * |
| * This is a native-only implementation. |
| * |
| * The server side VolumeHandler is used to maintain a list of volume handlers, |
| * keep state, and obtain volume. |
| * |
| * The client side VolumeHandler is used to maintain a list of volume handlers, |
| * keep some partial state, and restore if the server dies. |
| */ |
| class VolumeHandler : public RefBase { |
| public: |
| using S = float; |
| using T = float; |
| |
| // A volume handler which just keeps track of active VolumeShapers does not need sampleRate. |
| VolumeHandler() |
| : VolumeHandler(0 /* sampleRate */) { |
| } |
| |
| explicit VolumeHandler(uint32_t sampleRate) |
| : mSampleRate((double)sampleRate) |
| , mLastFrame(0) |
| , mVolumeShaperIdCounter(VolumeShaper::kSystemVolumeShapersMax) |
| , mLastVolume(1.f, false) { |
| } |
| |
| VolumeShaper::Status applyVolumeShaper( |
| const sp<VolumeShaper::Configuration> &configuration, |
| const sp<VolumeShaper::Operation> &operation_in) { |
| // make a local copy of operation, as we modify it. |
| sp<VolumeShaper::Operation> operation(new VolumeShaper::Operation(operation_in)); |
| VS_LOG("applyVolumeShaper:configuration: %s", configuration->toString().c_str()); |
| VS_LOG("applyVolumeShaper:operation: %s", operation->toString().c_str()); |
| 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_SCALE: { |
| const int replaceId = operation->getReplaceId(); |
| if (replaceId >= 0) { |
| VS_LOG("replacing %d", replaceId); |
| auto replaceIt = findId_l(replaceId); |
| if (replaceIt == mVolumeShapers.end()) { |
| ALOGW("cannot find replace id: %d", replaceId); |
| } else { |
| if ((operation->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(); |
| ALOGD("join: state:%s", state->toString().c_str()); |
| 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); |
| } |
| operation->setReplaceId(-1); |
| } |
| // check if we have another of the same id. |
| auto oldIt = findId_l(id); |
| if (oldIt != mVolumeShapers.end()) { |
| if ((operation->getFlags() |
| & VolumeShaper::Operation::FLAG_CREATE_IF_NECESSARY) != 0) { |
| // TODO: move the case to a separate function. |
| goto HANDLE_TYPE_ID; // no need to create, take over existing id. |
| } |
| ALOGW("duplicate id, removing old %d", id); |
| (void)mVolumeShapers.erase(oldIt); |
| } |
| |
| /* Check if too many application VolumeShapers (with id >= kSystemVolumeShapersMax). |
| * We check on the server side to ensure synchronization and robustness. |
| * |
| * This shouldn't fail on a replace command unless the replaced id is |
| * already invalid (which *should* be checked in the Java layer). |
| */ |
| if (id >= VolumeShaper::kSystemVolumeShapersMax |
| && numberOfUserVolumeShapers_l() >= VolumeShaper::kUserVolumeShapersMax) { |
| ALOGW("Too many app VolumeShapers, cannot add to VolumeHandler"); |
| return VolumeShaper::Status(INVALID_OPERATION); |
| } |
| |
| // create new VolumeShaper with default behavior. |
| mVolumeShapers.emplace_back(configuration, new VolumeShaper::Operation()); |
| VS_LOG("after adding, number of volumeShapers:%zu", mVolumeShapers.size()); |
| } |
| // fall through to handle the operation |
| HANDLE_TYPE_ID: |
| 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", id); |
| return VolumeShaper::Status(INVALID_OPERATION); |
| } |
| if ((operation->getFlags() & VolumeShaper::Operation::FLAG_TERMINATE) != 0) { |
| VS_LOG("terminate id: %d", id); |
| mVolumeShapers.erase(it); |
| break; |
| } |
| const bool clockTime = (it->mConfiguration->getOptionFlags() |
| & VolumeShaper::Configuration::OPTION_FLAG_CLOCK_TIME) != 0; |
| if ((it->getFlags() & VolumeShaper::Operation::FLAG_REVERSE) != |
| (operation->getFlags() & VolumeShaper::Operation::FLAG_REVERSE)) { |
| if (it->isStarted()) { |
| const int64_t frameCount = clockTime ? VolumeShaper::getNowUs() : mLastFrame; |
| const S x = it->mXTranslate((T)frameCount); |
| VS_LOG("reverse normalizedTime: %f", x); |
| // reflect position |
| S target = MAX_CURVE_TIME - x; |
| if (target < MIN_CURVE_TIME) { |
| VS_LOG("clamp to start - begin immediately"); |
| target = MIN_CURVE_TIME; |
| } |
| VS_LOG("reverse normalizedTime target: %f", target); |
| it->mXTranslate.setOffset(it->mXTranslate.getOffset() |
| + (x - target) / it->mXTranslate.getScale()); |
| } |
| // if not started, the delay offset doesn't change. |
| } |
| const S xOffset = operation->getXOffset(); |
| if (!std::isnan(xOffset)) { |
| if (it->isStarted()) { |
| const int64_t frameCount = clockTime ? VolumeShaper::getNowUs() : mLastFrame; |
| const S x = it->mXTranslate((T)frameCount); |
| VS_LOG("normalizedTime translation: %f", x); |
| const S target = |
| (operation->getFlags() & VolumeShaper::Operation::FLAG_REVERSE) != 0 ? |
| MAX_CURVE_TIME - xOffset : xOffset; |
| VS_LOG("normalizedTime target x offset: %f", target); |
| it->mXTranslate.setOffset(it->mXTranslate.getOffset() |
| + (x - target) / it->mXTranslate.getScale()); |
| } else { |
| it->setDelayXOffset(xOffset); |
| } |
| } |
| it->mOperation = operation; // replace the operation |
| } break; |
| } |
| return VolumeShaper::Status(id); |
| } |
| |
| sp<VolumeShaper::State> getVolumeShaperState(int id) { |
| AutoMutex _l(mLock); |
| auto it = findId_l(id); |
| if (it == mVolumeShapers.end()) { |
| VS_LOG("cannot find state for id: %d", id); |
| return nullptr; |
| } |
| return it->getState(); |
| } |
| |
| /* getVolume() is not const, as it updates internal state. |
| * Once called, any VolumeShapers not already started begin running. |
| */ |
| std::pair<T /* volume */, bool /* active */> getVolume(int64_t trackFrameCount) { |
| AutoMutex _l(mLock); |
| mLastFrame = trackFrameCount; |
| T volume(1); |
| size_t activeCount = 0; |
| for (auto it = mVolumeShapers.begin(); it != mVolumeShapers.end();) { |
| const std::pair<T, bool> shaperVolume = |
| it->getVolume(trackFrameCount, mSampleRate); |
| volume *= shaperVolume.first; |
| activeCount += shaperVolume.second; |
| ++it; |
| } |
| mLastVolume = std::make_pair(volume, activeCount != 0); |
| VS_LOG("getVolume: <%f, %s>", mLastVolume.first, mLastVolume.second ? "true" : "false"); |
| return mLastVolume; |
| } |
| |
| /* Used by a client side VolumeHandler to ensure all the VolumeShapers |
| * indicate that they have been started. Upon a change in audioserver |
| * output sink, this information is used for restoration of the server side |
| * VolumeHandler. |
| */ |
| void setStarted() { |
| (void)getVolume(mLastFrame); // getVolume() will start the individual VolumeShapers. |
| } |
| |
| std::pair<T /* volume */, bool /* active */> getLastVolume() const { |
| AutoMutex _l(mLock); |
| return mLastVolume; |
| } |
| |
| std::string toString() const { |
| AutoMutex _l(mLock); |
| std::stringstream ss; |
| ss << "VolumeHandler{mSampleRate=" << mSampleRate; |
| ss << ", mLastFrame=" << mLastFrame; |
| ss << ", mVolumeShapers={"; |
| bool first = true; |
| for (const auto &shaper : mVolumeShapers) { |
| if (first) { |
| first = false; |
| } else { |
| ss << ", "; |
| } |
| ss << shaper.toString().c_str(); |
| } |
| ss << "}}"; |
| return ss.str(); |
| } |
| |
| void forall(const std::function<VolumeShaper::Status (const VolumeShaper &)> &lambda) { |
| AutoMutex _l(mLock); |
| VS_LOG("forall: mVolumeShapers.size() %zu", mVolumeShapers.size()); |
| for (const auto &shaper : mVolumeShapers) { |
| VolumeShaper::Status status = lambda(shaper); |
| VS_LOG("forall applying lambda on shaper (%p): %d", &shaper, (int)status); |
| } |
| } |
| |
| void reset() { |
| AutoMutex _l(mLock); |
| mVolumeShapers.clear(); |
| mLastFrame = 0; |
| // keep mVolumeShaperIdCounter as is. |
| } |
| |
| /* Sets the configuration id if necessary - This is based on the counter |
| * internal to the VolumeHandler. |
| */ |
| void setIdIfNecessary(const sp<VolumeShaper::Configuration> &configuration) { |
| if (configuration->getType() == VolumeShaper::Configuration::TYPE_SCALE) { |
| const int id = configuration->getId(); |
| if (id == -1) { |
| // Reassign to a unique id, skipping system ids. |
| AutoMutex _l(mLock); |
| while (true) { |
| if (mVolumeShaperIdCounter == INT32_MAX) { |
| mVolumeShaperIdCounter = VolumeShaper::kSystemVolumeShapersMax; |
| } else { |
| ++mVolumeShaperIdCounter; |
| } |
| if (findId_l(mVolumeShaperIdCounter) != mVolumeShapers.end()) { |
| continue; // collision with an existing id. |
| } |
| configuration->setId(mVolumeShaperIdCounter); |
| ALOGD("setting id to %d", mVolumeShaperIdCounter); |
| break; |
| } |
| } |
| } |
| } |
| |
| 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; |
| } |
| |
| size_t numberOfUserVolumeShapers_l() const { |
| size_t count = 0; |
| for (const auto &shaper : mVolumeShapers) { |
| count += (shaper.mConfiguration->getId() >= VolumeShaper::kSystemVolumeShapersMax); |
| } |
| return count; |
| } |
| |
| mutable Mutex mLock; |
| double mSampleRate; // in samples (frames) per second |
| int64_t mLastFrame; // logging purpose only, 0 on start |
| int32_t mVolumeShaperIdCounter; // a counter to return a unique volume shaper id. |
| std::pair<T /* volume */, bool /* active */> mLastVolume; |
| std::list<VolumeShaper> mVolumeShapers; // list provides stable iterators on erase |
| }; // VolumeHandler |
| |
| } // namespace media |
| |
| } // namespace android |
| |
| #pragma pop_macro("LOG_TAG") |
| |
| #endif // ANDROID_VOLUME_SHAPER_H |