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