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