1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.media; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.IntDef; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.RequiresPermission; 25 import android.annotation.SystemApi; 26 import android.media.permission.ClearCallingIdentityContext; 27 import android.media.permission.SafeCloseable; 28 import android.os.RemoteException; 29 import android.util.Log; 30 31 import com.android.internal.annotations.GuardedBy; 32 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 import java.util.ArrayList; 36 import java.util.List; 37 import java.util.Objects; 38 import java.util.concurrent.Executor; 39 40 /** 41 * Spatializer provides access to querying capabilities and behavior of sound spatialization 42 * on the device. 43 * Sound spatialization simulates sounds originating around the listener as if they were coming 44 * from virtual speakers placed around the listener.<br> 45 * Support for spatialization is optional, use {@link AudioManager#getSpatializer()} to obtain an 46 * instance of this class if the feature is supported. 47 * 48 */ 49 public class Spatializer { 50 51 private final @NonNull AudioManager mAm; 52 53 private static final String TAG = "Spatializer"; 54 55 /** 56 * @hide 57 * Constructor with AudioManager acting as proxy to AudioService 58 * @param am a non-null AudioManager 59 */ Spatializer(@onNull AudioManager am)60 protected Spatializer(@NonNull AudioManager am) { 61 mAm = Objects.requireNonNull(am); 62 } 63 64 /** 65 * Returns whether spatialization is enabled or not. 66 * A false value can originate for instance from the user electing to 67 * disable the feature, or when the feature is not supported on the device (indicated 68 * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}). 69 * <br> 70 * Note that this state reflects a platform-wide state of the "desire" to use spatialization, 71 * but availability of the audio processing is still dictated by the compatibility between 72 * the effect and the hardware configuration, as indicated by {@link #isAvailable()}. 73 * @return {@code true} if spatialization is enabled 74 * @see #isAvailable() 75 */ isEnabled()76 public boolean isEnabled() { 77 try { 78 return mAm.getService().isSpatializerEnabled(); 79 } catch (RemoteException e) { 80 Log.e(TAG, "Error querying isSpatializerEnabled, returning false", e); 81 return false; 82 } 83 } 84 85 /** 86 * Returns whether spatialization is available. 87 * Reasons for spatialization being unavailable include situations where audio output is 88 * incompatible with sound spatialization, such as playback on a monophonic speaker.<br> 89 * Note that spatialization can be available, but disabled by the user, in which case this 90 * method would still return {@code true}, whereas {@link #isEnabled()} 91 * would return {@code false}.<br> 92 * Also when the feature is not supported on the device (indicated 93 * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}), 94 * the return value will be false. 95 * @return {@code true} if the spatializer effect is available and capable 96 * of processing the audio for the current configuration of the device, 97 * {@code false} otherwise. 98 * @see #isEnabled() 99 */ isAvailable()100 public boolean isAvailable() { 101 try { 102 return mAm.getService().isSpatializerAvailable(); 103 } catch (RemoteException e) { 104 Log.e(TAG, "Error querying isSpatializerAvailable, returning false", e); 105 return false; 106 } 107 } 108 109 /** @hide */ 110 @IntDef(flag = false, value = { 111 SPATIALIZER_IMMERSIVE_LEVEL_OTHER, 112 SPATIALIZER_IMMERSIVE_LEVEL_NONE, 113 SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL, 114 }) 115 @Retention(RetentionPolicy.SOURCE) 116 public @interface ImmersiveAudioLevel {}; 117 118 /** 119 * Constant indicating the {@code Spatializer} on this device supports a spatialization 120 * mode that differs from the ones available at this SDK level. 121 * @see #getImmersiveAudioLevel() 122 */ 123 public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1; 124 125 /** 126 * Constant indicating there are no spatialization capabilities supported on this device. 127 * @see #getImmersiveAudioLevel() 128 */ 129 public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0; 130 131 /** 132 * Constant indicating the {@code Spatializer} on this device supports multichannel 133 * spatialization. 134 * @see #getImmersiveAudioLevel() 135 */ 136 public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; 137 138 /** 139 * @hide 140 * Constant indicating the {@code Spatializer} on this device supports the spatialization of 141 * multichannel bed plus objects. 142 * @see #getImmersiveAudioLevel() 143 */ 144 public static final int SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS = 2; 145 146 /** @hide */ 147 @IntDef(flag = false, value = { 148 HEAD_TRACKING_MODE_UNSUPPORTED, 149 HEAD_TRACKING_MODE_DISABLED, 150 HEAD_TRACKING_MODE_RELATIVE_WORLD, 151 HEAD_TRACKING_MODE_RELATIVE_DEVICE, 152 }) public @interface HeadTrackingMode {}; 153 154 /** @hide */ 155 @IntDef(flag = false, value = { 156 HEAD_TRACKING_MODE_DISABLED, 157 HEAD_TRACKING_MODE_RELATIVE_WORLD, 158 HEAD_TRACKING_MODE_RELATIVE_DEVICE, 159 }) public @interface HeadTrackingModeSet {}; 160 161 /** @hide */ 162 @IntDef(flag = false, value = { 163 HEAD_TRACKING_MODE_RELATIVE_WORLD, 164 HEAD_TRACKING_MODE_RELATIVE_DEVICE, 165 }) public @interface HeadTrackingModeSupported {}; 166 167 /** 168 * @hide 169 * Constant indicating head tracking is not supported by this {@code Spatializer} 170 * @see #getHeadTrackingMode() 171 */ 172 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 173 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) 174 public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2; 175 176 /** 177 * @hide 178 * Constant indicating head tracking is disabled on this {@code Spatializer} 179 * @see #getHeadTrackingMode() 180 */ 181 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 182 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) 183 public static final int HEAD_TRACKING_MODE_DISABLED = -1; 184 185 /** 186 * @hide 187 * Constant indicating head tracking is in a mode whose behavior is unknown. This is not an 188 * error state but represents a customized behavior not defined by this API. 189 * @see #getHeadTrackingMode() 190 */ 191 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 192 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) 193 public static final int HEAD_TRACKING_MODE_OTHER = 0; 194 195 /** 196 * @hide 197 * Constant indicating head tracking is tracking the user's position / orientation relative to 198 * the world around them 199 * @see #getHeadTrackingMode() 200 */ 201 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 202 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) 203 public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1; 204 205 /** 206 * @hide 207 * Constant indicating head tracking is tracking the user's position / orientation relative to 208 * the device 209 * @see #getHeadTrackingMode() 210 */ 211 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 212 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) 213 public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2; 214 215 /** 216 * Return the level of support for the spatialization feature on this device. 217 * This level of support is independent of whether the {@code Spatializer} is currently 218 * enabled or available and will not change over time. 219 * @return the level of spatialization support 220 * @see #isEnabled() 221 * @see #isAvailable() 222 */ getImmersiveAudioLevel()223 public @ImmersiveAudioLevel int getImmersiveAudioLevel() { 224 int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; 225 try { 226 level = mAm.getService().getSpatializerImmersiveAudioLevel(); 227 } catch (Exception e) { /* using NONE */ } 228 return level; 229 } 230 231 /** 232 * @hide 233 * Enables / disables the spatializer effect. 234 * Changing the enabled state will trigger the public 235 * {@link OnSpatializerStateChangedListener#onSpatializerEnabledChanged(Spatializer, boolean)} 236 * registered listeners. 237 * @param enabled {@code true} for enabling the effect 238 */ 239 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 240 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) setEnabled(boolean enabled)241 public void setEnabled(boolean enabled) { 242 try { 243 mAm.getService().setSpatializerEnabled(enabled); 244 } catch (RemoteException e) { 245 Log.e(TAG, "Error calling setSpatializerEnabled", e); 246 } 247 } 248 249 /** 250 * An interface to be notified of changes to the state of the spatializer effect. 251 */ 252 public interface OnSpatializerStateChangedListener { 253 /** 254 * Called when the enabled state of the spatializer effect changes 255 * @param spat the {@code Spatializer} instance whose state changed 256 * @param enabled {@code true} if the spatializer effect is enabled on the device, 257 * {@code false} otherwise 258 * @see #isEnabled() 259 */ onSpatializerEnabledChanged(@onNull Spatializer spat, boolean enabled)260 void onSpatializerEnabledChanged(@NonNull Spatializer spat, boolean enabled); 261 262 /** 263 * Called when the availability of the spatializer effect changes 264 * @param spat the {@code Spatializer} instance whose state changed 265 * @param available {@code true} if the spatializer effect is available and capable 266 * of processing the audio for the current configuration of the device, 267 * {@code false} otherwise. 268 * @see #isAvailable() 269 */ onSpatializerAvailableChanged(@onNull Spatializer spat, boolean available)270 void onSpatializerAvailableChanged(@NonNull Spatializer spat, boolean available); 271 } 272 273 /** 274 * @hide 275 * An interface to be notified of changes to the head tracking mode, used by the spatializer 276 * effect. 277 * Changes to the mode may come from explicitly setting a different mode 278 * (see {@link #setDesiredHeadTrackingMode(int)}) or a change in system conditions (see 279 * {@link #getHeadTrackingMode()} 280 * @see #addOnHeadTrackingModeChangedListener(Executor, OnHeadTrackingModeChangedListener) 281 * @see #removeOnHeadTrackingModeChangedListener(OnHeadTrackingModeChangedListener) 282 */ 283 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 284 public interface OnHeadTrackingModeChangedListener { 285 /** 286 * Called when the actual head tracking mode of the spatializer changed. 287 * @param spatializer the {@code Spatializer} instance whose head tracking mode is changing 288 * @param mode the new head tracking mode 289 */ onHeadTrackingModeChanged(@onNull Spatializer spatializer, @HeadTrackingMode int mode)290 void onHeadTrackingModeChanged(@NonNull Spatializer spatializer, 291 @HeadTrackingMode int mode); 292 293 /** 294 * Called when the desired head tracking mode of the spatializer changed 295 * @param spatializer the {@code Spatializer} instance whose head tracking mode was set 296 * @param mode the newly set head tracking mode 297 */ onDesiredHeadTrackingModeChanged(@onNull Spatializer spatializer, @HeadTrackingModeSet int mode)298 void onDesiredHeadTrackingModeChanged(@NonNull Spatializer spatializer, 299 @HeadTrackingModeSet int mode); 300 } 301 302 303 /** 304 * @hide 305 * An interface to be notified of changes to the output stream used by the spatializer 306 * effect. 307 * @see #getOutput() 308 */ 309 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 310 public interface OnSpatializerOutputChangedListener { 311 /** 312 * Called when the id of the output stream of the spatializer effect changed. 313 * @param spatializer the {@code Spatializer} instance whose output is updated 314 * @param output the id of the output stream, or 0 when there is no spatializer output 315 */ onSpatializerOutputChanged(@onNull Spatializer spatializer, @IntRange(from = 0) int output)316 void onSpatializerOutputChanged(@NonNull Spatializer spatializer, 317 @IntRange(from = 0) int output); 318 } 319 320 /** 321 * @hide 322 * An interface to be notified of updates to the head to soundstage pose, as represented by the 323 * current head tracking mode. 324 * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener) 325 */ 326 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 327 public interface OnHeadToSoundstagePoseUpdatedListener { 328 /** 329 * Called when the head to soundstage transform is updated 330 * @param spatializer the {@code Spatializer} instance affected by the pose update 331 * @param pose the new pose data representing the transform between the frame 332 * of reference for the current head tracking mode (see 333 * {@link #getHeadTrackingMode()}) and the device being tracked (for 334 * instance a pair of headphones with a head tracker).<br> 335 * The head pose data is represented as an array of six float values, where 336 * the first three values are the translation vector, and the next three 337 * are the rotation vector. 338 */ onHeadToSoundstagePoseUpdated(@onNull Spatializer spatializer, @NonNull float[] pose)339 void onHeadToSoundstagePoseUpdated(@NonNull Spatializer spatializer, 340 @NonNull float[] pose); 341 } 342 343 /** 344 * Returns whether audio of the given {@link AudioFormat}, played with the given 345 * {@link AudioAttributes} can be spatialized. 346 * Note that the result reflects the capabilities of the device and may change when 347 * audio accessories are connected/disconnected (e.g. wired headphones plugged in or not). 348 * The result is independent from whether spatialization processing is enabled or not. 349 * @param attributes the {@code AudioAttributes} of the content as used for playback 350 * @param format the {@code AudioFormat} of the content as used for playback 351 * @return {@code true} if the device is capable of spatializing the combination of audio format 352 * and attributes, {@code false} otherwise. 353 */ canBeSpatialized( @onNull AudioAttributes attributes, @NonNull AudioFormat format)354 public boolean canBeSpatialized( 355 @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { 356 try { 357 return mAm.getService().canBeSpatialized( 358 Objects.requireNonNull(attributes), Objects.requireNonNull(format)); 359 } catch (RemoteException e) { 360 Log.e(TAG, "Error querying canBeSpatialized for attr:" + attributes 361 + " format:" + format + " returning false", e); 362 return false; 363 } 364 } 365 366 /** 367 * Adds a listener to be notified of changes to the enabled state of the 368 * {@code Spatializer}. 369 * @param executor the {@code Executor} handling the callback 370 * @param listener the listener to receive enabled state updates 371 * @see #isEnabled() 372 */ addOnSpatializerStateChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnSpatializerStateChangedListener listener)373 public void addOnSpatializerStateChangedListener( 374 @NonNull @CallbackExecutor Executor executor, 375 @NonNull OnSpatializerStateChangedListener listener) { 376 Objects.requireNonNull(executor); 377 Objects.requireNonNull(listener); 378 synchronized (mStateListenerLock) { 379 if (hasSpatializerStateListener(listener)) { 380 throw new IllegalArgumentException( 381 "Called addOnSpatializerStateChangedListener() " 382 + "on a previously registered listener"); 383 } 384 // lazy initialization of the list of strategy-preferred device listener 385 if (mStateListeners == null) { 386 mStateListeners = new ArrayList<>(); 387 } 388 mStateListeners.add(new StateListenerInfo(listener, executor)); 389 if (mStateListeners.size() == 1) { 390 // register binder for callbacks 391 if (mInfoDispatcherStub == null) { 392 mInfoDispatcherStub = 393 new SpatializerInfoDispatcherStub(); 394 } 395 try { 396 mAm.getService().registerSpatializerCallback( 397 mInfoDispatcherStub); 398 } catch (RemoteException e) { 399 throw e.rethrowFromSystemServer(); 400 } 401 } 402 } 403 } 404 405 /** 406 * Removes a previously added listener for changes to the enabled state of the 407 * {@code Spatializer}. 408 * @param listener the listener to receive enabled state updates 409 * @see #isEnabled() 410 */ removeOnSpatializerStateChangedListener( @onNull OnSpatializerStateChangedListener listener)411 public void removeOnSpatializerStateChangedListener( 412 @NonNull OnSpatializerStateChangedListener listener) { 413 Objects.requireNonNull(listener); 414 synchronized (mStateListenerLock) { 415 if (!removeStateListener(listener)) { 416 throw new IllegalArgumentException( 417 "Called removeOnSpatializerStateChangedListener() " 418 + "on an unregistered listener"); 419 } 420 if (mStateListeners.size() == 0) { 421 // unregister binder for callbacks 422 try { 423 mAm.getService().unregisterSpatializerCallback(mInfoDispatcherStub); 424 } catch (RemoteException e) { 425 throw e.rethrowFromSystemServer(); 426 } finally { 427 mInfoDispatcherStub = null; 428 mStateListeners = null; 429 } 430 } 431 } 432 } 433 434 /** 435 * @hide 436 * Returns the list of playback devices that are compatible with the playback of multichannel 437 * audio through virtualization 438 * @return a list of devices. An empty list indicates virtualization is not supported. 439 */ 440 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 441 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) getCompatibleAudioDevices()442 public @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() { 443 try { 444 return mAm.getService().getSpatializerCompatibleAudioDevices(); 445 } catch (RemoteException e) { 446 Log.e(TAG, "Error querying getSpatializerCompatibleAudioDevices(), " 447 + " returning empty list", e); 448 return new ArrayList<AudioDeviceAttributes>(0); 449 } 450 } 451 452 /** 453 * @hide 454 * Adds a playback device to the list of devices compatible with the playback of multichannel 455 * audio through spatialization. 456 * @see #getCompatibleAudioDevices() 457 * @param ada the audio device compatible with spatialization 458 */ 459 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 460 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) addCompatibleAudioDevice(@onNull AudioDeviceAttributes ada)461 public void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { 462 try { 463 mAm.getService().addSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); 464 } catch (RemoteException e) { 465 Log.e(TAG, "Error calling addSpatializerCompatibleAudioDevice(), ", e); 466 } 467 } 468 469 /** 470 * @hide 471 * Remove a playback device from the list of devices compatible with the playback of 472 * multichannel audio through spatialization. 473 * @see #getCompatibleAudioDevices() 474 * @param ada the audio device incompatible with spatialization 475 */ 476 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 477 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) removeCompatibleAudioDevice(@onNull AudioDeviceAttributes ada)478 public void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { 479 try { 480 mAm.getService().removeSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); 481 } catch (RemoteException e) { 482 Log.e(TAG, "Error calling removeSpatializerCompatibleAudioDevice(), ", e); 483 } 484 } 485 486 private final Object mStateListenerLock = new Object(); 487 /** 488 * List of listeners for state listener and their associated Executor. 489 * List is lazy-initialized on first registration 490 */ 491 @GuardedBy("mStateListenerLock") 492 private @Nullable ArrayList<StateListenerInfo> mStateListeners; 493 494 @GuardedBy("mStateListenerLock") 495 private @Nullable SpatializerInfoDispatcherStub mInfoDispatcherStub; 496 497 private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub { 498 @Override dispatchSpatializerEnabledChanged(boolean enabled)499 public void dispatchSpatializerEnabledChanged(boolean enabled) { 500 // make a shallow copy of listeners so callback is not executed under lock 501 final ArrayList<StateListenerInfo> stateListeners; 502 synchronized (mStateListenerLock) { 503 if (mStateListeners == null || mStateListeners.size() == 0) { 504 return; 505 } 506 stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone(); 507 } 508 try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { 509 for (StateListenerInfo info : stateListeners) { 510 info.mExecutor.execute(() -> 511 info.mListener.onSpatializerEnabledChanged(Spatializer.this, enabled)); 512 } 513 } 514 } 515 516 @Override dispatchSpatializerAvailableChanged(boolean available)517 public void dispatchSpatializerAvailableChanged(boolean available) { 518 // make a shallow copy of listeners so callback is not executed under lock 519 final ArrayList<StateListenerInfo> stateListeners; 520 synchronized (mStateListenerLock) { 521 if (mStateListeners == null || mStateListeners.size() == 0) { 522 return; 523 } 524 stateListeners = (ArrayList<StateListenerInfo>) mStateListeners.clone(); 525 } 526 try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { 527 for (StateListenerInfo info : stateListeners) { 528 info.mExecutor.execute(() -> 529 info.mListener.onSpatializerAvailableChanged( 530 Spatializer.this, available)); 531 } 532 } 533 } 534 } 535 536 private static class StateListenerInfo { 537 final @NonNull OnSpatializerStateChangedListener mListener; 538 final @NonNull Executor mExecutor; 539 StateListenerInfo(@onNull OnSpatializerStateChangedListener listener, @NonNull Executor exe)540 StateListenerInfo(@NonNull OnSpatializerStateChangedListener listener, 541 @NonNull Executor exe) { 542 mListener = listener; 543 mExecutor = exe; 544 } 545 } 546 547 @GuardedBy("mStateListenerLock") hasSpatializerStateListener(OnSpatializerStateChangedListener listener)548 private boolean hasSpatializerStateListener(OnSpatializerStateChangedListener listener) { 549 return getStateListenerInfo(listener) != null; 550 } 551 552 @GuardedBy("mStateListenerLock") getStateListenerInfo( OnSpatializerStateChangedListener listener)553 private @Nullable StateListenerInfo getStateListenerInfo( 554 OnSpatializerStateChangedListener listener) { 555 if (mStateListeners == null) { 556 return null; 557 } 558 for (StateListenerInfo info : mStateListeners) { 559 if (info.mListener == listener) { 560 return info; 561 } 562 } 563 return null; 564 } 565 566 @GuardedBy("mStateListenerLock") 567 /** 568 * @return true if the listener was removed from the list 569 */ removeStateListener(OnSpatializerStateChangedListener listener)570 private boolean removeStateListener(OnSpatializerStateChangedListener listener) { 571 final StateListenerInfo infoToRemove = getStateListenerInfo(listener); 572 if (infoToRemove != null) { 573 mStateListeners.remove(infoToRemove); 574 return true; 575 } 576 return false; 577 } 578 579 580 /** 581 * @hide 582 * Return the current head tracking mode as used by the system. 583 * Note this may differ from the desired head tracking mode. Reasons for the two to differ 584 * include: a head tracking device is not available for the current audio output device, 585 * the transmission conditions between the tracker and device have deteriorated and tracking 586 * has been disabled. 587 * @see #getDesiredHeadTrackingMode() 588 * @return the current head tracking mode 589 */ 590 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 591 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) getHeadTrackingMode()592 public @HeadTrackingMode int getHeadTrackingMode() { 593 try { 594 return mAm.getService().getActualHeadTrackingMode(); 595 } catch (RemoteException e) { 596 Log.e(TAG, "Error calling getActualHeadTrackingMode", e); 597 return HEAD_TRACKING_MODE_UNSUPPORTED; 598 } 599 600 } 601 602 /** 603 * @hide 604 * Return the desired head tracking mode. 605 * Note this may differ from the actual head tracking mode, reflected by 606 * {@link #getHeadTrackingMode()}. 607 * @return the desired head tring mode 608 */ 609 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 610 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) getDesiredHeadTrackingMode()611 public @HeadTrackingMode int getDesiredHeadTrackingMode() { 612 try { 613 return mAm.getService().getDesiredHeadTrackingMode(); 614 } catch (RemoteException e) { 615 Log.e(TAG, "Error calling getDesiredHeadTrackingMode", e); 616 return HEAD_TRACKING_MODE_UNSUPPORTED; 617 } 618 } 619 620 /** 621 * @hide 622 * Returns the list of supported head tracking modes. 623 * @return the list of modes that can be used in {@link #setDesiredHeadTrackingMode(int)} to 624 * enable head tracking. The list will be empty if {@link #getHeadTrackingMode()} 625 * is {@link #HEAD_TRACKING_MODE_UNSUPPORTED}. Values can be 626 * {@link #HEAD_TRACKING_MODE_OTHER}, 627 * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD} or 628 * {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} 629 */ 630 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 631 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) getSupportedHeadTrackingModes()632 public @NonNull List<Integer> getSupportedHeadTrackingModes() { 633 try { 634 final int[] modes = mAm.getService().getSupportedHeadTrackingModes(); 635 final ArrayList<Integer> list = new ArrayList<>(0); 636 for (int mode : modes) { 637 list.add(mode); 638 } 639 return list; 640 } catch (RemoteException e) { 641 Log.e(TAG, "Error calling getSupportedHeadTrackModes", e); 642 return new ArrayList(0); 643 } 644 } 645 646 /** 647 * @hide 648 * Sets the desired head tracking mode. 649 * Note a set desired mode may differ from the actual head tracking mode. 650 * @see #getHeadTrackingMode() 651 * @param mode the desired head tracking mode, one of the values returned by 652 * {@link #getSupportedHeadTrackModes()}, or {@link #HEAD_TRACKING_MODE_DISABLED} to 653 * disable head tracking. 654 */ 655 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 656 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) setDesiredHeadTrackingMode(@eadTrackingModeSet int mode)657 public void setDesiredHeadTrackingMode(@HeadTrackingModeSet int mode) { 658 try { 659 mAm.getService().setDesiredHeadTrackingMode(mode); 660 } catch (RemoteException e) { 661 Log.e(TAG, "Error calling setDesiredHeadTrackingMode to " + mode, e); 662 } 663 } 664 665 /** 666 * @hide 667 * Recenters the head tracking at the current position / orientation. 668 */ 669 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 670 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) recenterHeadTracker()671 public void recenterHeadTracker() { 672 try { 673 mAm.getService().recenterHeadTracker(); 674 } catch (RemoteException e) { 675 Log.e(TAG, "Error calling recenterHeadTracker", e); 676 } 677 } 678 679 /** 680 * @hide 681 * Adds a listener to be notified of changes to the head tracking mode of the 682 * {@code Spatializer} 683 * @param executor the {@code Executor} handling the callbacks 684 * @param listener the listener to register 685 */ 686 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 687 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) addOnHeadTrackingModeChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnHeadTrackingModeChangedListener listener)688 public void addOnHeadTrackingModeChangedListener( 689 @NonNull @CallbackExecutor Executor executor, 690 @NonNull OnHeadTrackingModeChangedListener listener) { 691 Objects.requireNonNull(executor); 692 Objects.requireNonNull(listener); 693 synchronized (mHeadTrackingListenerLock) { 694 if (hasListener(listener, mHeadTrackingListeners)) { 695 throw new IllegalArgumentException( 696 "Called addOnHeadTrackingModeChangedListener() " 697 + "on a previously registered listener"); 698 } 699 // lazy initialization of the list of strategy-preferred device listener 700 if (mHeadTrackingListeners == null) { 701 mHeadTrackingListeners = new ArrayList<>(); 702 } 703 mHeadTrackingListeners.add( 704 new ListenerInfo<OnHeadTrackingModeChangedListener>(listener, executor)); 705 if (mHeadTrackingListeners.size() == 1) { 706 // register binder for callbacks 707 if (mHeadTrackingDispatcherStub == null) { 708 mHeadTrackingDispatcherStub = 709 new SpatializerHeadTrackingDispatcherStub(); 710 } 711 try { 712 mAm.getService().registerSpatializerHeadTrackingCallback( 713 mHeadTrackingDispatcherStub); 714 } catch (RemoteException e) { 715 throw e.rethrowFromSystemServer(); 716 } 717 } 718 } 719 } 720 721 /** 722 * @hide 723 * Removes a previously added listener for changes to the head tracking mode of the 724 * {@code Spatializer}. 725 * @param listener the listener to unregister 726 */ 727 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 728 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) removeOnHeadTrackingModeChangedListener( @onNull OnHeadTrackingModeChangedListener listener)729 public void removeOnHeadTrackingModeChangedListener( 730 @NonNull OnHeadTrackingModeChangedListener listener) { 731 Objects.requireNonNull(listener); 732 synchronized (mHeadTrackingListenerLock) { 733 if (!removeListener(listener, mHeadTrackingListeners)) { 734 throw new IllegalArgumentException( 735 "Called removeOnHeadTrackingModeChangedListener() " 736 + "on an unregistered listener"); 737 } 738 if (mHeadTrackingListeners.size() == 0) { 739 // unregister binder for callbacks 740 try { 741 mAm.getService().unregisterSpatializerHeadTrackingCallback( 742 mHeadTrackingDispatcherStub); 743 } catch (RemoteException e) { 744 throw e.rethrowFromSystemServer(); 745 } finally { 746 mHeadTrackingDispatcherStub = null; 747 mHeadTrackingListeners = null; 748 } 749 } 750 } 751 } 752 753 /** 754 * @hide 755 * Set the listener to receive head to soundstage pose updates. 756 * @param executor the {@code Executor} handling the callbacks 757 * @param listener the listener to register 758 * @see #clearOnHeadToSoundstagePoseUpdatedListener() 759 */ 760 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 761 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) setOnHeadToSoundstagePoseUpdatedListener( @onNull @allbackExecutor Executor executor, @NonNull OnHeadToSoundstagePoseUpdatedListener listener)762 public void setOnHeadToSoundstagePoseUpdatedListener( 763 @NonNull @CallbackExecutor Executor executor, 764 @NonNull OnHeadToSoundstagePoseUpdatedListener listener) { 765 Objects.requireNonNull(executor); 766 Objects.requireNonNull(listener); 767 synchronized (mPoseListenerLock) { 768 if (mPoseListener != null) { 769 throw new IllegalStateException("Trying to overwrite existing listener"); 770 } 771 mPoseListener = 772 new ListenerInfo<OnHeadToSoundstagePoseUpdatedListener>(listener, executor); 773 mPoseDispatcher = new SpatializerPoseDispatcherStub(); 774 try { 775 mAm.getService().registerHeadToSoundstagePoseCallback(mPoseDispatcher); 776 } catch (RemoteException e) { 777 mPoseListener = null; 778 mPoseDispatcher = null; 779 } 780 } 781 } 782 783 /** 784 * @hide 785 * Clears the listener for head to soundstage pose updates 786 * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener) 787 */ 788 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 789 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) clearOnHeadToSoundstagePoseUpdatedListener()790 public void clearOnHeadToSoundstagePoseUpdatedListener() { 791 synchronized (mPoseListenerLock) { 792 if (mPoseDispatcher == null) { 793 throw (new IllegalStateException("No listener to clear")); 794 } 795 try { 796 mAm.getService().unregisterHeadToSoundstagePoseCallback(mPoseDispatcher); 797 } catch (RemoteException e) { } 798 mPoseListener = null; 799 mPoseDispatcher = null; 800 } 801 } 802 803 /** 804 * @hide 805 * Sets an additional transform over the soundstage. 806 * The transform represents the pose of the soundstage, relative 807 * to either the device (in {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} mode), the world (in 808 * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD}) or the listener’s head (in 809 * {@link #HEAD_TRACKING_MODE_DISABLED} mode). 810 * @param transform an array of 6 float values, the first 3 are the translation vector, the 811 * other 3 are the rotation vector. 812 */ 813 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 814 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) setGlobalTransform(@onNull float[] transform)815 public void setGlobalTransform(@NonNull float[] transform) { 816 if (Objects.requireNonNull(transform).length != 6) { 817 throw new IllegalArgumentException("transform array must be of size 6, was " 818 + transform.length); 819 } 820 try { 821 mAm.getService().setSpatializerGlobalTransform(transform); 822 } catch (RemoteException e) { 823 Log.e(TAG, "Error calling setGlobalTransform", e); 824 } 825 } 826 827 /** 828 * @hide 829 * Sets a parameter on the platform spatializer effect implementation. 830 * This is to be used for vendor-specific configurations of their effect, keys and values are 831 * not reuseable across implementations. 832 * @param key the parameter to change 833 * @param value an array for the value of the parameter to change 834 */ 835 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 836 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) setEffectParameter(int key, @NonNull byte[] value)837 public void setEffectParameter(int key, @NonNull byte[] value) { 838 Objects.requireNonNull(value); 839 try { 840 mAm.getService().setSpatializerParameter(key, value); 841 } catch (RemoteException e) { 842 Log.e(TAG, "Error calling setEffectParameter", e); 843 } 844 } 845 846 /** 847 * @hide 848 * Retrieves a parameter value from the platform spatializer effect implementation. 849 * This is to be used for vendor-specific configurations of their effect, keys and values are 850 * not reuseable across implementations. 851 * @param key the parameter for which the value is queried 852 * @param value a non-empty array to contain the return value. The caller is responsible for 853 * passing an array of size matching the parameter. 854 */ 855 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 856 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) getEffectParameter(int key, @NonNull byte[] value)857 public void getEffectParameter(int key, @NonNull byte[] value) { 858 Objects.requireNonNull(value); 859 try { 860 mAm.getService().getSpatializerParameter(key, value); 861 } catch (RemoteException e) { 862 Log.e(TAG, "Error calling getEffectParameter", e); 863 } 864 } 865 866 /** 867 * @hide 868 * Returns the id of the output stream used for the spatializer effect playback 869 * @return id of the output stream, or 0 if no spatializer playback is active 870 */ 871 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 872 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) getOutput()873 public @IntRange(from = 0) int getOutput() { 874 try { 875 return mAm.getService().getSpatializerOutput(); 876 } catch (RemoteException e) { 877 Log.e(TAG, "Error calling getSpatializerOutput", e); 878 return 0; 879 } 880 } 881 882 /** 883 * @hide 884 * Sets the listener to receive spatializer effect output updates 885 * @param executor the {@code Executor} handling the callbacks 886 * @param listener the listener to register 887 * @see #clearOnSpatializerOutputChangedListener() 888 * @see #getOutput() 889 */ 890 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 891 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) setOnSpatializerOutputChangedListener( @onNull @allbackExecutor Executor executor, @NonNull OnSpatializerOutputChangedListener listener)892 public void setOnSpatializerOutputChangedListener( 893 @NonNull @CallbackExecutor Executor executor, 894 @NonNull OnSpatializerOutputChangedListener listener) { 895 Objects.requireNonNull(executor); 896 Objects.requireNonNull(listener); 897 synchronized (mOutputListenerLock) { 898 if (mOutputListener != null) { 899 throw new IllegalStateException("Trying to overwrite existing listener"); 900 } 901 mOutputListener = 902 new ListenerInfo<OnSpatializerOutputChangedListener>(listener, executor); 903 mOutputDispatcher = new SpatializerOutputDispatcherStub(); 904 try { 905 mAm.getService().registerSpatializerOutputCallback(mOutputDispatcher); 906 } catch (RemoteException e) { 907 mOutputListener = null; 908 mOutputDispatcher = null; 909 } 910 } 911 } 912 913 /** 914 * @hide 915 * Clears the listener for spatializer effect output updates 916 * @see #setOnSpatializerOutputChangedListener(Executor, OnSpatializerOutputChangedListener) 917 */ 918 @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) 919 @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) clearOnSpatializerOutputChangedListener()920 public void clearOnSpatializerOutputChangedListener() { 921 synchronized (mOutputListenerLock) { 922 if (mOutputDispatcher == null) { 923 throw (new IllegalStateException("No listener to clear")); 924 } 925 try { 926 mAm.getService().unregisterSpatializerOutputCallback(mOutputDispatcher); 927 } catch (RemoteException e) { } 928 mOutputListener = null; 929 mOutputDispatcher = null; 930 } 931 } 932 933 //----------------------------------------------------------------------------- 934 // callback helper definitions 935 936 private static class ListenerInfo<T> { 937 final @NonNull T mListener; 938 final @NonNull Executor mExecutor; 939 ListenerInfo(T listener, Executor exe)940 ListenerInfo(T listener, Executor exe) { 941 mListener = listener; 942 mExecutor = exe; 943 } 944 } 945 getListenerInfo( T listener, ArrayList<ListenerInfo<T>> listeners)946 private static <T> ListenerInfo<T> getListenerInfo( 947 T listener, ArrayList<ListenerInfo<T>> listeners) { 948 if (listeners == null) { 949 return null; 950 } 951 for (ListenerInfo<T> info : listeners) { 952 if (info.mListener == listener) { 953 return info; 954 } 955 } 956 return null; 957 } 958 hasListener(T listener, ArrayList<ListenerInfo<T>> listeners)959 private static <T> boolean hasListener(T listener, ArrayList<ListenerInfo<T>> listeners) { 960 return getListenerInfo(listener, listeners) != null; 961 } 962 removeListener(T listener, ArrayList<ListenerInfo<T>> listeners)963 private static <T> boolean removeListener(T listener, ArrayList<ListenerInfo<T>> listeners) { 964 final ListenerInfo<T> infoToRemove = getListenerInfo(listener, listeners); 965 if (infoToRemove != null) { 966 listeners.remove(infoToRemove); 967 return true; 968 } 969 return false; 970 } 971 972 //----------------------------------------------------------------------------- 973 // head tracking callback management and stub 974 975 private final Object mHeadTrackingListenerLock = new Object(); 976 /** 977 * List of listeners for head tracking mode listener and their associated Executor. 978 * List is lazy-initialized on first registration 979 */ 980 @GuardedBy("mHeadTrackingListenerLock") 981 private @Nullable ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> 982 mHeadTrackingListeners; 983 984 @GuardedBy("mHeadTrackingListenerLock") 985 private @Nullable SpatializerHeadTrackingDispatcherStub mHeadTrackingDispatcherStub; 986 987 private final class SpatializerHeadTrackingDispatcherStub 988 extends ISpatializerHeadTrackingModeCallback.Stub { 989 @Override dispatchSpatializerActualHeadTrackingModeChanged(int mode)990 public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) { 991 // make a shallow copy of listeners so callback is not executed under lock 992 final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners; 993 synchronized (mHeadTrackingListenerLock) { 994 if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { 995 return; 996 } 997 headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>) 998 mHeadTrackingListeners.clone(); 999 } 1000 try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { 1001 for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) { 1002 info.mExecutor.execute(() -> info.mListener 1003 .onHeadTrackingModeChanged(Spatializer.this, mode)); 1004 } 1005 } 1006 } 1007 1008 @Override dispatchSpatializerDesiredHeadTrackingModeChanged(int mode)1009 public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) { 1010 // make a shallow copy of listeners so callback is not executed under lock 1011 final ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>> headTrackingListeners; 1012 synchronized (mHeadTrackingListenerLock) { 1013 if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { 1014 return; 1015 } 1016 headTrackingListeners = (ArrayList<ListenerInfo<OnHeadTrackingModeChangedListener>>) 1017 mHeadTrackingListeners.clone(); 1018 } 1019 try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { 1020 for (ListenerInfo<OnHeadTrackingModeChangedListener> info : headTrackingListeners) { 1021 info.mExecutor.execute(() -> info.mListener 1022 .onDesiredHeadTrackingModeChanged(Spatializer.this, mode)); 1023 } 1024 } 1025 } 1026 } 1027 1028 //----------------------------------------------------------------------------- 1029 // head pose callback management and stub 1030 private final Object mPoseListenerLock = new Object(); 1031 /** 1032 * Listener for head to soundstage updates 1033 */ 1034 @GuardedBy("mPoseListenerLock") 1035 private @Nullable ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> mPoseListener; 1036 @GuardedBy("mPoseListenerLock") 1037 private @Nullable SpatializerPoseDispatcherStub mPoseDispatcher; 1038 1039 private final class SpatializerPoseDispatcherStub 1040 extends ISpatializerHeadToSoundStagePoseCallback.Stub { 1041 1042 @Override dispatchPoseChanged(float[] pose)1043 public void dispatchPoseChanged(float[] pose) { 1044 // make a copy of ref to listener so callback is not executed under lock 1045 final ListenerInfo<OnHeadToSoundstagePoseUpdatedListener> listener; 1046 synchronized (mPoseListenerLock) { 1047 listener = mPoseListener; 1048 } 1049 if (listener == null) { 1050 return; 1051 } 1052 try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { 1053 listener.mExecutor.execute(() -> listener.mListener 1054 .onHeadToSoundstagePoseUpdated(Spatializer.this, pose)); 1055 } 1056 } 1057 } 1058 1059 //----------------------------------------------------------------------------- 1060 // output callback management and stub 1061 private final Object mOutputListenerLock = new Object(); 1062 /** 1063 * Listener for output updates 1064 */ 1065 @GuardedBy("mOutputListenerLock") 1066 private @Nullable ListenerInfo<OnSpatializerOutputChangedListener> mOutputListener; 1067 @GuardedBy("mOutputListenerLock") 1068 private @Nullable SpatializerOutputDispatcherStub mOutputDispatcher; 1069 1070 private final class SpatializerOutputDispatcherStub 1071 extends ISpatializerOutputCallback.Stub { 1072 1073 @Override dispatchSpatializerOutputChanged(int output)1074 public void dispatchSpatializerOutputChanged(int output) { 1075 // make a copy of ref to listener so callback is not executed under lock 1076 final ListenerInfo<OnSpatializerOutputChangedListener> listener; 1077 synchronized (mOutputListenerLock) { 1078 listener = mOutputListener; 1079 } 1080 if (listener == null) { 1081 return; 1082 } 1083 try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { 1084 listener.mExecutor.execute(() -> listener.mListener 1085 .onSpatializerOutputChanged(Spatializer.this, output)); 1086 } 1087 } 1088 } 1089 } 1090