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