Automatic sources dropoff on 2020-06-10 18:32:38.095721
The change is generated with prebuilt drop tool.
Change-Id: I24cbf6ba6db262a1ae1445db1427a08fee35b3b4
diff --git a/android/media/VolumeShaper.java b/android/media/VolumeShaper.java
new file mode 100644
index 0000000..99dfe1e
--- /dev/null
+++ b/android/media/VolumeShaper.java
@@ -0,0 +1,1437 @@
+/*
+ * 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.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
+ private final int mType;
+
+ // valid when mType is TYPE_ID
+ @UnsupportedAppUsage
+ private final int mId;
+
+ // valid when mType is TYPE_SCALE
+ @UnsupportedAppUsage
+ private final int mOptionFlags;
+ @UnsupportedAppUsage
+ private final double mDurationMs;
+ @UnsupportedAppUsage
+ private final int mInterpolatorType;
+ @UnsupportedAppUsage
+ private final float[] mTimes;
+ @UnsupportedAppUsage
+ 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) {
+ // this needs to match the native VolumeShaper.Configuration parceling
+ dest.writeInt(mType);
+ dest.writeInt(mId);
+ if (mType != TYPE_ID) {
+ dest.writeInt(mOptionFlags);
+ dest.writeDouble(mDurationMs);
+ // this needs to match the native Interpolator parceling
+ dest.writeInt(mInterpolatorType);
+ dest.writeFloat(0.f); // first slope (specifying for native side)
+ dest.writeFloat(0.f); // last slope (specifying for native side)
+ // mTimes and mVolumes should have the same length.
+ dest.writeInt(mTimes.length);
+ for (int i = 0; i < mTimes.length; ++i) {
+ dest.writeFloat(mTimes[i]);
+ dest.writeFloat(mVolumes[i]);
+ }
+ }
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Configuration> CREATOR
+ = new Parcelable.Creator<VolumeShaper.Configuration>() {
+ @Override
+ public VolumeShaper.Configuration createFromParcel(Parcel p) {
+ // this needs to match the native VolumeShaper.Configuration parceling
+ final int type = p.readInt();
+ final int id = p.readInt();
+ if (type == TYPE_ID) {
+ return new VolumeShaper.Configuration(id);
+ } else {
+ final int optionFlags = p.readInt();
+ final double durationMs = p.readDouble();
+ // this needs to match the native Interpolator parceling
+ final int interpolatorType = p.readInt();
+ final float firstSlope = p.readFloat(); // ignored on the Java side
+ final float lastSlope = p.readFloat(); // ignored on the Java side
+ final int length = p.readInt();
+ final float[] times = new float[length];
+ final float[] volumes = new float[length];
+ for (int i = 0; i < length; ++i) {
+ times[i] = p.readFloat();
+ volumes[i] = p.readFloat();
+ }
+
+ return new VolumeShaper.Configuration(
+ type,
+ id,
+ optionFlags,
+ durationMs,
+ interpolatorType,
+ times,
+ volumes);
+ }
+ }
+
+ @Override
+ public VolumeShaper.Configuration[] newArray(int size) {
+ return new VolumeShaper.Configuration[size];
+ }
+ };
+
+ /**
+ * @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
+ 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
+ private final int mFlags;
+ @UnsupportedAppUsage
+ private final int mReplaceId;
+ @UnsupportedAppUsage
+ 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) {
+ // this needs to match the native VolumeShaper.Operation parceling
+ dest.writeInt(mFlags);
+ dest.writeInt(mReplaceId);
+ dest.writeFloat(mXOffset);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Operation> CREATOR
+ = new Parcelable.Creator<VolumeShaper.Operation>() {
+ @Override
+ public VolumeShaper.Operation createFromParcel(Parcel p) {
+ // this needs to match the native VolumeShaper.Operation parceling
+ final int flags = p.readInt();
+ final int replaceId = p.readInt();
+ final float xOffset = p.readFloat();
+
+ return new VolumeShaper.Operation(
+ flags
+ , replaceId
+ , xOffset);
+ }
+
+ @Override
+ public VolumeShaper.Operation[] newArray(int size) {
+ return new VolumeShaper.Operation[size];
+ }
+ };
+
+ @UnsupportedAppUsage
+ 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
+ private float mVolume;
+ @UnsupportedAppUsage
+ 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) {
+ dest.writeFloat(mVolume);
+ dest.writeFloat(mXOffset);
+ }
+
+ public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.State> CREATOR
+ = new Parcelable.Creator<VolumeShaper.State>() {
+ @Override
+ public VolumeShaper.State createFromParcel(Parcel p) {
+ return new VolumeShaper.State(
+ p.readFloat() // volume
+ , p.readFloat()); // xOffset
+ }
+
+ @Override
+ public VolumeShaper.State[] newArray(int size) {
+ return new VolumeShaper.State[size];
+ }
+ };
+
+ @UnsupportedAppUsage
+ /* 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
+}