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