You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1158 lines
44 KiB
1158 lines
44 KiB
/*
|
|
* 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
|