content: /*
   * 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.annotation.UnsupportedAppUsage;
  import android.os.Parcel;
  import android.os.Parcelable;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.AutoCloseable;
  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
  }
  