/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.media; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.media.CallbackUtil.ListenerInfo; import android.media.permission.ClearCallingIdentityContext; import android.media.permission.SafeCloseable; import android.os.RemoteException; import android.util.Log; import com.android.internal.annotations.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; /** * Spatializer provides access to querying capabilities and behavior of sound spatialization * on the device. * Sound spatialization simulates sounds originating around the listener as if they were coming * from virtual speakers placed around the listener.
* Support for spatialization is optional, use {@link AudioManager#getSpatializer()} to obtain an * instance of this class if the feature is supported. * */ public class Spatializer { private final @NonNull AudioManager mAm; private static final String TAG = "Spatializer"; /** * @hide * Constructor with AudioManager acting as proxy to AudioService * @param am a non-null AudioManager */ protected Spatializer(@NonNull AudioManager am) { mAm = Objects.requireNonNull(am); } /** * Returns whether spatialization is enabled or not. * A false value can originate for instance from the user electing to * disable the feature, or when the feature is not supported on the device (indicated * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}). *
* Note that this state reflects a platform-wide state of the "desire" to use spatialization, * but availability of the audio processing is still dictated by the compatibility between * the effect and the hardware configuration, as indicated by {@link #isAvailable()}. * @return {@code true} if spatialization is enabled * @see #isAvailable() */ public boolean isEnabled() { try { return mAm.getService().isSpatializerEnabled(); } catch (RemoteException e) { Log.e(TAG, "Error querying isSpatializerEnabled, returning false", e); return false; } } /** * Returns whether spatialization is available. * Reasons for spatialization being unavailable include situations where audio output is * incompatible with sound spatialization, such as playback on a monophonic speaker.
* Note that spatialization can be available, but disabled by the user, in which case this * method would still return {@code true}, whereas {@link #isEnabled()} * would return {@code false}.
* Also when the feature is not supported on the device (indicated * by {@link #getImmersiveAudioLevel()} returning {@link #SPATIALIZER_IMMERSIVE_LEVEL_NONE}), * the return value will be false. * @return {@code true} if the spatializer effect is available and capable * of processing the audio for the current configuration of the device, * {@code false} otherwise. * @see #isEnabled() */ public boolean isAvailable() { try { return mAm.getService().isSpatializerAvailable(); } catch (RemoteException e) { Log.e(TAG, "Error querying isSpatializerAvailable, returning false", e); return false; } } /** * @hide * Returns whether spatialization is available for a given audio device * Reasons for spatialization being unavailable include situations where audio output is * incompatible with sound spatialization, such as the device being a monophonic speaker, or * the spatializer effect not supporting transaural processing when querying for speaker. * @param device the audio device for which spatializer availability is queried * @return {@code true} if the spatializer effect is available and capable * of processing the audio over the given audio device, * {@code false} otherwise. * @see #isEnabled() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public boolean isAvailableForDevice(@NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device); try { return mAm.getService().isSpatializerAvailableForDevice(device); } catch (RemoteException e) { e.rethrowFromSystemServer(); } return false; } /** * @hide * Returns whether the given device has an associated headtracker * @param device the audio device to query * @return true if the device has a head tracker, false otherwise */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public boolean hasHeadTracker(@NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device); try { return mAm.getService().hasHeadTracker(device); } catch (RemoteException e) { e.rethrowFromSystemServer(); } return false; } /** * @hide * Enables or disables the head tracker of the given device * @param enabled true to enable, false to disable * @param device the device whose head tracker state is changed */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void setHeadTrackerEnabled(boolean enabled, @NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device); try { mAm.getService().setHeadTrackerEnabled(enabled, device); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * @hide * Returns whether the head tracker of the device is enabled * @param device the device to query * @return true if the head tracker is enabled, false if disabled or if there isn't one */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public boolean isHeadTrackerEnabled(@NonNull AudioDeviceAttributes device) { Objects.requireNonNull(device); try { return mAm.getService().isHeadTrackerEnabled(device); } catch (RemoteException e) { e.rethrowFromSystemServer(); } return false; } /** * Returns whether a head tracker is currently available for the audio device used by the * spatializer effect. * @return true if a head tracker is available and the effect is enabled, false otherwise. * @see OnHeadTrackerAvailableListener * @see #addOnHeadTrackerAvailableListener(Executor, OnHeadTrackerAvailableListener) */ public boolean isHeadTrackerAvailable() { try { return mAm.getService().isHeadTrackerAvailable(); } catch (RemoteException e) { e.rethrowFromSystemServer(); } return false; } /** * Adds a listener to be notified of changes to the availability of a head tracker. * @param executor the {@code Executor} handling the callback * @param listener the listener to receive availability updates * @see #removeOnHeadTrackerAvailableListener(OnHeadTrackerAvailableListener) */ public void addOnHeadTrackerAvailableListener(@NonNull @CallbackExecutor Executor executor, @NonNull OnHeadTrackerAvailableListener listener) { mHeadTrackerListenerMgr.addListener(executor, listener, "addOnHeadTrackerAvailableListener", () -> new SpatializerHeadTrackerAvailableDispatcherStub()); } /** * Removes a previously registered listener for the availability of a head tracker. * @param listener the listener previously registered with * {@link #addOnHeadTrackerAvailableListener(Executor, OnHeadTrackerAvailableListener)} */ public void removeOnHeadTrackerAvailableListener( @NonNull OnHeadTrackerAvailableListener listener) { mHeadTrackerListenerMgr.removeListener(listener, "removeOnHeadTrackerAvailableListener"); } /** @hide */ @IntDef(flag = false, value = { SPATIALIZER_IMMERSIVE_LEVEL_OTHER, SPATIALIZER_IMMERSIVE_LEVEL_NONE, SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL, }) @Retention(RetentionPolicy.SOURCE) public @interface ImmersiveAudioLevel {}; /** * Constant indicating the {@code Spatializer} on this device supports a spatialization * mode that differs from the ones available at this SDK level. * @see #getImmersiveAudioLevel() */ public static final int SPATIALIZER_IMMERSIVE_LEVEL_OTHER = -1; /** * Constant indicating there are no spatialization capabilities supported on this device. * @see #getImmersiveAudioLevel() */ public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0; /** * Constant indicating the {@code Spatializer} on this device supports multichannel * spatialization. * @see #getImmersiveAudioLevel() */ public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; /** * @hide * Constant indicating the {@code Spatializer} on this device supports the spatialization of * multichannel bed plus objects. * @see #getImmersiveAudioLevel() */ public static final int SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS = 2; /** @hide */ @IntDef(flag = false, value = { HEAD_TRACKING_MODE_UNSUPPORTED, HEAD_TRACKING_MODE_DISABLED, HEAD_TRACKING_MODE_RELATIVE_WORLD, HEAD_TRACKING_MODE_RELATIVE_DEVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface HeadTrackingMode {}; /** @hide */ @IntDef(flag = false, value = { HEAD_TRACKING_MODE_DISABLED, HEAD_TRACKING_MODE_RELATIVE_WORLD, HEAD_TRACKING_MODE_RELATIVE_DEVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface HeadTrackingModeSet {}; /** @hide */ @IntDef(flag = false, value = { HEAD_TRACKING_MODE_RELATIVE_WORLD, HEAD_TRACKING_MODE_RELATIVE_DEVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface HeadTrackingModeSupported {}; /** * @hide * Constant indicating head tracking is not supported by this {@code Spatializer} * @see #getHeadTrackingMode() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2; /** * @hide * Constant indicating head tracking is disabled on this {@code Spatializer} * @see #getHeadTrackingMode() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public static final int HEAD_TRACKING_MODE_DISABLED = -1; /** * @hide * Constant indicating head tracking is in a mode whose behavior is unknown. This is not an * error state but represents a customized behavior not defined by this API. * @see #getHeadTrackingMode() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public static final int HEAD_TRACKING_MODE_OTHER = 0; /** * @hide * Constant indicating head tracking is tracking the user's position / orientation relative to * the world around them * @see #getHeadTrackingMode() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1; /** * @hide * Constant indicating head tracking is tracking the user's position / orientation relative to * the device * @see #getHeadTrackingMode() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2; /** * @hide * Head tracking mode to string conversion * @param mode a valid head tracking mode * @return a string containing the matching constant name */ public static final String headtrackingModeToString(int mode) { switch(mode) { case HEAD_TRACKING_MODE_UNSUPPORTED: return "HEAD_TRACKING_MODE_UNSUPPORTED"; case HEAD_TRACKING_MODE_DISABLED: return "HEAD_TRACKING_MODE_DISABLED"; case HEAD_TRACKING_MODE_OTHER: return "HEAD_TRACKING_MODE_OTHER"; case HEAD_TRACKING_MODE_RELATIVE_WORLD: return "HEAD_TRACKING_MODE_RELATIVE_WORLD"; case HEAD_TRACKING_MODE_RELATIVE_DEVICE: return "HEAD_TRACKING_MODE_RELATIVE_DEVICE"; default: return "head tracking mode unknown " + mode; } } /** * Return the level of support for the spatialization feature on this device. * This level of support is independent of whether the {@code Spatializer} is currently * enabled or available and will not change over time. * @return the level of spatialization support * @see #isEnabled() * @see #isAvailable() */ public @ImmersiveAudioLevel int getImmersiveAudioLevel() { int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; try { level = mAm.getService().getSpatializerImmersiveAudioLevel(); } catch (Exception e) { /* using NONE */ } return level; } /** * @hide * Enables / disables the spatializer effect. * Changing the enabled state will trigger the public * {@link OnSpatializerStateChangedListener#onSpatializerEnabledChanged(Spatializer, boolean)} * registered listeners. * @param enabled {@code true} for enabling the effect */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void setEnabled(boolean enabled) { try { mAm.getService().setSpatializerEnabled(enabled); } catch (RemoteException e) { Log.e(TAG, "Error calling setSpatializerEnabled", e); } } /** * An interface to be notified of changes to the state of the spatializer effect. */ public interface OnSpatializerStateChangedListener { /** * Called when the enabled state of the spatializer effect changes * @param spat the {@code Spatializer} instance whose state changed * @param enabled {@code true} if the spatializer effect is enabled on the device, * {@code false} otherwise * @see #isEnabled() */ void onSpatializerEnabledChanged(@NonNull Spatializer spat, boolean enabled); /** * Called when the availability of the spatializer effect changes * @param spat the {@code Spatializer} instance whose state changed * @param available {@code true} if the spatializer effect is available and capable * of processing the audio for the current configuration of the device, * {@code false} otherwise. * @see #isAvailable() */ void onSpatializerAvailableChanged(@NonNull Spatializer spat, boolean available); } /** * @hide * An interface to be notified of changes to the head tracking mode, used by the spatializer * effect. * Changes to the mode may come from explicitly setting a different mode * (see {@link #setDesiredHeadTrackingMode(int)}) or a change in system conditions (see * {@link #getHeadTrackingMode()} * @see #addOnHeadTrackingModeChangedListener(Executor, OnHeadTrackingModeChangedListener) * @see #removeOnHeadTrackingModeChangedListener(OnHeadTrackingModeChangedListener) */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) public interface OnHeadTrackingModeChangedListener { /** * Called when the actual head tracking mode of the spatializer changed. * @param spatializer the {@code Spatializer} instance whose head tracking mode is changing * @param mode the new head tracking mode */ void onHeadTrackingModeChanged(@NonNull Spatializer spatializer, @HeadTrackingMode int mode); /** * Called when the desired head tracking mode of the spatializer changed * @param spatializer the {@code Spatializer} instance whose head tracking mode was set * @param mode the newly set head tracking mode */ void onDesiredHeadTrackingModeChanged(@NonNull Spatializer spatializer, @HeadTrackingModeSet int mode); } /** * Interface to be notified of changes to the availability of a head tracker on the audio * device to be used by the spatializer effect. */ public interface OnHeadTrackerAvailableListener { /** * Called when the availability of the head tracker changed. * @param spatializer the {@code Spatializer} instance for which the head tracker * availability was updated * @param available true if the audio device that would output audio processed by * the {@code Spatializer} has a head tracker associated with it, false * otherwise. */ void onHeadTrackerAvailableChanged(@NonNull Spatializer spatializer, boolean available); } /** * @hide * An interface to be notified of changes to the output stream used by the spatializer * effect. * @see #getOutput() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) public interface OnSpatializerOutputChangedListener { /** * Called when the id of the output stream of the spatializer effect changed. * @param spatializer the {@code Spatializer} instance whose output is updated * @param output the id of the output stream, or 0 when there is no spatializer output */ void onSpatializerOutputChanged(@NonNull Spatializer spatializer, @IntRange(from = 0) int output); } /** * @hide * An interface to be notified of updates to the head to soundstage pose, as represented by the * current head tracking mode. * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener) */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) public interface OnHeadToSoundstagePoseUpdatedListener { /** * Called when the head to soundstage transform is updated * @param spatializer the {@code Spatializer} instance affected by the pose update * @param pose the new pose data representing the transform between the frame * of reference for the current head tracking mode (see * {@link #getHeadTrackingMode()}) and the device being tracked (for * instance a pair of headphones with a head tracker).
* The head pose data is represented as an array of six float values, where * the first three values are the translation vector, and the next three * are the rotation vector. */ void onHeadToSoundstagePoseUpdated(@NonNull Spatializer spatializer, @NonNull float[] pose); } /** * Returns whether audio of the given {@link AudioFormat}, played with the given * {@link AudioAttributes} can be spatialized. * Note that the result reflects the capabilities of the device and may change when * audio accessories are connected/disconnected (e.g. wired headphones plugged in or not). * The result is independent from whether spatialization processing is enabled or not. * @param attributes the {@code AudioAttributes} of the content as used for playback * @param format the {@code AudioFormat} of the content as used for playback * @return {@code true} if the device is capable of spatializing the combination of audio format * and attributes, {@code false} otherwise. */ public boolean canBeSpatialized( @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { try { return mAm.getService().canBeSpatialized( Objects.requireNonNull(attributes), Objects.requireNonNull(format)); } catch (RemoteException e) { Log.e(TAG, "Error querying canBeSpatialized for attr:" + attributes + " format:" + format + " returning false", e); return false; } } /** * Adds a listener to be notified of changes to the enabled state of the * {@code Spatializer}. * @param executor the {@code Executor} handling the callback * @param listener the listener to receive enabled state updates * @see #isEnabled() */ public void addOnSpatializerStateChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnSpatializerStateChangedListener listener) { mStateListenerMgr.addListener(executor, listener, "addOnSpatializerStateChangedListener", () -> new SpatializerInfoDispatcherStub()); } /** * Removes a previously added listener for changes to the enabled state of the * {@code Spatializer}. * @param listener the listener to receive enabled state updates * @see #isEnabled() */ public void removeOnSpatializerStateChangedListener( @NonNull OnSpatializerStateChangedListener listener) { mStateListenerMgr.removeListener(listener, "removeOnSpatializerStateChangedListener"); } /** * @hide * Returns the list of playback devices that are compatible with the playback of multichannel * audio through virtualization * @return a list of devices. An empty list indicates virtualization is not supported. */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public @NonNull List getCompatibleAudioDevices() { try { return mAm.getService().getSpatializerCompatibleAudioDevices(); } catch (RemoteException e) { Log.e(TAG, "Error querying getSpatializerCompatibleAudioDevices(), " + " returning empty list", e); return new ArrayList(0); } } /** * @hide * Adds a playback device to the list of devices compatible with the playback of multichannel * audio through spatialization. * @see #getCompatibleAudioDevices() * @param ada the audio device compatible with spatialization */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { try { mAm.getService().addSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); } catch (RemoteException e) { Log.e(TAG, "Error calling addSpatializerCompatibleAudioDevice(), ", e); } } /** * @hide * Remove a playback device from the list of devices compatible with the playback of * multichannel audio through spatialization. * @see #getCompatibleAudioDevices() * @param ada the audio device incompatible with spatialization */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { try { mAm.getService().removeSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); } catch (RemoteException e) { Log.e(TAG, "Error calling removeSpatializerCompatibleAudioDevice(), ", e); } } /** * manages the OnSpatializerStateChangedListener listeners and the * SpatializerInfoDispatcherStub */ private final CallbackUtil.LazyListenerManager mStateListenerMgr = new CallbackUtil.LazyListenerManager(); private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub implements CallbackUtil.DispatcherStub { @Override public void register(boolean register) { try { if (register) { mAm.getService().registerSpatializerCallback(this); } else { mAm.getService().unregisterSpatializerCallback(this); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } @Override @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerEnabledChanged(boolean enabled) { mStateListenerMgr.callListeners( (listener) -> listener.onSpatializerEnabledChanged( Spatializer.this, enabled)); } @Override @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerAvailableChanged(boolean available) { mStateListenerMgr.callListeners( (listener) -> listener.onSpatializerAvailableChanged( Spatializer.this, available)); } } /** * @hide * Return the current head tracking mode as used by the system. * Note this may differ from the desired head tracking mode. Reasons for the two to differ * include: a head tracking device is not available for the current audio output device, * the transmission conditions between the tracker and device have deteriorated and tracking * has been disabled. * @see #getDesiredHeadTrackingMode() * @return the current head tracking mode */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public @HeadTrackingMode int getHeadTrackingMode() { try { return mAm.getService().getActualHeadTrackingMode(); } catch (RemoteException e) { Log.e(TAG, "Error calling getActualHeadTrackingMode", e); return HEAD_TRACKING_MODE_UNSUPPORTED; } } /** * @hide * Return the desired head tracking mode. * Note this may differ from the actual head tracking mode, reflected by * {@link #getHeadTrackingMode()}. * @return the desired head tring mode */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public @HeadTrackingMode int getDesiredHeadTrackingMode() { try { return mAm.getService().getDesiredHeadTrackingMode(); } catch (RemoteException e) { Log.e(TAG, "Error calling getDesiredHeadTrackingMode", e); return HEAD_TRACKING_MODE_UNSUPPORTED; } } /** * @hide * Returns the list of supported head tracking modes. * @return the list of modes that can be used in {@link #setDesiredHeadTrackingMode(int)} to * enable head tracking. The list will be empty if {@link #getHeadTrackingMode()} * is {@link #HEAD_TRACKING_MODE_UNSUPPORTED}. Values can be * {@link #HEAD_TRACKING_MODE_OTHER}, * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD} or * {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public @NonNull List getSupportedHeadTrackingModes() { try { final int[] modes = mAm.getService().getSupportedHeadTrackingModes(); final ArrayList list = new ArrayList<>(0); for (int mode : modes) { list.add(mode); } return list; } catch (RemoteException e) { Log.e(TAG, "Error calling getSupportedHeadTrackModes", e); return new ArrayList(0); } } /** * @hide * Sets the desired head tracking mode. * Note a set desired mode may differ from the actual head tracking mode. * @see #getHeadTrackingMode() * @param mode the desired head tracking mode, one of the values returned by * {@link #getSupportedHeadTrackModes()}, or {@link #HEAD_TRACKING_MODE_DISABLED} to * disable head tracking. */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void setDesiredHeadTrackingMode(@HeadTrackingModeSet int mode) { try { mAm.getService().setDesiredHeadTrackingMode(mode); } catch (RemoteException e) { Log.e(TAG, "Error calling setDesiredHeadTrackingMode to " + mode, e); } } /** * @hide * Recenters the head tracking at the current position / orientation. */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void recenterHeadTracker() { try { mAm.getService().recenterHeadTracker(); } catch (RemoteException e) { Log.e(TAG, "Error calling recenterHeadTracker", e); } } /** * @hide * Adds a listener to be notified of changes to the head tracking mode of the * {@code Spatializer} * @param executor the {@code Executor} handling the callbacks * @param listener the listener to register */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void addOnHeadTrackingModeChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnHeadTrackingModeChangedListener listener) { mHeadTrackingListenerMgr.addListener(executor, listener, "addOnHeadTrackingModeChangedListener", () -> new SpatializerHeadTrackingDispatcherStub()); } /** * @hide * Removes a previously added listener for changes to the head tracking mode of the * {@code Spatializer}. * @param listener the listener to unregister */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void removeOnHeadTrackingModeChangedListener( @NonNull OnHeadTrackingModeChangedListener listener) { mHeadTrackingListenerMgr.removeListener(listener, "removeOnHeadTrackingModeChangedListener"); } /** * @hide * Set the listener to receive head to soundstage pose updates. * @param executor the {@code Executor} handling the callbacks * @param listener the listener to register * @see #clearOnHeadToSoundstagePoseUpdatedListener() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void setOnHeadToSoundstagePoseUpdatedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnHeadToSoundstagePoseUpdatedListener listener) { Objects.requireNonNull(executor); Objects.requireNonNull(listener); synchronized (mPoseListenerLock) { if (mPoseListener != null) { throw new IllegalStateException("Trying to overwrite existing listener"); } mPoseListener = new ListenerInfo(listener, executor); mPoseDispatcher = new SpatializerPoseDispatcherStub(); try { mAm.getService().registerHeadToSoundstagePoseCallback(mPoseDispatcher); } catch (RemoteException e) { mPoseListener = null; mPoseDispatcher = null; } } } /** * @hide * Clears the listener for head to soundstage pose updates * @see #setOnHeadToSoundstagePoseUpdatedListener(Executor, OnHeadToSoundstagePoseUpdatedListener) */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void clearOnHeadToSoundstagePoseUpdatedListener() { synchronized (mPoseListenerLock) { if (mPoseDispatcher == null) { throw (new IllegalStateException("No listener to clear")); } try { mAm.getService().unregisterHeadToSoundstagePoseCallback(mPoseDispatcher); } catch (RemoteException e) { } mPoseListener = null; mPoseDispatcher = null; } } /** * @hide * Sets an additional transform over the soundstage. * The transform represents the pose of the soundstage, relative * to either the device (in {@link #HEAD_TRACKING_MODE_RELATIVE_DEVICE} mode), the world (in * {@link #HEAD_TRACKING_MODE_RELATIVE_WORLD}) or the listener’s head (in * {@link #HEAD_TRACKING_MODE_DISABLED} mode). * @param transform an array of 6 float values, the first 3 are the translation vector, the * other 3 are the rotation vector. */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void setGlobalTransform(@NonNull float[] transform) { if (Objects.requireNonNull(transform).length != 6) { throw new IllegalArgumentException("transform array must be of size 6, was " + transform.length); } try { mAm.getService().setSpatializerGlobalTransform(transform); } catch (RemoteException e) { Log.e(TAG, "Error calling setGlobalTransform", e); } } /** * @hide * Sets a parameter on the platform spatializer effect implementation. * This is to be used for vendor-specific configurations of their effect, keys and values are * not reuseable across implementations. * @param key the parameter to change * @param value an array for the value of the parameter to change */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void setEffectParameter(int key, @NonNull byte[] value) { Objects.requireNonNull(value); try { mAm.getService().setSpatializerParameter(key, value); } catch (RemoteException e) { Log.e(TAG, "Error calling setEffectParameter", e); } } /** * @hide * Retrieves a parameter value from the platform spatializer effect implementation. * This is to be used for vendor-specific configurations of their effect, keys and values are * not reuseable across implementations. * @param key the parameter for which the value is queried * @param value a non-empty array to contain the return value. The caller is responsible for * passing an array of size matching the parameter. */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void getEffectParameter(int key, @NonNull byte[] value) { Objects.requireNonNull(value); try { mAm.getService().getSpatializerParameter(key, value); } catch (RemoteException e) { Log.e(TAG, "Error calling getEffectParameter", e); } } /** * @hide * Returns the id of the output stream used for the spatializer effect playback. * This getter or associated listener {@link OnSpatializerOutputChangedListener} can be used for * handling spatializer output-specific configurations (e.g. disabling speaker post-processing * to avoid double-processing of the spatialized path). * @return id of the output stream, or 0 if no spatializer playback is active * @see #setOnSpatializerOutputChangedListener(Executor, OnSpatializerOutputChangedListener) */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public @IntRange(from = 0) int getOutput() { try { return mAm.getService().getSpatializerOutput(); } catch (RemoteException e) { Log.e(TAG, "Error calling getSpatializerOutput", e); return 0; } } /** * @hide * Sets the listener to receive spatializer effect output updates * @param executor the {@code Executor} handling the callbacks * @param listener the listener to register * @see #clearOnSpatializerOutputChangedListener() * @see #getOutput() */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void setOnSpatializerOutputChangedListener( @NonNull @CallbackExecutor Executor executor, @NonNull OnSpatializerOutputChangedListener listener) { Objects.requireNonNull(executor); Objects.requireNonNull(listener); synchronized (mOutputListenerLock) { if (mOutputListener != null) { throw new IllegalStateException("Trying to overwrite existing listener"); } mOutputListener = new ListenerInfo(listener, executor); mOutputDispatcher = new SpatializerOutputDispatcherStub(); try { mAm.getService().registerSpatializerOutputCallback(mOutputDispatcher); // immediately report the current output mOutputDispatcher.dispatchSpatializerOutputChanged(getOutput()); } catch (RemoteException e) { mOutputListener = null; mOutputDispatcher = null; } } } /** * @hide * Clears the listener for spatializer effect output updates * @see #setOnSpatializerOutputChangedListener(Executor, OnSpatializerOutputChangedListener) */ @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) public void clearOnSpatializerOutputChangedListener() { synchronized (mOutputListenerLock) { if (mOutputDispatcher == null) { throw (new IllegalStateException("No listener to clear")); } try { mAm.getService().unregisterSpatializerOutputCallback(mOutputDispatcher); } catch (RemoteException e) { } mOutputListener = null; mOutputDispatcher = null; } } //----------------------------------------------------------------------------- // head tracking callback management and stub /** * manages the OnHeadTrackingModeChangedListener listeners and the * SpatializerHeadTrackingDispatcherStub */ private final CallbackUtil.LazyListenerManager mHeadTrackingListenerMgr = new CallbackUtil.LazyListenerManager(); private final class SpatializerHeadTrackingDispatcherStub extends ISpatializerHeadTrackingModeCallback.Stub implements CallbackUtil.DispatcherStub { @Override public void register(boolean register) { try { if (register) { mAm.getService().registerSpatializerHeadTrackingCallback(this); } else { mAm.getService().unregisterSpatializerHeadTrackingCallback(this); } } catch (RemoteException e) { e.rethrowFromSystemServer(); } } @Override @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) { mHeadTrackingListenerMgr.callListeners( (listener) -> listener.onHeadTrackingModeChanged(Spatializer.this, mode)); } @Override @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) { mHeadTrackingListenerMgr.callListeners( (listener) -> listener.onDesiredHeadTrackingModeChanged( Spatializer.this, mode)); } } //----------------------------------------------------------------------------- // head tracker availability callback management and stub /** * manages the OnHeadTrackerAvailableListener listeners and the * SpatializerHeadTrackerAvailableDispatcherStub */ private final CallbackUtil.LazyListenerManager mHeadTrackerListenerMgr = new CallbackUtil.LazyListenerManager(); private final class SpatializerHeadTrackerAvailableDispatcherStub extends ISpatializerHeadTrackerAvailableCallback.Stub implements CallbackUtil.DispatcherStub { @Override public void register(boolean register) { try { mAm.getService().registerSpatializerHeadTrackerAvailableCallback(this, register); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } @Override @SuppressLint("GuardedBy") // lock applied inside callListeners method public void dispatchSpatializerHeadTrackerAvailable(boolean available) { mHeadTrackerListenerMgr.callListeners( (listener) -> listener.onHeadTrackerAvailableChanged( Spatializer.this, available)); } } //----------------------------------------------------------------------------- // head pose callback management and stub private final Object mPoseListenerLock = new Object(); /** * Listener for head to soundstage updates */ @GuardedBy("mPoseListenerLock") private @Nullable ListenerInfo mPoseListener; @GuardedBy("mPoseListenerLock") private @Nullable SpatializerPoseDispatcherStub mPoseDispatcher; private final class SpatializerPoseDispatcherStub extends ISpatializerHeadToSoundStagePoseCallback.Stub { @Override public void dispatchPoseChanged(float[] pose) { // make a copy of ref to listener so callback is not executed under lock final ListenerInfo listener; synchronized (mPoseListenerLock) { listener = mPoseListener; } if (listener == null) { return; } try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { listener.mExecutor.execute(() -> listener.mListener .onHeadToSoundstagePoseUpdated(Spatializer.this, pose)); } } } //----------------------------------------------------------------------------- // output callback management and stub private final Object mOutputListenerLock = new Object(); /** * Listener for output updates */ @GuardedBy("mOutputListenerLock") private @Nullable ListenerInfo mOutputListener; @GuardedBy("mOutputListenerLock") private @Nullable SpatializerOutputDispatcherStub mOutputDispatcher; private final class SpatializerOutputDispatcherStub extends ISpatializerOutputCallback.Stub { @Override public void dispatchSpatializerOutputChanged(int output) { // make a copy of ref to listener so callback is not executed under lock final ListenerInfo listener; synchronized (mOutputListenerLock) { listener = mOutputListener; } if (listener == null) { return; } try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { listener.mExecutor.execute(() -> listener.mListener .onSpatializerOutputChanged(Spatializer.this, output)); } } } }