1 /* 2 * Copyright 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.media; 17 18 import android.annotation.IntDef; 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.TestApi; 22 import android.annotation.UnsupportedAppUsage; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 26 import java.lang.annotation.Retention; 27 import java.lang.annotation.RetentionPolicy; 28 import java.lang.AutoCloseable; 29 import java.lang.ref.WeakReference; 30 import java.util.Arrays; 31 import java.util.Objects; 32 33 /** 34 * The {@code VolumeShaper} class is used to automatically control audio volume during media 35 * playback, allowing simple implementation of transition effects and ducking. 36 * It is created from implementations of {@code VolumeAutomation}, 37 * such as {@code MediaPlayer} and {@code AudioTrack} (referred to as "players" below), 38 * by {@link MediaPlayer#createVolumeShaper} or {@link AudioTrack#createVolumeShaper}. 39 * 40 * A {@code VolumeShaper} is intended for short volume changes. 41 * If the audio output sink changes during 42 * a {@code VolumeShaper} transition, the precise curve position may be lost, and the 43 * {@code VolumeShaper} may advance to the end of the curve for the new audio output sink. 44 * 45 * The {@code VolumeShaper} appears as an additional scaling on the audio output, 46 * and adjusts independently of track or stream volume controls. 47 */ 48 public final class VolumeShaper implements AutoCloseable { 49 /* member variables */ 50 private int mId; 51 private final WeakReference<PlayerBase> mWeakPlayerBase; 52 VolumeShaper( @onNull Configuration configuration, @NonNull PlayerBase playerBase)53 /* package */ VolumeShaper( 54 @NonNull Configuration configuration, @NonNull PlayerBase playerBase) { 55 mWeakPlayerBase = new WeakReference<PlayerBase>(playerBase); 56 mId = applyPlayer(configuration, new Operation.Builder().defer().build()); 57 } 58 getId()59 /* package */ int getId() { 60 return mId; 61 } 62 63 /** 64 * Applies the {@link VolumeShaper.Operation} to the {@code VolumeShaper}. 65 * 66 * Applying {@link VolumeShaper.Operation#PLAY} after {@code PLAY} 67 * or {@link VolumeShaper.Operation#REVERSE} after 68 * {@code REVERSE} has no effect. 69 * 70 * Applying {@link VolumeShaper.Operation#PLAY} when the player 71 * hasn't started will synchronously start the {@code VolumeShaper} when 72 * playback begins. 73 * 74 * @param operation the {@code operation} to apply. 75 * @throws IllegalStateException if the player is uninitialized or if there 76 * is a critical failure. In that case, the {@code VolumeShaper} should be 77 * recreated. 78 */ apply(@onNull Operation operation)79 public void apply(@NonNull Operation operation) { 80 /* void */ applyPlayer(new VolumeShaper.Configuration(mId), operation); 81 } 82 83 /** 84 * Replaces the current {@code VolumeShaper} 85 * {@code configuration} with a new {@code configuration}. 86 * 87 * This allows the user to change the volume shape 88 * while the existing {@code VolumeShaper} is in effect. 89 * 90 * The effect of {@code replace()} is similar to an atomic close of 91 * the existing {@code VolumeShaper} and creation of a new {@code VolumeShaper}. 92 * 93 * If the {@code operation} is {@link VolumeShaper.Operation#PLAY} then the 94 * new curve starts immediately. 95 * 96 * If the {@code operation} is 97 * {@link VolumeShaper.Operation#REVERSE}, then the new curve will 98 * be delayed until {@code PLAY} is applied. 99 * 100 * @param configuration the new {@code configuration} to use. 101 * @param operation the {@code operation} to apply to the {@code VolumeShaper} 102 * @param join if true, match the start volume of the 103 * new {@code configuration} to the current volume of the existing 104 * {@code VolumeShaper}, to avoid discontinuity. 105 * @throws IllegalStateException if the player is uninitialized or if there 106 * is a critical failure. In that case, the {@code VolumeShaper} should be 107 * recreated. 108 */ replace( @onNull Configuration configuration, @NonNull Operation operation, boolean join)109 public void replace( 110 @NonNull Configuration configuration, @NonNull Operation operation, boolean join) { 111 mId = applyPlayer( 112 configuration, 113 new Operation.Builder(operation).replace(mId, join).build()); 114 } 115 116 /** 117 * Returns the current volume scale attributable to the {@code VolumeShaper}. 118 * 119 * This is the last volume from the {@code VolumeShaper} used for the player, 120 * or the initial volume if the {@code VolumeShaper} hasn't been started with 121 * {@link VolumeShaper.Operation#PLAY}. 122 * 123 * @return the volume, linearly represented as a value between 0.f and 1.f. 124 * @throws IllegalStateException if the player is uninitialized or if there 125 * is a critical failure. In that case, the {@code VolumeShaper} should be 126 * recreated. 127 */ getVolume()128 public float getVolume() { 129 return getStatePlayer(mId).getVolume(); 130 } 131 132 /** 133 * Releases the {@code VolumeShaper} object; any volume scale due to the 134 * {@code VolumeShaper} is removed after closing. 135 * 136 * If the volume does not reach 1.f when the {@code VolumeShaper} is closed 137 * (or finalized), there may be an abrupt change of volume. 138 * 139 * {@code close()} may be safely called after a prior {@code close()}. 140 * This class implements the Java {@code AutoClosable} interface and 141 * may be used with try-with-resources. 142 */ 143 @Override close()144 public void close() { 145 try { 146 /* void */ applyPlayer( 147 new VolumeShaper.Configuration(mId), 148 new Operation.Builder().terminate().build()); 149 } catch (IllegalStateException ise) { 150 ; // ok 151 } 152 if (mWeakPlayerBase != null) { 153 mWeakPlayerBase.clear(); 154 } 155 } 156 157 @Override finalize()158 protected void finalize() { 159 close(); // ensure we remove the native VolumeShaper 160 } 161 162 /** 163 * Internal call to apply the {@code configuration} and {@code operation} to the player. 164 * Returns a valid shaper id or throws the appropriate exception. 165 * @param configuration 166 * @param operation 167 * @return id a non-negative shaper id. 168 * @throws IllegalStateException if the player has been deallocated or is uninitialized. 169 */ applyPlayer( @onNull VolumeShaper.Configuration configuration, @NonNull VolumeShaper.Operation operation)170 private int applyPlayer( 171 @NonNull VolumeShaper.Configuration configuration, 172 @NonNull VolumeShaper.Operation operation) { 173 final int id; 174 if (mWeakPlayerBase != null) { 175 PlayerBase player = mWeakPlayerBase.get(); 176 if (player == null) { 177 throw new IllegalStateException("player deallocated"); 178 } 179 id = player.playerApplyVolumeShaper(configuration, operation); 180 } else { 181 throw new IllegalStateException("uninitialized shaper"); 182 } 183 if (id < 0) { 184 // TODO - get INVALID_OPERATION from platform. 185 final int VOLUME_SHAPER_INVALID_OPERATION = -38; // must match with platform 186 // Due to RPC handling, we translate integer codes to exceptions right before 187 // delivering to the user. 188 if (id == VOLUME_SHAPER_INVALID_OPERATION) { 189 throw new IllegalStateException("player or VolumeShaper deallocated"); 190 } else { 191 throw new IllegalArgumentException("invalid configuration or operation: " + id); 192 } 193 } 194 return id; 195 } 196 197 /** 198 * Internal call to retrieve the current {@code VolumeShaper} state. 199 * @param id 200 * @return the current {@code VolumeShaper.State} 201 * @throws IllegalStateException if the player has been deallocated or is uninitialized. 202 */ getStatePlayer(int id)203 private @NonNull VolumeShaper.State getStatePlayer(int id) { 204 final VolumeShaper.State state; 205 if (mWeakPlayerBase != null) { 206 PlayerBase player = mWeakPlayerBase.get(); 207 if (player == null) { 208 throw new IllegalStateException("player deallocated"); 209 } 210 state = player.playerGetVolumeShaperState(id); 211 } else { 212 throw new IllegalStateException("uninitialized shaper"); 213 } 214 if (state == null) { 215 throw new IllegalStateException("shaper cannot be found"); 216 } 217 return state; 218 } 219 220 /** 221 * The {@code VolumeShaper.Configuration} class contains curve 222 * and duration information. 223 * It is constructed by the {@link VolumeShaper.Configuration.Builder}. 224 * <p> 225 * A {@code VolumeShaper.Configuration} is used by 226 * {@link VolumeAutomation#createVolumeShaper(Configuration) 227 * VolumeAutomation.createVolumeShaper(Configuration)} to create 228 * a {@code VolumeShaper} and 229 * by {@link VolumeShaper#replace(Configuration, Operation, boolean) 230 * VolumeShaper.replace(Configuration, Operation, boolean)} 231 * to replace an existing {@code configuration}. 232 * <p> 233 * The {@link AudioTrack} and {@link MediaPlayer} classes implement 234 * the {@link VolumeAutomation} interface. 235 */ 236 public static final class Configuration implements Parcelable { 237 private static final int MAXIMUM_CURVE_POINTS = 16; 238 239 /** 240 * Returns the maximum number of curve points allowed for 241 * {@link VolumeShaper.Builder#setCurve(float[], float[])}. 242 */ getMaximumCurvePoints()243 public static int getMaximumCurvePoints() { 244 return MAXIMUM_CURVE_POINTS; 245 } 246 247 // These values must match the native VolumeShaper::Configuration::Type 248 /** @hide */ 249 @IntDef({ 250 TYPE_ID, 251 TYPE_SCALE, 252 }) 253 @Retention(RetentionPolicy.SOURCE) 254 public @interface Type {} 255 256 /** 257 * Specifies a {@link VolumeShaper} handle created by {@link #VolumeShaper(int)} 258 * from an id returned by {@code setVolumeShaper()}. 259 * The type, curve, etc. may not be queried from 260 * a {@code VolumeShaper} object of this type; 261 * the handle is used to identify and change the operation of 262 * an existing {@code VolumeShaper} sent to the player. 263 */ 264 /* package */ static final int TYPE_ID = 0; 265 266 /** 267 * Specifies a {@link VolumeShaper} to be used 268 * as an additional scale to the current volume. 269 * This is created by the {@link VolumeShaper.Builder}. 270 */ 271 /* package */ static final int TYPE_SCALE = 1; 272 273 // These values must match the native InterpolatorType enumeration. 274 /** @hide */ 275 @IntDef({ 276 INTERPOLATOR_TYPE_STEP, 277 INTERPOLATOR_TYPE_LINEAR, 278 INTERPOLATOR_TYPE_CUBIC, 279 INTERPOLATOR_TYPE_CUBIC_MONOTONIC, 280 }) 281 @Retention(RetentionPolicy.SOURCE) 282 public @interface InterpolatorType {} 283 284 /** 285 * Stepwise volume curve. 286 */ 287 public static final int INTERPOLATOR_TYPE_STEP = 0; 288 289 /** 290 * Linear interpolated volume curve. 291 */ 292 public static final int INTERPOLATOR_TYPE_LINEAR = 1; 293 294 /** 295 * Cubic interpolated volume curve. 296 * This is default if unspecified. 297 */ 298 public static final int INTERPOLATOR_TYPE_CUBIC = 2; 299 300 /** 301 * Cubic interpolated volume curve 302 * that preserves local monotonicity. 303 * So long as the control points are locally monotonic, 304 * the curve interpolation between those points are monotonic. 305 * This is useful for cubic spline interpolated 306 * volume ramps and ducks. 307 */ 308 public static final int INTERPOLATOR_TYPE_CUBIC_MONOTONIC = 3; 309 310 // These values must match the native VolumeShaper::Configuration::InterpolatorType 311 /** @hide */ 312 @IntDef({ 313 OPTION_FLAG_VOLUME_IN_DBFS, 314 OPTION_FLAG_CLOCK_TIME, 315 }) 316 @Retention(RetentionPolicy.SOURCE) 317 public @interface OptionFlag {} 318 319 /** 320 * @hide 321 * Use a dB full scale volume range for the volume curve. 322 *<p> 323 * The volume scale is typically from 0.f to 1.f on a linear scale; 324 * this option changes to -inf to 0.f on a db full scale, 325 * where 0.f is equivalent to a scale of 1.f. 326 */ 327 public static final int OPTION_FLAG_VOLUME_IN_DBFS = (1 << 0); 328 329 /** 330 * @hide 331 * Use clock time instead of media time. 332 *<p> 333 * The default implementation of {@code VolumeShaper} is to apply 334 * volume changes by the media time of the player. 335 * Hence, the {@code VolumeShaper} will speed or slow down to 336 * match player changes of playback rate, pause, or resume. 337 *<p> 338 * The {@code OPTION_FLAG_CLOCK_TIME} option allows the {@code VolumeShaper} 339 * progress to be determined by clock time instead of media time. 340 */ 341 public static final int OPTION_FLAG_CLOCK_TIME = (1 << 1); 342 343 private static final int OPTION_FLAG_PUBLIC_ALL = 344 OPTION_FLAG_VOLUME_IN_DBFS | OPTION_FLAG_CLOCK_TIME; 345 346 /** 347 * A one second linear ramp from silence to full volume. 348 * Use {@link VolumeShaper.Builder#reflectTimes()} 349 * or {@link VolumeShaper.Builder#invertVolumes()} to generate 350 * the matching linear duck. 351 */ 352 public static final Configuration LINEAR_RAMP = new VolumeShaper.Configuration.Builder() 353 .setInterpolatorType(INTERPOLATOR_TYPE_LINEAR) 354 .setCurve(new float[] {0.f, 1.f} /* times */, 355 new float[] {0.f, 1.f} /* volumes */) 356 .setDuration(1000) 357 .build(); 358 359 /** 360 * A one second cubic ramp from silence to full volume. 361 * Use {@link VolumeShaper.Builder#reflectTimes()} 362 * or {@link VolumeShaper.Builder#invertVolumes()} to generate 363 * the matching cubic duck. 364 */ 365 public static final Configuration CUBIC_RAMP = new VolumeShaper.Configuration.Builder() 366 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC) 367 .setCurve(new float[] {0.f, 1.f} /* times */, 368 new float[] {0.f, 1.f} /* volumes */) 369 .setDuration(1000) 370 .build(); 371 372 /** 373 * A one second sine curve 374 * from silence to full volume for energy preserving cross fades. 375 * Use {@link VolumeShaper.Builder#reflectTimes()} to generate 376 * the matching cosine duck. 377 */ 378 public static final Configuration SINE_RAMP; 379 380 /** 381 * A one second sine-squared s-curve ramp 382 * from silence to full volume. 383 * Use {@link VolumeShaper.Builder#reflectTimes()} 384 * or {@link VolumeShaper.Builder#invertVolumes()} to generate 385 * the matching sine-squared s-curve duck. 386 */ 387 public static final Configuration SCURVE_RAMP; 388 389 static { 390 final int POINTS = MAXIMUM_CURVE_POINTS; 391 final float times[] = new float[POINTS]; 392 final float sines[] = new float[POINTS]; 393 final float scurve[] = new float[POINTS]; 394 for (int i = 0; i < POINTS; ++i) { 395 times[i] = (float)i / (POINTS - 1); 396 final float sine = (float)Math.sin(times[i] * Math.PI / 2.); 397 sines[i] = sine; 398 scurve[i] = sine * sine; 399 } 400 SINE_RAMP = new VolumeShaper.Configuration.Builder() 401 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC) 402 .setCurve(times, sines) 403 .setDuration(1000) 404 .build(); 405 SCURVE_RAMP = new VolumeShaper.Configuration.Builder() 406 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC) 407 .setCurve(times, scurve) 408 .setDuration(1000) 409 .build(); 410 } 411 412 /* 413 * member variables - these are all final 414 */ 415 416 // type of VolumeShaper 417 @UnsupportedAppUsage 418 private final int mType; 419 420 // valid when mType is TYPE_ID 421 @UnsupportedAppUsage 422 private final int mId; 423 424 // valid when mType is TYPE_SCALE 425 @UnsupportedAppUsage 426 private final int mOptionFlags; 427 @UnsupportedAppUsage 428 private final double mDurationMs; 429 @UnsupportedAppUsage 430 private final int mInterpolatorType; 431 @UnsupportedAppUsage 432 private final float[] mTimes; 433 @UnsupportedAppUsage 434 private final float[] mVolumes; 435 436 @Override toString()437 public String toString() { 438 return "VolumeShaper.Configuration{" 439 + "mType = " + mType 440 + ", mId = " + mId 441 + (mType == TYPE_ID 442 ? "}" 443 : ", mOptionFlags = 0x" + Integer.toHexString(mOptionFlags).toUpperCase() 444 + ", mDurationMs = " + mDurationMs 445 + ", mInterpolatorType = " + mInterpolatorType 446 + ", mTimes[] = " + Arrays.toString(mTimes) 447 + ", mVolumes[] = " + Arrays.toString(mVolumes) 448 + "}"); 449 } 450 451 @Override hashCode()452 public int hashCode() { 453 return mType == TYPE_ID 454 ? Objects.hash(mType, mId) 455 : Objects.hash(mType, mId, 456 mOptionFlags, mDurationMs, mInterpolatorType, 457 Arrays.hashCode(mTimes), Arrays.hashCode(mVolumes)); 458 } 459 460 @Override equals(Object o)461 public boolean equals(Object o) { 462 if (!(o instanceof Configuration)) return false; 463 if (o == this) return true; 464 final Configuration other = (Configuration) o; 465 // Note that exact floating point equality may not be guaranteed 466 // for a theoretically idempotent operation; for example, 467 // there are many cases where a + b - b != a. 468 return mType == other.mType 469 && mId == other.mId 470 && (mType == TYPE_ID 471 || (mOptionFlags == other.mOptionFlags 472 && mDurationMs == other.mDurationMs 473 && mInterpolatorType == other.mInterpolatorType 474 && Arrays.equals(mTimes, other.mTimes) 475 && Arrays.equals(mVolumes, other.mVolumes))); 476 } 477 478 @Override describeContents()479 public int describeContents() { 480 return 0; 481 } 482 483 @Override writeToParcel(Parcel dest, int flags)484 public void writeToParcel(Parcel dest, int flags) { 485 // this needs to match the native VolumeShaper.Configuration parceling 486 dest.writeInt(mType); 487 dest.writeInt(mId); 488 if (mType != TYPE_ID) { 489 dest.writeInt(mOptionFlags); 490 dest.writeDouble(mDurationMs); 491 // this needs to match the native Interpolator parceling 492 dest.writeInt(mInterpolatorType); 493 dest.writeFloat(0.f); // first slope (specifying for native side) 494 dest.writeFloat(0.f); // last slope (specifying for native side) 495 // mTimes and mVolumes should have the same length. 496 dest.writeInt(mTimes.length); 497 for (int i = 0; i < mTimes.length; ++i) { 498 dest.writeFloat(mTimes[i]); 499 dest.writeFloat(mVolumes[i]); 500 } 501 } 502 } 503 504 public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Configuration> CREATOR 505 = new Parcelable.Creator<VolumeShaper.Configuration>() { 506 @Override 507 public VolumeShaper.Configuration createFromParcel(Parcel p) { 508 // this needs to match the native VolumeShaper.Configuration parceling 509 final int type = p.readInt(); 510 final int id = p.readInt(); 511 if (type == TYPE_ID) { 512 return new VolumeShaper.Configuration(id); 513 } else { 514 final int optionFlags = p.readInt(); 515 final double durationMs = p.readDouble(); 516 // this needs to match the native Interpolator parceling 517 final int interpolatorType = p.readInt(); 518 final float firstSlope = p.readFloat(); // ignored on the Java side 519 final float lastSlope = p.readFloat(); // ignored on the Java side 520 final int length = p.readInt(); 521 final float[] times = new float[length]; 522 final float[] volumes = new float[length]; 523 for (int i = 0; i < length; ++i) { 524 times[i] = p.readFloat(); 525 volumes[i] = p.readFloat(); 526 } 527 528 return new VolumeShaper.Configuration( 529 type, 530 id, 531 optionFlags, 532 durationMs, 533 interpolatorType, 534 times, 535 volumes); 536 } 537 } 538 539 @Override 540 public VolumeShaper.Configuration[] newArray(int size) { 541 return new VolumeShaper.Configuration[size]; 542 } 543 }; 544 545 /** 546 * @hide 547 * Constructs a {@code VolumeShaper} from an id. 548 * 549 * This is an opaque handle for controlling a {@code VolumeShaper} that has 550 * already been sent to a player. The {@code id} is returned from the 551 * initial {@code setVolumeShaper()} call on success. 552 * 553 * These configurations are for native use only, 554 * they are never returned directly to the user. 555 * 556 * @param id 557 * @throws IllegalArgumentException if id is negative. 558 */ Configuration(int id)559 public Configuration(int id) { 560 if (id < 0) { 561 throw new IllegalArgumentException("negative id " + id); 562 } 563 mType = TYPE_ID; 564 mId = id; 565 mInterpolatorType = 0; 566 mOptionFlags = 0; 567 mDurationMs = 0; 568 mTimes = null; 569 mVolumes = null; 570 } 571 572 /** 573 * Direct constructor for VolumeShaper. 574 * Use the Builder instead. 575 */ 576 @UnsupportedAppUsage Configuration(@ype int type, int id, @OptionFlag int optionFlags, double durationMs, @InterpolatorType int interpolatorType, @NonNull float[] times, @NonNull float[] volumes)577 private Configuration(@Type int type, 578 int id, 579 @OptionFlag int optionFlags, 580 double durationMs, 581 @InterpolatorType int interpolatorType, 582 @NonNull float[] times, 583 @NonNull float[] volumes) { 584 mType = type; 585 mId = id; 586 mOptionFlags = optionFlags; 587 mDurationMs = durationMs; 588 mInterpolatorType = interpolatorType; 589 // Builder should have cloned these arrays already. 590 mTimes = times; 591 mVolumes = volumes; 592 } 593 594 /** 595 * @hide 596 * Returns the {@code VolumeShaper} type. 597 */ getType()598 public @Type int getType() { 599 return mType; 600 } 601 602 /** 603 * @hide 604 * Returns the {@code VolumeShaper} id. 605 */ getId()606 public int getId() { 607 return mId; 608 } 609 610 /** 611 * Returns the interpolator type. 612 */ getInterpolatorType()613 public @InterpolatorType int getInterpolatorType() { 614 return mInterpolatorType; 615 } 616 617 /** 618 * @hide 619 * Returns the option flags 620 */ getOptionFlags()621 public @OptionFlag int getOptionFlags() { 622 return mOptionFlags & OPTION_FLAG_PUBLIC_ALL; 623 } 624 getAllOptionFlags()625 /* package */ @OptionFlag int getAllOptionFlags() { 626 return mOptionFlags; 627 } 628 629 /** 630 * Returns the duration of the volume shape in milliseconds. 631 */ getDuration()632 public long getDuration() { 633 // casting is safe here as the duration was set as a long in the Builder 634 return (long) mDurationMs; 635 } 636 637 /** 638 * Returns the times (x) coordinate array of the volume curve points. 639 */ getTimes()640 public float[] getTimes() { 641 return mTimes; 642 } 643 644 /** 645 * Returns the volumes (y) coordinate array of the volume curve points. 646 */ getVolumes()647 public float[] getVolumes() { 648 return mVolumes; 649 } 650 651 /** 652 * Checks the validity of times and volumes point representation. 653 * 654 * {@code times[]} and {@code volumes[]} are two arrays representing points 655 * for the volume curve. 656 * 657 * Note that {@code times[]} and {@code volumes[]} are explicitly checked against 658 * null here to provide the proper error string - those are legitimate 659 * arguments to this method. 660 * 661 * @param times the x coordinates for the points, 662 * must be between 0.f and 1.f and be monotonic. 663 * @param volumes the y coordinates for the points, 664 * must be between 0.f and 1.f for linear and 665 * must be no greater than 0.f for log (dBFS). 666 * @param log set to true if the scale is logarithmic. 667 * @return null if no error, or the reason in a {@code String} for an error. 668 */ checkCurveForErrors( @ullable float[] times, @Nullable float[] volumes, boolean log)669 private static @Nullable String checkCurveForErrors( 670 @Nullable float[] times, @Nullable float[] volumes, boolean log) { 671 if (times == null) { 672 return "times array must be non-null"; 673 } else if (volumes == null) { 674 return "volumes array must be non-null"; 675 } else if (times.length != volumes.length) { 676 return "array length must match"; 677 } else if (times.length < 2) { 678 return "array length must be at least 2"; 679 } else if (times.length > MAXIMUM_CURVE_POINTS) { 680 return "array length must be no larger than " + MAXIMUM_CURVE_POINTS; 681 } else if (times[0] != 0.f) { 682 return "times must start at 0.f"; 683 } else if (times[times.length - 1] != 1.f) { 684 return "times must end at 1.f"; 685 } 686 687 // validate points along the curve 688 for (int i = 1; i < times.length; ++i) { 689 if (!(times[i] > times[i - 1]) /* handle nan */) { 690 return "times not monotonic increasing, check index " + i; 691 } 692 } 693 if (log) { 694 for (int i = 0; i < volumes.length; ++i) { 695 if (!(volumes[i] <= 0.f) /* handle nan */) { 696 return "volumes for log scale cannot be positive, " 697 + "check index " + i; 698 } 699 } 700 } else { 701 for (int i = 0; i < volumes.length; ++i) { 702 if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) { 703 return "volumes for linear scale must be between 0.f and 1.f, " 704 + "check index " + i; 705 } 706 } 707 } 708 return null; // no errors 709 } 710 checkCurveForErrorsAndThrowException( @ullable float[] times, @Nullable float[] volumes, boolean log, boolean ise)711 private static void checkCurveForErrorsAndThrowException( 712 @Nullable float[] times, @Nullable float[] volumes, boolean log, boolean ise) { 713 final String error = checkCurveForErrors(times, volumes, log); 714 if (error != null) { 715 if (ise) { 716 throw new IllegalStateException(error); 717 } else { 718 throw new IllegalArgumentException(error); 719 } 720 } 721 } 722 checkValidVolumeAndThrowException(float volume, boolean log)723 private static void checkValidVolumeAndThrowException(float volume, boolean log) { 724 if (log) { 725 if (!(volume <= 0.f) /* handle nan */) { 726 throw new IllegalArgumentException("dbfs volume must be 0.f or less"); 727 } 728 } else { 729 if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) { 730 throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f"); 731 } 732 } 733 } 734 clampVolume(float[] volumes, boolean log)735 private static void clampVolume(float[] volumes, boolean log) { 736 if (log) { 737 for (int i = 0; i < volumes.length; ++i) { 738 if (!(volumes[i] <= 0.f) /* handle nan */) { 739 volumes[i] = 0.f; 740 } 741 } 742 } else { 743 for (int i = 0; i < volumes.length; ++i) { 744 if (!(volumes[i] >= 0.f) /* handle nan */) { 745 volumes[i] = 0.f; 746 } else if (!(volumes[i] <= 1.f)) { 747 volumes[i] = 1.f; 748 } 749 } 750 } 751 } 752 753 /** 754 * Builder class for a {@link VolumeShaper.Configuration} object. 755 * <p> Here is an example where {@code Builder} is used to define the 756 * {@link VolumeShaper.Configuration}. 757 * 758 * <pre class="prettyprint"> 759 * VolumeShaper.Configuration LINEAR_RAMP = 760 * new VolumeShaper.Configuration.Builder() 761 * .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR) 762 * .setCurve(new float[] { 0.f, 1.f }, // times 763 * new float[] { 0.f, 1.f }) // volumes 764 * .setDuration(1000) 765 * .build(); 766 * </pre> 767 * <p> 768 */ 769 public static final class Builder { 770 private int mType = TYPE_SCALE; 771 private int mId = -1; // invalid 772 private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC; 773 private int mOptionFlags = OPTION_FLAG_CLOCK_TIME; 774 private double mDurationMs = 1000.; 775 private float[] mTimes = null; 776 private float[] mVolumes = null; 777 778 /** 779 * Constructs a new {@code Builder} with the defaults. 780 */ Builder()781 public Builder() { 782 } 783 784 /** 785 * Constructs a new {@code Builder} with settings 786 * copied from a given {@code VolumeShaper.Configuration}. 787 * @param configuration prototypical configuration 788 * which will be reused in the new {@code Builder}. 789 */ Builder(@onNull Configuration configuration)790 public Builder(@NonNull Configuration configuration) { 791 mType = configuration.getType(); 792 mId = configuration.getId(); 793 mOptionFlags = configuration.getAllOptionFlags(); 794 mInterpolatorType = configuration.getInterpolatorType(); 795 mDurationMs = configuration.getDuration(); 796 mTimes = configuration.getTimes().clone(); 797 mVolumes = configuration.getVolumes().clone(); 798 } 799 800 /** 801 * @hide 802 * Set the {@code id} for system defined shapers. 803 * @param id the {@code id} to set. If non-negative, then it is used. 804 * If -1, then the system is expected to assign one. 805 * @return the same {@code Builder} instance. 806 * @throws IllegalArgumentException if {@code id} < -1. 807 */ setId(int id)808 public @NonNull Builder setId(int id) { 809 if (id < -1) { 810 throw new IllegalArgumentException("invalid id: " + id); 811 } 812 mId = id; 813 return this; 814 } 815 816 /** 817 * Sets the interpolator type. 818 * 819 * If omitted the default interpolator type is {@link #INTERPOLATOR_TYPE_CUBIC}. 820 * 821 * @param interpolatorType method of interpolation used for the volume curve. 822 * One of {@link #INTERPOLATOR_TYPE_STEP}, 823 * {@link #INTERPOLATOR_TYPE_LINEAR}, 824 * {@link #INTERPOLATOR_TYPE_CUBIC}, 825 * {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}. 826 * @return the same {@code Builder} instance. 827 * @throws IllegalArgumentException if {@code interpolatorType} is not valid. 828 */ setInterpolatorType(@nterpolatorType int interpolatorType)829 public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) { 830 switch (interpolatorType) { 831 case INTERPOLATOR_TYPE_STEP: 832 case INTERPOLATOR_TYPE_LINEAR: 833 case INTERPOLATOR_TYPE_CUBIC: 834 case INTERPOLATOR_TYPE_CUBIC_MONOTONIC: 835 mInterpolatorType = interpolatorType; 836 break; 837 default: 838 throw new IllegalArgumentException("invalid interpolatorType: " 839 + interpolatorType); 840 } 841 return this; 842 } 843 844 /** 845 * @hide 846 * Sets the optional flags 847 * 848 * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has 849 * changed the volume curve needs to be set again as the acceptable 850 * volume domain has changed. 851 * 852 * @param optionFlags new value to replace the old {@code optionFlags}. 853 * @return the same {@code Builder} instance. 854 * @throws IllegalArgumentException if flag is not recognized. 855 */ 856 @TestApi setOptionFlags(@ptionFlag int optionFlags)857 public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) { 858 if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) { 859 throw new IllegalArgumentException("invalid bits in flag: " + optionFlags); 860 } 861 mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags; 862 return this; 863 } 864 865 /** 866 * Sets the {@code VolumeShaper} duration in milliseconds. 867 * 868 * If omitted, the default duration is 1 second. 869 * 870 * @param durationMillis 871 * @return the same {@code Builder} instance. 872 * @throws IllegalArgumentException if {@code durationMillis} 873 * is not strictly positive. 874 */ setDuration(long durationMillis)875 public @NonNull Builder setDuration(long durationMillis) { 876 if (durationMillis <= 0) { 877 throw new IllegalArgumentException( 878 "duration: " + durationMillis + " not positive"); 879 } 880 mDurationMs = (double) durationMillis; 881 return this; 882 } 883 884 /** 885 * Sets the volume curve. 886 * 887 * The volume curve is represented by a set of control points given by 888 * two float arrays of equal length, 889 * one representing the time (x) coordinates 890 * and one corresponding to the volume (y) coordinates. 891 * The length must be at least 2 892 * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}. 893 * <p> 894 * The volume curve is normalized as follows: 895 * time (x) coordinates should be monotonically increasing, from 0.f to 1.f; 896 * volume (y) coordinates must be within 0.f to 1.f. 897 * <p> 898 * The time scale is set by {@link #setDuration}. 899 * <p> 900 * @param times an array of float values representing 901 * the time line of the volume curve. 902 * @param volumes an array of float values representing 903 * the amplitude of the volume curve. 904 * @return the same {@code Builder} instance. 905 * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid. 906 */ 907 908 /* Note: volume (y) coordinates must be non-positive for log scaling, 909 * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set. 910 */ 911 setCurve(@onNull float[] times, @NonNull float[] volumes)912 public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) { 913 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 914 checkCurveForErrorsAndThrowException(times, volumes, log, false /* ise */); 915 mTimes = times.clone(); 916 mVolumes = volumes.clone(); 917 return this; 918 } 919 920 /** 921 * Reflects the volume curve so that 922 * the shaper changes volume from the end 923 * to the start. 924 * 925 * @return the same {@code Builder} instance. 926 * @throws IllegalStateException if curve has not been set. 927 */ reflectTimes()928 public @NonNull Builder reflectTimes() { 929 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 930 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 931 int i; 932 for (i = 0; i < mTimes.length / 2; ++i) { 933 float temp = mTimes[i]; 934 mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i]; 935 mTimes[mTimes.length - 1 - i] = 1.f - temp; 936 temp = mVolumes[i]; 937 mVolumes[i] = mVolumes[mVolumes.length - 1 - i]; 938 mVolumes[mVolumes.length - 1 - i] = temp; 939 } 940 if ((mTimes.length & 1) != 0) { 941 mTimes[i] = 1.f - mTimes[i]; 942 } 943 return this; 944 } 945 946 /** 947 * Inverts the volume curve so that the max volume 948 * becomes the min volume and vice versa. 949 * 950 * @return the same {@code Builder} instance. 951 * @throws IllegalStateException if curve has not been set. 952 */ invertVolumes()953 public @NonNull Builder invertVolumes() { 954 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 955 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 956 float min = mVolumes[0]; 957 float max = mVolumes[0]; 958 for (int i = 1; i < mVolumes.length; ++i) { 959 if (mVolumes[i] < min) { 960 min = mVolumes[i]; 961 } else if (mVolumes[i] > max) { 962 max = mVolumes[i]; 963 } 964 } 965 966 final float maxmin = max + min; 967 for (int i = 0; i < mVolumes.length; ++i) { 968 mVolumes[i] = maxmin - mVolumes[i]; 969 } 970 return this; 971 } 972 973 /** 974 * Scale the curve end volume to a target value. 975 * 976 * Keeps the start volume the same. 977 * This works best if the volume curve is monotonic. 978 * 979 * @param volume the target end volume to use. 980 * @return the same {@code Builder} instance. 981 * @throws IllegalArgumentException if {@code volume} is not valid. 982 * @throws IllegalStateException if curve has not been set. 983 */ scaleToEndVolume(float volume)984 public @NonNull Builder scaleToEndVolume(float volume) { 985 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 986 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 987 checkValidVolumeAndThrowException(volume, log); 988 final float startVolume = mVolumes[0]; 989 final float endVolume = mVolumes[mVolumes.length - 1]; 990 if (endVolume == startVolume) { 991 // match with linear ramp 992 final float offset = volume - startVolume; 993 for (int i = 0; i < mVolumes.length; ++i) { 994 mVolumes[i] = mVolumes[i] + offset * mTimes[i]; 995 } 996 } else { 997 // scale 998 final float scale = (volume - startVolume) / (endVolume - startVolume); 999 for (int i = 0; i < mVolumes.length; ++i) { 1000 mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume; 1001 } 1002 } 1003 clampVolume(mVolumes, log); 1004 return this; 1005 } 1006 1007 /** 1008 * Scale the curve start volume to a target value. 1009 * 1010 * Keeps the end volume the same. 1011 * This works best if the volume curve is monotonic. 1012 * 1013 * @param volume the target start volume to use. 1014 * @return the same {@code Builder} instance. 1015 * @throws IllegalArgumentException if {@code volume} is not valid. 1016 * @throws IllegalStateException if curve has not been set. 1017 */ scaleToStartVolume(float volume)1018 public @NonNull Builder scaleToStartVolume(float volume) { 1019 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 1020 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 1021 checkValidVolumeAndThrowException(volume, log); 1022 final float startVolume = mVolumes[0]; 1023 final float endVolume = mVolumes[mVolumes.length - 1]; 1024 if (endVolume == startVolume) { 1025 // match with linear ramp 1026 final float offset = volume - startVolume; 1027 for (int i = 0; i < mVolumes.length; ++i) { 1028 mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]); 1029 } 1030 } else { 1031 final float scale = (volume - endVolume) / (startVolume - endVolume); 1032 for (int i = 0; i < mVolumes.length; ++i) { 1033 mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume; 1034 } 1035 } 1036 clampVolume(mVolumes, log); 1037 return this; 1038 } 1039 1040 /** 1041 * Builds a new {@link VolumeShaper} object. 1042 * 1043 * @return a new {@link VolumeShaper} object. 1044 * @throws IllegalStateException if curve is not properly set. 1045 */ build()1046 public @NonNull Configuration build() { 1047 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 1048 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 1049 return new Configuration(mType, mId, mOptionFlags, mDurationMs, 1050 mInterpolatorType, mTimes, mVolumes); 1051 } 1052 } // Configuration.Builder 1053 } // Configuration 1054 1055 /** 1056 * The {@code VolumeShaper.Operation} class is used to specify operations 1057 * to the {@code VolumeShaper} that affect the volume change. 1058 */ 1059 public static final class Operation implements Parcelable { 1060 /** 1061 * Forward playback from current volume time position. 1062 * At the end of the {@code VolumeShaper} curve, 1063 * the last volume value persists. 1064 */ 1065 public static final Operation PLAY = 1066 new VolumeShaper.Operation.Builder() 1067 .build(); 1068 1069 /** 1070 * Reverse playback from current volume time position. 1071 * When the position reaches the start of the {@code VolumeShaper} curve, 1072 * the first volume value persists. 1073 */ 1074 public static final Operation REVERSE = 1075 new VolumeShaper.Operation.Builder() 1076 .reverse() 1077 .build(); 1078 1079 // No user serviceable parts below. 1080 1081 // These flags must match the native VolumeShaper::Operation::Flag 1082 /** @hide */ 1083 @IntDef({ 1084 FLAG_NONE, 1085 FLAG_REVERSE, 1086 FLAG_TERMINATE, 1087 FLAG_JOIN, 1088 FLAG_DEFER, 1089 }) 1090 @Retention(RetentionPolicy.SOURCE) 1091 public @interface Flag {} 1092 1093 /** 1094 * No special {@code VolumeShaper} operation. 1095 */ 1096 private static final int FLAG_NONE = 0; 1097 1098 /** 1099 * Reverse the {@code VolumeShaper} progress. 1100 * 1101 * Reverses the {@code VolumeShaper} curve from its current 1102 * position. If the {@code VolumeShaper} curve has not started, 1103 * it automatically is considered finished. 1104 */ 1105 private static final int FLAG_REVERSE = 1 << 0; 1106 1107 /** 1108 * Terminate the existing {@code VolumeShaper}. 1109 * This flag is generally used by itself; 1110 * it takes precedence over all other flags. 1111 */ 1112 private static final int FLAG_TERMINATE = 1 << 1; 1113 1114 /** 1115 * Attempt to join as best as possible to the previous {@code VolumeShaper}. 1116 * This requires the previous {@code VolumeShaper} to be active and 1117 * {@link #setReplaceId} to be set. 1118 */ 1119 private static final int FLAG_JOIN = 1 << 2; 1120 1121 /** 1122 * Defer playback until next operation is sent. This is used 1123 * when starting a {@code VolumeShaper} effect. 1124 */ 1125 private static final int FLAG_DEFER = 1 << 3; 1126 1127 /** 1128 * Use the id specified in the configuration, creating 1129 * {@code VolumeShaper} as needed; the configuration should be 1130 * TYPE_SCALE. 1131 */ 1132 private static final int FLAG_CREATE_IF_NEEDED = 1 << 4; 1133 1134 private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE; 1135 1136 @UnsupportedAppUsage 1137 private final int mFlags; 1138 @UnsupportedAppUsage 1139 private final int mReplaceId; 1140 @UnsupportedAppUsage 1141 private final float mXOffset; 1142 1143 @Override toString()1144 public String toString() { 1145 return "VolumeShaper.Operation{" 1146 + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase() 1147 + ", mReplaceId = " + mReplaceId 1148 + ", mXOffset = " + mXOffset 1149 + "}"; 1150 } 1151 1152 @Override hashCode()1153 public int hashCode() { 1154 return Objects.hash(mFlags, mReplaceId, mXOffset); 1155 } 1156 1157 @Override equals(Object o)1158 public boolean equals(Object o) { 1159 if (!(o instanceof Operation)) return false; 1160 if (o == this) return true; 1161 final Operation other = (Operation) o; 1162 1163 return mFlags == other.mFlags 1164 && mReplaceId == other.mReplaceId 1165 && Float.compare(mXOffset, other.mXOffset) == 0; 1166 } 1167 1168 @Override describeContents()1169 public int describeContents() { 1170 return 0; 1171 } 1172 1173 @Override writeToParcel(Parcel dest, int flags)1174 public void writeToParcel(Parcel dest, int flags) { 1175 // this needs to match the native VolumeShaper.Operation parceling 1176 dest.writeInt(mFlags); 1177 dest.writeInt(mReplaceId); 1178 dest.writeFloat(mXOffset); 1179 } 1180 1181 public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Operation> CREATOR 1182 = new Parcelable.Creator<VolumeShaper.Operation>() { 1183 @Override 1184 public VolumeShaper.Operation createFromParcel(Parcel p) { 1185 // this needs to match the native VolumeShaper.Operation parceling 1186 final int flags = p.readInt(); 1187 final int replaceId = p.readInt(); 1188 final float xOffset = p.readFloat(); 1189 1190 return new VolumeShaper.Operation( 1191 flags 1192 , replaceId 1193 , xOffset); 1194 } 1195 1196 @Override 1197 public VolumeShaper.Operation[] newArray(int size) { 1198 return new VolumeShaper.Operation[size]; 1199 } 1200 }; 1201 1202 @UnsupportedAppUsage Operation(@lag int flags, int replaceId, float xOffset)1203 private Operation(@Flag int flags, int replaceId, float xOffset) { 1204 mFlags = flags; 1205 mReplaceId = replaceId; 1206 mXOffset = xOffset; 1207 } 1208 1209 /** 1210 * @hide 1211 * {@code Builder} class for {@link VolumeShaper.Operation} object. 1212 * 1213 * Not for public use. 1214 */ 1215 public static final class Builder { 1216 int mFlags; 1217 int mReplaceId; 1218 float mXOffset; 1219 1220 /** 1221 * Constructs a new {@code Builder} with the defaults. 1222 */ Builder()1223 public Builder() { 1224 mFlags = 0; 1225 mReplaceId = -1; 1226 mXOffset = Float.NaN; 1227 } 1228 1229 /** 1230 * Constructs a new {@code Builder} from a given {@code VolumeShaper.Operation} 1231 * @param operation the {@code VolumeShaper.operation} whose data will be 1232 * reused in the new {@code Builder}. 1233 */ Builder(@onNull VolumeShaper.Operation operation)1234 public Builder(@NonNull VolumeShaper.Operation operation) { 1235 mReplaceId = operation.mReplaceId; 1236 mFlags = operation.mFlags; 1237 mXOffset = operation.mXOffset; 1238 } 1239 1240 /** 1241 * Replaces the previous {@code VolumeShaper} specified by {@code id}. 1242 * 1243 * The {@code VolumeShaper} specified by the {@code id} is removed 1244 * if it exists. The configuration should be TYPE_SCALE. 1245 * 1246 * @param id the {@code id} of the previous {@code VolumeShaper}. 1247 * @param join if true, match the volume of the previous 1248 * shaper to the start volume of the new {@code VolumeShaper}. 1249 * @return the same {@code Builder} instance. 1250 */ replace(int id, boolean join)1251 public @NonNull Builder replace(int id, boolean join) { 1252 mReplaceId = id; 1253 if (join) { 1254 mFlags |= FLAG_JOIN; 1255 } else { 1256 mFlags &= ~FLAG_JOIN; 1257 } 1258 return this; 1259 } 1260 1261 /** 1262 * Defers all operations. 1263 * @return the same {@code Builder} instance. 1264 */ defer()1265 public @NonNull Builder defer() { 1266 mFlags |= FLAG_DEFER; 1267 return this; 1268 } 1269 1270 /** 1271 * Terminates the {@code VolumeShaper}. 1272 * 1273 * Do not call directly, use {@link VolumeShaper#close()}. 1274 * @return the same {@code Builder} instance. 1275 */ terminate()1276 public @NonNull Builder terminate() { 1277 mFlags |= FLAG_TERMINATE; 1278 return this; 1279 } 1280 1281 /** 1282 * Reverses direction. 1283 * @return the same {@code Builder} instance. 1284 */ reverse()1285 public @NonNull Builder reverse() { 1286 mFlags ^= FLAG_REVERSE; 1287 return this; 1288 } 1289 1290 /** 1291 * Use the id specified in the configuration, creating 1292 * {@code VolumeShaper} only as needed; the configuration should be 1293 * TYPE_SCALE. 1294 * 1295 * If the {@code VolumeShaper} with the same id already exists 1296 * then the operation has no effect. 1297 * 1298 * @return the same {@code Builder} instance. 1299 */ createIfNeeded()1300 public @NonNull Builder createIfNeeded() { 1301 mFlags |= FLAG_CREATE_IF_NEEDED; 1302 return this; 1303 } 1304 1305 /** 1306 * Sets the {@code xOffset} to use for the {@code VolumeShaper}. 1307 * 1308 * The {@code xOffset} is the position on the volume curve, 1309 * and setting takes effect when the {@code VolumeShaper} is used next. 1310 * 1311 * @param xOffset a value between (or equal to) 0.f and 1.f, or Float.NaN to ignore. 1312 * @return the same {@code Builder} instance. 1313 * @throws IllegalArgumentException if {@code xOffset} is not between 0.f and 1.f, 1314 * or a Float.NaN. 1315 */ setXOffset(float xOffset)1316 public @NonNull Builder setXOffset(float xOffset) { 1317 if (xOffset < -0.f) { 1318 throw new IllegalArgumentException("Negative xOffset not allowed"); 1319 } else if (xOffset > 1.f) { 1320 throw new IllegalArgumentException("xOffset > 1.f not allowed"); 1321 } 1322 // Float.NaN passes through 1323 mXOffset = xOffset; 1324 return this; 1325 } 1326 1327 /** 1328 * Sets the operation flag. Do not call this directly but one of the 1329 * other builder methods. 1330 * 1331 * @param flags new value for {@code flags}, consisting of ORed flags. 1332 * @return the same {@code Builder} instance. 1333 * @throws IllegalArgumentException if {@code flags} contains invalid set bits. 1334 */ setFlags(@lag int flags)1335 private @NonNull Builder setFlags(@Flag int flags) { 1336 if ((flags & ~FLAG_PUBLIC_ALL) != 0) { 1337 throw new IllegalArgumentException("flag has unknown bits set: " + flags); 1338 } 1339 mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags; 1340 return this; 1341 } 1342 1343 /** 1344 * Builds a new {@link VolumeShaper.Operation} object. 1345 * 1346 * @return a new {@code VolumeShaper.Operation} object 1347 */ build()1348 public @NonNull Operation build() { 1349 return new Operation(mFlags, mReplaceId, mXOffset); 1350 } 1351 } // Operation.Builder 1352 } // Operation 1353 1354 /** 1355 * @hide 1356 * {@code VolumeShaper.State} represents the current progress 1357 * of the {@code VolumeShaper}. 1358 * 1359 * Not for public use. 1360 */ 1361 public static final class State implements Parcelable { 1362 @UnsupportedAppUsage 1363 private float mVolume; 1364 @UnsupportedAppUsage 1365 private float mXOffset; 1366 1367 @Override toString()1368 public String toString() { 1369 return "VolumeShaper.State{" 1370 + "mVolume = " + mVolume 1371 + ", mXOffset = " + mXOffset 1372 + "}"; 1373 } 1374 1375 @Override hashCode()1376 public int hashCode() { 1377 return Objects.hash(mVolume, mXOffset); 1378 } 1379 1380 @Override equals(Object o)1381 public boolean equals(Object o) { 1382 if (!(o instanceof State)) return false; 1383 if (o == this) return true; 1384 final State other = (State) o; 1385 return mVolume == other.mVolume 1386 && mXOffset == other.mXOffset; 1387 } 1388 1389 @Override describeContents()1390 public int describeContents() { 1391 return 0; 1392 } 1393 1394 @Override writeToParcel(Parcel dest, int flags)1395 public void writeToParcel(Parcel dest, int flags) { 1396 dest.writeFloat(mVolume); 1397 dest.writeFloat(mXOffset); 1398 } 1399 1400 public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.State> CREATOR 1401 = new Parcelable.Creator<VolumeShaper.State>() { 1402 @Override 1403 public VolumeShaper.State createFromParcel(Parcel p) { 1404 return new VolumeShaper.State( 1405 p.readFloat() // volume 1406 , p.readFloat()); // xOffset 1407 } 1408 1409 @Override 1410 public VolumeShaper.State[] newArray(int size) { 1411 return new VolumeShaper.State[size]; 1412 } 1413 }; 1414 1415 @UnsupportedAppUsage State(float volume, float xOffset)1416 /* package */ State(float volume, float xOffset) { 1417 mVolume = volume; 1418 mXOffset = xOffset; 1419 } 1420 1421 /** 1422 * Gets the volume of the {@link VolumeShaper.State}. 1423 * @return linear volume between 0.f and 1.f. 1424 */ getVolume()1425 public float getVolume() { 1426 return mVolume; 1427 } 1428 1429 /** 1430 * Gets the {@code xOffset} position on the normalized curve 1431 * of the {@link VolumeShaper.State}. 1432 * @return the curve x position between 0.f and 1.f. 1433 */ getXOffset()1434 public float getXOffset() { 1435 return mXOffset; 1436 } 1437 } // State 1438 } 1439