VolumeShaper.java revision d4f1e86190fbe6b280635902a3cd734d65eded52
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 .setDurationMs(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 .setDurationMs(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 .setDurationMs(1000.) 352 .build(); 353 SCURVE_RAMP = new VolumeShaper.Configuration.Builder() 354 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC) 355 .setCurve(times, scurve) 356 .setDurationMs(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 435 dest.writeFloat(0.f); // last slope 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 460 final float lastSlope = p.readFloat(); // ignored 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 double getDurationMs() { 573 return mDurationMs; 574 } 575 576 /** 577 * Returns the times (x) coordinate array of the volume curve points. 578 */ 579 public float[] getTimes() { 580 return mTimes; 581 } 582 583 /** 584 * Returns the volumes (y) coordinate array of the volume curve points. 585 */ 586 public float[] getVolumes() { 587 return mVolumes; 588 } 589 590 /** 591 * Checks the validity of times and volumes point representation. 592 * 593 * {@code times[]} and {@code volumes[]} are two arrays representing points 594 * for the volume curve. 595 * 596 * @param times the x coordinates for the points, 597 * must be between 0.f and 1.f and be monotonic. 598 * @param volumes the y coordinates for the points, 599 * must be between 0.f and 1.f for linear and 600 * must be no greater than 0.f for log (dBFS). 601 * @param log set to true if the scale is logarithmic. 602 * @return null if no error, or the reason in a {@code String} for an error. 603 */ 604 private static @Nullable String checkCurveForErrors( 605 @Nullable float[] times, @Nullable float[] volumes, boolean log) { 606 if (times == null) { 607 return "times array must be non-null"; 608 } else if (volumes == null) { 609 return "volumes array must be non-null"; 610 } else if (times.length != volumes.length) { 611 return "array length must match"; 612 } else if (times.length < 2) { 613 return "array length must be at least 2"; 614 } else if (times.length > MAXIMUM_CURVE_POINTS) { 615 return "array length must be no larger than " + MAXIMUM_CURVE_POINTS; 616 } else if (times[0] != 0.f) { 617 return "times must start at 0.f"; 618 } else if (times[times.length - 1] != 1.f) { 619 return "times must end at 1.f"; 620 } 621 622 // validate points along the curve 623 for (int i = 1; i < times.length; ++i) { 624 if (!(times[i] > times[i - 1]) /* handle nan */) { 625 return "times not monotonic increasing, check index " + i; 626 } 627 } 628 if (log) { 629 for (int i = 0; i < volumes.length; ++i) { 630 if (!(volumes[i] <= 0.f) /* handle nan */) { 631 return "volumes for log scale cannot be positive, " 632 + "check index " + i; 633 } 634 } 635 } else { 636 for (int i = 0; i < volumes.length; ++i) { 637 if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) { 638 return "volumes for linear scale must be between 0.f and 1.f, " 639 + "check index " + i; 640 } 641 } 642 } 643 return null; // no errors 644 } 645 646 private static void checkCurveForErrorsAndThrowException( 647 @Nullable float[] times, @Nullable float[] volumes, boolean log) { 648 final String error = checkCurveForErrors(times, volumes, log); 649 if (error != null) { 650 throw new IllegalArgumentException(error); 651 } 652 } 653 654 private static void checkValidVolumeAndThrowException(float volume, boolean log) { 655 if (log) { 656 if (!(volume <= 0.f) /* handle nan */) { 657 throw new IllegalArgumentException("dbfs volume must be 0.f or less"); 658 } 659 } else { 660 if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) { 661 throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f"); 662 } 663 } 664 } 665 666 private static void clampVolume(float[] volumes, boolean log) { 667 if (log) { 668 for (int i = 0; i < volumes.length; ++i) { 669 if (!(volumes[i] <= 0.f) /* handle nan */) { 670 volumes[i] = 0.f; 671 } 672 } 673 } else { 674 for (int i = 0; i < volumes.length; ++i) { 675 if (!(volumes[i] >= 0.f) /* handle nan */) { 676 volumes[i] = 0.f; 677 } else if (!(volumes[i] <= 1.f)) { 678 volumes[i] = 1.f; 679 } 680 } 681 } 682 } 683 684 /** 685 * Builder class for a {@link VolumeShaper.Configuration} object. 686 * <p> Here is an example where {@code Builder} is used to define the 687 * {@link VolumeShaper.Configuration}. 688 * 689 * <pre class="prettyprint"> 690 * VolumeShaper.Configuration LINEAR_RAMP = 691 * new VolumeShaper.Configuration.Builder() 692 * .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR) 693 * .setCurve(new float[] { 0.f, 1.f }, // times 694 * new float[] { 0.f, 1.f }) // volumes 695 * .setDurationMs(1000.) 696 * .build(); 697 * </pre> 698 * <p> 699 */ 700 public static final class Builder { 701 private int mType = TYPE_SCALE; 702 private int mId = -1; // invalid 703 private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC; 704 private int mOptionFlags = OPTION_FLAG_CLOCK_TIME; 705 private double mDurationMs = 1000.; 706 private float[] mTimes = null; 707 private float[] mVolumes = null; 708 709 /** 710 * Constructs a new {@code Builder} with the defaults. 711 */ 712 public Builder() { 713 } 714 715 /** 716 * Constructs a new {@code Builder} with settings 717 * copied from a given {@code VolumeShaper.Configuration}. 718 * @param configuration prototypical configuration 719 * which will be reused in the new {@code Builder}. 720 */ 721 public Builder(@NonNull Configuration configuration) { 722 mType = configuration.getType(); 723 mId = configuration.getId(); 724 mOptionFlags = configuration.getAllOptionFlags(); 725 mInterpolatorType = configuration.getInterpolatorType(); 726 mDurationMs = configuration.getDurationMs(); 727 mTimes = configuration.getTimes().clone(); 728 mVolumes = configuration.getVolumes().clone(); 729 } 730 731 /** 732 * @hide 733 * Set the {@code id} for system defined shapers. 734 * @param id the {@code id} to set. If non-negative, then it is used. 735 * If -1, then the system is expected to assign one. 736 * @return the same {@code Builder} instance. 737 * @throws IllegalArgumentException if {@code id} < -1. 738 */ 739 public @NonNull Builder setId(int id) { 740 if (id < -1) { 741 throw new IllegalArgumentException("invalid id: " + id); 742 } 743 mId = id; 744 return this; 745 } 746 747 /** 748 * Sets the interpolator type. 749 * 750 * If omitted the interplator type is {@link #INTERPOLATOR_TYPE_CUBIC}. 751 * 752 * @param interpolatorType method of interpolation used for the volume curve. 753 * One of {@link #INTERPOLATOR_TYPE_STEP}, 754 * {@link #INTERPOLATOR_TYPE_LINEAR}, 755 * {@link #INTERPOLATOR_TYPE_CUBIC}, 756 * {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}. 757 * @return the same {@code Builder} instance. 758 * @throws IllegalArgumentException if {@code interpolatorType} is not valid. 759 */ 760 public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) { 761 switch (interpolatorType) { 762 case INTERPOLATOR_TYPE_STEP: 763 case INTERPOLATOR_TYPE_LINEAR: 764 case INTERPOLATOR_TYPE_CUBIC: 765 case INTERPOLATOR_TYPE_CUBIC_MONOTONIC: 766 mInterpolatorType = interpolatorType; 767 break; 768 default: 769 throw new IllegalArgumentException("invalid interpolatorType: " 770 + interpolatorType); 771 } 772 return this; 773 } 774 775 /** 776 * @hide 777 * Sets the optional flags 778 * 779 * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has 780 * changed the volume curve needs to be set again as the acceptable 781 * volume domain has changed. 782 * 783 * @param optionFlags new value to replace the old {@code optionFlags}. 784 * @return the same {@code Builder} instance. 785 * @throws IllegalArgumentException if flag is not recognized. 786 */ 787 public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) { 788 if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) { 789 throw new IllegalArgumentException("invalid bits in flag: " + optionFlags); 790 } 791 mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags; 792 return this; 793 } 794 795 /** 796 * Sets the volume shaper duration in milliseconds. 797 * 798 * If omitted, the default duration is 1 second. 799 * 800 * @param durationMs 801 * @return the same {@code Builder} instance. 802 * @throws IllegalArgumentException if {@code durationMs} 803 * is not strictly positive. 804 */ 805 public @NonNull Builder setDurationMs(double durationMs) { 806 if (durationMs <= 0.) { 807 throw new IllegalArgumentException( 808 "duration: " + durationMs + " not positive"); 809 } 810 mDurationMs = durationMs; 811 return this; 812 } 813 814 /** 815 * Sets the volume curve. 816 * 817 * The volume curve is represented by a set of control points given by 818 * two float arrays of equal length, 819 * one representing the time (x) coordinates 820 * and one corresponding to the volume (y) coordinates. 821 * The length must be at least 2 822 * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}. 823 * <p> 824 * The volume curve is normalized as follows: 825 * time (x) coordinates should be monotonically increasing, from 0.f to 1.f; 826 * volume (y) coordinates must be within 0.f to 1.f. 827 * <p> 828 * The time scale is set by {@link #setDurationMs}. 829 * <p> 830 * @param times an array of float values representing 831 * the time line of the volume curve. 832 * @param volumes an array of float values representing 833 * the amplitude of the volume curve. 834 * @return the same {@code Builder} instance. 835 * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid. 836 */ 837 838 /* Note: volume (y) coordinates must be non-positive for log scaling, 839 * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set. 840 */ 841 842 public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) { 843 checkCurveForErrorsAndThrowException( 844 times, volumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0); 845 mTimes = times.clone(); 846 mVolumes = volumes.clone(); 847 return this; 848 } 849 850 /** 851 * Reflects the volume curve so that 852 * the shaper changes volume from the end 853 * to the start. 854 * 855 * @return the same {@code Builder} instance. 856 * @throws IllegalArgumentException if curve has not been set. 857 */ 858 public @NonNull Builder reflectTimes() { 859 checkCurveForErrorsAndThrowException( 860 mTimes, mVolumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0); 861 int i; 862 for (i = 0; i < mTimes.length / 2; ++i) { 863 float temp = mTimes[i]; 864 mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i]; 865 mTimes[mTimes.length - 1 - i] = 1.f - temp; 866 temp = mVolumes[i]; 867 mVolumes[i] = mVolumes[mVolumes.length - 1 - i]; 868 mVolumes[mVolumes.length - 1 - i] = temp; 869 } 870 if ((mTimes.length & 1) != 0) { 871 mTimes[i] = 1.f - mTimes[i]; 872 } 873 return this; 874 } 875 876 /** 877 * Inverts the volume curve so that the max volume 878 * becomes the min volume and vice versa. 879 * 880 * @return the same {@code Builder} instance. 881 * @throws IllegalArgumentException if curve has not been set. 882 */ 883 public @NonNull Builder invertVolumes() { 884 checkCurveForErrorsAndThrowException( 885 mTimes, mVolumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0); 886 float min = mVolumes[0]; 887 float max = mVolumes[0]; 888 for (int i = 1; i < mVolumes.length; ++i) { 889 if (mVolumes[i] < min) { 890 min = mVolumes[i]; 891 } else if (mVolumes[i] > max) { 892 max = mVolumes[i]; 893 } 894 } 895 896 final float maxmin = max + min; 897 for (int i = 0; i < mVolumes.length; ++i) { 898 mVolumes[i] = maxmin - mVolumes[i]; 899 } 900 return this; 901 } 902 903 /** 904 * Scale the curve end volume to a target value. 905 * 906 * Keeps the start volume the same. 907 * This works best if the volume curve is monotonic. 908 * 909 * @param volume the target end volume to use. 910 * @return the same {@code Builder} instance. 911 * @throws IllegalArgumentException if {@code volume} 912 * is not valid or if curve has not been set. 913 */ 914 public @NonNull Builder scaleToEndVolume(float volume) { 915 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 916 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log); 917 checkValidVolumeAndThrowException(volume, log); 918 final float startVolume = mVolumes[0]; 919 final float endVolume = mVolumes[mVolumes.length - 1]; 920 if (endVolume == startVolume) { 921 // match with linear ramp 922 final float offset = volume - startVolume; 923 for (int i = 0; i < mVolumes.length; ++i) { 924 mVolumes[i] = mVolumes[i] + offset * mTimes[i]; 925 } 926 } else { 927 // scale 928 final float scale = (volume - startVolume) / (endVolume - startVolume); 929 for (int i = 0; i < mVolumes.length; ++i) { 930 mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume; 931 } 932 } 933 clampVolume(mVolumes, log); 934 return this; 935 } 936 937 /** 938 * Scale the curve start volume to a target value. 939 * 940 * Keeps the end volume the same. 941 * This works best if the volume curve is monotonic. 942 * 943 * @param volume the target start volume to use. 944 * @return the same {@code Builder} instance. 945 * @throws IllegalArgumentException if {@code volume} 946 * is not valid or if curve has not been set. 947 */ 948 public @NonNull Builder scaleToStartVolume(float volume) { 949 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0; 950 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log); 951 checkValidVolumeAndThrowException(volume, log); 952 final float startVolume = mVolumes[0]; 953 final float endVolume = mVolumes[mVolumes.length - 1]; 954 if (endVolume == startVolume) { 955 // match with linear ramp 956 final float offset = volume - startVolume; 957 for (int i = 0; i < mVolumes.length; ++i) { 958 mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]); 959 } 960 } else { 961 final float scale = (volume - endVolume) / (startVolume - endVolume); 962 for (int i = 0; i < mVolumes.length; ++i) { 963 mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume; 964 } 965 } 966 clampVolume(mVolumes, log); 967 return this; 968 } 969 970 /** 971 * Builds a new {@link VolumeShaper} object. 972 * 973 * @return a new {@link VolumeShaper} object. 974 * @throws IllegalArgumentException if curve is not properly set. 975 */ 976 public @NonNull Configuration build() { 977 checkCurveForErrorsAndThrowException( 978 mTimes, mVolumes, (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0); 979 return new Configuration(mType, mId, mOptionFlags, mDurationMs, 980 mInterpolatorType, mTimes, mVolumes); 981 } 982 } // Configuration.Builder 983 } // Configuration 984 985 /** 986 * The {@code VolumeShaper.Operation} class is used to specify operations 987 * to the {@code VolumeShaper} that affect the volume change. 988 */ 989 public static final class Operation implements Parcelable { 990 /** 991 * Forward playback from current volume time position. 992 * At the end of the {@code VolumeShaper} curve, 993 * the last volume value persists. 994 */ 995 public static final Operation PLAY = 996 new VolumeShaper.Operation.Builder() 997 .build(); 998 999 /** 1000 * Reverse playback from current volume time position. 1001 * When the position reaches the start of the {@code VolumeShaper} curve, 1002 * the first volume value persists. 1003 */ 1004 public static final Operation REVERSE = 1005 new VolumeShaper.Operation.Builder() 1006 .reverse() 1007 .build(); 1008 1009 // No user serviceable parts below. 1010 1011 // These flags must match the native VolumeShaper::Operation::Flag 1012 /** @hide */ 1013 @IntDef({ 1014 FLAG_NONE, 1015 FLAG_REVERSE, 1016 FLAG_TERMINATE, 1017 FLAG_JOIN, 1018 FLAG_DEFER, 1019 }) 1020 @Retention(RetentionPolicy.SOURCE) 1021 public @interface Flag {} 1022 1023 /** 1024 * No special {@code VolumeShaper} operation. 1025 */ 1026 private static final int FLAG_NONE = 0; 1027 1028 /** 1029 * Reverse the {@code VolumeShaper} progress. 1030 * 1031 * Reverses the {@code VolumeShaper} curve from its current 1032 * position. If the {@code VolumeShaper} curve has not started, 1033 * it automatically is considered finished. 1034 */ 1035 private static final int FLAG_REVERSE = 1 << 0; 1036 1037 /** 1038 * Terminate the existing {@code VolumeShaper}. 1039 * This flag is generally used by itself; 1040 * it takes precedence over all other flags. 1041 */ 1042 private static final int FLAG_TERMINATE = 1 << 1; 1043 1044 /** 1045 * Attempt to join as best as possible to the previous {@code VolumeShaper}. 1046 * This requires the previous {@code VolumeShaper} to be active and 1047 * {@link #setReplaceId} to be set. 1048 */ 1049 private static final int FLAG_JOIN = 1 << 2; 1050 1051 /** 1052 * Defer playback until next operation is sent. This is used 1053 * when starting a VolumeShaper effect. 1054 */ 1055 private static final int FLAG_DEFER = 1 << 3; 1056 1057 /** 1058 * Use the id specified in the configuration, creating 1059 * VolumeShaper as needed; the configuration should be 1060 * TYPE_SCALE. 1061 */ 1062 private static final int FLAG_CREATE_IF_NEEDED = 1 << 4; 1063 1064 private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE; 1065 1066 private final int mFlags; 1067 private final int mReplaceId; 1068 1069 @Override 1070 public String toString() { 1071 return "VolumeShaper.Operation{" 1072 + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase() 1073 + ", mReplaceId = " + mReplaceId 1074 + "}"; 1075 } 1076 1077 @Override 1078 public int hashCode() { 1079 return Objects.hash(mFlags, mReplaceId); 1080 } 1081 1082 @Override 1083 public boolean equals(Object o) { 1084 if (!(o instanceof Operation)) return false; 1085 if (o == this) return true; 1086 final Operation other = (Operation) o; 1087 // if xOffset (native field only) is brought into Java 1088 // we need to do proper NaN comparison as that is allowed. 1089 return mFlags == other.mFlags 1090 && mReplaceId == other.mReplaceId; 1091 } 1092 1093 @Override 1094 public int describeContents() { 1095 return 0; 1096 } 1097 1098 @Override 1099 public void writeToParcel(Parcel dest, int flags) { 1100 // this needs to match the native VolumeShaper.Operation parceling 1101 dest.writeInt(mFlags); 1102 dest.writeInt(mReplaceId); 1103 dest.writeFloat(Float.NaN); // xOffset (ignored at Java level) 1104 } 1105 1106 public static final Parcelable.Creator<VolumeShaper.Operation> CREATOR 1107 = new Parcelable.Creator<VolumeShaper.Operation>() { 1108 @Override 1109 public VolumeShaper.Operation createFromParcel(Parcel p) { 1110 // this needs to match the native VolumeShaper.Operation parceling 1111 final int flags = p.readInt(); 1112 final int replaceId = p.readInt(); 1113 final float xOffset = p.readFloat(); // ignored at Java level 1114 1115 return new VolumeShaper.Operation( 1116 flags 1117 , replaceId); 1118 } 1119 1120 @Override 1121 public VolumeShaper.Operation[] newArray(int size) { 1122 return new VolumeShaper.Operation[size]; 1123 } 1124 }; 1125 1126 private Operation(@Flag int flags, int replaceId) { 1127 mFlags = flags; 1128 mReplaceId = replaceId; 1129 } 1130 1131 /** 1132 * @hide 1133 * {@code Builder} class for {@link VolumeShaper.Operation} object. 1134 * 1135 * Not for public use. 1136 */ 1137 public static final class Builder { 1138 int mFlags; 1139 int mReplaceId; 1140 1141 /** 1142 * Constructs a new {@code Builder} with the defaults. 1143 */ 1144 public Builder() { 1145 mFlags = 0; 1146 mReplaceId = -1; 1147 } 1148 1149 /** 1150 * Constructs a new Builder from a given {@code VolumeShaper.Operation} 1151 * @param operation the {@code VolumeShaper.operation} whose data will be 1152 * reused in the new Builder. 1153 */ 1154 public Builder(@NonNull VolumeShaper.Operation operation) { 1155 mReplaceId = operation.mReplaceId; 1156 mFlags = operation.mFlags; 1157 } 1158 1159 /** 1160 * Replaces the previous {@code VolumeShaper} specified by id. 1161 * It has no other effect if the {@code VolumeShaper} is 1162 * already expired. 1163 * @param id the id of the previous {@code VolumeShaper}. 1164 * @param join if true, match the volume of the previous 1165 * shaper to the start volume of the new {@code VolumeShaper}. 1166 * @return the same {@code Builder} instance. 1167 */ 1168 public @NonNull Builder replace(int id, boolean join) { 1169 mReplaceId = id; 1170 if (join) { 1171 mFlags |= FLAG_JOIN; 1172 } else { 1173 mFlags &= ~FLAG_JOIN; 1174 } 1175 return this; 1176 } 1177 1178 /** 1179 * Defers all operations. 1180 * @return the same {@code Builder} instance. 1181 */ 1182 public @NonNull Builder defer() { 1183 mFlags |= FLAG_DEFER; 1184 return this; 1185 } 1186 1187 /** 1188 * Terminates the VolumeShaper. 1189 * Do not call directly, use {@link VolumeShaper#release()}. 1190 * @return the same {@code Builder} instance. 1191 */ 1192 public @NonNull Builder terminate() { 1193 mFlags |= FLAG_TERMINATE; 1194 return this; 1195 } 1196 1197 /** 1198 * Reverses direction. 1199 * @return the same {@code Builder} instance. 1200 */ 1201 public @NonNull Builder reverse() { 1202 mFlags ^= FLAG_REVERSE; 1203 return this; 1204 } 1205 1206 /** 1207 * Use the id specified in the configuration, creating 1208 * VolumeShaper as needed; the configuration should be 1209 * TYPE_SCALE. 1210 * @return the same {@code Builder} instance. 1211 */ 1212 public @NonNull Builder createIfNeeded() { 1213 mFlags |= FLAG_CREATE_IF_NEEDED; 1214 return this; 1215 } 1216 1217 /** 1218 * Sets the operation flag. Do not call this directly but one of the 1219 * other builder methods. 1220 * 1221 * @param flags new value for {@code flags}, consisting of ORed flags. 1222 * @return the same {@code Builder} instance. 1223 * @throws IllegalArgumentException if {@code flags} contains invalid set bits. 1224 */ 1225 private @NonNull Builder setFlags(@Flag int flags) { 1226 if ((flags & ~FLAG_PUBLIC_ALL) != 0) { 1227 throw new IllegalArgumentException("flag has unknown bits set: " + flags); 1228 } 1229 mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags; 1230 return this; 1231 } 1232 1233 /** 1234 * Builds a new {@link VolumeShaper.Operation} object. 1235 * 1236 * @return a new {@code VolumeShaper.Operation} object 1237 */ 1238 public @NonNull Operation build() { 1239 return new Operation(mFlags, mReplaceId); 1240 } 1241 } // Operation.Builder 1242 } // Operation 1243 1244 /** 1245 * @hide 1246 * {@code VolumeShaper.State} represents the current progress 1247 * of the {@code VolumeShaper}. 1248 * 1249 * Not for public use. 1250 */ 1251 public static final class State implements Parcelable { 1252 private float mVolume; 1253 private float mXOffset; 1254 1255 @Override 1256 public String toString() { 1257 return "VolumeShaper.State{" 1258 + "mVolume = " + mVolume 1259 + ", mXOffset = " + mXOffset 1260 + "}"; 1261 } 1262 1263 @Override 1264 public int hashCode() { 1265 return Objects.hash(mVolume, mXOffset); 1266 } 1267 1268 @Override 1269 public boolean equals(Object o) { 1270 if (!(o instanceof State)) return false; 1271 if (o == this) return true; 1272 final State other = (State) o; 1273 return mVolume == other.mVolume 1274 && mXOffset == other.mXOffset; 1275 } 1276 1277 @Override 1278 public int describeContents() { 1279 return 0; 1280 } 1281 1282 @Override 1283 public void writeToParcel(Parcel dest, int flags) { 1284 dest.writeFloat(mVolume); 1285 dest.writeFloat(mXOffset); 1286 } 1287 1288 public static final Parcelable.Creator<VolumeShaper.State> CREATOR 1289 = new Parcelable.Creator<VolumeShaper.State>() { 1290 @Override 1291 public VolumeShaper.State createFromParcel(Parcel p) { 1292 return new VolumeShaper.State( 1293 p.readFloat() // volume 1294 , p.readFloat()); // xOffset 1295 } 1296 1297 @Override 1298 public VolumeShaper.State[] newArray(int size) { 1299 return new VolumeShaper.State[size]; 1300 } 1301 }; 1302 1303 /* package */ State(float volume, float xOffset) { 1304 mVolume = volume; 1305 mXOffset = xOffset; 1306 } 1307 1308 /** 1309 * Gets the volume of the {@link VolumeShaper.State}. 1310 */ 1311 public float getVolume() { 1312 return mVolume; 1313 } 1314 1315 /** 1316 * Gets the elapsed ms of the {@link VolumeShaper.State} 1317 */ 1318 public double getXOffset() { 1319 return mXOffset; 1320 } 1321 } // State 1322} 1323