1593 lines
62 KiB
Java
1593 lines
62 KiB
Java
/*
|
|
* 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.
|
|
*/
|
|
package android.media;
|
|
|
|
import android.annotation.IntDef;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.TestApi;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.os.Build;
|
|
import android.os.BadParcelableException;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.lang.ref.WeakReference;
|
|
import java.util.Arrays;
|
|
import java.util.Objects;
|
|
|
|
/**
|
|
* The {@code VolumeShaper} class is used to automatically control audio volume during media
|
|
* playback, allowing simple implementation of transition effects and ducking.
|
|
* It is created from implementations of {@code VolumeAutomation},
|
|
* such as {@code MediaPlayer} and {@code AudioTrack} (referred to as "players" below),
|
|
* by {@link MediaPlayer#createVolumeShaper} or {@link AudioTrack#createVolumeShaper}.
|
|
*
|
|
* A {@code VolumeShaper} is intended for short volume changes.
|
|
* If the audio output sink changes during
|
|
* a {@code VolumeShaper} transition, the precise curve position may be lost, and the
|
|
* {@code VolumeShaper} may advance to the end of the curve for the new audio output sink.
|
|
*
|
|
* The {@code VolumeShaper} appears as an additional scaling on the audio output,
|
|
* and adjusts independently of track or stream volume controls.
|
|
*/
|
|
public final class VolumeShaper implements AutoCloseable {
|
|
/* member variables */
|
|
private int mId;
|
|
private final WeakReference<PlayerBase> mWeakPlayerBase;
|
|
|
|
/* package */ VolumeShaper(
|
|
@NonNull Configuration configuration, @NonNull PlayerBase playerBase) {
|
|
mWeakPlayerBase = new WeakReference<PlayerBase>(playerBase);
|
|
mId = applyPlayer(configuration, new Operation.Builder().defer().build());
|
|
}
|
|
|
|
/* package */ int getId() {
|
|
return mId;
|
|
}
|
|
|
|
/**
|
|
* Applies the {@link VolumeShaper.Operation} to the {@code VolumeShaper}.
|
|
*
|
|
* Applying {@link VolumeShaper.Operation#PLAY} after {@code PLAY}
|
|
* or {@link VolumeShaper.Operation#REVERSE} after
|
|
* {@code REVERSE} has no effect.
|
|
*
|
|
* Applying {@link VolumeShaper.Operation#PLAY} when the player
|
|
* hasn't started will synchronously start the {@code VolumeShaper} when
|
|
* playback begins.
|
|
*
|
|
* @param operation the {@code operation} to apply.
|
|
* @throws IllegalStateException if the player is uninitialized or if there
|
|
* is a critical failure. In that case, the {@code VolumeShaper} should be
|
|
* recreated.
|
|
*/
|
|
public void apply(@NonNull Operation operation) {
|
|
/* void */ applyPlayer(new VolumeShaper.Configuration(mId), operation);
|
|
}
|
|
|
|
/**
|
|
* Replaces the current {@code VolumeShaper}
|
|
* {@code configuration} with a new {@code configuration}.
|
|
*
|
|
* This allows the user to change the volume shape
|
|
* while the existing {@code VolumeShaper} is in effect.
|
|
*
|
|
* The effect of {@code replace()} is similar to an atomic close of
|
|
* the existing {@code VolumeShaper} and creation of a new {@code VolumeShaper}.
|
|
*
|
|
* If the {@code operation} is {@link VolumeShaper.Operation#PLAY} then the
|
|
* new curve starts immediately.
|
|
*
|
|
* If the {@code operation} is
|
|
* {@link VolumeShaper.Operation#REVERSE}, then the new curve will
|
|
* be delayed until {@code PLAY} is applied.
|
|
*
|
|
* @param configuration the new {@code configuration} to use.
|
|
* @param operation the {@code operation} to apply to the {@code VolumeShaper}
|
|
* @param join if true, match the start volume of the
|
|
* new {@code configuration} to the current volume of the existing
|
|
* {@code VolumeShaper}, to avoid discontinuity.
|
|
* @throws IllegalStateException if the player is uninitialized or if there
|
|
* is a critical failure. In that case, the {@code VolumeShaper} should be
|
|
* recreated.
|
|
*/
|
|
public void replace(
|
|
@NonNull Configuration configuration, @NonNull Operation operation, boolean join) {
|
|
mId = applyPlayer(
|
|
configuration,
|
|
new Operation.Builder(operation).replace(mId, join).build());
|
|
}
|
|
|
|
/**
|
|
* Returns the current volume scale attributable to the {@code VolumeShaper}.
|
|
*
|
|
* This is the last volume from the {@code VolumeShaper} used for the player,
|
|
* or the initial volume if the {@code VolumeShaper} hasn't been started with
|
|
* {@link VolumeShaper.Operation#PLAY}.
|
|
*
|
|
* @return the volume, linearly represented as a value between 0.f and 1.f.
|
|
* @throws IllegalStateException if the player is uninitialized or if there
|
|
* is a critical failure. In that case, the {@code VolumeShaper} should be
|
|
* recreated.
|
|
*/
|
|
public float getVolume() {
|
|
return getStatePlayer(mId).getVolume();
|
|
}
|
|
|
|
/**
|
|
* Releases the {@code VolumeShaper} object; any volume scale due to the
|
|
* {@code VolumeShaper} is removed after closing.
|
|
*
|
|
* If the volume does not reach 1.f when the {@code VolumeShaper} is closed
|
|
* (or finalized), there may be an abrupt change of volume.
|
|
*
|
|
* {@code close()} may be safely called after a prior {@code close()}.
|
|
* This class implements the Java {@code AutoClosable} interface and
|
|
* may be used with try-with-resources.
|
|
*/
|
|
@Override
|
|
public void close() {
|
|
try {
|
|
/* void */ applyPlayer(
|
|
new VolumeShaper.Configuration(mId),
|
|
new Operation.Builder().terminate().build());
|
|
} catch (IllegalStateException ise) {
|
|
; // ok
|
|
}
|
|
if (mWeakPlayerBase != null) {
|
|
mWeakPlayerBase.clear();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void finalize() {
|
|
close(); // ensure we remove the native VolumeShaper
|
|
}
|
|
|
|
/**
|
|
* Internal call to apply the {@code configuration} and {@code operation} to the player.
|
|
* Returns a valid shaper id or throws the appropriate exception.
|
|
* @param configuration
|
|
* @param operation
|
|
* @return id a non-negative shaper id.
|
|
* @throws IllegalStateException if the player has been deallocated or is uninitialized.
|
|
*/
|
|
private int applyPlayer(
|
|
@NonNull VolumeShaper.Configuration configuration,
|
|
@NonNull VolumeShaper.Operation operation) {
|
|
final int id;
|
|
if (mWeakPlayerBase != null) {
|
|
PlayerBase player = mWeakPlayerBase.get();
|
|
if (player == null) {
|
|
throw new IllegalStateException("player deallocated");
|
|
}
|
|
id = player.playerApplyVolumeShaper(configuration, operation);
|
|
} else {
|
|
throw new IllegalStateException("uninitialized shaper");
|
|
}
|
|
if (id < 0) {
|
|
// TODO - get INVALID_OPERATION from platform.
|
|
final int VOLUME_SHAPER_INVALID_OPERATION = -38; // must match with platform
|
|
// Due to RPC handling, we translate integer codes to exceptions right before
|
|
// delivering to the user.
|
|
if (id == VOLUME_SHAPER_INVALID_OPERATION) {
|
|
throw new IllegalStateException("player or VolumeShaper deallocated");
|
|
} else {
|
|
throw new IllegalArgumentException("invalid configuration or operation: " + id);
|
|
}
|
|
}
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Internal call to retrieve the current {@code VolumeShaper} state.
|
|
* @param id
|
|
* @return the current {@code VolumeShaper.State}
|
|
* @throws IllegalStateException if the player has been deallocated or is uninitialized.
|
|
*/
|
|
private @NonNull VolumeShaper.State getStatePlayer(int id) {
|
|
final VolumeShaper.State state;
|
|
if (mWeakPlayerBase != null) {
|
|
PlayerBase player = mWeakPlayerBase.get();
|
|
if (player == null) {
|
|
throw new IllegalStateException("player deallocated");
|
|
}
|
|
state = player.playerGetVolumeShaperState(id);
|
|
} else {
|
|
throw new IllegalStateException("uninitialized shaper");
|
|
}
|
|
if (state == null) {
|
|
throw new IllegalStateException("shaper cannot be found");
|
|
}
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* The {@code VolumeShaper.Configuration} class contains curve
|
|
* and duration information.
|
|
* It is constructed by the {@link VolumeShaper.Configuration.Builder}.
|
|
* <p>
|
|
* A {@code VolumeShaper.Configuration} is used by
|
|
* {@link VolumeAutomation#createVolumeShaper(Configuration)
|
|
* VolumeAutomation.createVolumeShaper(Configuration)} to create
|
|
* a {@code VolumeShaper} and
|
|
* by {@link VolumeShaper#replace(Configuration, Operation, boolean)
|
|
* VolumeShaper.replace(Configuration, Operation, boolean)}
|
|
* to replace an existing {@code configuration}.
|
|
* <p>
|
|
* The {@link AudioTrack} and {@link MediaPlayer} classes implement
|
|
* the {@link VolumeAutomation} interface.
|
|
*/
|
|
public static final class Configuration implements Parcelable {
|
|
private static final int MAXIMUM_CURVE_POINTS = 16;
|
|
|
|
/**
|
|
* Returns the maximum number of curve points allowed for
|
|
* {@link VolumeShaper.Builder#setCurve(float[], float[])}.
|
|
*/
|
|
public static int getMaximumCurvePoints() {
|
|
return MAXIMUM_CURVE_POINTS;
|
|
}
|
|
|
|
// These values must match the native VolumeShaper::Configuration::Type
|
|
/** @hide */
|
|
@IntDef({
|
|
TYPE_ID,
|
|
TYPE_SCALE,
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface Type {}
|
|
|
|
/**
|
|
* Specifies a {@link VolumeShaper} handle created by {@link #VolumeShaper(int)}
|
|
* from an id returned by {@code setVolumeShaper()}.
|
|
* The type, curve, etc. may not be queried from
|
|
* a {@code VolumeShaper} object of this type;
|
|
* the handle is used to identify and change the operation of
|
|
* an existing {@code VolumeShaper} sent to the player.
|
|
*/
|
|
/* package */ static final int TYPE_ID = 0;
|
|
|
|
/**
|
|
* Specifies a {@link VolumeShaper} to be used
|
|
* as an additional scale to the current volume.
|
|
* This is created by the {@link VolumeShaper.Builder}.
|
|
*/
|
|
/* package */ static final int TYPE_SCALE = 1;
|
|
|
|
// These values must match the native InterpolatorType enumeration.
|
|
/** @hide */
|
|
@IntDef({
|
|
INTERPOLATOR_TYPE_STEP,
|
|
INTERPOLATOR_TYPE_LINEAR,
|
|
INTERPOLATOR_TYPE_CUBIC,
|
|
INTERPOLATOR_TYPE_CUBIC_MONOTONIC,
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface InterpolatorType {}
|
|
|
|
/**
|
|
* Stepwise volume curve.
|
|
*/
|
|
public static final int INTERPOLATOR_TYPE_STEP = 0;
|
|
|
|
/**
|
|
* Linear interpolated volume curve.
|
|
*/
|
|
public static final int INTERPOLATOR_TYPE_LINEAR = 1;
|
|
|
|
/**
|
|
* Cubic interpolated volume curve.
|
|
* This is default if unspecified.
|
|
*/
|
|
public static final int INTERPOLATOR_TYPE_CUBIC = 2;
|
|
|
|
/**
|
|
* Cubic interpolated volume curve
|
|
* that preserves local monotonicity.
|
|
* So long as the control points are locally monotonic,
|
|
* the curve interpolation between those points are monotonic.
|
|
* This is useful for cubic spline interpolated
|
|
* volume ramps and ducks.
|
|
*/
|
|
public static final int INTERPOLATOR_TYPE_CUBIC_MONOTONIC = 3;
|
|
|
|
// These values must match the native VolumeShaper::Configuration::InterpolatorType
|
|
/** @hide */
|
|
@IntDef({
|
|
OPTION_FLAG_VOLUME_IN_DBFS,
|
|
OPTION_FLAG_CLOCK_TIME,
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface OptionFlag {}
|
|
|
|
/**
|
|
* @hide
|
|
* Use a dB full scale volume range for the volume curve.
|
|
*<p>
|
|
* The volume scale is typically from 0.f to 1.f on a linear scale;
|
|
* this option changes to -inf to 0.f on a db full scale,
|
|
* where 0.f is equivalent to a scale of 1.f.
|
|
*/
|
|
public static final int OPTION_FLAG_VOLUME_IN_DBFS = (1 << 0);
|
|
|
|
/**
|
|
* @hide
|
|
* Use clock time instead of media time.
|
|
*<p>
|
|
* The default implementation of {@code VolumeShaper} is to apply
|
|
* volume changes by the media time of the player.
|
|
* Hence, the {@code VolumeShaper} will speed or slow down to
|
|
* match player changes of playback rate, pause, or resume.
|
|
*<p>
|
|
* The {@code OPTION_FLAG_CLOCK_TIME} option allows the {@code VolumeShaper}
|
|
* progress to be determined by clock time instead of media time.
|
|
*/
|
|
public static final int OPTION_FLAG_CLOCK_TIME = (1 << 1);
|
|
|
|
private static final int OPTION_FLAG_PUBLIC_ALL =
|
|
OPTION_FLAG_VOLUME_IN_DBFS | OPTION_FLAG_CLOCK_TIME;
|
|
|
|
/**
|
|
* A one second linear ramp from silence to full volume.
|
|
* Use {@link VolumeShaper.Builder#reflectTimes()}
|
|
* or {@link VolumeShaper.Builder#invertVolumes()} to generate
|
|
* the matching linear duck.
|
|
*/
|
|
public static final Configuration LINEAR_RAMP = new VolumeShaper.Configuration.Builder()
|
|
.setInterpolatorType(INTERPOLATOR_TYPE_LINEAR)
|
|
.setCurve(new float[] {0.f, 1.f} /* times */,
|
|
new float[] {0.f, 1.f} /* volumes */)
|
|
.setDuration(1000)
|
|
.build();
|
|
|
|
/**
|
|
* A one second cubic ramp from silence to full volume.
|
|
* Use {@link VolumeShaper.Builder#reflectTimes()}
|
|
* or {@link VolumeShaper.Builder#invertVolumes()} to generate
|
|
* the matching cubic duck.
|
|
*/
|
|
public static final Configuration CUBIC_RAMP = new VolumeShaper.Configuration.Builder()
|
|
.setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
|
|
.setCurve(new float[] {0.f, 1.f} /* times */,
|
|
new float[] {0.f, 1.f} /* volumes */)
|
|
.setDuration(1000)
|
|
.build();
|
|
|
|
/**
|
|
* A one second sine curve
|
|
* from silence to full volume for energy preserving cross fades.
|
|
* Use {@link VolumeShaper.Builder#reflectTimes()} to generate
|
|
* the matching cosine duck.
|
|
*/
|
|
public static final Configuration SINE_RAMP;
|
|
|
|
/**
|
|
* A one second sine-squared s-curve ramp
|
|
* from silence to full volume.
|
|
* Use {@link VolumeShaper.Builder#reflectTimes()}
|
|
* or {@link VolumeShaper.Builder#invertVolumes()} to generate
|
|
* the matching sine-squared s-curve duck.
|
|
*/
|
|
public static final Configuration SCURVE_RAMP;
|
|
|
|
static {
|
|
final int POINTS = MAXIMUM_CURVE_POINTS;
|
|
final float times[] = new float[POINTS];
|
|
final float sines[] = new float[POINTS];
|
|
final float scurve[] = new float[POINTS];
|
|
for (int i = 0; i < POINTS; ++i) {
|
|
times[i] = (float)i / (POINTS - 1);
|
|
final float sine = (float)Math.sin(times[i] * Math.PI / 2.);
|
|
sines[i] = sine;
|
|
scurve[i] = sine * sine;
|
|
}
|
|
SINE_RAMP = new VolumeShaper.Configuration.Builder()
|
|
.setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
|
|
.setCurve(times, sines)
|
|
.setDuration(1000)
|
|
.build();
|
|
SCURVE_RAMP = new VolumeShaper.Configuration.Builder()
|
|
.setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
|
|
.setCurve(times, scurve)
|
|
.setDuration(1000)
|
|
.build();
|
|
}
|
|
|
|
/*
|
|
* member variables - these are all final
|
|
*/
|
|
|
|
// type of VolumeShaper
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final int mType;
|
|
|
|
// valid when mType is TYPE_ID
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final int mId;
|
|
|
|
// valid when mType is TYPE_SCALE
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final int mOptionFlags;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final double mDurationMs;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final int mInterpolatorType;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final float[] mTimes;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final float[] mVolumes;
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "VolumeShaper.Configuration{"
|
|
+ "mType = " + mType
|
|
+ ", mId = " + mId
|
|
+ (mType == TYPE_ID
|
|
? "}"
|
|
: ", mOptionFlags = 0x" + Integer.toHexString(mOptionFlags).toUpperCase()
|
|
+ ", mDurationMs = " + mDurationMs
|
|
+ ", mInterpolatorType = " + mInterpolatorType
|
|
+ ", mTimes[] = " + Arrays.toString(mTimes)
|
|
+ ", mVolumes[] = " + Arrays.toString(mVolumes)
|
|
+ "}");
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return mType == TYPE_ID
|
|
? Objects.hash(mType, mId)
|
|
: Objects.hash(mType, mId,
|
|
mOptionFlags, mDurationMs, mInterpolatorType,
|
|
Arrays.hashCode(mTimes), Arrays.hashCode(mVolumes));
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (!(o instanceof Configuration)) return false;
|
|
if (o == this) return true;
|
|
final Configuration other = (Configuration) o;
|
|
// Note that exact floating point equality may not be guaranteed
|
|
// for a theoretically idempotent operation; for example,
|
|
// there are many cases where a + b - b != a.
|
|
return mType == other.mType
|
|
&& mId == other.mId
|
|
&& (mType == TYPE_ID
|
|
|| (mOptionFlags == other.mOptionFlags
|
|
&& mDurationMs == other.mDurationMs
|
|
&& mInterpolatorType == other.mInterpolatorType
|
|
&& Arrays.equals(mTimes, other.mTimes)
|
|
&& Arrays.equals(mVolumes, other.mVolumes)));
|
|
}
|
|
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel dest, int flags) {
|
|
VolumeShaperConfiguration parcelable = toParcelable();
|
|
parcelable.writeToParcel(dest, flags);
|
|
}
|
|
|
|
/** @hide */
|
|
public VolumeShaperConfiguration toParcelable() {
|
|
VolumeShaperConfiguration parcelable = new VolumeShaperConfiguration();
|
|
parcelable.type = typeToAidl(mType);
|
|
parcelable.id = mId;
|
|
if (mType != TYPE_ID) {
|
|
parcelable.optionFlags = optionFlagsToAidl(mOptionFlags);
|
|
parcelable.durationMs = mDurationMs;
|
|
parcelable.interpolatorConfig = toInterpolatorParcelable();
|
|
}
|
|
return parcelable;
|
|
}
|
|
|
|
private InterpolatorConfig toInterpolatorParcelable() {
|
|
InterpolatorConfig parcelable = new InterpolatorConfig();
|
|
parcelable.type = interpolatorTypeToAidl(mInterpolatorType);
|
|
parcelable.firstSlope = 0.f; // first slope (specifying for native side)
|
|
parcelable.lastSlope = 0.f; // last slope (specifying for native side)
|
|
parcelable.xy = new float[mTimes.length * 2];
|
|
for (int i = 0; i < mTimes.length; ++i) {
|
|
parcelable.xy[i * 2] = mTimes[i];
|
|
parcelable.xy[i * 2 + 1] = mVolumes[i];
|
|
}
|
|
return parcelable;
|
|
}
|
|
|
|
/** @hide */
|
|
public static Configuration fromParcelable(VolumeShaperConfiguration parcelable) {
|
|
// this needs to match the native VolumeShaper.Configuration parceling
|
|
final int type = typeFromAidl(parcelable.type);
|
|
final int id = parcelable.id;
|
|
if (type == TYPE_ID) {
|
|
return new VolumeShaper.Configuration(id);
|
|
} else {
|
|
final int optionFlags = optionFlagsFromAidl(parcelable.optionFlags);
|
|
final double durationMs = parcelable.durationMs;
|
|
final int interpolatorType = interpolatorTypeFromAidl(
|
|
parcelable.interpolatorConfig.type);
|
|
// parcelable.interpolatorConfig.firstSlope is ignored on the Java side
|
|
// parcelable.interpolatorConfig.lastSlope is ignored on the Java side
|
|
final int length = parcelable.interpolatorConfig.xy.length;
|
|
if (length % 2 != 0) {
|
|
throw new android.os.BadParcelableException("xy length must be even");
|
|
}
|
|
final float[] times = new float[length / 2];
|
|
final float[] volumes = new float[length / 2];
|
|
for (int i = 0; i < length / 2; ++i) {
|
|
times[i] = parcelable.interpolatorConfig.xy[i * 2];
|
|
volumes[i] = parcelable.interpolatorConfig.xy[i * 2 + 1];
|
|
}
|
|
|
|
return new VolumeShaper.Configuration(
|
|
type,
|
|
id,
|
|
optionFlags,
|
|
durationMs,
|
|
interpolatorType,
|
|
times,
|
|
volumes);
|
|
}
|
|
}
|
|
|
|
public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Configuration> CREATOR
|
|
= new Parcelable.Creator<VolumeShaper.Configuration>() {
|
|
@Override
|
|
public VolumeShaper.Configuration createFromParcel(Parcel p) {
|
|
return fromParcelable(VolumeShaperConfiguration.CREATOR.createFromParcel(p));
|
|
}
|
|
|
|
@Override
|
|
public VolumeShaper.Configuration[] newArray(int size) {
|
|
return new VolumeShaper.Configuration[size];
|
|
}
|
|
};
|
|
|
|
private static @InterpolatorType
|
|
int interpolatorTypeFromAidl(@android.media.InterpolatorType int aidl) {
|
|
switch (aidl) {
|
|
case android.media.InterpolatorType.STEP:
|
|
return INTERPOLATOR_TYPE_STEP;
|
|
case android.media.InterpolatorType.LINEAR:
|
|
return INTERPOLATOR_TYPE_LINEAR;
|
|
case android.media.InterpolatorType.CUBIC:
|
|
return INTERPOLATOR_TYPE_CUBIC;
|
|
case android.media.InterpolatorType.CUBIC_MONOTONIC:
|
|
return INTERPOLATOR_TYPE_CUBIC_MONOTONIC;
|
|
default:
|
|
throw new BadParcelableException("Unknown interpolator type");
|
|
}
|
|
}
|
|
|
|
private static @android.media.InterpolatorType
|
|
int interpolatorTypeToAidl(@InterpolatorType int type) {
|
|
switch (type) {
|
|
case INTERPOLATOR_TYPE_STEP:
|
|
return android.media.InterpolatorType.STEP;
|
|
case INTERPOLATOR_TYPE_LINEAR:
|
|
return android.media.InterpolatorType.LINEAR;
|
|
case INTERPOLATOR_TYPE_CUBIC:
|
|
return android.media.InterpolatorType.CUBIC;
|
|
case INTERPOLATOR_TYPE_CUBIC_MONOTONIC:
|
|
return android.media.InterpolatorType.CUBIC_MONOTONIC;
|
|
default:
|
|
throw new RuntimeException("Unknown interpolator type");
|
|
}
|
|
}
|
|
|
|
private static @Type
|
|
int typeFromAidl(@android.media.VolumeShaperConfigurationType int aidl) {
|
|
switch (aidl) {
|
|
case VolumeShaperConfigurationType.ID:
|
|
return TYPE_ID;
|
|
case VolumeShaperConfigurationType.SCALE:
|
|
return TYPE_SCALE;
|
|
default:
|
|
throw new BadParcelableException("Unknown type");
|
|
}
|
|
}
|
|
|
|
private static @android.media.VolumeShaperConfigurationType
|
|
int typeToAidl(@Type int type) {
|
|
switch (type) {
|
|
case TYPE_ID:
|
|
return VolumeShaperConfigurationType.ID;
|
|
case TYPE_SCALE:
|
|
return VolumeShaperConfigurationType.SCALE;
|
|
default:
|
|
throw new RuntimeException("Unknown type");
|
|
}
|
|
}
|
|
|
|
private static int optionFlagsFromAidl(int aidl) {
|
|
int result = 0;
|
|
if ((aidl & (1 << VolumeShaperConfigurationOptionFlag.VOLUME_IN_DBFS)) != 0) {
|
|
result |= OPTION_FLAG_VOLUME_IN_DBFS;
|
|
}
|
|
if ((aidl & (1 << VolumeShaperConfigurationOptionFlag.CLOCK_TIME)) != 0) {
|
|
result |= OPTION_FLAG_CLOCK_TIME;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static int optionFlagsToAidl(int flags) {
|
|
int result = 0;
|
|
if ((flags & OPTION_FLAG_VOLUME_IN_DBFS) != 0) {
|
|
result |= (1 << VolumeShaperConfigurationOptionFlag.VOLUME_IN_DBFS);
|
|
}
|
|
if ((flags & OPTION_FLAG_CLOCK_TIME) != 0) {
|
|
result |= (1 << VolumeShaperConfigurationOptionFlag.CLOCK_TIME);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* Constructs a {@code VolumeShaper} from an id.
|
|
*
|
|
* This is an opaque handle for controlling a {@code VolumeShaper} that has
|
|
* already been sent to a player. The {@code id} is returned from the
|
|
* initial {@code setVolumeShaper()} call on success.
|
|
*
|
|
* These configurations are for native use only,
|
|
* they are never returned directly to the user.
|
|
*
|
|
* @param id
|
|
* @throws IllegalArgumentException if id is negative.
|
|
*/
|
|
public Configuration(int id) {
|
|
if (id < 0) {
|
|
throw new IllegalArgumentException("negative id " + id);
|
|
}
|
|
mType = TYPE_ID;
|
|
mId = id;
|
|
mInterpolatorType = 0;
|
|
mOptionFlags = 0;
|
|
mDurationMs = 0;
|
|
mTimes = null;
|
|
mVolumes = null;
|
|
}
|
|
|
|
/**
|
|
* Direct constructor for VolumeShaper.
|
|
* Use the Builder instead.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private Configuration(@Type int type,
|
|
int id,
|
|
@OptionFlag int optionFlags,
|
|
double durationMs,
|
|
@InterpolatorType int interpolatorType,
|
|
@NonNull float[] times,
|
|
@NonNull float[] volumes) {
|
|
mType = type;
|
|
mId = id;
|
|
mOptionFlags = optionFlags;
|
|
mDurationMs = durationMs;
|
|
mInterpolatorType = interpolatorType;
|
|
// Builder should have cloned these arrays already.
|
|
mTimes = times;
|
|
mVolumes = volumes;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* Returns the {@code VolumeShaper} type.
|
|
*/
|
|
public @Type int getType() {
|
|
return mType;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* Returns the {@code VolumeShaper} id.
|
|
*/
|
|
public int getId() {
|
|
return mId;
|
|
}
|
|
|
|
/**
|
|
* Returns the interpolator type.
|
|
*/
|
|
public @InterpolatorType int getInterpolatorType() {
|
|
return mInterpolatorType;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* Returns the option flags
|
|
*/
|
|
public @OptionFlag int getOptionFlags() {
|
|
return mOptionFlags & OPTION_FLAG_PUBLIC_ALL;
|
|
}
|
|
|
|
/* package */ @OptionFlag int getAllOptionFlags() {
|
|
return mOptionFlags;
|
|
}
|
|
|
|
/**
|
|
* Returns the duration of the volume shape in milliseconds.
|
|
*/
|
|
public long getDuration() {
|
|
// casting is safe here as the duration was set as a long in the Builder
|
|
return (long) mDurationMs;
|
|
}
|
|
|
|
/**
|
|
* Returns the times (x) coordinate array of the volume curve points.
|
|
*/
|
|
public float[] getTimes() {
|
|
return mTimes;
|
|
}
|
|
|
|
/**
|
|
* Returns the volumes (y) coordinate array of the volume curve points.
|
|
*/
|
|
public float[] getVolumes() {
|
|
return mVolumes;
|
|
}
|
|
|
|
/**
|
|
* Checks the validity of times and volumes point representation.
|
|
*
|
|
* {@code times[]} and {@code volumes[]} are two arrays representing points
|
|
* for the volume curve.
|
|
*
|
|
* Note that {@code times[]} and {@code volumes[]} are explicitly checked against
|
|
* null here to provide the proper error string - those are legitimate
|
|
* arguments to this method.
|
|
*
|
|
* @param times the x coordinates for the points,
|
|
* must be between 0.f and 1.f and be monotonic.
|
|
* @param volumes the y coordinates for the points,
|
|
* must be between 0.f and 1.f for linear and
|
|
* must be no greater than 0.f for log (dBFS).
|
|
* @param log set to true if the scale is logarithmic.
|
|
* @return null if no error, or the reason in a {@code String} for an error.
|
|
*/
|
|
private static @Nullable String checkCurveForErrors(
|
|
@Nullable float[] times, @Nullable float[] volumes, boolean log) {
|
|
if (times == null) {
|
|
return "times array must be non-null";
|
|
} else if (volumes == null) {
|
|
return "volumes array must be non-null";
|
|
} else if (times.length != volumes.length) {
|
|
return "array length must match";
|
|
} else if (times.length < 2) {
|
|
return "array length must be at least 2";
|
|
} else if (times.length > MAXIMUM_CURVE_POINTS) {
|
|
return "array length must be no larger than " + MAXIMUM_CURVE_POINTS;
|
|
} else if (times[0] != 0.f) {
|
|
return "times must start at 0.f";
|
|
} else if (times[times.length - 1] != 1.f) {
|
|
return "times must end at 1.f";
|
|
}
|
|
|
|
// validate points along the curve
|
|
for (int i = 1; i < times.length; ++i) {
|
|
if (!(times[i] > times[i - 1]) /* handle nan */) {
|
|
return "times not monotonic increasing, check index " + i;
|
|
}
|
|
}
|
|
if (log) {
|
|
for (int i = 0; i < volumes.length; ++i) {
|
|
if (!(volumes[i] <= 0.f) /* handle nan */) {
|
|
return "volumes for log scale cannot be positive, "
|
|
+ "check index " + i;
|
|
}
|
|
}
|
|
} else {
|
|
for (int i = 0; i < volumes.length; ++i) {
|
|
if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) {
|
|
return "volumes for linear scale must be between 0.f and 1.f, "
|
|
+ "check index " + i;
|
|
}
|
|
}
|
|
}
|
|
return null; // no errors
|
|
}
|
|
|
|
private static void checkCurveForErrorsAndThrowException(
|
|
@Nullable float[] times, @Nullable float[] volumes, boolean log, boolean ise) {
|
|
final String error = checkCurveForErrors(times, volumes, log);
|
|
if (error != null) {
|
|
if (ise) {
|
|
throw new IllegalStateException(error);
|
|
} else {
|
|
throw new IllegalArgumentException(error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void checkValidVolumeAndThrowException(float volume, boolean log) {
|
|
if (log) {
|
|
if (!(volume <= 0.f) /* handle nan */) {
|
|
throw new IllegalArgumentException("dbfs volume must be 0.f or less");
|
|
}
|
|
} else {
|
|
if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) {
|
|
throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void clampVolume(float[] volumes, boolean log) {
|
|
if (log) {
|
|
for (int i = 0; i < volumes.length; ++i) {
|
|
if (!(volumes[i] <= 0.f) /* handle nan */) {
|
|
volumes[i] = 0.f;
|
|
}
|
|
}
|
|
} else {
|
|
for (int i = 0; i < volumes.length; ++i) {
|
|
if (!(volumes[i] >= 0.f) /* handle nan */) {
|
|
volumes[i] = 0.f;
|
|
} else if (!(volumes[i] <= 1.f)) {
|
|
volumes[i] = 1.f;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builder class for a {@link VolumeShaper.Configuration} object.
|
|
* <p> Here is an example where {@code Builder} is used to define the
|
|
* {@link VolumeShaper.Configuration}.
|
|
*
|
|
* <pre class="prettyprint">
|
|
* VolumeShaper.Configuration LINEAR_RAMP =
|
|
* new VolumeShaper.Configuration.Builder()
|
|
* .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
|
|
* .setCurve(new float[] { 0.f, 1.f }, // times
|
|
* new float[] { 0.f, 1.f }) // volumes
|
|
* .setDuration(1000)
|
|
* .build();
|
|
* </pre>
|
|
* <p>
|
|
*/
|
|
public static final class Builder {
|
|
private int mType = TYPE_SCALE;
|
|
private int mId = -1; // invalid
|
|
private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC;
|
|
private int mOptionFlags = OPTION_FLAG_CLOCK_TIME;
|
|
private double mDurationMs = 1000.;
|
|
private float[] mTimes = null;
|
|
private float[] mVolumes = null;
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} with the defaults.
|
|
*/
|
|
public Builder() {
|
|
}
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} with settings
|
|
* copied from a given {@code VolumeShaper.Configuration}.
|
|
* @param configuration prototypical configuration
|
|
* which will be reused in the new {@code Builder}.
|
|
*/
|
|
public Builder(@NonNull Configuration configuration) {
|
|
mType = configuration.getType();
|
|
mId = configuration.getId();
|
|
mOptionFlags = configuration.getAllOptionFlags();
|
|
mInterpolatorType = configuration.getInterpolatorType();
|
|
mDurationMs = configuration.getDuration();
|
|
mTimes = configuration.getTimes().clone();
|
|
mVolumes = configuration.getVolumes().clone();
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* Set the {@code id} for system defined shapers.
|
|
* @param id the {@code id} to set. If non-negative, then it is used.
|
|
* If -1, then the system is expected to assign one.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code id} < -1.
|
|
*/
|
|
public @NonNull Builder setId(int id) {
|
|
if (id < -1) {
|
|
throw new IllegalArgumentException("invalid id: " + id);
|
|
}
|
|
mId = id;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the interpolator type.
|
|
*
|
|
* If omitted the default interpolator type is {@link #INTERPOLATOR_TYPE_CUBIC}.
|
|
*
|
|
* @param interpolatorType method of interpolation used for the volume curve.
|
|
* One of {@link #INTERPOLATOR_TYPE_STEP},
|
|
* {@link #INTERPOLATOR_TYPE_LINEAR},
|
|
* {@link #INTERPOLATOR_TYPE_CUBIC},
|
|
* {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code interpolatorType} is not valid.
|
|
*/
|
|
public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) {
|
|
switch (interpolatorType) {
|
|
case INTERPOLATOR_TYPE_STEP:
|
|
case INTERPOLATOR_TYPE_LINEAR:
|
|
case INTERPOLATOR_TYPE_CUBIC:
|
|
case INTERPOLATOR_TYPE_CUBIC_MONOTONIC:
|
|
mInterpolatorType = interpolatorType;
|
|
break;
|
|
default:
|
|
throw new IllegalArgumentException("invalid interpolatorType: "
|
|
+ interpolatorType);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* Sets the optional flags
|
|
*
|
|
* If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has
|
|
* changed the volume curve needs to be set again as the acceptable
|
|
* volume domain has changed.
|
|
*
|
|
* @param optionFlags new value to replace the old {@code optionFlags}.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if flag is not recognized.
|
|
*/
|
|
@TestApi
|
|
public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) {
|
|
if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) {
|
|
throw new IllegalArgumentException("invalid bits in flag: " + optionFlags);
|
|
}
|
|
mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the {@code VolumeShaper} duration in milliseconds.
|
|
*
|
|
* If omitted, the default duration is 1 second.
|
|
*
|
|
* @param durationMillis
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code durationMillis}
|
|
* is not strictly positive.
|
|
*/
|
|
public @NonNull Builder setDuration(long durationMillis) {
|
|
if (durationMillis <= 0) {
|
|
throw new IllegalArgumentException(
|
|
"duration: " + durationMillis + " not positive");
|
|
}
|
|
mDurationMs = (double) durationMillis;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the volume curve.
|
|
*
|
|
* The volume curve is represented by a set of control points given by
|
|
* two float arrays of equal length,
|
|
* one representing the time (x) coordinates
|
|
* and one corresponding to the volume (y) coordinates.
|
|
* The length must be at least 2
|
|
* and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}.
|
|
* <p>
|
|
* The volume curve is normalized as follows:
|
|
* time (x) coordinates should be monotonically increasing, from 0.f to 1.f;
|
|
* volume (y) coordinates must be within 0.f to 1.f.
|
|
* <p>
|
|
* The time scale is set by {@link #setDuration}.
|
|
* <p>
|
|
* @param times an array of float values representing
|
|
* the time line of the volume curve.
|
|
* @param volumes an array of float values representing
|
|
* the amplitude of the volume curve.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid.
|
|
*/
|
|
|
|
/* Note: volume (y) coordinates must be non-positive for log scaling,
|
|
* if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set.
|
|
*/
|
|
|
|
public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) {
|
|
final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
|
|
checkCurveForErrorsAndThrowException(times, volumes, log, false /* ise */);
|
|
mTimes = times.clone();
|
|
mVolumes = volumes.clone();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Reflects the volume curve so that
|
|
* the shaper changes volume from the end
|
|
* to the start.
|
|
*
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalStateException if curve has not been set.
|
|
*/
|
|
public @NonNull Builder reflectTimes() {
|
|
final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
|
|
checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
|
|
int i;
|
|
for (i = 0; i < mTimes.length / 2; ++i) {
|
|
float temp = mTimes[i];
|
|
mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i];
|
|
mTimes[mTimes.length - 1 - i] = 1.f - temp;
|
|
temp = mVolumes[i];
|
|
mVolumes[i] = mVolumes[mVolumes.length - 1 - i];
|
|
mVolumes[mVolumes.length - 1 - i] = temp;
|
|
}
|
|
if ((mTimes.length & 1) != 0) {
|
|
mTimes[i] = 1.f - mTimes[i];
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Inverts the volume curve so that the max volume
|
|
* becomes the min volume and vice versa.
|
|
*
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalStateException if curve has not been set.
|
|
*/
|
|
public @NonNull Builder invertVolumes() {
|
|
final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
|
|
checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
|
|
float min = mVolumes[0];
|
|
float max = mVolumes[0];
|
|
for (int i = 1; i < mVolumes.length; ++i) {
|
|
if (mVolumes[i] < min) {
|
|
min = mVolumes[i];
|
|
} else if (mVolumes[i] > max) {
|
|
max = mVolumes[i];
|
|
}
|
|
}
|
|
|
|
final float maxmin = max + min;
|
|
for (int i = 0; i < mVolumes.length; ++i) {
|
|
mVolumes[i] = maxmin - mVolumes[i];
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Scale the curve end volume to a target value.
|
|
*
|
|
* Keeps the start volume the same.
|
|
* This works best if the volume curve is monotonic.
|
|
*
|
|
* @param volume the target end volume to use.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code volume} is not valid.
|
|
* @throws IllegalStateException if curve has not been set.
|
|
*/
|
|
public @NonNull Builder scaleToEndVolume(float volume) {
|
|
final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
|
|
checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
|
|
checkValidVolumeAndThrowException(volume, log);
|
|
final float startVolume = mVolumes[0];
|
|
final float endVolume = mVolumes[mVolumes.length - 1];
|
|
if (endVolume == startVolume) {
|
|
// match with linear ramp
|
|
final float offset = volume - startVolume;
|
|
for (int i = 0; i < mVolumes.length; ++i) {
|
|
mVolumes[i] = mVolumes[i] + offset * mTimes[i];
|
|
}
|
|
} else {
|
|
// scale
|
|
final float scale = (volume - startVolume) / (endVolume - startVolume);
|
|
for (int i = 0; i < mVolumes.length; ++i) {
|
|
mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume;
|
|
}
|
|
}
|
|
clampVolume(mVolumes, log);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Scale the curve start volume to a target value.
|
|
*
|
|
* Keeps the end volume the same.
|
|
* This works best if the volume curve is monotonic.
|
|
*
|
|
* @param volume the target start volume to use.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code volume} is not valid.
|
|
* @throws IllegalStateException if curve has not been set.
|
|
*/
|
|
public @NonNull Builder scaleToStartVolume(float volume) {
|
|
final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
|
|
checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
|
|
checkValidVolumeAndThrowException(volume, log);
|
|
final float startVolume = mVolumes[0];
|
|
final float endVolume = mVolumes[mVolumes.length - 1];
|
|
if (endVolume == startVolume) {
|
|
// match with linear ramp
|
|
final float offset = volume - startVolume;
|
|
for (int i = 0; i < mVolumes.length; ++i) {
|
|
mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]);
|
|
}
|
|
} else {
|
|
final float scale = (volume - endVolume) / (startVolume - endVolume);
|
|
for (int i = 0; i < mVolumes.length; ++i) {
|
|
mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume;
|
|
}
|
|
}
|
|
clampVolume(mVolumes, log);
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Builds a new {@link VolumeShaper} object.
|
|
*
|
|
* @return a new {@link VolumeShaper} object.
|
|
* @throws IllegalStateException if curve is not properly set.
|
|
*/
|
|
public @NonNull Configuration build() {
|
|
final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
|
|
checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
|
|
return new Configuration(mType, mId, mOptionFlags, mDurationMs,
|
|
mInterpolatorType, mTimes, mVolumes);
|
|
}
|
|
} // Configuration.Builder
|
|
} // Configuration
|
|
|
|
/**
|
|
* The {@code VolumeShaper.Operation} class is used to specify operations
|
|
* to the {@code VolumeShaper} that affect the volume change.
|
|
*/
|
|
public static final class Operation implements Parcelable {
|
|
/**
|
|
* Forward playback from current volume time position.
|
|
* At the end of the {@code VolumeShaper} curve,
|
|
* the last volume value persists.
|
|
*/
|
|
public static final Operation PLAY =
|
|
new VolumeShaper.Operation.Builder()
|
|
.build();
|
|
|
|
/**
|
|
* Reverse playback from current volume time position.
|
|
* When the position reaches the start of the {@code VolumeShaper} curve,
|
|
* the first volume value persists.
|
|
*/
|
|
public static final Operation REVERSE =
|
|
new VolumeShaper.Operation.Builder()
|
|
.reverse()
|
|
.build();
|
|
|
|
// No user serviceable parts below.
|
|
|
|
// These flags must match the native VolumeShaper::Operation::Flag
|
|
/** @hide */
|
|
@IntDef({
|
|
FLAG_NONE,
|
|
FLAG_REVERSE,
|
|
FLAG_TERMINATE,
|
|
FLAG_JOIN,
|
|
FLAG_DEFER,
|
|
})
|
|
@Retention(RetentionPolicy.SOURCE)
|
|
public @interface Flag {}
|
|
|
|
/**
|
|
* No special {@code VolumeShaper} operation.
|
|
*/
|
|
private static final int FLAG_NONE = 0;
|
|
|
|
/**
|
|
* Reverse the {@code VolumeShaper} progress.
|
|
*
|
|
* Reverses the {@code VolumeShaper} curve from its current
|
|
* position. If the {@code VolumeShaper} curve has not started,
|
|
* it automatically is considered finished.
|
|
*/
|
|
private static final int FLAG_REVERSE = 1 << 0;
|
|
|
|
/**
|
|
* Terminate the existing {@code VolumeShaper}.
|
|
* This flag is generally used by itself;
|
|
* it takes precedence over all other flags.
|
|
*/
|
|
private static final int FLAG_TERMINATE = 1 << 1;
|
|
|
|
/**
|
|
* Attempt to join as best as possible to the previous {@code VolumeShaper}.
|
|
* This requires the previous {@code VolumeShaper} to be active and
|
|
* {@link #setReplaceId} to be set.
|
|
*/
|
|
private static final int FLAG_JOIN = 1 << 2;
|
|
|
|
/**
|
|
* Defer playback until next operation is sent. This is used
|
|
* when starting a {@code VolumeShaper} effect.
|
|
*/
|
|
private static final int FLAG_DEFER = 1 << 3;
|
|
|
|
/**
|
|
* Use the id specified in the configuration, creating
|
|
* {@code VolumeShaper} as needed; the configuration should be
|
|
* TYPE_SCALE.
|
|
*/
|
|
private static final int FLAG_CREATE_IF_NEEDED = 1 << 4;
|
|
|
|
private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE;
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final int mFlags;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final int mReplaceId;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private final float mXOffset;
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "VolumeShaper.Operation{"
|
|
+ "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase()
|
|
+ ", mReplaceId = " + mReplaceId
|
|
+ ", mXOffset = " + mXOffset
|
|
+ "}";
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(mFlags, mReplaceId, mXOffset);
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (!(o instanceof Operation)) return false;
|
|
if (o == this) return true;
|
|
final Operation other = (Operation) o;
|
|
|
|
return mFlags == other.mFlags
|
|
&& mReplaceId == other.mReplaceId
|
|
&& Float.compare(mXOffset, other.mXOffset) == 0;
|
|
}
|
|
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel dest, int flags) {
|
|
toParcelable().writeToParcel(dest, flags);
|
|
}
|
|
|
|
/** @hide */
|
|
public VolumeShaperOperation toParcelable() {
|
|
VolumeShaperOperation result = new VolumeShaperOperation();
|
|
result.flags = flagsToAidl(mFlags);
|
|
result.replaceId = mReplaceId;
|
|
result.xOffset = mXOffset;
|
|
return result;
|
|
}
|
|
|
|
/** @hide */
|
|
public static Operation fromParcelable(VolumeShaperOperation parcelable) {
|
|
return new VolumeShaper.Operation(
|
|
flagsFromAidl(parcelable.flags),
|
|
parcelable.replaceId,
|
|
parcelable.xOffset);
|
|
}
|
|
|
|
public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Operation> CREATOR
|
|
= new Parcelable.Creator<VolumeShaper.Operation>() {
|
|
@Override
|
|
public VolumeShaper.Operation createFromParcel(Parcel p) {
|
|
return fromParcelable(VolumeShaperOperation.CREATOR.createFromParcel(p));
|
|
}
|
|
|
|
@Override
|
|
public VolumeShaper.Operation[] newArray(int size) {
|
|
return new VolumeShaper.Operation[size];
|
|
}
|
|
};
|
|
|
|
private static int flagsFromAidl(int aidl) {
|
|
int result = 0;
|
|
if ((aidl & (1 << VolumeShaperOperationFlag.REVERSE)) != 0) {
|
|
result |= FLAG_REVERSE;
|
|
}
|
|
if ((aidl & (1 << VolumeShaperOperationFlag.TERMINATE)) != 0) {
|
|
result |= FLAG_TERMINATE;
|
|
}
|
|
if ((aidl & (1 << VolumeShaperOperationFlag.JOIN)) != 0) {
|
|
result |= FLAG_JOIN;
|
|
}
|
|
if ((aidl & (1 << VolumeShaperOperationFlag.DELAY)) != 0) {
|
|
result |= FLAG_DEFER;
|
|
}
|
|
if ((aidl & (1 << VolumeShaperOperationFlag.CREATE_IF_NECESSARY)) != 0) {
|
|
result |= FLAG_CREATE_IF_NEEDED;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private static int flagsToAidl(int flags) {
|
|
int result = 0;
|
|
if ((flags & FLAG_REVERSE) != 0) {
|
|
result |= (1 << VolumeShaperOperationFlag.REVERSE);
|
|
}
|
|
if ((flags & FLAG_TERMINATE) != 0) {
|
|
result |= (1 << VolumeShaperOperationFlag.TERMINATE);
|
|
}
|
|
if ((flags & FLAG_JOIN) != 0) {
|
|
result |= (1 << VolumeShaperOperationFlag.JOIN);
|
|
}
|
|
if ((flags & FLAG_DEFER) != 0) {
|
|
result |= (1 << VolumeShaperOperationFlag.DELAY);
|
|
}
|
|
if ((flags & FLAG_CREATE_IF_NEEDED) != 0) {
|
|
result |= (1 << VolumeShaperOperationFlag.CREATE_IF_NECESSARY);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private Operation(@Flag int flags, int replaceId, float xOffset) {
|
|
mFlags = flags;
|
|
mReplaceId = replaceId;
|
|
mXOffset = xOffset;
|
|
}
|
|
|
|
/**
|
|
* @hide
|
|
* {@code Builder} class for {@link VolumeShaper.Operation} object.
|
|
*
|
|
* Not for public use.
|
|
*/
|
|
public static final class Builder {
|
|
int mFlags;
|
|
int mReplaceId;
|
|
float mXOffset;
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} with the defaults.
|
|
*/
|
|
public Builder() {
|
|
mFlags = 0;
|
|
mReplaceId = -1;
|
|
mXOffset = Float.NaN;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new {@code Builder} from a given {@code VolumeShaper.Operation}
|
|
* @param operation the {@code VolumeShaper.operation} whose data will be
|
|
* reused in the new {@code Builder}.
|
|
*/
|
|
public Builder(@NonNull VolumeShaper.Operation operation) {
|
|
mReplaceId = operation.mReplaceId;
|
|
mFlags = operation.mFlags;
|
|
mXOffset = operation.mXOffset;
|
|
}
|
|
|
|
/**
|
|
* Replaces the previous {@code VolumeShaper} specified by {@code id}.
|
|
*
|
|
* The {@code VolumeShaper} specified by the {@code id} is removed
|
|
* if it exists. The configuration should be TYPE_SCALE.
|
|
*
|
|
* @param id the {@code id} of the previous {@code VolumeShaper}.
|
|
* @param join if true, match the volume of the previous
|
|
* shaper to the start volume of the new {@code VolumeShaper}.
|
|
* @return the same {@code Builder} instance.
|
|
*/
|
|
public @NonNull Builder replace(int id, boolean join) {
|
|
mReplaceId = id;
|
|
if (join) {
|
|
mFlags |= FLAG_JOIN;
|
|
} else {
|
|
mFlags &= ~FLAG_JOIN;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Defers all operations.
|
|
* @return the same {@code Builder} instance.
|
|
*/
|
|
public @NonNull Builder defer() {
|
|
mFlags |= FLAG_DEFER;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Terminates the {@code VolumeShaper}.
|
|
*
|
|
* Do not call directly, use {@link VolumeShaper#close()}.
|
|
* @return the same {@code Builder} instance.
|
|
*/
|
|
public @NonNull Builder terminate() {
|
|
mFlags |= FLAG_TERMINATE;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Reverses direction.
|
|
* @return the same {@code Builder} instance.
|
|
*/
|
|
public @NonNull Builder reverse() {
|
|
mFlags ^= FLAG_REVERSE;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Use the id specified in the configuration, creating
|
|
* {@code VolumeShaper} only as needed; the configuration should be
|
|
* TYPE_SCALE.
|
|
*
|
|
* If the {@code VolumeShaper} with the same id already exists
|
|
* then the operation has no effect.
|
|
*
|
|
* @return the same {@code Builder} instance.
|
|
*/
|
|
public @NonNull Builder createIfNeeded() {
|
|
mFlags |= FLAG_CREATE_IF_NEEDED;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the {@code xOffset} to use for the {@code VolumeShaper}.
|
|
*
|
|
* The {@code xOffset} is the position on the volume curve,
|
|
* and setting takes effect when the {@code VolumeShaper} is used next.
|
|
*
|
|
* @param xOffset a value between (or equal to) 0.f and 1.f, or Float.NaN to ignore.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code xOffset} is not between 0.f and 1.f,
|
|
* or a Float.NaN.
|
|
*/
|
|
public @NonNull Builder setXOffset(float xOffset) {
|
|
if (xOffset < -0.f) {
|
|
throw new IllegalArgumentException("Negative xOffset not allowed");
|
|
} else if (xOffset > 1.f) {
|
|
throw new IllegalArgumentException("xOffset > 1.f not allowed");
|
|
}
|
|
// Float.NaN passes through
|
|
mXOffset = xOffset;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets the operation flag. Do not call this directly but one of the
|
|
* other builder methods.
|
|
*
|
|
* @param flags new value for {@code flags}, consisting of ORed flags.
|
|
* @return the same {@code Builder} instance.
|
|
* @throws IllegalArgumentException if {@code flags} contains invalid set bits.
|
|
*/
|
|
private @NonNull Builder setFlags(@Flag int flags) {
|
|
if ((flags & ~FLAG_PUBLIC_ALL) != 0) {
|
|
throw new IllegalArgumentException("flag has unknown bits set: " + flags);
|
|
}
|
|
mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Builds a new {@link VolumeShaper.Operation} object.
|
|
*
|
|
* @return a new {@code VolumeShaper.Operation} object
|
|
*/
|
|
public @NonNull Operation build() {
|
|
return new Operation(mFlags, mReplaceId, mXOffset);
|
|
}
|
|
} // Operation.Builder
|
|
} // Operation
|
|
|
|
/**
|
|
* @hide
|
|
* {@code VolumeShaper.State} represents the current progress
|
|
* of the {@code VolumeShaper}.
|
|
*
|
|
* Not for public use.
|
|
*/
|
|
public static final class State implements Parcelable {
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private float mVolume;
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private float mXOffset;
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "VolumeShaper.State{"
|
|
+ "mVolume = " + mVolume
|
|
+ ", mXOffset = " + mXOffset
|
|
+ "}";
|
|
}
|
|
|
|
@Override
|
|
public int hashCode() {
|
|
return Objects.hash(mVolume, mXOffset);
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (!(o instanceof State)) return false;
|
|
if (o == this) return true;
|
|
final State other = (State) o;
|
|
return mVolume == other.mVolume
|
|
&& mXOffset == other.mXOffset;
|
|
}
|
|
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(Parcel dest, int flags) {
|
|
toParcelable().writeToParcel(dest, flags);
|
|
}
|
|
|
|
/** @hide */
|
|
public VolumeShaperState toParcelable() {
|
|
VolumeShaperState result = new VolumeShaperState();
|
|
result.volume = mVolume;
|
|
result.xOffset = mXOffset;
|
|
return result;
|
|
}
|
|
|
|
/** @hide */
|
|
public static State fromParcelable(VolumeShaperState p) {
|
|
return new VolumeShaper.State(p.volume, p.xOffset);
|
|
}
|
|
|
|
public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.State> CREATOR
|
|
= new Parcelable.Creator<VolumeShaper.State>() {
|
|
@Override
|
|
public VolumeShaper.State createFromParcel(Parcel p) {
|
|
return fromParcelable(VolumeShaperState.CREATOR.createFromParcel(p));
|
|
}
|
|
|
|
@Override
|
|
public VolumeShaper.State[] newArray(int size) {
|
|
return new VolumeShaper.State[size];
|
|
}
|
|
};
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
/* package */ State(float volume, float xOffset) {
|
|
mVolume = volume;
|
|
mXOffset = xOffset;
|
|
}
|
|
|
|
/**
|
|
* Gets the volume of the {@link VolumeShaper.State}.
|
|
* @return linear volume between 0.f and 1.f.
|
|
*/
|
|
public float getVolume() {
|
|
return mVolume;
|
|
}
|
|
|
|
/**
|
|
* Gets the {@code xOffset} position on the normalized curve
|
|
* of the {@link VolumeShaper.State}.
|
|
* @return the curve x position between 0.f and 1.f.
|
|
*/
|
|
public float getXOffset() {
|
|
return mXOffset;
|
|
}
|
|
} // State
|
|
}
|