blob: 663d564af23e0a04bf2d3bd94a26df2400309e9a [file] [log] [blame]
Andy Hung035d4ec2017-01-24 13:45:02 -08001/*
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;
Andy Hung3ce023b2018-04-05 17:38:11 -070021import android.annotation.TestApi;
Mathew Inwood31a792a2018-08-17 08:54:26 +010022import android.annotation.UnsupportedAppUsage;
Andy Hung035d4ec2017-01-24 13:45:02 -080023import android.os.Parcel;
24import android.os.Parcelable;
25
26import java.lang.annotation.Retention;
27import java.lang.annotation.RetentionPolicy;
Andy Hungfef734c2017-02-23 16:21:13 -080028import java.lang.AutoCloseable;
Andy Hung035d4ec2017-01-24 13:45:02 -080029import java.lang.ref.WeakReference;
Andy Hungd4f1e862017-03-06 11:39:10 -080030import java.util.Arrays;
Andy Hung035d4ec2017-01-24 13:45:02 -080031import java.util.Objects;
32
33/**
Andy Hung035d4ec2017-01-24 13:45:02 -080034 * The {@code VolumeShaper} class is used to automatically control audio volume during media
Andy Hungfef734c2017-02-23 16:21:13 -080035 * playback, allowing simple implementation of transition effects and ducking.
Andy Hung3c0f5d22017-05-15 15:41:14 -070036 * It is created from implementations of {@code VolumeAutomation},
37 * such as {@code MediaPlayer} and {@code AudioTrack} (referred to as "players" below),
38 * by {@link MediaPlayer#createVolumeShaper} or {@link AudioTrack#createVolumeShaper}.
Andy Hung035d4ec2017-01-24 13:45:02 -080039 *
Andy Hung3c0f5d22017-05-15 15:41:14 -070040 * A {@code VolumeShaper} is intended for short volume changes.
41 * If the audio output sink changes during
42 * a {@code VolumeShaper} transition, the precise curve position may be lost, and the
43 * {@code VolumeShaper} may advance to the end of the curve for the new audio output sink.
44 *
45 * The {@code VolumeShaper} appears as an additional scaling on the audio output,
Andy Hungfef734c2017-02-23 16:21:13 -080046 * and adjusts independently of track or stream volume controls.
Andy Hung035d4ec2017-01-24 13:45:02 -080047 */
Andy Hungfef734c2017-02-23 16:21:13 -080048public final class VolumeShaper implements AutoCloseable {
Andy Hung035d4ec2017-01-24 13:45:02 -080049 /* member variables */
50 private int mId;
Jean-Michel Trividce82ab2017-02-07 16:02:33 -080051 private final WeakReference<PlayerBase> mWeakPlayerBase;
Andy Hung035d4ec2017-01-24 13:45:02 -080052
Andy Hung035d4ec2017-01-24 13:45:02 -080053 /* package */ VolumeShaper(
54 @NonNull Configuration configuration, @NonNull PlayerBase playerBase) {
Jean-Michel Trividce82ab2017-02-07 16:02:33 -080055 mWeakPlayerBase = new WeakReference<PlayerBase>(playerBase);
Andy Hung035d4ec2017-01-24 13:45:02 -080056 mId = applyPlayer(configuration, new Operation.Builder().defer().build());
57 }
58
59 /* package */ int getId() {
60 return mId;
61 }
62
63 /**
64 * Applies the {@link VolumeShaper.Operation} to the {@code VolumeShaper}.
Andy Hung3c0f5d22017-05-15 15:41:14 -070065 *
66 * Applying {@link VolumeShaper.Operation#PLAY} after {@code PLAY}
67 * or {@link VolumeShaper.Operation#REVERSE} after
68 * {@code REVERSE} has no effect.
69 *
70 * Applying {@link VolumeShaper.Operation#PLAY} when the player
71 * hasn't started will synchronously start the {@code VolumeShaper} when
72 * playback begins.
73 *
Andy Hungfef734c2017-02-23 16:21:13 -080074 * @param operation the {@code operation} to apply.
Andy Hung3c0f5d22017-05-15 15:41:14 -070075 * @throws IllegalStateException if the player is uninitialized or if there
76 * is a critical failure. In that case, the {@code VolumeShaper} should be
77 * recreated.
Andy Hung035d4ec2017-01-24 13:45:02 -080078 */
79 public void apply(@NonNull Operation operation) {
80 /* void */ applyPlayer(new VolumeShaper.Configuration(mId), operation);
81 }
82
83 /**
84 * Replaces the current {@code VolumeShaper}
Andy Hungfef734c2017-02-23 16:21:13 -080085 * {@code configuration} with a new {@code configuration}.
Andy Hung035d4ec2017-01-24 13:45:02 -080086 *
Andy Hungfef734c2017-02-23 16:21:13 -080087 * This allows the user to change the volume shape
88 * while the existing {@code VolumeShaper} is in effect.
Andy Hung035d4ec2017-01-24 13:45:02 -080089 *
Andy Hung3c0f5d22017-05-15 15:41:14 -070090 * The effect of {@code replace()} is similar to an atomic close of
91 * the existing {@code VolumeShaper} and creation of a new {@code VolumeShaper}.
92 *
93 * If the {@code operation} is {@link VolumeShaper.Operation#PLAY} then the
94 * new curve starts immediately.
95 *
96 * If the {@code operation} is
97 * {@link VolumeShaper.Operation#REVERSE}, then the new curve will
98 * be delayed until {@code PLAY} is applied.
99 *
Andy Hungfef734c2017-02-23 16:21:13 -0800100 * @param configuration the new {@code configuration} to use.
Andy Hung3c0f5d22017-05-15 15:41:14 -0700101 * @param operation the {@code operation} to apply to the {@code VolumeShaper}
Andy Hungfef734c2017-02-23 16:21:13 -0800102 * @param join if true, match the start volume of the
103 * new {@code configuration} to the current volume of the existing
104 * {@code VolumeShaper}, to avoid discontinuity.
Andy Hung3c0f5d22017-05-15 15:41:14 -0700105 * @throws IllegalStateException if the player is uninitialized or if there
106 * is a critical failure. In that case, the {@code VolumeShaper} should be
107 * recreated.
Andy Hung035d4ec2017-01-24 13:45:02 -0800108 */
109 public void replace(
110 @NonNull Configuration configuration, @NonNull Operation operation, boolean join) {
111 mId = applyPlayer(
112 configuration,
113 new Operation.Builder(operation).replace(mId, join).build());
114 }
115
116 /**
117 * Returns the current volume scale attributable to the {@code VolumeShaper}.
118 *
Andy Hung3c0f5d22017-05-15 15:41:14 -0700119 * This is the last volume from the {@code VolumeShaper} used for the player,
120 * or the initial volume if the {@code VolumeShaper} hasn't been started with
121 * {@link VolumeShaper.Operation#PLAY}.
122 *
Andy Hung035d4ec2017-01-24 13:45:02 -0800123 * @return the volume, linearly represented as a value between 0.f and 1.f.
Andy Hung3c0f5d22017-05-15 15:41:14 -0700124 * @throws IllegalStateException if the player is uninitialized or if there
125 * is a critical failure. In that case, the {@code VolumeShaper} should be
126 * recreated.
Andy Hung035d4ec2017-01-24 13:45:02 -0800127 */
128 public float getVolume() {
129 return getStatePlayer(mId).getVolume();
130 }
131
132 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800133 * Releases the {@code VolumeShaper} object; any volume scale due to the
Andy Hung3c0f5d22017-05-15 15:41:14 -0700134 * {@code VolumeShaper} is removed after closing.
135 *
136 * If the volume does not reach 1.f when the {@code VolumeShaper} is closed
137 * (or finalized), there may be an abrupt change of volume.
138 *
139 * {@code close()} may be safely called after a prior {@code close()}.
140 * This class implements the Java {@code AutoClosable} interface and
141 * may be used with try-with-resources.
Andy Hung035d4ec2017-01-24 13:45:02 -0800142 */
Andy Hungfef734c2017-02-23 16:21:13 -0800143 @Override
144 public void close() {
Andy Hung035d4ec2017-01-24 13:45:02 -0800145 try {
146 /* void */ applyPlayer(
147 new VolumeShaper.Configuration(mId),
148 new Operation.Builder().terminate().build());
149 } catch (IllegalStateException ise) {
150 ; // ok
151 }
Jean-Michel Trividce82ab2017-02-07 16:02:33 -0800152 if (mWeakPlayerBase != null) {
153 mWeakPlayerBase.clear();
Andy Hung035d4ec2017-01-24 13:45:02 -0800154 }
Andy Hung035d4ec2017-01-24 13:45:02 -0800155 }
156
157 @Override
158 protected void finalize() {
Andy Hung3c0f5d22017-05-15 15:41:14 -0700159 close(); // ensure we remove the native VolumeShaper
Andy Hung035d4ec2017-01-24 13:45:02 -0800160 }
161
162 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -0700163 * Internal call to apply the {@code configuration} and {@code operation} to the player.
Andy Hung035d4ec2017-01-24 13:45:02 -0800164 * Returns a valid shaper id or throws the appropriate exception.
165 * @param configuration
166 * @param operation
167 * @return id a non-negative shaper id.
Andy Hungd4f1e862017-03-06 11:39:10 -0800168 * @throws IllegalStateException if the player has been deallocated or is uninitialized.
Andy Hung035d4ec2017-01-24 13:45:02 -0800169 */
170 private int applyPlayer(
171 @NonNull VolumeShaper.Configuration configuration,
172 @NonNull VolumeShaper.Operation operation) {
173 final int id;
Andy Hung7da0e982017-02-22 12:34:21 -0800174 if (mWeakPlayerBase != null) {
Jean-Michel Trividce82ab2017-02-07 16:02:33 -0800175 PlayerBase player = mWeakPlayerBase.get();
Andy Hung035d4ec2017-01-24 13:45:02 -0800176 if (player == null) {
177 throw new IllegalStateException("player deallocated");
178 }
179 id = player.playerApplyVolumeShaper(configuration, operation);
180 } else {
181 throw new IllegalStateException("uninitialized shaper");
182 }
183 if (id < 0) {
184 // TODO - get INVALID_OPERATION from platform.
185 final int VOLUME_SHAPER_INVALID_OPERATION = -38; // must match with platform
186 // Due to RPC handling, we translate integer codes to exceptions right before
187 // delivering to the user.
188 if (id == VOLUME_SHAPER_INVALID_OPERATION) {
Andy Hung3c0f5d22017-05-15 15:41:14 -0700189 throw new IllegalStateException("player or VolumeShaper deallocated");
Andy Hung035d4ec2017-01-24 13:45:02 -0800190 } else {
191 throw new IllegalArgumentException("invalid configuration or operation: " + id);
192 }
193 }
194 return id;
195 }
196
197 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -0700198 * Internal call to retrieve the current {@code VolumeShaper} state.
Andy Hung035d4ec2017-01-24 13:45:02 -0800199 * @param id
Andy Hung3c0f5d22017-05-15 15:41:14 -0700200 * @return the current {@code VolumeShaper.State}
Andy Hungd4f1e862017-03-06 11:39:10 -0800201 * @throws IllegalStateException if the player has been deallocated or is uninitialized.
Andy Hung035d4ec2017-01-24 13:45:02 -0800202 */
203 private @NonNull VolumeShaper.State getStatePlayer(int id) {
204 final VolumeShaper.State state;
Andy Hung7da0e982017-02-22 12:34:21 -0800205 if (mWeakPlayerBase != null) {
Jean-Michel Trividce82ab2017-02-07 16:02:33 -0800206 PlayerBase player = mWeakPlayerBase.get();
Andy Hung035d4ec2017-01-24 13:45:02 -0800207 if (player == null) {
208 throw new IllegalStateException("player deallocated");
209 }
210 state = player.playerGetVolumeShaperState(id);
211 } else {
212 throw new IllegalStateException("uninitialized shaper");
213 }
214 if (state == null) {
215 throw new IllegalStateException("shaper cannot be found");
216 }
217 return state;
218 }
219
220 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800221 * The {@code VolumeShaper.Configuration} class contains curve
222 * and duration information.
223 * It is constructed by the {@link VolumeShaper.Configuration.Builder}.
224 * <p>
225 * A {@code VolumeShaper.Configuration} is used by
226 * {@link VolumeAutomation#createVolumeShaper(Configuration)
Andy Hungd4f1e862017-03-06 11:39:10 -0800227 * VolumeAutomation.createVolumeShaper(Configuration)} to create
Andy Hungfef734c2017-02-23 16:21:13 -0800228 * a {@code VolumeShaper} and
229 * by {@link VolumeShaper#replace(Configuration, Operation, boolean)
Andy Hungd4f1e862017-03-06 11:39:10 -0800230 * VolumeShaper.replace(Configuration, Operation, boolean)}
Andy Hungfef734c2017-02-23 16:21:13 -0800231 * to replace an existing {@code configuration}.
Andy Hung3c0f5d22017-05-15 15:41:14 -0700232 * <p>
233 * The {@link AudioTrack} and {@link MediaPlayer} classes implement
234 * the {@link VolumeAutomation} interface.
Andy Hung035d4ec2017-01-24 13:45:02 -0800235 */
236 public static final class Configuration implements Parcelable {
237 private static final int MAXIMUM_CURVE_POINTS = 16;
238
239 /**
240 * Returns the maximum number of curve points allowed for
241 * {@link VolumeShaper.Builder#setCurve(float[], float[])}.
242 */
243 public static int getMaximumCurvePoints() {
244 return MAXIMUM_CURVE_POINTS;
245 }
246
247 // These values must match the native VolumeShaper::Configuration::Type
248 /** @hide */
249 @IntDef({
250 TYPE_ID,
251 TYPE_SCALE,
252 })
253 @Retention(RetentionPolicy.SOURCE)
254 public @interface Type {}
255
256 /**
257 * Specifies a {@link VolumeShaper} handle created by {@link #VolumeShaper(int)}
258 * from an id returned by {@code setVolumeShaper()}.
259 * The type, curve, etc. may not be queried from
260 * a {@code VolumeShaper} object of this type;
261 * the handle is used to identify and change the operation of
262 * an existing {@code VolumeShaper} sent to the player.
263 */
264 /* package */ static final int TYPE_ID = 0;
265
266 /**
267 * Specifies a {@link VolumeShaper} to be used
268 * as an additional scale to the current volume.
269 * This is created by the {@link VolumeShaper.Builder}.
270 */
271 /* package */ static final int TYPE_SCALE = 1;
272
273 // These values must match the native InterpolatorType enumeration.
274 /** @hide */
275 @IntDef({
276 INTERPOLATOR_TYPE_STEP,
277 INTERPOLATOR_TYPE_LINEAR,
278 INTERPOLATOR_TYPE_CUBIC,
279 INTERPOLATOR_TYPE_CUBIC_MONOTONIC,
280 })
281 @Retention(RetentionPolicy.SOURCE)
282 public @interface InterpolatorType {}
283
284 /**
285 * Stepwise volume curve.
286 */
287 public static final int INTERPOLATOR_TYPE_STEP = 0;
288
289 /**
290 * Linear interpolated volume curve.
291 */
292 public static final int INTERPOLATOR_TYPE_LINEAR = 1;
293
294 /**
295 * Cubic interpolated volume curve.
296 * This is default if unspecified.
297 */
298 public static final int INTERPOLATOR_TYPE_CUBIC = 2;
299
300 /**
301 * Cubic interpolated volume curve
Andy Hungfef734c2017-02-23 16:21:13 -0800302 * that preserves local monotonicity.
Andy Hung035d4ec2017-01-24 13:45:02 -0800303 * So long as the control points are locally monotonic,
Andy Hungfef734c2017-02-23 16:21:13 -0800304 * the curve interpolation between those points are monotonic.
Andy Hung035d4ec2017-01-24 13:45:02 -0800305 * This is useful for cubic spline interpolated
306 * volume ramps and ducks.
307 */
308 public static final int INTERPOLATOR_TYPE_CUBIC_MONOTONIC = 3;
309
310 // These values must match the native VolumeShaper::Configuration::InterpolatorType
311 /** @hide */
312 @IntDef({
313 OPTION_FLAG_VOLUME_IN_DBFS,
314 OPTION_FLAG_CLOCK_TIME,
315 })
316 @Retention(RetentionPolicy.SOURCE)
317 public @interface OptionFlag {}
318
319 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800320 * @hide
Andy Hung035d4ec2017-01-24 13:45:02 -0800321 * Use a dB full scale volume range for the volume curve.
322 *<p>
323 * The volume scale is typically from 0.f to 1.f on a linear scale;
324 * this option changes to -inf to 0.f on a db full scale,
325 * where 0.f is equivalent to a scale of 1.f.
326 */
327 public static final int OPTION_FLAG_VOLUME_IN_DBFS = (1 << 0);
328
329 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800330 * @hide
Andy Hung035d4ec2017-01-24 13:45:02 -0800331 * Use clock time instead of media time.
332 *<p>
333 * The default implementation of {@code VolumeShaper} is to apply
334 * volume changes by the media time of the player.
335 * Hence, the {@code VolumeShaper} will speed or slow down to
336 * match player changes of playback rate, pause, or resume.
337 *<p>
338 * The {@code OPTION_FLAG_CLOCK_TIME} option allows the {@code VolumeShaper}
339 * progress to be determined by clock time instead of media time.
340 */
341 public static final int OPTION_FLAG_CLOCK_TIME = (1 << 1);
342
343 private static final int OPTION_FLAG_PUBLIC_ALL =
344 OPTION_FLAG_VOLUME_IN_DBFS | OPTION_FLAG_CLOCK_TIME;
345
346 /**
347 * A one second linear ramp from silence to full volume.
Andy Hungfef734c2017-02-23 16:21:13 -0800348 * Use {@link VolumeShaper.Builder#reflectTimes()}
349 * or {@link VolumeShaper.Builder#invertVolumes()} to generate
Andy Hung035d4ec2017-01-24 13:45:02 -0800350 * the matching linear duck.
351 */
352 public static final Configuration LINEAR_RAMP = new VolumeShaper.Configuration.Builder()
353 .setInterpolatorType(INTERPOLATOR_TYPE_LINEAR)
354 .setCurve(new float[] {0.f, 1.f} /* times */,
355 new float[] {0.f, 1.f} /* volumes */)
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700356 .setDuration(1000)
Andy Hung035d4ec2017-01-24 13:45:02 -0800357 .build();
358
359 /**
360 * A one second cubic ramp from silence to full volume.
Andy Hungfef734c2017-02-23 16:21:13 -0800361 * Use {@link VolumeShaper.Builder#reflectTimes()}
362 * or {@link VolumeShaper.Builder#invertVolumes()} to generate
Andy Hung035d4ec2017-01-24 13:45:02 -0800363 * the matching cubic duck.
364 */
365 public static final Configuration CUBIC_RAMP = new VolumeShaper.Configuration.Builder()
366 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
367 .setCurve(new float[] {0.f, 1.f} /* times */,
368 new float[] {0.f, 1.f} /* volumes */)
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700369 .setDuration(1000)
Andy Hung035d4ec2017-01-24 13:45:02 -0800370 .build();
371
372 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800373 * A one second sine curve
374 * from silence to full volume for energy preserving cross fades.
Andy Hung035d4ec2017-01-24 13:45:02 -0800375 * Use {@link VolumeShaper.Builder#reflectTimes()} to generate
376 * the matching cosine duck.
377 */
378 public static final Configuration SINE_RAMP;
379
380 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800381 * A one second sine-squared s-curve ramp
382 * from silence to full volume.
Andy Hung035d4ec2017-01-24 13:45:02 -0800383 * Use {@link VolumeShaper.Builder#reflectTimes()}
384 * or {@link VolumeShaper.Builder#invertVolumes()} to generate
Andy Hungfef734c2017-02-23 16:21:13 -0800385 * the matching sine-squared s-curve duck.
Andy Hung035d4ec2017-01-24 13:45:02 -0800386 */
387 public static final Configuration SCURVE_RAMP;
388
389 static {
390 final int POINTS = MAXIMUM_CURVE_POINTS;
391 final float times[] = new float[POINTS];
392 final float sines[] = new float[POINTS];
393 final float scurve[] = new float[POINTS];
394 for (int i = 0; i < POINTS; ++i) {
395 times[i] = (float)i / (POINTS - 1);
396 final float sine = (float)Math.sin(times[i] * Math.PI / 2.);
397 sines[i] = sine;
398 scurve[i] = sine * sine;
399 }
400 SINE_RAMP = new VolumeShaper.Configuration.Builder()
401 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
402 .setCurve(times, sines)
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700403 .setDuration(1000)
Andy Hung035d4ec2017-01-24 13:45:02 -0800404 .build();
405 SCURVE_RAMP = new VolumeShaper.Configuration.Builder()
406 .setInterpolatorType(INTERPOLATOR_TYPE_CUBIC)
407 .setCurve(times, scurve)
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700408 .setDuration(1000)
Andy Hung035d4ec2017-01-24 13:45:02 -0800409 .build();
410 }
411
412 /*
413 * member variables - these are all final
414 */
415
416 // type of VolumeShaper
Mathew Inwood31a792a2018-08-17 08:54:26 +0100417 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -0800418 private final int mType;
419
420 // valid when mType is TYPE_ID
Mathew Inwood31a792a2018-08-17 08:54:26 +0100421 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -0800422 private final int mId;
423
424 // valid when mType is TYPE_SCALE
Mathew Inwood31a792a2018-08-17 08:54:26 +0100425 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -0800426 private final int mOptionFlags;
Mathew Inwood31a792a2018-08-17 08:54:26 +0100427 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -0800428 private final double mDurationMs;
Mathew Inwood31a792a2018-08-17 08:54:26 +0100429 @UnsupportedAppUsage
Andy Hungd4f1e862017-03-06 11:39:10 -0800430 private final int mInterpolatorType;
Mathew Inwood31a792a2018-08-17 08:54:26 +0100431 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -0800432 private final float[] mTimes;
Mathew Inwood31a792a2018-08-17 08:54:26 +0100433 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -0800434 private final float[] mVolumes;
435
436 @Override
437 public String toString() {
Andy Hungd4f1e862017-03-06 11:39:10 -0800438 return "VolumeShaper.Configuration{"
439 + "mType = " + mType
440 + ", mId = " + mId
Andy Hung035d4ec2017-01-24 13:45:02 -0800441 + (mType == TYPE_ID
Andy Hungd4f1e862017-03-06 11:39:10 -0800442 ? "}"
443 : ", mOptionFlags = 0x" + Integer.toHexString(mOptionFlags).toUpperCase()
444 + ", mDurationMs = " + mDurationMs
445 + ", mInterpolatorType = " + mInterpolatorType
446 + ", mTimes[] = " + Arrays.toString(mTimes)
447 + ", mVolumes[] = " + Arrays.toString(mVolumes)
448 + "}");
Andy Hung035d4ec2017-01-24 13:45:02 -0800449 }
450
451 @Override
452 public int hashCode() {
453 return mType == TYPE_ID
454 ? Objects.hash(mType, mId)
Andy Hungd4f1e862017-03-06 11:39:10 -0800455 : Objects.hash(mType, mId,
456 mOptionFlags, mDurationMs, mInterpolatorType,
457 Arrays.hashCode(mTimes), Arrays.hashCode(mVolumes));
Andy Hung035d4ec2017-01-24 13:45:02 -0800458 }
459
460 @Override
461 public boolean equals(Object o) {
462 if (!(o instanceof Configuration)) return false;
463 if (o == this) return true;
464 final Configuration other = (Configuration) o;
Andy Hungd4f1e862017-03-06 11:39:10 -0800465 // Note that exact floating point equality may not be guaranteed
466 // for a theoretically idempotent operation; for example,
467 // there are many cases where a + b - b != a.
468 return mType == other.mType
469 && mId == other.mId
470 && (mType == TYPE_ID
471 || (mOptionFlags == other.mOptionFlags
472 && mDurationMs == other.mDurationMs
473 && mInterpolatorType == other.mInterpolatorType
474 && Arrays.equals(mTimes, other.mTimes)
475 && Arrays.equals(mVolumes, other.mVolumes)));
Andy Hung035d4ec2017-01-24 13:45:02 -0800476 }
477
478 @Override
479 public int describeContents() {
480 return 0;
481 }
482
483 @Override
484 public void writeToParcel(Parcel dest, int flags) {
Andy Hungd4f1e862017-03-06 11:39:10 -0800485 // this needs to match the native VolumeShaper.Configuration parceling
Andy Hung035d4ec2017-01-24 13:45:02 -0800486 dest.writeInt(mType);
487 dest.writeInt(mId);
488 if (mType != TYPE_ID) {
Andy Hung035d4ec2017-01-24 13:45:02 -0800489 dest.writeInt(mOptionFlags);
490 dest.writeDouble(mDurationMs);
Andy Hungd4f1e862017-03-06 11:39:10 -0800491 // this needs to match the native Interpolator parceling
492 dest.writeInt(mInterpolatorType);
Andy Hung40a07a82017-03-09 12:10:39 -0800493 dest.writeFloat(0.f); // first slope (specifying for native side)
494 dest.writeFloat(0.f); // last slope (specifying for native side)
Andy Hungd4f1e862017-03-06 11:39:10 -0800495 // mTimes and mVolumes should have the same length.
496 dest.writeInt(mTimes.length);
497 for (int i = 0; i < mTimes.length; ++i) {
498 dest.writeFloat(mTimes[i]);
499 dest.writeFloat(mVolumes[i]);
500 }
Andy Hung035d4ec2017-01-24 13:45:02 -0800501 }
502 }
503
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -0700504 public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Configuration> CREATOR
Andy Hung035d4ec2017-01-24 13:45:02 -0800505 = new Parcelable.Creator<VolumeShaper.Configuration>() {
506 @Override
507 public VolumeShaper.Configuration createFromParcel(Parcel p) {
Andy Hungd4f1e862017-03-06 11:39:10 -0800508 // this needs to match the native VolumeShaper.Configuration parceling
Andy Hung035d4ec2017-01-24 13:45:02 -0800509 final int type = p.readInt();
510 final int id = p.readInt();
511 if (type == TYPE_ID) {
512 return new VolumeShaper.Configuration(id);
513 } else {
Andy Hungd4f1e862017-03-06 11:39:10 -0800514 final int optionFlags = p.readInt();
515 final double durationMs = p.readDouble();
516 // this needs to match the native Interpolator parceling
517 final int interpolatorType = p.readInt();
Andy Hung40a07a82017-03-09 12:10:39 -0800518 final float firstSlope = p.readFloat(); // ignored on the Java side
519 final float lastSlope = p.readFloat(); // ignored on the Java side
Andy Hungd4f1e862017-03-06 11:39:10 -0800520 final int length = p.readInt();
521 final float[] times = new float[length];
522 final float[] volumes = new float[length];
523 for (int i = 0; i < length; ++i) {
524 times[i] = p.readFloat();
525 volumes[i] = p.readFloat();
526 }
527
Andy Hung035d4ec2017-01-24 13:45:02 -0800528 return new VolumeShaper.Configuration(
529 type,
Andy Hungd4f1e862017-03-06 11:39:10 -0800530 id,
531 optionFlags,
532 durationMs,
533 interpolatorType,
534 times,
535 volumes);
Andy Hung035d4ec2017-01-24 13:45:02 -0800536 }
537 }
538
539 @Override
540 public VolumeShaper.Configuration[] newArray(int size) {
541 return new VolumeShaper.Configuration[size];
542 }
543 };
544
545 /**
Andy Hung7da0e982017-02-22 12:34:21 -0800546 * @hide
Andy Hung3c0f5d22017-05-15 15:41:14 -0700547 * Constructs a {@code VolumeShaper} from an id.
Andy Hung035d4ec2017-01-24 13:45:02 -0800548 *
549 * This is an opaque handle for controlling a {@code VolumeShaper} that has
550 * already been sent to a player. The {@code id} is returned from the
551 * initial {@code setVolumeShaper()} call on success.
552 *
553 * These configurations are for native use only,
554 * they are never returned directly to the user.
555 *
556 * @param id
557 * @throws IllegalArgumentException if id is negative.
558 */
Andy Hung7da0e982017-02-22 12:34:21 -0800559 public Configuration(int id) {
Andy Hung035d4ec2017-01-24 13:45:02 -0800560 if (id < 0) {
561 throw new IllegalArgumentException("negative id " + id);
562 }
563 mType = TYPE_ID;
564 mId = id;
565 mInterpolatorType = 0;
566 mOptionFlags = 0;
567 mDurationMs = 0;
568 mTimes = null;
569 mVolumes = null;
570 }
571
572 /**
573 * Direct constructor for VolumeShaper.
574 * Use the Builder instead.
575 */
Mathew Inwood31a792a2018-08-17 08:54:26 +0100576 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -0800577 private Configuration(@Type int type,
578 int id,
Andy Hung035d4ec2017-01-24 13:45:02 -0800579 @OptionFlag int optionFlags,
580 double durationMs,
Andy Hungd4f1e862017-03-06 11:39:10 -0800581 @InterpolatorType int interpolatorType,
Andy Hung035d4ec2017-01-24 13:45:02 -0800582 @NonNull float[] times,
583 @NonNull float[] volumes) {
584 mType = type;
585 mId = id;
Andy Hung035d4ec2017-01-24 13:45:02 -0800586 mOptionFlags = optionFlags;
587 mDurationMs = durationMs;
Andy Hungd4f1e862017-03-06 11:39:10 -0800588 mInterpolatorType = interpolatorType;
Andy Hung035d4ec2017-01-24 13:45:02 -0800589 // Builder should have cloned these arrays already.
590 mTimes = times;
591 mVolumes = volumes;
592 }
593
594 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800595 * @hide
Andy Hung035d4ec2017-01-24 13:45:02 -0800596 * Returns the {@code VolumeShaper} type.
597 */
598 public @Type int getType() {
599 return mType;
600 }
601
602 /**
603 * @hide
604 * Returns the {@code VolumeShaper} id.
605 */
606 public int getId() {
607 return mId;
608 }
609
610 /**
611 * Returns the interpolator type.
612 */
613 public @InterpolatorType int getInterpolatorType() {
614 return mInterpolatorType;
615 }
616
617 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800618 * @hide
Andy Hung035d4ec2017-01-24 13:45:02 -0800619 * Returns the option flags
620 */
621 public @OptionFlag int getOptionFlags() {
622 return mOptionFlags & OPTION_FLAG_PUBLIC_ALL;
623 }
624
625 /* package */ @OptionFlag int getAllOptionFlags() {
626 return mOptionFlags;
627 }
628
629 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800630 * Returns the duration of the volume shape in milliseconds.
Andy Hung035d4ec2017-01-24 13:45:02 -0800631 */
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700632 public long getDuration() {
633 // casting is safe here as the duration was set as a long in the Builder
634 return (long) mDurationMs;
Andy Hung035d4ec2017-01-24 13:45:02 -0800635 }
636
637 /**
638 * Returns the times (x) coordinate array of the volume curve points.
639 */
640 public float[] getTimes() {
641 return mTimes;
642 }
643
644 /**
645 * Returns the volumes (y) coordinate array of the volume curve points.
646 */
647 public float[] getVolumes() {
648 return mVolumes;
649 }
650
651 /**
652 * Checks the validity of times and volumes point representation.
653 *
654 * {@code times[]} and {@code volumes[]} are two arrays representing points
655 * for the volume curve.
656 *
Andy Hung40a07a82017-03-09 12:10:39 -0800657 * Note that {@code times[]} and {@code volumes[]} are explicitly checked against
658 * null here to provide the proper error string - those are legitimate
659 * arguments to this method.
660 *
Andy Hung035d4ec2017-01-24 13:45:02 -0800661 * @param times the x coordinates for the points,
662 * must be between 0.f and 1.f and be monotonic.
663 * @param volumes the y coordinates for the points,
664 * must be between 0.f and 1.f for linear and
665 * must be no greater than 0.f for log (dBFS).
666 * @param log set to true if the scale is logarithmic.
667 * @return null if no error, or the reason in a {@code String} for an error.
668 */
669 private static @Nullable String checkCurveForErrors(
Andy Hungd4f1e862017-03-06 11:39:10 -0800670 @Nullable float[] times, @Nullable float[] volumes, boolean log) {
671 if (times == null) {
672 return "times array must be non-null";
673 } else if (volumes == null) {
674 return "volumes array must be non-null";
675 } else if (times.length != volumes.length) {
Andy Hung035d4ec2017-01-24 13:45:02 -0800676 return "array length must match";
677 } else if (times.length < 2) {
678 return "array length must be at least 2";
679 } else if (times.length > MAXIMUM_CURVE_POINTS) {
680 return "array length must be no larger than " + MAXIMUM_CURVE_POINTS;
681 } else if (times[0] != 0.f) {
682 return "times must start at 0.f";
683 } else if (times[times.length - 1] != 1.f) {
684 return "times must end at 1.f";
685 }
686
687 // validate points along the curve
688 for (int i = 1; i < times.length; ++i) {
689 if (!(times[i] > times[i - 1]) /* handle nan */) {
690 return "times not monotonic increasing, check index " + i;
691 }
692 }
693 if (log) {
694 for (int i = 0; i < volumes.length; ++i) {
695 if (!(volumes[i] <= 0.f) /* handle nan */) {
696 return "volumes for log scale cannot be positive, "
697 + "check index " + i;
698 }
699 }
700 } else {
701 for (int i = 0; i < volumes.length; ++i) {
702 if (!(volumes[i] >= 0.f) || !(volumes[i] <= 1.f) /* handle nan */) {
703 return "volumes for linear scale must be between 0.f and 1.f, "
704 + "check index " + i;
705 }
706 }
707 }
708 return null; // no errors
709 }
710
Andy Hungd4f1e862017-03-06 11:39:10 -0800711 private static void checkCurveForErrorsAndThrowException(
Andy Hung40a07a82017-03-09 12:10:39 -0800712 @Nullable float[] times, @Nullable float[] volumes, boolean log, boolean ise) {
Andy Hungd4f1e862017-03-06 11:39:10 -0800713 final String error = checkCurveForErrors(times, volumes, log);
714 if (error != null) {
Andy Hung40a07a82017-03-09 12:10:39 -0800715 if (ise) {
716 throw new IllegalStateException(error);
717 } else {
718 throw new IllegalArgumentException(error);
719 }
Andy Hungd4f1e862017-03-06 11:39:10 -0800720 }
721 }
722
723 private static void checkValidVolumeAndThrowException(float volume, boolean log) {
Andy Hung035d4ec2017-01-24 13:45:02 -0800724 if (log) {
725 if (!(volume <= 0.f) /* handle nan */) {
726 throw new IllegalArgumentException("dbfs volume must be 0.f or less");
727 }
728 } else {
729 if (!(volume >= 0.f) || !(volume <= 1.f) /* handle nan */) {
730 throw new IllegalArgumentException("volume must be >= 0.f and <= 1.f");
731 }
732 }
733 }
734
735 private static void clampVolume(float[] volumes, boolean log) {
736 if (log) {
737 for (int i = 0; i < volumes.length; ++i) {
738 if (!(volumes[i] <= 0.f) /* handle nan */) {
739 volumes[i] = 0.f;
740 }
741 }
742 } else {
743 for (int i = 0; i < volumes.length; ++i) {
744 if (!(volumes[i] >= 0.f) /* handle nan */) {
745 volumes[i] = 0.f;
746 } else if (!(volumes[i] <= 1.f)) {
747 volumes[i] = 1.f;
748 }
749 }
750 }
751 }
752
753 /**
754 * Builder class for a {@link VolumeShaper.Configuration} object.
755 * <p> Here is an example where {@code Builder} is used to define the
756 * {@link VolumeShaper.Configuration}.
757 *
758 * <pre class="prettyprint">
759 * VolumeShaper.Configuration LINEAR_RAMP =
760 * new VolumeShaper.Configuration.Builder()
761 * .setInterpolatorType(VolumeShaper.Configuration.INTERPOLATOR_TYPE_LINEAR)
762 * .setCurve(new float[] { 0.f, 1.f }, // times
763 * new float[] { 0.f, 1.f }) // volumes
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700764 * .setDuration(1000)
Andy Hung035d4ec2017-01-24 13:45:02 -0800765 * .build();
766 * </pre>
767 * <p>
768 */
769 public static final class Builder {
770 private int mType = TYPE_SCALE;
771 private int mId = -1; // invalid
772 private int mInterpolatorType = INTERPOLATOR_TYPE_CUBIC;
Andy Hungfef734c2017-02-23 16:21:13 -0800773 private int mOptionFlags = OPTION_FLAG_CLOCK_TIME;
Andy Hung035d4ec2017-01-24 13:45:02 -0800774 private double mDurationMs = 1000.;
775 private float[] mTimes = null;
776 private float[] mVolumes = null;
777
778 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800779 * Constructs a new {@code Builder} with the defaults.
Andy Hung035d4ec2017-01-24 13:45:02 -0800780 */
781 public Builder() {
782 }
783
784 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800785 * Constructs a new {@code Builder} with settings
786 * copied from a given {@code VolumeShaper.Configuration}.
Andy Hung035d4ec2017-01-24 13:45:02 -0800787 * @param configuration prototypical configuration
Andy Hungfef734c2017-02-23 16:21:13 -0800788 * which will be reused in the new {@code Builder}.
Andy Hung035d4ec2017-01-24 13:45:02 -0800789 */
790 public Builder(@NonNull Configuration configuration) {
791 mType = configuration.getType();
792 mId = configuration.getId();
793 mOptionFlags = configuration.getAllOptionFlags();
794 mInterpolatorType = configuration.getInterpolatorType();
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700795 mDurationMs = configuration.getDuration();
Andy Hungd4f1e862017-03-06 11:39:10 -0800796 mTimes = configuration.getTimes().clone();
797 mVolumes = configuration.getVolumes().clone();
Andy Hung035d4ec2017-01-24 13:45:02 -0800798 }
799
800 /**
801 * @hide
Andy Hungd4f1e862017-03-06 11:39:10 -0800802 * Set the {@code id} for system defined shapers.
803 * @param id the {@code id} to set. If non-negative, then it is used.
804 * If -1, then the system is expected to assign one.
805 * @return the same {@code Builder} instance.
806 * @throws IllegalArgumentException if {@code id} < -1.
Andy Hung035d4ec2017-01-24 13:45:02 -0800807 */
808 public @NonNull Builder setId(int id) {
Andy Hungd4f1e862017-03-06 11:39:10 -0800809 if (id < -1) {
810 throw new IllegalArgumentException("invalid id: " + id);
811 }
Andy Hung035d4ec2017-01-24 13:45:02 -0800812 mId = id;
813 return this;
814 }
815
816 /**
817 * Sets the interpolator type.
818 *
Andy Hung3c0f5d22017-05-15 15:41:14 -0700819 * If omitted the default interpolator type is {@link #INTERPOLATOR_TYPE_CUBIC}.
Andy Hung035d4ec2017-01-24 13:45:02 -0800820 *
821 * @param interpolatorType method of interpolation used for the volume curve.
Andy Hungfef734c2017-02-23 16:21:13 -0800822 * One of {@link #INTERPOLATOR_TYPE_STEP},
823 * {@link #INTERPOLATOR_TYPE_LINEAR},
824 * {@link #INTERPOLATOR_TYPE_CUBIC},
825 * {@link #INTERPOLATOR_TYPE_CUBIC_MONOTONIC}.
826 * @return the same {@code Builder} instance.
Andy Hung035d4ec2017-01-24 13:45:02 -0800827 * @throws IllegalArgumentException if {@code interpolatorType} is not valid.
828 */
829 public @NonNull Builder setInterpolatorType(@InterpolatorType int interpolatorType) {
830 switch (interpolatorType) {
831 case INTERPOLATOR_TYPE_STEP:
832 case INTERPOLATOR_TYPE_LINEAR:
833 case INTERPOLATOR_TYPE_CUBIC:
834 case INTERPOLATOR_TYPE_CUBIC_MONOTONIC:
835 mInterpolatorType = interpolatorType;
836 break;
837 default:
838 throw new IllegalArgumentException("invalid interpolatorType: "
839 + interpolatorType);
840 }
841 return this;
842 }
843
844 /**
Andy Hungfef734c2017-02-23 16:21:13 -0800845 * @hide
Andy Hung035d4ec2017-01-24 13:45:02 -0800846 * Sets the optional flags
847 *
848 * If omitted, flags are 0. If {@link #OPTION_FLAG_VOLUME_IN_DBFS} has
849 * changed the volume curve needs to be set again as the acceptable
850 * volume domain has changed.
851 *
852 * @param optionFlags new value to replace the old {@code optionFlags}.
Andy Hungfef734c2017-02-23 16:21:13 -0800853 * @return the same {@code Builder} instance.
Andy Hung035d4ec2017-01-24 13:45:02 -0800854 * @throws IllegalArgumentException if flag is not recognized.
855 */
Andy Hung3ce023b2018-04-05 17:38:11 -0700856 @TestApi
Andy Hung035d4ec2017-01-24 13:45:02 -0800857 public @NonNull Builder setOptionFlags(@OptionFlag int optionFlags) {
858 if ((optionFlags & ~OPTION_FLAG_PUBLIC_ALL) != 0) {
859 throw new IllegalArgumentException("invalid bits in flag: " + optionFlags);
860 }
861 mOptionFlags = mOptionFlags & ~OPTION_FLAG_PUBLIC_ALL | optionFlags;
862 return this;
863 }
864
865 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -0700866 * Sets the {@code VolumeShaper} duration in milliseconds.
Andy Hung035d4ec2017-01-24 13:45:02 -0800867 *
868 * If omitted, the default duration is 1 second.
869 *
Jean-Michel Trivi0dfbd152017-04-11 18:52:43 -0700870 * @param durationMillis
Andy Hungfef734c2017-02-23 16:21:13 -0800871 * @return the same {@code Builder} instance.
Jean-Michel Trivi0dfbd152017-04-11 18:52:43 -0700872 * @throws IllegalArgumentException if {@code durationMillis}
Andy Hungfef734c2017-02-23 16:21:13 -0800873 * is not strictly positive.
Andy Hung035d4ec2017-01-24 13:45:02 -0800874 */
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700875 public @NonNull Builder setDuration(long durationMillis) {
876 if (durationMillis <= 0) {
Andy Hung035d4ec2017-01-24 13:45:02 -0800877 throw new IllegalArgumentException(
Jean-Michel Trivi0dfbd152017-04-11 18:52:43 -0700878 "duration: " + durationMillis + " not positive");
Andy Hung035d4ec2017-01-24 13:45:02 -0800879 }
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700880 mDurationMs = (double) durationMillis;
Andy Hung035d4ec2017-01-24 13:45:02 -0800881 return this;
882 }
883
884 /**
885 * Sets the volume curve.
886 *
887 * The volume curve is represented by a set of control points given by
888 * two float arrays of equal length,
889 * one representing the time (x) coordinates
890 * and one corresponding to the volume (y) coordinates.
891 * The length must be at least 2
892 * and no greater than {@link VolumeShaper.Configuration#getMaximumCurvePoints()}.
893 * <p>
894 * The volume curve is normalized as follows:
Andy Hungfef734c2017-02-23 16:21:13 -0800895 * time (x) coordinates should be monotonically increasing, from 0.f to 1.f;
896 * volume (y) coordinates must be within 0.f to 1.f.
Andy Hung035d4ec2017-01-24 13:45:02 -0800897 * <p>
Jean-Michel Trivi4c86efa12017-04-20 18:13:34 -0700898 * The time scale is set by {@link #setDuration}.
Andy Hung035d4ec2017-01-24 13:45:02 -0800899 * <p>
900 * @param times an array of float values representing
901 * the time line of the volume curve.
902 * @param volumes an array of float values representing
903 * the amplitude of the volume curve.
Andy Hungfef734c2017-02-23 16:21:13 -0800904 * @return the same {@code Builder} instance.
Andy Hung035d4ec2017-01-24 13:45:02 -0800905 * @throws IllegalArgumentException if {@code times} or {@code volumes} is invalid.
906 */
Andy Hungfef734c2017-02-23 16:21:13 -0800907
908 /* Note: volume (y) coordinates must be non-positive for log scaling,
909 * if {@link VolumeShaper.Configuration#OPTION_FLAG_VOLUME_IN_DBFS} is set.
910 */
911
Andy Hung035d4ec2017-01-24 13:45:02 -0800912 public @NonNull Builder setCurve(@NonNull float[] times, @NonNull float[] volumes) {
Andy Hung40a07a82017-03-09 12:10:39 -0800913 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
914 checkCurveForErrorsAndThrowException(times, volumes, log, false /* ise */);
Andy Hung035d4ec2017-01-24 13:45:02 -0800915 mTimes = times.clone();
916 mVolumes = volumes.clone();
917 return this;
918 }
919
920 /**
921 * Reflects the volume curve so that
922 * the shaper changes volume from the end
923 * to the start.
924 *
Andy Hungfef734c2017-02-23 16:21:13 -0800925 * @return the same {@code Builder} instance.
Andy Hung40a07a82017-03-09 12:10:39 -0800926 * @throws IllegalStateException if curve has not been set.
Andy Hung035d4ec2017-01-24 13:45:02 -0800927 */
928 public @NonNull Builder reflectTimes() {
Andy Hung40a07a82017-03-09 12:10:39 -0800929 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
930 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
Andy Hung035d4ec2017-01-24 13:45:02 -0800931 int i;
932 for (i = 0; i < mTimes.length / 2; ++i) {
Andy Hungd4f1e862017-03-06 11:39:10 -0800933 float temp = mTimes[i];
Andy Hung035d4ec2017-01-24 13:45:02 -0800934 mTimes[i] = 1.f - mTimes[mTimes.length - 1 - i];
935 mTimes[mTimes.length - 1 - i] = 1.f - temp;
Andy Hungd4f1e862017-03-06 11:39:10 -0800936 temp = mVolumes[i];
937 mVolumes[i] = mVolumes[mVolumes.length - 1 - i];
938 mVolumes[mVolumes.length - 1 - i] = temp;
Andy Hung035d4ec2017-01-24 13:45:02 -0800939 }
940 if ((mTimes.length & 1) != 0) {
941 mTimes[i] = 1.f - mTimes[i];
942 }
943 return this;
944 }
945
946 /**
947 * Inverts the volume curve so that the max volume
948 * becomes the min volume and vice versa.
949 *
Andy Hungfef734c2017-02-23 16:21:13 -0800950 * @return the same {@code Builder} instance.
Andy Hung40a07a82017-03-09 12:10:39 -0800951 * @throws IllegalStateException if curve has not been set.
Andy Hung035d4ec2017-01-24 13:45:02 -0800952 */
953 public @NonNull Builder invertVolumes() {
Andy Hung40a07a82017-03-09 12:10:39 -0800954 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
955 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
Andy Hungd4f1e862017-03-06 11:39:10 -0800956 float min = mVolumes[0];
957 float max = mVolumes[0];
958 for (int i = 1; i < mVolumes.length; ++i) {
959 if (mVolumes[i] < min) {
960 min = mVolumes[i];
961 } else if (mVolumes[i] > max) {
962 max = mVolumes[i];
Andy Hung035d4ec2017-01-24 13:45:02 -0800963 }
Andy Hungd4f1e862017-03-06 11:39:10 -0800964 }
Andy Hung035d4ec2017-01-24 13:45:02 -0800965
Andy Hungd4f1e862017-03-06 11:39:10 -0800966 final float maxmin = max + min;
967 for (int i = 0; i < mVolumes.length; ++i) {
968 mVolumes[i] = maxmin - mVolumes[i];
Andy Hung035d4ec2017-01-24 13:45:02 -0800969 }
970 return this;
971 }
972
973 /**
974 * Scale the curve end volume to a target value.
975 *
976 * Keeps the start volume the same.
977 * This works best if the volume curve is monotonic.
978 *
Andy Hungfef734c2017-02-23 16:21:13 -0800979 * @param volume the target end volume to use.
980 * @return the same {@code Builder} instance.
Andy Hung40a07a82017-03-09 12:10:39 -0800981 * @throws IllegalArgumentException if {@code volume} is not valid.
982 * @throws IllegalStateException if curve has not been set.
Andy Hung035d4ec2017-01-24 13:45:02 -0800983 */
984 public @NonNull Builder scaleToEndVolume(float volume) {
985 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
Andy Hung40a07a82017-03-09 12:10:39 -0800986 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
Andy Hungd4f1e862017-03-06 11:39:10 -0800987 checkValidVolumeAndThrowException(volume, log);
Andy Hung035d4ec2017-01-24 13:45:02 -0800988 final float startVolume = mVolumes[0];
989 final float endVolume = mVolumes[mVolumes.length - 1];
990 if (endVolume == startVolume) {
991 // match with linear ramp
992 final float offset = volume - startVolume;
993 for (int i = 0; i < mVolumes.length; ++i) {
994 mVolumes[i] = mVolumes[i] + offset * mTimes[i];
995 }
996 } else {
997 // scale
998 final float scale = (volume - startVolume) / (endVolume - startVolume);
999 for (int i = 0; i < mVolumes.length; ++i) {
1000 mVolumes[i] = scale * (mVolumes[i] - startVolume) + startVolume;
1001 }
1002 }
1003 clampVolume(mVolumes, log);
1004 return this;
1005 }
1006
1007 /**
1008 * Scale the curve start volume to a target value.
1009 *
1010 * Keeps the end volume the same.
1011 * This works best if the volume curve is monotonic.
1012 *
Andy Hungfef734c2017-02-23 16:21:13 -08001013 * @param volume the target start volume to use.
1014 * @return the same {@code Builder} instance.
Andy Hung40a07a82017-03-09 12:10:39 -08001015 * @throws IllegalArgumentException if {@code volume} is not valid.
1016 * @throws IllegalStateException if curve has not been set.
Andy Hung035d4ec2017-01-24 13:45:02 -08001017 */
1018 public @NonNull Builder scaleToStartVolume(float volume) {
1019 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
Andy Hung40a07a82017-03-09 12:10:39 -08001020 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
Andy Hungd4f1e862017-03-06 11:39:10 -08001021 checkValidVolumeAndThrowException(volume, log);
Andy Hung035d4ec2017-01-24 13:45:02 -08001022 final float startVolume = mVolumes[0];
1023 final float endVolume = mVolumes[mVolumes.length - 1];
1024 if (endVolume == startVolume) {
1025 // match with linear ramp
1026 final float offset = volume - startVolume;
1027 for (int i = 0; i < mVolumes.length; ++i) {
1028 mVolumes[i] = mVolumes[i] + offset * (1.f - mTimes[i]);
1029 }
1030 } else {
1031 final float scale = (volume - endVolume) / (startVolume - endVolume);
1032 for (int i = 0; i < mVolumes.length; ++i) {
1033 mVolumes[i] = scale * (mVolumes[i] - endVolume) + endVolume;
1034 }
1035 }
1036 clampVolume(mVolumes, log);
1037 return this;
1038 }
1039
1040 /**
1041 * Builds a new {@link VolumeShaper} object.
1042 *
Andy Hungd4f1e862017-03-06 11:39:10 -08001043 * @return a new {@link VolumeShaper} object.
Andy Hung40a07a82017-03-09 12:10:39 -08001044 * @throws IllegalStateException if curve is not properly set.
Andy Hung035d4ec2017-01-24 13:45:02 -08001045 */
1046 public @NonNull Configuration build() {
Andy Hung40a07a82017-03-09 12:10:39 -08001047 final boolean log = (mOptionFlags & OPTION_FLAG_VOLUME_IN_DBFS) != 0;
1048 checkCurveForErrorsAndThrowException(mTimes, mVolumes, log, true /* ise */);
Andy Hungd4f1e862017-03-06 11:39:10 -08001049 return new Configuration(mType, mId, mOptionFlags, mDurationMs,
1050 mInterpolatorType, mTimes, mVolumes);
Andy Hung035d4ec2017-01-24 13:45:02 -08001051 }
1052 } // Configuration.Builder
1053 } // Configuration
1054
1055 /**
1056 * The {@code VolumeShaper.Operation} class is used to specify operations
1057 * to the {@code VolumeShaper} that affect the volume change.
1058 */
1059 public static final class Operation implements Parcelable {
1060 /**
1061 * Forward playback from current volume time position.
Andy Hungfef734c2017-02-23 16:21:13 -08001062 * At the end of the {@code VolumeShaper} curve,
1063 * the last volume value persists.
Andy Hung035d4ec2017-01-24 13:45:02 -08001064 */
1065 public static final Operation PLAY =
1066 new VolumeShaper.Operation.Builder()
1067 .build();
1068
1069 /**
1070 * Reverse playback from current volume time position.
Andy Hungfef734c2017-02-23 16:21:13 -08001071 * When the position reaches the start of the {@code VolumeShaper} curve,
1072 * the first volume value persists.
Andy Hung035d4ec2017-01-24 13:45:02 -08001073 */
1074 public static final Operation REVERSE =
1075 new VolumeShaper.Operation.Builder()
1076 .reverse()
1077 .build();
1078
1079 // No user serviceable parts below.
1080
1081 // These flags must match the native VolumeShaper::Operation::Flag
1082 /** @hide */
1083 @IntDef({
1084 FLAG_NONE,
1085 FLAG_REVERSE,
1086 FLAG_TERMINATE,
1087 FLAG_JOIN,
1088 FLAG_DEFER,
1089 })
1090 @Retention(RetentionPolicy.SOURCE)
1091 public @interface Flag {}
1092
1093 /**
1094 * No special {@code VolumeShaper} operation.
1095 */
1096 private static final int FLAG_NONE = 0;
1097
1098 /**
1099 * Reverse the {@code VolumeShaper} progress.
1100 *
1101 * Reverses the {@code VolumeShaper} curve from its current
1102 * position. If the {@code VolumeShaper} curve has not started,
1103 * it automatically is considered finished.
1104 */
1105 private static final int FLAG_REVERSE = 1 << 0;
1106
1107 /**
1108 * Terminate the existing {@code VolumeShaper}.
1109 * This flag is generally used by itself;
1110 * it takes precedence over all other flags.
1111 */
1112 private static final int FLAG_TERMINATE = 1 << 1;
1113
1114 /**
1115 * Attempt to join as best as possible to the previous {@code VolumeShaper}.
1116 * This requires the previous {@code VolumeShaper} to be active and
1117 * {@link #setReplaceId} to be set.
1118 */
1119 private static final int FLAG_JOIN = 1 << 2;
1120
1121 /**
1122 * Defer playback until next operation is sent. This is used
Andy Hung3c0f5d22017-05-15 15:41:14 -07001123 * when starting a {@code VolumeShaper} effect.
Andy Hung035d4ec2017-01-24 13:45:02 -08001124 */
1125 private static final int FLAG_DEFER = 1 << 3;
1126
Andy Hung7da0e982017-02-22 12:34:21 -08001127 /**
1128 * Use the id specified in the configuration, creating
Andy Hung3c0f5d22017-05-15 15:41:14 -07001129 * {@code VolumeShaper} as needed; the configuration should be
Andy Hung7da0e982017-02-22 12:34:21 -08001130 * TYPE_SCALE.
1131 */
1132 private static final int FLAG_CREATE_IF_NEEDED = 1 << 4;
1133
Andy Hung035d4ec2017-01-24 13:45:02 -08001134 private static final int FLAG_PUBLIC_ALL = FLAG_REVERSE | FLAG_TERMINATE;
1135
Mathew Inwood31a792a2018-08-17 08:54:26 +01001136 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -08001137 private final int mFlags;
Mathew Inwood31a792a2018-08-17 08:54:26 +01001138 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -08001139 private final int mReplaceId;
Mathew Inwood31a792a2018-08-17 08:54:26 +01001140 @UnsupportedAppUsage
Andy Hung3c0f5d22017-05-15 15:41:14 -07001141 private final float mXOffset;
Andy Hung035d4ec2017-01-24 13:45:02 -08001142
1143 @Override
1144 public String toString() {
Andy Hungd4f1e862017-03-06 11:39:10 -08001145 return "VolumeShaper.Operation{"
1146 + "mFlags = 0x" + Integer.toHexString(mFlags).toUpperCase()
1147 + ", mReplaceId = " + mReplaceId
Andy Hung3c0f5d22017-05-15 15:41:14 -07001148 + ", mXOffset = " + mXOffset
Andy Hungd4f1e862017-03-06 11:39:10 -08001149 + "}";
Andy Hung035d4ec2017-01-24 13:45:02 -08001150 }
1151
1152 @Override
1153 public int hashCode() {
Andy Hung3c0f5d22017-05-15 15:41:14 -07001154 return Objects.hash(mFlags, mReplaceId, mXOffset);
Andy Hung035d4ec2017-01-24 13:45:02 -08001155 }
1156
1157 @Override
1158 public boolean equals(Object o) {
1159 if (!(o instanceof Operation)) return false;
1160 if (o == this) return true;
1161 final Operation other = (Operation) o;
Andy Hung3c0f5d22017-05-15 15:41:14 -07001162
Andy Hung035d4ec2017-01-24 13:45:02 -08001163 return mFlags == other.mFlags
Andy Hung3c0f5d22017-05-15 15:41:14 -07001164 && mReplaceId == other.mReplaceId
1165 && Float.compare(mXOffset, other.mXOffset) == 0;
Andy Hung035d4ec2017-01-24 13:45:02 -08001166 }
1167
1168 @Override
1169 public int describeContents() {
1170 return 0;
1171 }
1172
1173 @Override
1174 public void writeToParcel(Parcel dest, int flags) {
Andy Hungd4f1e862017-03-06 11:39:10 -08001175 // this needs to match the native VolumeShaper.Operation parceling
Andy Hung035d4ec2017-01-24 13:45:02 -08001176 dest.writeInt(mFlags);
1177 dest.writeInt(mReplaceId);
Andy Hung3c0f5d22017-05-15 15:41:14 -07001178 dest.writeFloat(mXOffset);
Andy Hung035d4ec2017-01-24 13:45:02 -08001179 }
1180
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -07001181 public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.Operation> CREATOR
Andy Hung035d4ec2017-01-24 13:45:02 -08001182 = new Parcelable.Creator<VolumeShaper.Operation>() {
1183 @Override
1184 public VolumeShaper.Operation createFromParcel(Parcel p) {
Andy Hungd4f1e862017-03-06 11:39:10 -08001185 // this needs to match the native VolumeShaper.Operation parceling
1186 final int flags = p.readInt();
1187 final int replaceId = p.readInt();
Andy Hung3c0f5d22017-05-15 15:41:14 -07001188 final float xOffset = p.readFloat();
Andy Hungd4f1e862017-03-06 11:39:10 -08001189
Andy Hung035d4ec2017-01-24 13:45:02 -08001190 return new VolumeShaper.Operation(
Andy Hungd4f1e862017-03-06 11:39:10 -08001191 flags
Andy Hung3c0f5d22017-05-15 15:41:14 -07001192 , replaceId
1193 , xOffset);
Andy Hung035d4ec2017-01-24 13:45:02 -08001194 }
1195
1196 @Override
1197 public VolumeShaper.Operation[] newArray(int size) {
1198 return new VolumeShaper.Operation[size];
1199 }
1200 };
1201
Mathew Inwood31a792a2018-08-17 08:54:26 +01001202 @UnsupportedAppUsage
Andy Hung3c0f5d22017-05-15 15:41:14 -07001203 private Operation(@Flag int flags, int replaceId, float xOffset) {
Andy Hung035d4ec2017-01-24 13:45:02 -08001204 mFlags = flags;
1205 mReplaceId = replaceId;
Andy Hung3c0f5d22017-05-15 15:41:14 -07001206 mXOffset = xOffset;
Andy Hung035d4ec2017-01-24 13:45:02 -08001207 }
1208
1209 /**
1210 * @hide
1211 * {@code Builder} class for {@link VolumeShaper.Operation} object.
1212 *
1213 * Not for public use.
1214 */
1215 public static final class Builder {
1216 int mFlags;
1217 int mReplaceId;
Andy Hung3c0f5d22017-05-15 15:41:14 -07001218 float mXOffset;
Andy Hung035d4ec2017-01-24 13:45:02 -08001219
1220 /**
1221 * Constructs a new {@code Builder} with the defaults.
1222 */
1223 public Builder() {
1224 mFlags = 0;
1225 mReplaceId = -1;
Andy Hung3c0f5d22017-05-15 15:41:14 -07001226 mXOffset = Float.NaN;
Andy Hung035d4ec2017-01-24 13:45:02 -08001227 }
1228
1229 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -07001230 * Constructs a new {@code Builder} from a given {@code VolumeShaper.Operation}
Andy Hung035d4ec2017-01-24 13:45:02 -08001231 * @param operation the {@code VolumeShaper.operation} whose data will be
Andy Hung3c0f5d22017-05-15 15:41:14 -07001232 * reused in the new {@code Builder}.
Andy Hung035d4ec2017-01-24 13:45:02 -08001233 */
1234 public Builder(@NonNull VolumeShaper.Operation operation) {
1235 mReplaceId = operation.mReplaceId;
1236 mFlags = operation.mFlags;
Andy Hung3c0f5d22017-05-15 15:41:14 -07001237 mXOffset = operation.mXOffset;
Andy Hung035d4ec2017-01-24 13:45:02 -08001238 }
1239
1240 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -07001241 * Replaces the previous {@code VolumeShaper} specified by {@code id}.
1242 *
1243 * The {@code VolumeShaper} specified by the {@code id} is removed
1244 * if it exists. The configuration should be TYPE_SCALE.
1245 *
1246 * @param id the {@code id} of the previous {@code VolumeShaper}.
Andy Hungfef734c2017-02-23 16:21:13 -08001247 * @param join if true, match the volume of the previous
1248 * shaper to the start volume of the new {@code VolumeShaper}.
1249 * @return the same {@code Builder} instance.
Andy Hung035d4ec2017-01-24 13:45:02 -08001250 */
1251 public @NonNull Builder replace(int id, boolean join) {
1252 mReplaceId = id;
1253 if (join) {
1254 mFlags |= FLAG_JOIN;
1255 } else {
1256 mFlags &= ~FLAG_JOIN;
1257 }
1258 return this;
1259 }
1260
1261 /**
1262 * Defers all operations.
Andy Hungfef734c2017-02-23 16:21:13 -08001263 * @return the same {@code Builder} instance.
Andy Hung035d4ec2017-01-24 13:45:02 -08001264 */
1265 public @NonNull Builder defer() {
1266 mFlags |= FLAG_DEFER;
1267 return this;
1268 }
1269
1270 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -07001271 * Terminates the {@code VolumeShaper}.
1272 *
1273 * Do not call directly, use {@link VolumeShaper#close()}.
Andy Hungfef734c2017-02-23 16:21:13 -08001274 * @return the same {@code Builder} instance.
Andy Hung035d4ec2017-01-24 13:45:02 -08001275 */
1276 public @NonNull Builder terminate() {
1277 mFlags |= FLAG_TERMINATE;
1278 return this;
1279 }
1280
1281 /**
1282 * Reverses direction.
Andy Hungfef734c2017-02-23 16:21:13 -08001283 * @return the same {@code Builder} instance.
Andy Hung035d4ec2017-01-24 13:45:02 -08001284 */
1285 public @NonNull Builder reverse() {
1286 mFlags ^= FLAG_REVERSE;
1287 return this;
1288 }
1289
1290 /**
Andy Hung7da0e982017-02-22 12:34:21 -08001291 * Use the id specified in the configuration, creating
Andy Hung3c0f5d22017-05-15 15:41:14 -07001292 * {@code VolumeShaper} only as needed; the configuration should be
Andy Hung7da0e982017-02-22 12:34:21 -08001293 * TYPE_SCALE.
Andy Hung3c0f5d22017-05-15 15:41:14 -07001294 *
1295 * If the {@code VolumeShaper} with the same id already exists
1296 * then the operation has no effect.
1297 *
Andy Hungfef734c2017-02-23 16:21:13 -08001298 * @return the same {@code Builder} instance.
Andy Hung7da0e982017-02-22 12:34:21 -08001299 */
1300 public @NonNull Builder createIfNeeded() {
1301 mFlags |= FLAG_CREATE_IF_NEEDED;
1302 return this;
1303 }
1304
1305 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -07001306 * Sets the {@code xOffset} to use for the {@code VolumeShaper}.
1307 *
1308 * The {@code xOffset} is the position on the volume curve,
1309 * and setting takes effect when the {@code VolumeShaper} is used next.
1310 *
1311 * @param xOffset a value between (or equal to) 0.f and 1.f, or Float.NaN to ignore.
1312 * @return the same {@code Builder} instance.
1313 * @throws IllegalArgumentException if {@code xOffset} is not between 0.f and 1.f,
1314 * or a Float.NaN.
1315 */
1316 public @NonNull Builder setXOffset(float xOffset) {
1317 if (xOffset < -0.f) {
1318 throw new IllegalArgumentException("Negative xOffset not allowed");
1319 } else if (xOffset > 1.f) {
1320 throw new IllegalArgumentException("xOffset > 1.f not allowed");
1321 }
1322 // Float.NaN passes through
1323 mXOffset = xOffset;
1324 return this;
1325 }
1326
1327 /**
Andy Hung035d4ec2017-01-24 13:45:02 -08001328 * Sets the operation flag. Do not call this directly but one of the
1329 * other builder methods.
1330 *
1331 * @param flags new value for {@code flags}, consisting of ORed flags.
Andy Hungfef734c2017-02-23 16:21:13 -08001332 * @return the same {@code Builder} instance.
Andy Hungd4f1e862017-03-06 11:39:10 -08001333 * @throws IllegalArgumentException if {@code flags} contains invalid set bits.
Andy Hung035d4ec2017-01-24 13:45:02 -08001334 */
1335 private @NonNull Builder setFlags(@Flag int flags) {
1336 if ((flags & ~FLAG_PUBLIC_ALL) != 0) {
1337 throw new IllegalArgumentException("flag has unknown bits set: " + flags);
1338 }
1339 mFlags = mFlags & ~FLAG_PUBLIC_ALL | flags;
1340 return this;
1341 }
1342
1343 /**
1344 * Builds a new {@link VolumeShaper.Operation} object.
1345 *
1346 * @return a new {@code VolumeShaper.Operation} object
1347 */
1348 public @NonNull Operation build() {
Andy Hung3c0f5d22017-05-15 15:41:14 -07001349 return new Operation(mFlags, mReplaceId, mXOffset);
Andy Hung035d4ec2017-01-24 13:45:02 -08001350 }
1351 } // Operation.Builder
1352 } // Operation
1353
1354 /**
1355 * @hide
1356 * {@code VolumeShaper.State} represents the current progress
1357 * of the {@code VolumeShaper}.
1358 *
1359 * Not for public use.
1360 */
1361 public static final class State implements Parcelable {
Mathew Inwood31a792a2018-08-17 08:54:26 +01001362 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -08001363 private float mVolume;
Mathew Inwood31a792a2018-08-17 08:54:26 +01001364 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -08001365 private float mXOffset;
1366
1367 @Override
1368 public String toString() {
Andy Hungd4f1e862017-03-06 11:39:10 -08001369 return "VolumeShaper.State{"
1370 + "mVolume = " + mVolume
1371 + ", mXOffset = " + mXOffset
1372 + "}";
Andy Hung035d4ec2017-01-24 13:45:02 -08001373 }
1374
1375 @Override
1376 public int hashCode() {
1377 return Objects.hash(mVolume, mXOffset);
1378 }
1379
1380 @Override
1381 public boolean equals(Object o) {
1382 if (!(o instanceof State)) return false;
1383 if (o == this) return true;
1384 final State other = (State) o;
1385 return mVolume == other.mVolume
1386 && mXOffset == other.mXOffset;
1387 }
1388
1389 @Override
1390 public int describeContents() {
1391 return 0;
1392 }
1393
1394 @Override
1395 public void writeToParcel(Parcel dest, int flags) {
1396 dest.writeFloat(mVolume);
1397 dest.writeFloat(mXOffset);
1398 }
1399
Jeff Sharkey9e8f83d2019-02-28 12:06:45 -07001400 public static final @android.annotation.NonNull Parcelable.Creator<VolumeShaper.State> CREATOR
Andy Hung035d4ec2017-01-24 13:45:02 -08001401 = new Parcelable.Creator<VolumeShaper.State>() {
1402 @Override
1403 public VolumeShaper.State createFromParcel(Parcel p) {
1404 return new VolumeShaper.State(
1405 p.readFloat() // volume
1406 , p.readFloat()); // xOffset
1407 }
1408
1409 @Override
1410 public VolumeShaper.State[] newArray(int size) {
1411 return new VolumeShaper.State[size];
1412 }
1413 };
1414
Mathew Inwood31a792a2018-08-17 08:54:26 +01001415 @UnsupportedAppUsage
Andy Hung035d4ec2017-01-24 13:45:02 -08001416 /* package */ State(float volume, float xOffset) {
1417 mVolume = volume;
1418 mXOffset = xOffset;
1419 }
1420
1421 /**
1422 * Gets the volume of the {@link VolumeShaper.State}.
Andy Hung3c0f5d22017-05-15 15:41:14 -07001423 * @return linear volume between 0.f and 1.f.
Andy Hung035d4ec2017-01-24 13:45:02 -08001424 */
1425 public float getVolume() {
1426 return mVolume;
1427 }
1428
1429 /**
Andy Hung3c0f5d22017-05-15 15:41:14 -07001430 * Gets the {@code xOffset} position on the normalized curve
1431 * of the {@link VolumeShaper.State}.
1432 * @return the curve x position between 0.f and 1.f.
Andy Hung035d4ec2017-01-24 13:45:02 -08001433 */
Andy Hung3c0f5d22017-05-15 15:41:14 -07001434 public float getXOffset() {
Andy Hung035d4ec2017-01-24 13:45:02 -08001435 return mXOffset;
1436 }
1437 } // State
1438}