diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 0d3dd7cb886e9..9c01bcada1ee7 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -5375,9 +5375,33 @@ package android.media { public class Spatializer { method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addOnHeadTrackingModeChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadTrackingModeChangedListener); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void clearOnHeadToSoundstagePoseUpdatedListener(); method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List getCompatibleAudioDevices(); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getDesiredHeadTrackingMode(); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getHeadTrackingMode(); + method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List getSupportedHeadTrackingModes(); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void recenterHeadTracker(); method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void removeCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void removeOnHeadTrackingModeChangedListener(@NonNull android.media.Spatializer.OnHeadTrackingModeChangedListener); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setDesiredHeadTrackingMode(int); method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setEnabled(boolean); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setGlobalTransform(@NonNull float[]); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnHeadToSoundstagePoseUpdatedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadToSoundstagePoseUpdatedListener); + field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_DISABLED = -1; // 0xffffffff + field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_OTHER = 0; // 0x0 + field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_RELATIVE_DEVICE = 2; // 0x2 + field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_RELATIVE_WORLD = 1; // 0x1 + field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_UNSUPPORTED = -2; // 0xfffffffe + } + + public static interface Spatializer.OnHeadToSoundstagePoseUpdatedListener { + method public void onHeadToSoundstagePoseUpdated(@NonNull android.media.Spatializer, @NonNull float[]); + } + + public static interface Spatializer.OnHeadTrackingModeChangedListener { + method public void onDesiredHeadTrackingModeChanged(@NonNull android.media.Spatializer, int); + method public void onHeadTrackingModeChanged(@NonNull android.media.Spatializer, int); } } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 8480c5289d151..bc1040115417a 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -36,6 +36,8 @@ import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.ISpatializerCallback; +import android.media.ISpatializerHeadTrackingModeCallback; +import android.media.ISpatializerHeadToSoundStagePoseCallback; import android.media.IVolumeController; import android.media.IVolumeController; import android.media.PlayerBase; @@ -405,13 +407,33 @@ interface IAudioService { boolean canBeSpatialized(in AudioAttributes aa, in AudioFormat af); - void registerSpatializerCallback(in ISpatializerCallback callback); + void registerSpatializerCallback(in ISpatializerCallback cb); - void unregisterSpatializerCallback(in ISpatializerCallback callback); + void unregisterSpatializerCallback(in ISpatializerCallback cb); + + void registerSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb); + + void unregisterSpatializerHeadTrackingCallback(in ISpatializerHeadTrackingModeCallback cb); + + void registerHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb); + + void unregisterHeadToSoundstagePoseCallback(in ISpatializerHeadToSoundStagePoseCallback cb); List getSpatializerCompatibleAudioDevices(); void addSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada); void removeSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada); + + void setDesiredHeadTrackingMode(int mode); + + int getDesiredHeadTrackingMode(); + + int[] getSupportedHeadTrackingModes(); + + int getActualHeadTrackingMode(); + + oneway void setSpatializerGlobalTransform(in float[] transform); + + oneway void recenterHeadTracker(); } diff --git a/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl new file mode 100644 index 0000000000000..01a146599394c --- /dev/null +++ b/media/java/android/media/ISpatializerHeadToSoundStagePoseCallback.aidl @@ -0,0 +1,31 @@ +/* + * 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; + +/** + * AIDL for the AudioService to signal Spatializer state changes. + * + * {@hide} + */ +oneway interface ISpatializerHeadToSoundStagePoseCallback { + + /** + * The pose is sent as an array of 6 float values, the first 3 are the translation vector, the + * other 3 are the rotation vector. + */ + void dispatchPoseChanged(in float[] pose); +} diff --git a/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl new file mode 100644 index 0000000000000..c61f86e4c60e0 --- /dev/null +++ b/media/java/android/media/ISpatializerHeadTrackingModeCallback.aidl @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * AIDL for the AudioService to signal Spatializer head tracking mode changes. + * + * {@hide} + */ +oneway interface ISpatializerHeadTrackingModeCallback { + + void dispatchSpatializerActualHeadTrackingModeChanged(int mode); + + void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode); +} diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java index d8519b689a562..b062eea5017c5 100644 --- a/media/java/android/media/Spatializer.java +++ b/media/java/android/media/Spatializer.java @@ -49,20 +49,8 @@ public class Spatializer { private final @NonNull AudioManager mAm; - private final Object mStateListenerLock = new Object(); - private static final String TAG = "Spatializer"; - /** - * List of listeners for state listener and their associated Executor. - * List is lazy-initialized on first registration - */ - @GuardedBy("mStateListenerLock") - private @Nullable ArrayList mStateListeners; - - @GuardedBy("mStateListenerLock") - private SpatializerInfoDispatcherStub mInfoDispatcherStub; - /** * @hide * Constructor with AudioManager acting as proxy to AudioService @@ -141,6 +129,75 @@ public class Spatializer { */ public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; + /** @hide */ + @IntDef(flag = false, value = { + HEAD_TRACKING_MODE_UNSUPPORTED, + HEAD_TRACKING_MODE_DISABLED, + HEAD_TRACKING_MODE_RELATIVE_WORLD, + HEAD_TRACKING_MODE_RELATIVE_DEVICE, + }) public @interface HeadTrackingMode {}; + + /** @hide */ + @IntDef(flag = false, value = { + HEAD_TRACKING_MODE_DISABLED, + HEAD_TRACKING_MODE_RELATIVE_WORLD, + HEAD_TRACKING_MODE_RELATIVE_DEVICE, + }) public @interface HeadTrackingModeSet {}; + + /** @hide */ + @IntDef(flag = false, value = { + HEAD_TRACKING_MODE_RELATIVE_WORLD, + HEAD_TRACKING_MODE_RELATIVE_DEVICE, + }) 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; + /** * 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 @@ -176,7 +233,7 @@ public class Spatializer { } /** - * An interface to be notified of changes to the state of the spatializer. + * An interface to be notified of changes to the state of the spatializer effect. */ public interface OnSpatializerStateChangedListener { /** @@ -199,6 +256,58 @@ public class Spatializer { 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); + } + + /** + * @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. @@ -342,6 +451,17 @@ public class Spatializer { } } + private final Object mStateListenerLock = new Object(); + /** + * List of listeners for state listener and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mStateListenerLock") + private @Nullable ArrayList mStateListeners; + + @GuardedBy("mStateListenerLock") + private @Nullable SpatializerInfoDispatcherStub mInfoDispatcherStub; + private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub { @Override public void dispatchSpatializerEnabledChanged(boolean enabled) { @@ -423,4 +543,378 @@ public class Spatializer { } return false; } + + + /** + * @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) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mHeadTrackingListenerLock) { + if (hasListener(listener, mHeadTrackingListeners)) { + throw new IllegalArgumentException( + "Called addOnHeadTrackingModeChangedListener() " + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (mHeadTrackingListeners == null) { + mHeadTrackingListeners = new ArrayList<>(); + } + mHeadTrackingListeners.add( + new ListenerInfo(listener, executor)); + if (mHeadTrackingListeners.size() == 1) { + // register binder for callbacks + if (mHeadTrackingDispatcherStub == null) { + mHeadTrackingDispatcherStub = + new SpatializerHeadTrackingDispatcherStub(); + } + try { + mAm.getService().registerSpatializerHeadTrackingCallback( + mHeadTrackingDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * @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) { + Objects.requireNonNull(listener); + synchronized (mHeadTrackingListenerLock) { + if (!removeListener(listener, mHeadTrackingListeners)) { + throw new IllegalArgumentException( + "Called removeOnHeadTrackingModeChangedListener() " + + "on an unregistered listener"); + } + if (mHeadTrackingListeners.size() == 0) { + // unregister binder for callbacks + try { + mAm.getService().unregisterSpatializerHeadTrackingCallback( + mHeadTrackingDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + mHeadTrackingDispatcherStub = null; + mHeadTrackingListeners = null; + } + } + } + } + + /** + * @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); + } + } + + //----------------------------------------------------------------------------- + // callback helper definitions + + private static class ListenerInfo { + final @NonNull T mListener; + final @NonNull Executor mExecutor; + + ListenerInfo(T listener, Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + private static ListenerInfo getListenerInfo( + T listener, ArrayList> listeners) { + if (listeners == null) { + return null; + } + for (ListenerInfo info : listeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + private static boolean hasListener(T listener, ArrayList> listeners) { + return getListenerInfo(listener, listeners) != null; + } + + private static boolean removeListener(T listener, ArrayList> listeners) { + final ListenerInfo infoToRemove = getListenerInfo(listener, listeners); + if (infoToRemove != null) { + listeners.remove(infoToRemove); + return true; + } + return false; + } + + //----------------------------------------------------------------------------- + // head tracking callback management and stub + + private final Object mHeadTrackingListenerLock = new Object(); + /** + * List of listeners for head tracking mode listener and their associated Executor. + * List is lazy-initialized on first registration + */ + @GuardedBy("mHeadTrackingListenerLock") + private @Nullable ArrayList> + mHeadTrackingListeners; + + @GuardedBy("mHeadTrackingListenerLock") + private @Nullable SpatializerHeadTrackingDispatcherStub mHeadTrackingDispatcherStub; + + private final class SpatializerHeadTrackingDispatcherStub + extends ISpatializerHeadTrackingModeCallback.Stub { + @Override + public void dispatchSpatializerActualHeadTrackingModeChanged(int mode) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList> headTrackingListeners; + synchronized (mHeadTrackingListenerLock) { + if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { + return; + } + headTrackingListeners = (ArrayList>) + mHeadTrackingListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (ListenerInfo info : headTrackingListeners) { + info.mExecutor.execute(() -> info.mListener + .onHeadTrackingModeChanged(Spatializer.this, mode)); + } + } + } + + @Override + public void dispatchSpatializerDesiredHeadTrackingModeChanged(int mode) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList> headTrackingListeners; + synchronized (mHeadTrackingListenerLock) { + if (mHeadTrackingListeners == null || mHeadTrackingListeners.size() == 0) { + return; + } + headTrackingListeners = (ArrayList>) + mHeadTrackingListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (ListenerInfo info : headTrackingListeners) { + info.mExecutor.execute(() -> info.mListener + .onDesiredHeadTrackingModeChanged(Spatializer.this, mode)); + } + } + } + } + + //----------------------------------------------------------------------------- + // 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)); + } + } + } } diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index b230200d0d7d6..5d0bad23f0243 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -94,6 +94,8 @@ import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.ISpatializerCallback; +import android.media.ISpatializerHeadToSoundStagePoseCallback; +import android.media.ISpatializerHeadTrackingModeCallback; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.IVolumeController; import android.media.MediaMetrics; @@ -8322,16 +8324,48 @@ public class AudioService extends IAudioService.Stub /** @see Spatializer.SpatializerInfoDispatcherStub */ public void registerSpatializerCallback( - @NonNull ISpatializerCallback dispatcher) { - Objects.requireNonNull(dispatcher); - mSpatializerHelper.registerStateCallback(dispatcher); + @NonNull ISpatializerCallback cb) { + Objects.requireNonNull(cb); + mSpatializerHelper.registerStateCallback(cb); } /** @see Spatializer.SpatializerInfoDispatcherStub */ public void unregisterSpatializerCallback( - @NonNull ISpatializerCallback dispatcher) { - Objects.requireNonNull(dispatcher); - mSpatializerHelper.unregisterStateCallback(dispatcher); + @NonNull ISpatializerCallback cb) { + Objects.requireNonNull(cb); + mSpatializerHelper.unregisterStateCallback(cb); + } + + /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */ + public void registerSpatializerHeadTrackingCallback( + @NonNull ISpatializerHeadTrackingModeCallback cb) { + enforceModifyDefaultAudioEffectsPermission(); + Objects.requireNonNull(cb); + mSpatializerHelper.registerHeadTrackingModeCallback(cb); + } + + /** @see Spatializer#SpatializerHeadTrackingDispatcherStub */ + public void unregisterSpatializerHeadTrackingCallback( + @NonNull ISpatializerHeadTrackingModeCallback cb) { + enforceModifyDefaultAudioEffectsPermission(); + Objects.requireNonNull(cb); + mSpatializerHelper.unregisterHeadTrackingModeCallback(cb); + } + + /** @see Spatializer#setOnHeadToSoundstagePoseUpdatedListener */ + public void registerHeadToSoundstagePoseCallback( + @NonNull ISpatializerHeadToSoundStagePoseCallback cb) { + enforceModifyDefaultAudioEffectsPermission(); + Objects.requireNonNull(cb); + mSpatializerHelper.registerHeadToSoundstagePoseCallback(cb); + } + + /** @see Spatializer#clearOnHeadToSoundstagePoseUpdatedListener */ + public void unregisterHeadToSoundstagePoseCallback( + @NonNull ISpatializerHeadToSoundStagePoseCallback cb) { + enforceModifyDefaultAudioEffectsPermission(); + Objects.requireNonNull(cb); + mSpatializerHelper.unregisterHeadToSoundstagePoseCallback(cb); } /** @see Spatializer#getSpatializerCompatibleAudioDevices() */ @@ -8354,6 +8388,51 @@ public class AudioService extends IAudioService.Stub mSpatializerHelper.removeCompatibleAudioDevice(ada); } + /** @see Spatializer#getSupportedHeadTrackingModes() */ + public int[] getSupportedHeadTrackingModes() { + enforceModifyDefaultAudioEffectsPermission(); + return mSpatializerHelper.getSupportedHeadTrackingModes(); + } + + /** @see Spatializer#getHeadTrackingMode() */ + public int getActualHeadTrackingMode() { + enforceModifyDefaultAudioEffectsPermission(); + return mSpatializerHelper.getActualHeadTrackingMode(); + } + + /** @see Spatializer#getDesiredHeadTrackingMode() */ + public int getDesiredHeadTrackingMode() { + enforceModifyDefaultAudioEffectsPermission(); + return mSpatializerHelper.getDesiredHeadTrackingMode(); + } + + /** @see Spatializer#setGlobalTransform */ + public void setSpatializerGlobalTransform(@NonNull float[] transform) { + enforceModifyDefaultAudioEffectsPermission(); + Objects.requireNonNull(transform); + mSpatializerHelper.setGlobalTransform(transform); + } + + /** @see Spatializer#recenterHeadTracker() */ + public void recenterHeadTracker() { + enforceModifyDefaultAudioEffectsPermission(); + mSpatializerHelper.recenterHeadTracker(); + } + + /** @see Spatializer#setDesiredHeadTrackingMode */ + public void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) { + enforceModifyDefaultAudioEffectsPermission(); + switch(mode) { + case Spatializer.HEAD_TRACKING_MODE_DISABLED: + case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD: + case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE: + break; + default: + return; + } + mSpatializerHelper.setDesiredHeadTrackingMode(mode); + } + //========================================================================================== private boolean readCameraSoundForced() { return SystemProperties.getBoolean("audio.camerasound.force", false) || diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 32ac7859fbe69..1d8597497a2c0 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -25,7 +25,10 @@ import android.media.AudioSystem; import android.media.INativeSpatializerCallback; import android.media.ISpatializer; import android.media.ISpatializerCallback; +import android.media.ISpatializerHeadToSoundStagePoseCallback; +import android.media.ISpatializerHeadTrackingModeCallback; import android.media.Spatializer; +import android.media.SpatializerHeadTrackingMode; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; @@ -64,6 +67,8 @@ public class SpatializerHelper { /** current level as reported by native Spatializer in callback */ private int mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + private int mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; + private int mDesiredHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; private @Nullable ISpatializer mSpat; private @Nullable SpatializerCallback mSpatCallback; @@ -150,6 +155,7 @@ public class SpatializerHelper { mState = STATE_UNINITIALIZED; mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; init(); setFeatureEnabled(featureEnabled); } @@ -189,6 +195,15 @@ public class SpatializerHelper { public void onHeadTrackingModeChanged(byte mode) { logd("SpatializerCallback.onHeadTrackingModeChanged mode:" + mode); + int oldMode, newMode; + synchronized (this) { + oldMode = mActualHeadTrackingMode; + mActualHeadTrackingMode = headTrackingModeTypeToSpatializerInt(mode); + newMode = mActualHeadTrackingMode; + } + if (oldMode != newMode) { + dispatchActualHeadTrackingMode(newMode); + } } public void onHeadToSoundStagePoseUpdated(float[] headToStage) { @@ -196,6 +211,11 @@ public class SpatializerHelper { Log.e(TAG, "SpatializerCallback.onHeadToStagePoseUpdated null transform"); return; } + if (headToStage.length != 6) { + Log.e(TAG, "SpatializerCallback.onHeadToStagePoseUpdated invalid transform length" + + headToStage.length); + return; + } if (DEBUG) { // 6 values * (4 digits + 1 dot + 2 brackets) = 42 characters StringBuilder t = new StringBuilder(42); @@ -204,6 +224,7 @@ public class SpatializerHelper { } logd("SpatializerCallback.onHeadToStagePoseUpdated headToStage:" + t); } + dispatchPoseUpdate(headToStage); } }; @@ -472,4 +493,239 @@ public class SpatializerHelper { logd("canBeSpatialized returning " + able); return able; } + + //------------------------------------------------------ + // head tracking + final RemoteCallbackList mHeadTrackingModeCallbacks = + new RemoteCallbackList(); + + synchronized void registerHeadTrackingModeCallback( + @NonNull ISpatializerHeadTrackingModeCallback callback) { + mHeadTrackingModeCallbacks.register(callback); + } + + synchronized void unregisterHeadTrackingModeCallback( + @NonNull ISpatializerHeadTrackingModeCallback callback) { + mHeadTrackingModeCallbacks.unregister(callback); + } + + synchronized int[] getSupportedHeadTrackingModes() { + switch (mState) { + case STATE_UNINITIALIZED: + return new int[0]; + case STATE_NOT_SUPPORTED: + // return an empty list when Spatializer functionality is not supported + // because the list of head tracking modes you can set is actually empty + // as defined in {@link Spatializer#getSupportedHeadTrackingModes()} + return new int[0]; + case STATE_ENABLED_UNAVAILABLE: + case STATE_DISABLED_UNAVAILABLE: + case STATE_DISABLED_AVAILABLE: + case STATE_ENABLED_AVAILABLE: + if (mSpat == null) { + return new int[0]; + } + break; + } + // mSpat != null + try { + final byte[] values = mSpat.getSupportedHeadTrackingModes(); + ArrayList list = new ArrayList<>(0); + for (byte value : values) { + switch (value) { + case SpatializerHeadTrackingMode.OTHER: + case SpatializerHeadTrackingMode.DISABLED: + // not expected here, skip + break; + case SpatializerHeadTrackingMode.RELATIVE_WORLD: + case SpatializerHeadTrackingMode.RELATIVE_SCREEN: + list.add(headTrackingModeTypeToSpatializerInt(value)); + break; + default: + Log.e(TAG, "Unexpected head tracking mode:" + value, + new IllegalArgumentException("invalid mode")); + break; + } + } + int[] modes = new int[list.size()]; + for (int i = 0; i < list.size(); i++) { + modes[i] = list.get(i); + } + return modes; + } catch (RemoteException e) { + Log.e(TAG, "Error calling getSupportedHeadTrackingModes", e); + return new int[] { Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED }; + } + } + + synchronized int getActualHeadTrackingMode() { + switch (mState) { + case STATE_UNINITIALIZED: + return Spatializer.HEAD_TRACKING_MODE_DISABLED; + case STATE_NOT_SUPPORTED: + return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; + case STATE_ENABLED_UNAVAILABLE: + case STATE_DISABLED_UNAVAILABLE: + case STATE_DISABLED_AVAILABLE: + case STATE_ENABLED_AVAILABLE: + if (mSpat == null) { + return Spatializer.HEAD_TRACKING_MODE_DISABLED; + } + break; + } + // mSpat != null + try { + return headTrackingModeTypeToSpatializerInt(mSpat.getActualHeadTrackingMode()); + } catch (RemoteException e) { + Log.e(TAG, "Error calling getActualHeadTrackingMode", e); + return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; + } + } + + synchronized int getDesiredHeadTrackingMode() { + return mDesiredHeadTrackingMode; + } + + synchronized void setGlobalTransform(@NonNull float[] transform) { + if (transform.length != 6) { + throw new IllegalArgumentException("invalid array size" + transform.length); + } + if (!checkSpatForHeadTracking("setGlobalTransform")) { + return; + } + try { + mSpat.setGlobalTransform(transform); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setGlobalTransform", e); + } + } + + synchronized void recenterHeadTracker() { + if (!checkSpatForHeadTracking("recenterHeadTracker")) { + return; + } + try { + mSpat.recenterHeadTracker(); + } catch (RemoteException e) { + Log.e(TAG, "Error calling recenterHeadTracker", e); + } + } + + synchronized void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) { + if (!checkSpatForHeadTracking("setDesiredHeadTrackingMode")) { + return; + } + try { + if (mode != mDesiredHeadTrackingMode) { + mSpat.setDesiredHeadTrackingMode(spatializerIntToHeadTrackingModeType(mode)); + mDesiredHeadTrackingMode = mode; + dispatchDesiredHeadTrackingMode(mode); + } + + } catch (RemoteException e) { + Log.e(TAG, "Error calling setDesiredHeadTrackingMode", e); + } + } + + private int headTrackingModeTypeToSpatializerInt(byte mode) { + switch (mode) { + case SpatializerHeadTrackingMode.OTHER: + return Spatializer.HEAD_TRACKING_MODE_OTHER; + case SpatializerHeadTrackingMode.DISABLED: + return Spatializer.HEAD_TRACKING_MODE_DISABLED; + case SpatializerHeadTrackingMode.RELATIVE_WORLD: + return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD; + case SpatializerHeadTrackingMode.RELATIVE_SCREEN: + return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE; + default: + throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode)); + } + } + + private byte spatializerIntToHeadTrackingModeType(int sdkMode) { + switch (sdkMode) { + case Spatializer.HEAD_TRACKING_MODE_OTHER: + return SpatializerHeadTrackingMode.OTHER; + case Spatializer.HEAD_TRACKING_MODE_DISABLED: + return SpatializerHeadTrackingMode.DISABLED; + case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD: + return SpatializerHeadTrackingMode.RELATIVE_WORLD; + case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE: + return SpatializerHeadTrackingMode.RELATIVE_SCREEN; + default: + throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode)); + } + } + + private boolean checkSpatForHeadTracking(String funcName) { + switch (mState) { + case STATE_UNINITIALIZED: + case STATE_NOT_SUPPORTED: + return false; + case STATE_ENABLED_UNAVAILABLE: + case STATE_DISABLED_UNAVAILABLE: + case STATE_DISABLED_AVAILABLE: + case STATE_ENABLED_AVAILABLE: + if (mSpat == null) { + throw (new IllegalStateException( + "null Spatializer when calling " + funcName)); + } + break; + } + return true; + } + + private void dispatchActualHeadTrackingMode(int newMode) { + final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast(); + for (int i = 0; i < nbCallbacks; i++) { + try { + mHeadTrackingModeCallbacks.getBroadcastItem(i) + .dispatchSpatializerActualHeadTrackingModeChanged(newMode); + } catch (RemoteException e) { + Log.e(TAG, "Error in dispatchSpatializerActualHeadTrackingModeChanged", e); + } + } + mHeadTrackingModeCallbacks.finishBroadcast(); + } + + private void dispatchDesiredHeadTrackingMode(int newMode) { + final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast(); + for (int i = 0; i < nbCallbacks; i++) { + try { + mHeadTrackingModeCallbacks.getBroadcastItem(i) + .dispatchSpatializerDesiredHeadTrackingModeChanged(newMode); + } catch (RemoteException e) { + Log.e(TAG, "Error in dispatchSpatializerDesiredHeadTrackingModeChanged", e); + } + } + mHeadTrackingModeCallbacks.finishBroadcast(); + } + + //------------------------------------------------------ + // head pose + final RemoteCallbackList mHeadPoseCallbacks = + new RemoteCallbackList(); + + synchronized void registerHeadToSoundstagePoseCallback( + @NonNull ISpatializerHeadToSoundStagePoseCallback callback) { + mHeadPoseCallbacks.register(callback); + } + + synchronized void unregisterHeadToSoundstagePoseCallback( + @NonNull ISpatializerHeadToSoundStagePoseCallback callback) { + mHeadPoseCallbacks.unregister(callback); + } + + private void dispatchPoseUpdate(float[] pose) { + final int nbCallbacks = mHeadPoseCallbacks.beginBroadcast(); + for (int i = 0; i < nbCallbacks; i++) { + try { + mHeadPoseCallbacks.getBroadcastItem(i) + .dispatchPoseChanged(pose); + } catch (RemoteException e) { + Log.e(TAG, "Error in dispatchPoseChanged", e); + } + } + mHeadPoseCallbacks.finishBroadcast(); + } }