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.os.Parcel; 23 import android.os.Parcelable; 24 25 import java.lang.annotation.Retention; 26 import java.lang.annotation.RetentionPolicy; 27 import java.lang.AutoCloseable; 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 private final int mType; 417 418 // valid when mType is TYPE_ID 419 private final int mId; 420 421 // valid when mType is TYPE_SCALE 422 private final int mOptionFlags; 423 private final double mDurationMs; 424 private final int mInterpolatorType; 425 private final float[] mTimes; 426 private final float[] mVolumes; 427 428 @Override toString()429 public String toString() { 430 return "VolumeShaper.Configuration{" 431 + "mType = " + mType 432 + ", mId = " + mId 433 + (mType == TYPE_ID 434 ? "}" 435 : ", mOptionFlags = 0x" + Integer.toHexString(mOptionFlags).toUpperCase() 436 + ", mDurationMs = " + mDurationMs 437 + ", mInterpolatorType = " + mInterpolatorType 438 + ", mTimes[] = " + Arrays.toString(mTimes) 439 + ", mVolumes[] = " + Arrays.toString(mVolumes) 440 + "}"); 441 } 442 443 @Override hashCode()444 public int hashCode() { 445 return mType == TYPE_ID 446 ? Objects.hash(mType, mId) 447 : Objects.hash(mType, mId, 448 mOptionFlags, mDurationMs, mInterpolatorType, 449 Arrays.hashCode(mTimes), Arrays.hashCode(mVolumes)); 450 } 451 452 @Override equals(Object o)453 public boolean equals(Object o) { 454 if (!(o instanceof Configuration)) return false; 455 if (o == this) return true; 456 final Configuration other = (Configuration) o; 457 // Note that exact floating point equality may not be guaranteed 458 // for a theoretically idempotent operation; for example, 459 // there are many cases where a + b - b != a. 460 return mType == other.mType 461 && mId == other.mId 462 && (mType == TYPE_ID 463 || (mOptionFlags == other.mOptionFlags 464 && mDurationMs == other.mDurationMs 465 && mInterpolatorType == other.mInterpolatorType 466 && Arrays.equals(mTimes, other.mTimes) 467 && Arrays.equals(mVolumes, other.mVolumes))); 468 } 469 470 @Override describeContents()471 public int describeContents() { 472 return 0; 473 } 474 475 @Override writeToParcel(Parcel dest, int flags)476 public void writeToParcel(Parcel dest, int flags) { 477 // this needs to match the native VolumeShaper.Configuration parceling 478 dest.writeInt(mType); 479 dest.writeInt(mId); 480 if (mType != TYPE_ID) { 481 dest.writeInt(mOptionFlags); 482 dest.writeDouble(mDurationMs); 483 // this needs to match the native Interpolator parceling 484 dest.writeInt(mInterpolatorType); 485 dest.writeFloat(0.f); // first slope (specifying for native side) 486 dest.writeFloat(0.f); // last slope (specifying for native side) 487 // mTimes and mVolumes should have the same length. 488 dest.writeInt(mTimes.length); 489 for (int i = 0; i < mTimes.length; ++i) { 490 dest.writeFloat(mTimes[i]); 491 dest.writeFloat(mVolumes[i]); 492 } 493 } 494 } 495 496 public static final Parcelable.Creator<VolumeShaper.Configuration> CREATOR 497 = new Parcelable.Creator<VolumeShaper.Configuration>() { 498 @Override 499 public VolumeShaper.Configuration createFromParcel(Parcel p) { 500 // this needs to match the native VolumeShaper.Configuration parceling 501 final int type = p.readInt(); 502 final int id = p.readInt(); 503 if (type == TYPE_ID) { 504 return new VolumeShaper.Configuration(id); 505 } else { 506 final int optionFlags = p.readInt(); 507 final double durationMs = p.readDouble(); 508 // this needs to match the native Interpolator parceling 509 final int interpolatorType = p.readInt(); 510 final float firstSlope = p.readFloat(); // ignored on the Java side 511 final float lastSlope = p.readFloat(); // ignored on the Java side 512 final int length = p.readInt(); 513 final float[] times = new float[length]; 514 final float[] volumes = new float[length]; 515 for (int i = 0; i < length; ++i) { 516 times[i] = p.readFloat(); 517 volumes[i] = p.readFloat(); 518 } 519 520 return new VolumeShaper.Configuration( 521 type, 522 id, 523 optionFlags, 524 durationMs, 525 interpolatorType, 526 times, 527 volumes); 528 } 529 } 530 531 @Override 532 public VolumeShaper.Configuration[] newArray(int size) { 533 return new VolumeShaper.Configuration[size]; 534 } 535 }; 536 537 /** 538 * @hide 539 * Constructs a {@code VolumeShaper} from an id. 540 * 541 * This is an opaque handle for controlling a {@code VolumeShaper} that has 542 * already been sent to a player. The {@code id} is returned from the 543 * initial {@code setVolumeShaper()} call on success. 544 * 545 * These configurations are for native use only, 546 * they are never returned directly to the user. 547 * 548 * @param id 549 * @throws IllegalArgumentException if id is negative. 550 */ Configuration(int id)551 public Configuration(int id) { 552 if (id < 0) { 553 throw new IllegalArgumentException("negative id " + id); 554 } 555 mType = TYPE_ID; 556 mId = id; 557 mInterpolatorType = 0; 558 mOptionFlags = 0; 559 mDurationMs = 0; 560 mTimes = null; 561 mVolumes = null; 562 } 563 564 /** 565 * Direct constructor for VolumeShaper. 566 * Use the Builder instead. 567 */ Configuration(@ype int type, int id, @OptionFlag int optionFlags, double durationMs, @InterpolatorType int interpolatorType, @NonNull float[] times, @NonNull float[] volumes)568 private Configuration(@Type int type, 569 int id, 570 @OptionFlag int optionFlags, 571 double durationMs, 572 @InterpolatorType int interpolatorType, 573 @NonNull float[] times, 574 @NonNull float[] volumes) { 575 mType = type; 576 mId = id; 577 mOptionFlags = optionFlags; 578 mDurationMs = durationMs; 579 mInterpolatorType = interpolatorType; 580 // Builder should have cloned these arrays already. 581 mTimes = times; 582 mVolumes = volumes; 583 } 584 585 /** 586 * @hide 587 * Returns the {@code VolumeShaper} type. 588 */ getType()589 public @Type int getType() { 590 return mType; 591 } 592 593 /** 594 * @hide 595 * Returns the {@code VolumeShaper} id. 596 */ getId()597 public int getId() { 598 return mId; 599 } 600 601 /** 602 * Returns the interpolator type. 603 */ getInterpolatorType()604 public @InterpolatorType int getInterpolatorType() { 605 return mInterpolatorType; 606 } 607 608 /** 609 * @hide 610 * Returns the option flags 611 */ getOptionFlags()612 public @OptionFlag int getOptionFlags() { 613 return mOptionFlags & OPTION_FLAG_PUBLIC_ALL; 614 } 615 getAllOptionFlags()616 /* package */ @OptionFlag int getAllOptionFlags() { 617 return mOptionFlags; 618 } 619 620 /** 621 * Returns the duration of the volume shape in milliseconds. 622 */ getDuration()623 public long getDuration() { 624 // casting is safe here as the duration was set as a long in the Builder 625 return (long) mDurationMs; 626 } 627 628 /** 629 * Returns the times (x) coordinate array of the volume curve points. 630 */ getTimes()631 public float[] getTimes() { 632 return mTimes; 633 } 634 635 /** 636 * Returns the volumes (y) coordinate array of the volume curve points. 637 */ getVolumes()638 public float[] getVolumes() { 639 return mVolumes; 640 } 641 642 /** 643 * Checks the validity of times and volumes point representation. 644 * 645 * {@code times[]} and {@code volumes[]} are two arrays representing points 646 * for the volume curve. 647 * 648 * Note that {@code times[]} and {@code volumes[]} are explicitly checked against 649 * null here to provide the proper error string - those are legitimate 650 * arguments to this method. 651 * 652 * @param times the x coordinates for the points, 653 * must be between 0.f and 1.f and be monotonic. 654 * @param volumes the y coordinates for the points, 655 * must be between 0.f and 1.f for linear and 656 * must be no greater than 0.f for log (dBFS). 657 * @param log set to true if the scale is logarithmic. 658 * @return null if no error, or the reason in a {@code String} for an error. 659 */ checkCurveForErrors( @ullable float[] times, @Nullable float[] volumes, boolean log)660 private static @Nullable String checkCurveForErrors( 661 @Nullable float[] times, @Nullable float[] volumes, boolean log) { 662 if (times == null) { 663 return "times array must be non-null"; 664 } else if (volumes == null) { 665 return "volumes array must be non-null"; 666 } else if (times.length != volumes.length) { 667 return "array length must match"; 668 } else if (times.length < 2) { 669 return "array length must be at least 2"; 670 } else if (times.length > MAXIMUM_CURVE_POINTS) { 671 return "array length must be no larger than " + MAXIMUM_CURVE_POINTS; 672 } else if (times[0] != 0.f) { 673 return "times must start at 0.f"; 674 } else if (times[times.length - 1] != 1.f) { 675 return "times must end at 1.f"; 676 } 677 678 // validate points along the curve 679 for (int i = 1; i < times.length; ++i) { 680 if (!(times[i] > times[i - 1]) /* handle nan */) { 681 return "times not monotonic increasing, check index " + i; 682 } 683 } 684 if (log) { 685 for (int i = 0; i < volumes.length; ++i) { 686 if (!(volumes[i] <= 0.f) /* handle nan */) { 687 return "volumes for log scale cannot be positive, " 688 + "check index " + i; 689 } 690 } 691 } else { 692 for (int i = 0; i < volumes.length; ++i) { 693 if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) { 694 return "volumes for linear scale must be between 0.f and 1.f, " 695 + "check index " + i; 696 } 697 } 698 } 699 return null; // no errors 700 } 701 checkCurveForErrorsAndThrowException( @ullable float[] times, @Nullable float[] volumes, boolean log, boolean ise)702 private static void checkCurveForErrorsAndThrowException( 703 @Nullable float[] times, @Nullable float[] volumes, boolean log, boolean ise) { 704 final String error = checkCurveForErrors(times, volumes, log); 705 if (error != null) { 706 if (ise) { 707 throw new IllegalStateException(error); 708 } else { 709 throw new IllegalArgumentException(error); 710 } 711 } 712 } 713 checkValidVolumeAndThrowException(float volume, boolean log)714 private static void checkValidVolumeAndThrowException(float volume, boolean log) { 715 if (log) { 716 if (!(volume <= 0.f) /* handle nan */) { 717 throw new IllegalArgumentException("dbfs volume must be 0.f or less"); 718 } 719 } else { 720 if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) { 721 throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f"); 722 } 723 } 724 } 725 clampVolume(float[] volumes, boolean log)726 private static void clampVolume(float[] volumes, boolean log) { 727 if (log) { 728 for (int i = 0; i < volumes.length; ++i) { 729 if (!(volumes[i] <= 0.f) /* handle nan */) { 730 volumes[i] = 0.f; 731 } 732 } 733 } else { 734 for (int i = 0; i < volumes.length; ++i) { 735 if (!(volumes[i] >= 0.f) /* handle nan */) { 736 volumes[i] = 0.f; 737 } else if (!(volumes[i] <= 1.f)) { 738 volumes[i] = 1.f; 739 } 740 } 741 } 742 } 743 744 /** 745 * Builder class for a {@link VolumeShaper.Configuration} object. 746 * <p> Here is an example where {@code Builder} is used to define the 747 * {@link VolumeShaper.Configuration}. 748 * 749 * <pre class="prettyprint"> 750 * VolumeShaper.Configuration LINEAR_RAMP = 751 * new VolumeShaper.Configuration.Builder() 752 * .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR) 753 * .setCurve(new float[] { 0.f, 1.f }, // times 754 * new float[] { 0.f, 1.f }) // volumes 755 * .setDuration(1000) 756 * .build(); 757 * </pre> 758 * <p> 759 */ 760 public static final class Builder { 761 private int mType = TYPE_SCALE; 762 private int mId = -1; // invalid 763 private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC; 764 private int mOptionFlags = OPTION_FLAG_CLOCK_TIME; 765 private double mDurationMs = 1000.; 766 private float[] mTimes = null; 767 private float[] mVolumes = null; 768 769 /** 770 * Constructs a new {@code Builder} with the defaults. 771 */ Builder()772 public Builder() { 773 } 774 775 /** 776 * Constructs a new {@code Builder} with settings 777 * copied from a given {@code VolumeShaper.Configuration}. 778 * @param configuration prototypical configuration 779 * which will be reused in the new {@code Builder}. 780 */ Builder(@onNull Configuration configuration)781 public Builder(@NonNull Configuration configuration) { 782 mType = configuration.getType(); 783 mId = configuration.getId(); 784 mOptionFlags = configuration.getAllOptionFlags(); 785 mInterpolatorType = configuration.getInterpolatorType(); 786 mDurationMs = configuration.getDuration(); 787 mTimes = configuration.getTimes().clone(); 788 mVolumes = configuration.getVolumes().clone(); 789 } 790 791 /** 792 * @hide 793 * Set the {@code id} for system defined shapers. 794 * @param id the {@code id} to set. If non-negative, then it is used. 795 * If -1, then the system is expected to assign one. 796 * @return the same {@code Builder} instance. 797 * @throws IllegalArgumentException if {@code id} < -1. 798 */ setId(int id)799 public @NonNull Builder setId(int id) { 800 if (id < -1) { 801 throw new IllegalArgumentException("invalid id: " + id); 802 } 803 mId = id; 804 return this; 805 } 806 807 /** 808 * Sets the interpolator type. 809 * 810 * If omitted the default interpolator type is {@link #INTERPOLATOR_TYPE_CUBIC}. 811 * 812 * @param interpolatorType method of interpolation used for the volume curve. 813 * One of {@link #INTERPOLATOR_TYPE_STEP}, 814 * {@link #INTERPOLATOR_TYPE_LINEAR}, 815 * {@link #INTERPOLATOR_TYPE_CUBIC}, 816 * {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}. 817 * @return the same {@code Builder} instance. 818 * @throws IllegalArgumentException if {@code interpolatorType} is not valid. 819 */ setInterpolatorType(@nterpolatorType int interpolatorType)820 public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) { 821 switch (interpolatorType) { 822 case INTERPOLATOR_TYPE_STEP: 823 case INTERPOLATOR_TYPE_LINEAR: 824 case INTERPOLATOR_TYPE_CUBIC: 825 case INTERPOLATOR_TYPE_CUBIC_MONOTONIC: 826 mInterpolatorType = interpolatorType; 827 break; 828 default: 829 throw new IllegalArgumentException("invalid interpolatorType: " 830 + interpolatorType); 831 } 832 return this; 833 } 834 835 /** 836 * @hide 837 * Sets the optional flags 838 * 839 * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has 840 * changed the volume curve needs to be set again as the acceptable 841 * volume domain has changed. 842 * 843 * @param optionFlags new value to replace the old {@code optionFlags}. 844 * @return the same {@code Builder} instance. 845 * @throws IllegalArgumentException if flag is not recognized. 846 */ 847 @TestApi setOptionFlags(@ptionFlag int optionFlags)848 public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) { 849 if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) { 850 throw new IllegalArgumentException("invalid bits in flag: " + optionFlags); 851 } 852 mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags; 853 return this; 854 } 855 856 /** 857 * Sets the {@code VolumeShaper} duration in milliseconds. 858 * 859 * If omitted, the default duration is 1 second. 860 * 861 * @param durationMillis 862 * @return the same {@code Builder} instance. 863 * @throws IllegalArgumentException if {@code durationMillis} 864 * is not strictly positive. 865 */ setDuration(long durationMillis)866 public @NonNull Builder setDuration(long durationMillis) { 867 if (durationMillis <= 0) { 868 throw new IllegalArgumentException( 869 "duration: " + durationMillis + " not positive"); 870 } 871 mDurationMs = (double) durationMillis; 872 return this; 873 } 874 875 /** 876 * Sets the volume curve. 877 * 878 * The volume curve is represented by a set of control points given by 879 * two float arrays of equal length, 880 * one representing the time (x) coordinates 881 * and one corresponding to the volume (y) coordinates. 882 * The length must be at least 2 883 * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}. 884 * <p> 885 * The volume curve is normalized as follows: 886 * time (x) coordinates should be monotonically increasing, from 0.f to 1.f; 887 * volume (y) coordinates must be within 0.f to 1.f. 888 * <p> 889 * The time scale is set by {@link #setDuration}. 890 * <p> 891 * @param times an array of float values representing 892 * the time line of the volume curve. 893 * @param volumes an array of float values representing 894 * the amplitude of the volume curve. 895 * @return the same {@code Builder} instance. 896 * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid. 897 */ 898 899 /* Note: volume (y) coordinates must be non-positive for log scaling, 900 * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set. 901 */ 902 setCurve(@onNull float[] times, @NonNull float[] volumes)903 public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) { 904 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 905 checkCurveForErrorsAndThrowException(times, volumes, log, false /* ise */); 906 mTimes = times.clone(); 907 mVolumes = volumes.clone(); 908 return this; 909 } 910 911 /** 912 * Reflects the volume curve so that 913 * the shaper changes volume from the end 914 * to the start. 915 * 916 * @return the same {@code Builder} instance. 917 * @throws IllegalStateException if curve has not been set. 918 */ reflectTimes()919 public @NonNull Builder reflectTimes() { 920 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 921 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 922 int i; 923 for (i = 0; i < mTimes.length / 2; ++i) { 924 float temp = mTimes[i]; 925 mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i]; 926 mTimes[mTimes.length - 1 - i] = 1.f - temp; 927 temp = mVolumes[i]; 928 mVolumes[i] = mVolumes[mVolumes.length - 1 - i]; 929 mVolumes[mVolumes.length - 1 - i] = temp; 930 } 931 if ((mTimes.length & 1) != 0) { 932 mTimes[i] = 1.f - mTimes[i]; 933 } 934 return this; 935 } 936 937 /** 938 * Inverts the volume curve so that the max volume 939 * becomes the min volume and vice versa. 940 * 941 * @return the same {@code Builder} instance. 942 * @throws IllegalStateException if curve has not been set. 943 */ invertVolumes()944 public @NonNull Builder invertVolumes() { 945 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 946 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 947 float min = mVolumes[0]; 948 float max = mVolumes[0]; 949 for (int i = 1; i < mVolumes.length; ++i) { 950 if (mVolumes[i] < min) { 951 min = mVolumes[i]; 952 } else if (mVolumes[i] > max) { 953 max = mVolumes[i]; 954 } 955 } 956 957 final float maxmin = max + min; 958 for (int i = 0; i < mVolumes.length; ++i) { 959 mVolumes[i] = maxmin - mVolumes[i]; 960 } 961 return this; 962 } 963 964 /** 965 * Scale the curve end volume to a target value. 966 * 967 * Keeps the start volume the same. 968 * This works best if the volume curve is monotonic. 969 * 970 * @param volume the target end volume to use. 971 * @return the same {@code Builder} instance. 972 * @throws IllegalArgumentException if {@code volume} is not valid. 973 * @throws IllegalStateException if curve has not been set. 974 */ scaleToEndVolume(float volume)975 public @NonNull Builder scaleToEndVolume(float volume) { 976 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 977 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 978 checkValidVolumeAndThrowException(volume, log); 979 final float startVolume = mVolumes[0]; 980 final float endVolume = mVolumes[mVolumes.length - 1]; 981 if (endVolume == startVolume) { 982 // match with linear ramp 983 final float offset = volume - startVolume; 984 for (int i = 0; i < mVolumes.length; ++i) { 985 mVolumes[i] = mVolumes[i] + offset * mTimes[i]; 986 } 987 } else { 988 // scale 989 final float scale = (volume - startVolume) / (endVolume - startVolume); 990 for (int i = 0; i < mVolumes.length; ++i) { 991 mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume; 992 } 993 } 994 clampVolume(mVolumes, log); 995 return this; 996 } 997 998 /** 999 * Scale the curve start volume to a target value. 1000 * 1001 * Keeps the end volume the same. 1002 * This works best if the volume curve is monotonic. 1003 * 1004 * @param volume the target start volume to use. 1005 * @return the same {@code Builder} instance. 1006 * @throws IllegalArgumentException if {@code volume} is not valid. 1007 * @throws IllegalStateException if curve has not been set. 1008 */ scaleToStartVolume(float volume)1009 public @NonNull Builder scaleToStartVolume(float volume) { 1010 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 1011 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 1012 checkValidVolumeAndThrowException(volume, log); 1013 final float startVolume = mVolumes[0]; 1014 final float endVolume = mVolumes[mVolumes.length - 1]; 1015 if (endVolume == startVolume) { 1016 // match with linear ramp 1017 final float offset = volume - startVolume; 1018 for (int i = 0; i < mVolumes.length; ++i) { 1019 mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]); 1020 } 1021 } else { 1022 final float scale = (volume - endVolume) / (startVolume - endVolume); 1023 for (int i = 0; i < mVolumes.length; ++i) { 1024 mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume; 1025 } 1026 } 1027 clampVolume(mVolumes, log); 1028 return this; 1029 } 1030 1031 /** 1032 * Builds a new {@link VolumeShaper} object. 1033 * 1034 * @return a new {@link VolumeShaper} object. 1035 * @throws IllegalStateException if curve is not properly set. 1036 */ build()1037 public @NonNull Configuration build() { 1038 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 1039 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */); 1040 return new Configuration(mType, mId, mOptionFlags, mDurationMs, 1041 mInterpolatorType, mTimes, mVolumes); 1042 } 1043 } // Configuration.Builder 1044 } // Configuration 1045 1046 /** 1047 * The {@code VolumeShaper.Operation} class is used to specify operations 1048 * to the {@code VolumeShaper} that affect the volume change. 1049 */ 1050 public static final class Operation implements Parcelable { 1051 /** 1052 * Forward playback from current volume time position. 1053 * At the end of the {@code VolumeShaper} curve, 1054 * the last volume value persists. 1055 */ 1056 public static final Operation PLAY = 1057 new VolumeShaper.Operation.Builder() 1058 .build(); 1059 1060 /** 1061 * Reverse playback from current volume time position. 1062 * When the position reaches the start of the {@code VolumeShaper} curve, 1063 * the first volume value persists. 1064 */ 1065 public static final Operation REVERSE = 1066 new VolumeShaper.Operation.Builder() 1067 .reverse() 1068 .build(); 1069 1070 // No user serviceable parts below. 1071 1072 // These flags must match the native VolumeShaper::Operation::Flag 1073 /** @hide */ 1074 @IntDef({ 1075 FLAG_NONE, 1076 FLAG_REVERSE, 1077 FLAG_TERMINATE, 1078 FLAG_JOIN, 1079 FLAG_DEFER, 1080 }) 1081 @Retention(RetentionPolicy.SOURCE) 1082 public @interface Flag {} 1083 1084 /** 1085 * No special {@code VolumeShaper} operation. 1086 */ 1087 private static final int FLAG_NONE = 0; 1088 1089 /** 1090 * Reverse the {@code VolumeShaper} progress. 1091 * 1092 * Reverses the {@code VolumeShaper} curve from its current 1093 * position. If the {@code VolumeShaper} curve has not started, 1094 * it automatically is considered finished. 1095 */ 1096 private static final int FLAG_REVERSE = 1 << 0; 1097 1098 /** 1099 * Terminate the existing {@code VolumeShaper}. 1100 * This flag is generally used by itself; 1101 * it takes precedence over all other flags. 1102 */ 1103 private static final int FLAG_TERMINATE = 1 << 1; 1104 1105 /** 1106 * Attempt to join as best as possible to the previous {@code VolumeShaper}. 1107 * This requires the previous {@code VolumeShaper} to be active and 1108 * {@link #setReplaceId} to be set. 1109 */ 1110 private static final int FLAG_JOIN = 1 << 2; 1111 1112 /** 1113 * Defer playback until next operation is sent. This is used 1114 * when starting a {@code VolumeShaper} effect. 1115 */ 1116 private static final int FLAG_DEFER = 1 << 3; 1117 1118 /** 1119 * Use the id specified in the configuration, creating 1120 * {@code VolumeShaper} as needed; the configuration should be 1121 * TYPE_SCALE. 1122 */ 1123 private static final int FLAG_CREATE_IF_NEEDED = 1 << 4; 1124 1125 private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE; 1126 1127 private final int mFlags; 1128 private final int mReplaceId; 1129 private final float mXOffset; 1130 1131 @Override toString()1132 public String toString() { 1133 return "VolumeShaper.Operation{" 1134 + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase() 1135 + ", mReplaceId = " + mReplaceId 1136 + ", mXOffset = " + mXOffset 1137 + "}"; 1138 } 1139 1140 @Override hashCode()1141 public int hashCode() { 1142 return Objects.hash(mFlags, mReplaceId, mXOffset); 1143 } 1144 1145 @Override equals(Object o)1146 public boolean equals(Object o) { 1147 if (!(o instanceof Operation)) return false; 1148 if (o == this) return true; 1149 final Operation other = (Operation) o; 1150 1151 return mFlags == other.mFlags 1152 && mReplaceId == other.mReplaceId 1153 && Float.compare(mXOffset, other.mXOffset) == 0; 1154 } 1155 1156 @Override describeContents()1157 public int describeContents() { 1158 return 0; 1159 } 1160 1161 @Override writeToParcel(Parcel dest, int flags)1162 public void writeToParcel(Parcel dest, int flags) { 1163 // this needs to match the native VolumeShaper.Operation parceling 1164 dest.writeInt(mFlags); 1165 dest.writeInt(mReplaceId); 1166 dest.writeFloat(mXOffset); 1167 } 1168 1169 public static final Parcelable.Creator<VolumeShaper.Operation> CREATOR 1170 = new Parcelable.Creator<VolumeShaper.Operation>() { 1171 @Override 1172 public VolumeShaper.Operation createFromParcel(Parcel p) { 1173 // this needs to match the native VolumeShaper.Operation parceling 1174 final int flags = p.readInt(); 1175 final int replaceId = p.readInt(); 1176 final float xOffset = p.readFloat(); 1177 1178 return new VolumeShaper.Operation( 1179 flags 1180 , replaceId 1181 , xOffset); 1182 } 1183 1184 @Override 1185 public VolumeShaper.Operation[] newArray(int size) { 1186 return new VolumeShaper.Operation[size]; 1187 } 1188 }; 1189 Operation(@lag int flags, int replaceId, float xOffset)1190 private Operation(@Flag int flags, int replaceId, float xOffset) { 1191 mFlags = flags; 1192 mReplaceId = replaceId; 1193 mXOffset = xOffset; 1194 } 1195 1196 /** 1197 * @hide 1198 * {@code Builder} class for {@link VolumeShaper.Operation} object. 1199 * 1200 * Not for public use. 1201 */ 1202 public static final class Builder { 1203 int mFlags; 1204 int mReplaceId; 1205 float mXOffset; 1206 1207 /** 1208 * Constructs a new {@code Builder} with the defaults. 1209 */ Builder()1210 public Builder() { 1211 mFlags = 0; 1212 mReplaceId = -1; 1213 mXOffset = Float.NaN; 1214 } 1215 1216 /** 1217 * Constructs a new {@code Builder} from a given {@code VolumeShaper.Operation} 1218 * @param operation the {@code VolumeShaper.operation} whose data will be 1219 * reused in the new {@code Builder}. 1220 */ Builder(@onNull VolumeShaper.Operation operation)1221 public Builder(@NonNull VolumeShaper.Operation operation) { 1222 mReplaceId = operation.mReplaceId; 1223 mFlags = operation.mFlags; 1224 mXOffset = operation.mXOffset; 1225 } 1226 1227 /** 1228 * Replaces the previous {@code VolumeShaper} specified by {@code id}. 1229 * 1230 * The {@code VolumeShaper} specified by the {@code id} is removed 1231 * if it exists. The configuration should be TYPE_SCALE. 1232 * 1233 * @param id the {@code id} of the previous {@code VolumeShaper}. 1234 * @param join if true, match the volume of the previous 1235 * shaper to the start volume of the new {@code VolumeShaper}. 1236 * @return the same {@code Builder} instance. 1237 */ replace(int id, boolean join)1238 public @NonNull Builder replace(int id, boolean join) { 1239 mReplaceId = id; 1240 if (join) { 1241 mFlags |= FLAG_JOIN; 1242 } else { 1243 mFlags &= ~FLAG_JOIN; 1244 } 1245 return this; 1246 } 1247 1248 /** 1249 * Defers all operations. 1250 * @return the same {@code Builder} instance. 1251 */ defer()1252 public @NonNull Builder defer() { 1253 mFlags |= FLAG_DEFER; 1254 return this; 1255 } 1256 1257 /** 1258 * Terminates the {@code VolumeShaper}. 1259 * 1260 * Do not call directly, use {@link VolumeShaper#close()}. 1261 * @return the same {@code Builder} instance. 1262 */ terminate()1263 public @NonNull Builder terminate() { 1264 mFlags |= FLAG_TERMINATE; 1265 return this; 1266 } 1267 1268 /** 1269 * Reverses direction. 1270 * @return the same {@code Builder} instance. 1271 */ reverse()1272 public @NonNull Builder reverse() { 1273 mFlags ^= FLAG_REVERSE; 1274 return this; 1275 } 1276 1277 /** 1278 * Use the id specified in the configuration, creating 1279 * {@code VolumeShaper} only as needed; the configuration should be 1280 * TYPE_SCALE. 1281 * 1282 * If the {@code VolumeShaper} with the same id already exists 1283 * then the operation has no effect. 1284 * 1285 * @return the same {@code Builder} instance. 1286 */ createIfNeeded()1287 public @NonNull Builder createIfNeeded() { 1288 mFlags |= FLAG_CREATE_IF_NEEDED; 1289 return this; 1290 } 1291 1292 /** 1293 * Sets the {@code xOffset} to use for the {@code VolumeShaper}. 1294 * 1295 * The {@code xOffset} is the position on the volume curve, 1296 * and setting takes effect when the {@code VolumeShaper} is used next. 1297 * 1298 * @param xOffset a value between (or equal to) 0.f and 1.f, or Float.NaN to ignore. 1299 * @return the same {@code Builder} instance. 1300 * @throws IllegalArgumentException if {@code xOffset} is not between 0.f and 1.f, 1301 * or a Float.NaN. 1302 */ setXOffset(float xOffset)1303 public @NonNull Builder setXOffset(float xOffset) { 1304 if (xOffset < -0.f) { 1305 throw new IllegalArgumentException("Negative xOffset not allowed"); 1306 } else if (xOffset > 1.f) { 1307 throw new IllegalArgumentException("xOffset > 1.f not allowed"); 1308 } 1309 // Float.NaN passes through 1310 mXOffset = xOffset; 1311 return this; 1312 } 1313 1314 /** 1315 * Sets the operation flag. Do not call this directly but one of the 1316 * other builder methods. 1317 * 1318 * @param flags new value for {@code flags}, consisting of ORed flags. 1319 * @return the same {@code Builder} instance. 1320 * @throws IllegalArgumentException if {@code flags} contains invalid set bits. 1321 */ setFlags(@lag int flags)1322 private @NonNull Builder setFlags(@Flag int flags) { 1323 if ((flags & ~FLAG_PUBLIC_ALL) != 0) { 1324 throw new IllegalArgumentException("flag has unknown bits set: " + flags); 1325 } 1326 mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags; 1327 return this; 1328 } 1329 1330 /** 1331 * Builds a new {@link VolumeShaper.Operation} object. 1332 * 1333 * @return a new {@code VolumeShaper.Operation} object 1334 */ build()1335 public @NonNull Operation build() { 1336 return new Operation(mFlags, mReplaceId, mXOffset); 1337 } 1338 } // Operation.Builder 1339 } // Operation 1340 1341 /** 1342 * @hide 1343 * {@code VolumeShaper.State} represents the current progress 1344 * of the {@code VolumeShaper}. 1345 * 1346 * Not for public use. 1347 */ 1348 public static final class State implements Parcelable { 1349 private float mVolume; 1350 private float mXOffset; 1351 1352 @Override toString()1353 public String toString() { 1354 return "VolumeShaper.State{" 1355 + "mVolume = " + mVolume 1356 + ", mXOffset = " + mXOffset 1357 + "}"; 1358 } 1359 1360 @Override hashCode()1361 public int hashCode() { 1362 return Objects.hash(mVolume, mXOffset); 1363 } 1364 1365 @Override equals(Object o)1366 public boolean equals(Object o) { 1367 if (!(o instanceof State)) return false; 1368 if (o == this) return true; 1369 final State other = (State) o; 1370 return mVolume == other.mVolume 1371 && mXOffset == other.mXOffset; 1372 } 1373 1374 @Override describeContents()1375 public int describeContents() { 1376 return 0; 1377 } 1378 1379 @Override writeToParcel(Parcel dest, int flags)1380 public void writeToParcel(Parcel dest, int flags) { 1381 dest.writeFloat(mVolume); 1382 dest.writeFloat(mXOffset); 1383 } 1384 1385 public static final Parcelable.Creator<VolumeShaper.State> CREATOR 1386 = new Parcelable.Creator<VolumeShaper.State>() { 1387 @Override 1388 public VolumeShaper.State createFromParcel(Parcel p) { 1389 return new VolumeShaper.State( 1390 p.readFloat() // volume 1391 , p.readFloat()); // xOffset 1392 } 1393 1394 @Override 1395 public VolumeShaper.State[] newArray(int size) { 1396 return new VolumeShaper.State[size]; 1397 } 1398 }; 1399 State(float volume, float xOffset)1400 /* package */ State(float volume, float xOffset) { 1401 mVolume = volume; 1402 mXOffset = xOffset; 1403 } 1404 1405 /** 1406 * Gets the volume of the {@link VolumeShaper.State}. 1407 * @return linear volume between 0.f and 1.f. 1408 */ getVolume()1409 public float getVolume() { 1410 return mVolume; 1411 } 1412 1413 /** 1414 * Gets the {@code xOffset} position on the normalized curve 1415 * of the {@link VolumeShaper.State}. 1416 * @return the curve x position between 0.f and 1.f. 1417 */ getXOffset()1418 public float getXOffset() { 1419 return mXOffset; 1420 } 1421 } // State 1422 } 1423