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