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