From e9dd14e011a2b85e3997910d4dc9e7d4796c491a Mon Sep 17 00:00:00 2001 From: Jean-Michel Trivi Date: Mon, 12 Jul 2021 11:10:18 -0700 Subject: [PATCH] New APIs for immersive audio - AudioManager: entry point for getting a Spatializer object, if supported - Spatializer: entry point for spatializer capabilities, system APIs for configuration and compatibility with devices - AudioAttributes: additions for behavior of a track for virtualization Bug: 191404931 Test: atest AudioAttributesTest SpatializerTest Change-Id: I06f8acdd979f8d4374042f8209082c79f49a1f75 --- core/api/current.txt | 20 + core/api/system-current.txt | 7 + media/java/android/media/AudioAttributes.java | 99 ++++- media/java/android/media/AudioManager.java | 21 +- media/java/android/media/IAudioService.aidl | 22 + .../android/media/ISpatializerCallback.aidl | 29 ++ media/java/android/media/Spatializer.java | 404 ++++++++++++++++++ .../android/server/audio/AudioService.java | 76 ++++ .../server/audio/SpatializerHelper.java | 128 ++++++ 9 files changed, 804 insertions(+), 2 deletions(-) create mode 100644 media/java/android/media/ISpatializerCallback.aidl create mode 100644 media/java/android/media/Spatializer.java create mode 100644 services/core/java/com/android/server/audio/SpatializerHelper.java diff --git a/core/api/current.txt b/core/api/current.txt index 3ddb374c8a050..ef886e41d7821 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -20181,8 +20181,10 @@ package android.media { method public int getAllowedCapturePolicy(); method public int getContentType(); method public int getFlags(); + method public int getSpatializationBehavior(); method public int getUsage(); method public int getVolumeControlStream(); + method public boolean isContentSpatialized(); method public void writeToParcel(android.os.Parcel, int); field public static final int ALLOW_CAPTURE_BY_ALL = 1; // 0x1 field public static final int ALLOW_CAPTURE_BY_NONE = 3; // 0x3 @@ -20196,6 +20198,8 @@ package android.media { field public static final int FLAG_AUDIBILITY_ENFORCED = 1; // 0x1 field public static final int FLAG_HW_AV_SYNC = 16; // 0x10 field @Deprecated public static final int FLAG_LOW_LATENCY = 256; // 0x100 + field public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0; // 0x0 + field public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1; // 0x1 field public static final int USAGE_ALARM = 4; // 0x4 field public static final int USAGE_ASSISTANCE_ACCESSIBILITY = 11; // 0xb field public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = 12; // 0xc @@ -20222,7 +20226,9 @@ package android.media { method public android.media.AudioAttributes.Builder setContentType(int); method public android.media.AudioAttributes.Builder setFlags(int); method @NonNull public android.media.AudioAttributes.Builder setHapticChannelsMuted(boolean); + method @NonNull public android.media.AudioAttributes.Builder setIsContentSpatialized(boolean); method public android.media.AudioAttributes.Builder setLegacyStreamType(int); + method @NonNull public android.media.AudioAttributes.Builder setSpatializationBehavior(int); method public android.media.AudioAttributes.Builder setUsage(int); } @@ -20447,6 +20453,7 @@ package android.media { method public String getProperty(String); method public int getRingerMode(); method @Deprecated public int getRouting(int); + method @Nullable public android.media.Spatializer getSpatializer(); method public int getStreamMaxVolume(int); method public int getStreamMinVolume(int); method public int getStreamVolume(int); @@ -23843,6 +23850,19 @@ package android.media { method public void onLoadComplete(android.media.SoundPool, int, int); } + public class Spatializer { + method public void addOnSpatializerStateChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnSpatializerStateChangedListener); + method public boolean canBeSpatialized(@NonNull android.media.AudioAttributes, @NonNull android.media.AudioFormat); + method public boolean isAvailable(); + method public boolean isEnabled(); + method public void removeOnSpatializerStateChangedListener(@NonNull android.media.Spatializer.OnSpatializerStateChangedListener); + } + + public static interface Spatializer.OnSpatializerStateChangedListener { + method public void onSpatializerAvailableChanged(@NonNull android.media.Spatializer, boolean); + method public void onSpatializerEnabledChanged(@NonNull android.media.Spatializer, boolean); + } + public final class SubtitleData { ctor public SubtitleData(int, long, long, @NonNull byte[]); method @NonNull public byte[] getData(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2846b36959581..2ca073bcf4d62 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -5372,6 +5372,13 @@ package android.media { field public static final android.media.RouteDiscoveryPreference EMPTY; } + public class Spatializer { + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void addCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes); + method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List getCompatibleAudioDevices(); + 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 setEnabled(boolean); + } + } package android.media.audiofx { diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index a031b4cfc9112..c22ab94637364 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -447,13 +447,26 @@ public final class AudioAttributes implements Parcelable { */ public static final int FLAG_CAPTURE_PRIVATE = 0x1 << 13; + /** + * @hide + * Flag indicating the audio content has been processed to provide a virtual multichannel + * audio experience + */ + public static final int FLAG_CONTENT_SPATIALIZED = 0x1 << 14; + + /** + * @hide + * Flag indicating the audio content is to never be spatialized + */ + public static final int FLAG_NEVER_SPATIALIZE = 0x1 << 15; // Note that even though FLAG_MUTE_HAPTIC is stored as a flag bit, it is not here since // it is known as a boolean value outside of AudioAttributes. private static final int FLAG_ALL = FLAG_AUDIBILITY_ENFORCED | FLAG_SECURE | FLAG_SCO | FLAG_BEACON | FLAG_HW_AV_SYNC | FLAG_HW_HOTWORD | FLAG_BYPASS_INTERRUPTION_POLICY | FLAG_BYPASS_MUTE | FLAG_LOW_LATENCY | FLAG_DEEP_BUFFER | FLAG_NO_MEDIA_PROJECTION - | FLAG_NO_SYSTEM_CAPTURE | FLAG_CAPTURE_PRIVATE; + | FLAG_NO_SYSTEM_CAPTURE | FLAG_CAPTURE_PRIVATE | FLAG_CONTENT_SPATIALIZED + | FLAG_NEVER_SPATIALIZE; private final static int FLAG_ALL_PUBLIC = FLAG_AUDIBILITY_ENFORCED | FLAG_HW_AV_SYNC | FLAG_LOW_LATENCY; /* mask of flags that can be set by SDK and System APIs through the Builder */ @@ -614,6 +627,49 @@ public final class AudioAttributes implements Parcelable { return (mFlags & FLAG_MUTE_HAPTIC) != 0; } + /** + * Return true if the audio content associated with these attributes has already been + * spatialized, that is it has already been processed to offer a binaural or transaural + * immersive audio experience. + * @return {@code true} if the content has been processed + */ + public boolean isContentSpatialized() { + return (mFlags & FLAG_CONTENT_SPATIALIZED) != 0; + } + + /** @hide */ + @IntDef(flag = false, value = { + SPATIALIZATION_BEHAVIOR_AUTO, + SPATIALIZATION_BEHAVIOR_NEVER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SpatializationBehavior {}; + + /** + * Constant indicating the audio content associated with these attributes will follow the + * default platform behavior with regards to which content will be spatialized or not. + * @see #getSpatializationBehavior() + * @see Spatializer + */ + public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0; + + /** + * Constant indicating the audio content associated with these attributes should never + * be virtualized. + * @see #getSpatializationBehavior() + * @see Spatializer + */ + public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1; + + /** + * Return the behavior affecting whether spatialization will be used. + * @return the spatialization behavior + */ + public @SpatializationBehavior int getSpatializationBehavior() { + return ((mFlags & FLAG_NEVER_SPATIALIZE) != 0) + ? SPATIALIZATION_BEHAVIOR_NEVER : SPATIALIZATION_BEHAVIOR_AUTO; + } + /** * Return the capture policy. * @return the capture policy set by {@link Builder#setAllowedCapturePolicy(int)} or @@ -657,6 +713,8 @@ public final class AudioAttributes implements Parcelable { private int mSource = MediaRecorder.AudioSource.AUDIO_SOURCE_INVALID; private int mFlags = 0x0; private boolean mMuteHapticChannels = true; + private boolean mIsContentSpatialized = false; + private int mSpatializationBehavior = SPATIALIZATION_BEHAVIOR_AUTO; private HashSet mTags = new HashSet(); private Bundle mBundle; private int mPrivacySensitive = PRIVACY_SENSITIVE_DEFAULT; @@ -687,6 +745,8 @@ public final class AudioAttributes implements Parcelable { mFlags = aa.getAllFlags(); mTags = (HashSet) aa.mTags.clone(); mMuteHapticChannels = aa.areHapticChannelsMuted(); + mIsContentSpatialized = aa.isContentSpatialized(); + mSpatializationBehavior = aa.getSpatializationBehavior(); } /** @@ -719,6 +779,12 @@ public final class AudioAttributes implements Parcelable { if (mMuteHapticChannels) { aa.mFlags |= FLAG_MUTE_HAPTIC; } + if (mIsContentSpatialized) { + aa.mFlags |= FLAG_CONTENT_SPATIALIZED; + } + if (mSpatializationBehavior == SPATIALIZATION_BEHAVIOR_NEVER) { + aa.mFlags |= FLAG_NEVER_SPATIALIZE; + } if (mPrivacySensitive == PRIVACY_SENSITIVE_DEFAULT) { // capturing for camcorder or communication is private by default to @@ -905,6 +971,35 @@ public final class AudioAttributes implements Parcelable { return this; } + /** + * Specifies whether the content has already been processed for spatialization. + * If it has, setting this to true will prevent issues such as double-processing. + * @param isSpatialized + * @return the same Builder instance + */ + public @NonNull Builder setIsContentSpatialized(boolean isSpatialized) { + mIsContentSpatialized = isSpatialized; + return this; + } + + /** + * Sets the behavior affecting whether spatialization will be used. + * @param sb the spatialization behavior + * @return the same Builder instance + * + */ + public @NonNull Builder setSpatializationBehavior(@SpatializationBehavior int sb) { + switch (sb) { + case SPATIALIZATION_BEHAVIOR_NEVER: + case SPATIALIZATION_BEHAVIOR_AUTO: + break; + default: + throw new IllegalArgumentException("Invalid spatialization behavior " + sb); + } + mSpatializationBehavior = sb; + return this; + } + /** * @hide * Replaces flags. @@ -990,6 +1085,8 @@ public final class AudioAttributes implements Parcelable { mContentType = attributes.mContentType; mFlags = attributes.getAllFlags(); mMuteHapticChannels = attributes.areHapticChannelsMuted(); + mIsContentSpatialized = attributes.isContentSpatialized(); + mSpatializationBehavior = attributes.getSpatializationBehavior(); mTags = attributes.mTags; mBundle = attributes.mBundle; mSource = attributes.mSource; diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 3b9c05bbe64f2..5db9ddfac8701 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -805,7 +805,7 @@ public class AudioManager { } @UnsupportedAppUsage - private static IAudioService getService() + static IAudioService getService() { if (sService != null) { return sService; @@ -2438,6 +2438,25 @@ public class AudioManager { return AudioSystem.getOffloadSupport(format, attributes); } + //==================================================================== + // Immersive audio + + /** + * Return a handle to the optional platform's {@link Spatializer} + * @return {@code null} if spatialization is not supported, the {@code Spatializer} instance + * otherwise. + */ + public @Nullable Spatializer getSpatializer() { + int level = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + try { + level = getService().getSpatializerImmersiveAudioLevel(); + } catch (Exception e) { /* using NONE */ } + if (level == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { + return null; + } + return new Spatializer(this); + } + //==================================================================== // Bluetooth SCO control /** diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 0b35ebed966ae..8480c5289d151 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -20,6 +20,7 @@ import android.bluetooth.BluetoothDevice; import android.content.ComponentName; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; +import android.media.AudioFormat; import android.media.AudioFocusInfo; import android.media.AudioPlaybackConfiguration; import android.media.AudioRecordingConfiguration; @@ -34,6 +35,7 @@ import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; import android.media.IStrategyPreferredDevicesDispatcher; +import android.media.ISpatializerCallback; import android.media.IVolumeController; import android.media.IVolumeController; import android.media.PlayerBase; @@ -392,4 +394,24 @@ interface IAudioService { void registerModeDispatcher(IAudioModeDispatcher dispatcher); oneway void unregisterModeDispatcher(IAudioModeDispatcher dispatcher); + + int getSpatializerImmersiveAudioLevel(); + + boolean isSpatializerEnabled(); + + boolean isSpatializerAvailable(); + + void setSpatializerEnabled(boolean enabled); + + boolean canBeSpatialized(in AudioAttributes aa, in AudioFormat af); + + void registerSpatializerCallback(in ISpatializerCallback callback); + + void unregisterSpatializerCallback(in ISpatializerCallback callback); + + List getSpatializerCompatibleAudioDevices(); + + void addSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada); + + void removeSpatializerCompatibleAudioDevice(in AudioDeviceAttributes ada); } diff --git a/media/java/android/media/ISpatializerCallback.aidl b/media/java/android/media/ISpatializerCallback.aidl new file mode 100644 index 0000000000000..50f91e737fc58 --- /dev/null +++ b/media/java/android/media/ISpatializerCallback.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 state changes. + * + * {@hide} + */ +oneway interface ISpatializerCallback { + + void dispatchSpatializerEnabledChanged(boolean enabled); + + void dispatchSpatializerAvailableChanged(boolean available); +} diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java new file mode 100644 index 0000000000000..3ed8b58959a19 --- /dev/null +++ b/media/java/android/media/Spatializer.java @@ -0,0 +1,404 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +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 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 + * @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.
+ * 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}. + * @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 */ + @IntDef(flag = false, value = { + SPATIALIZER_IMMERSIVE_LEVEL_NONE, + SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ImmersiveAudioLevel {}; + + /** + * @hide + * Constant indicating there are no spatialization capabilities supported on this device. + * @see AudioManager#getImmersiveAudioLevel() + */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_NONE = 0; + + /** + * @hide + * Constant indicating the {@link Spatializer} on this device supports multichannel + * spatialization. + * @see AudioManager#getImmersiveAudioLevel() + */ + public static final int SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL = 1; + + /** + * @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. + */ + 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); + } + + /** + * 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) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mStateListenerLock) { + if (hasSpatializerStateListener(listener)) { + throw new IllegalArgumentException( + "Called addOnSpatializerStateChangedListener() " + + "on a previously registered listener"); + } + // lazy initialization of the list of strategy-preferred device listener + if (mStateListeners == null) { + mStateListeners = new ArrayList<>(); + } + mStateListeners.add(new StateListenerInfo(listener, executor)); + if (mStateListeners.size() == 1) { + // register binder for callbacks + if (mInfoDispatcherStub == null) { + mInfoDispatcherStub = + new SpatializerInfoDispatcherStub(); + } + try { + mAm.getService().registerSpatializerCallback( + mInfoDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + } + + /** + * 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) { + Objects.requireNonNull(listener); + synchronized (mStateListenerLock) { + if (!removeStateListener(listener)) { + throw new IllegalArgumentException( + "Called removeOnSpatializerStateChangedListener() " + + "on an unregistered listener"); + } + if (mStateListeners.size() == 0) { + // unregister binder for callbacks + try { + mAm.getService().unregisterSpatializerCallback(mInfoDispatcherStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + mInfoDispatcherStub = null; + mStateListeners = null; + } + } + } + } + + /** + * @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); + } + } + + private final class SpatializerInfoDispatcherStub extends ISpatializerCallback.Stub { + @Override + public void dispatchSpatializerEnabledChanged(boolean enabled) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList stateListeners; + synchronized (mStateListenerLock) { + if (mStateListeners == null || mStateListeners.size() == 0) { + return; + } + stateListeners = (ArrayList) mStateListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (StateListenerInfo info : stateListeners) { + info.mExecutor.execute(() -> + info.mListener.onSpatializerEnabledChanged(Spatializer.this, enabled)); + } + } + } + + @Override + public void dispatchSpatializerAvailableChanged(boolean available) { + // make a shallow copy of listeners so callback is not executed under lock + final ArrayList stateListeners; + synchronized (mStateListenerLock) { + if (mStateListeners == null || mStateListeners.size() == 0) { + return; + } + stateListeners = (ArrayList) mStateListeners.clone(); + } + try (SafeCloseable ignored = ClearCallingIdentityContext.create()) { + for (StateListenerInfo info : stateListeners) { + info.mExecutor.execute(() -> + info.mListener.onSpatializerAvailableChanged( + Spatializer.this, available)); + } + } + } + } + + private static class StateListenerInfo { + final @NonNull OnSpatializerStateChangedListener mListener; + final @NonNull Executor mExecutor; + + StateListenerInfo(@NonNull OnSpatializerStateChangedListener listener, + @NonNull Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + @GuardedBy("mStateListenerLock") + private boolean hasSpatializerStateListener(OnSpatializerStateChangedListener listener) { + return getStateListenerInfo(listener) != null; + } + + @GuardedBy("mStateListenerLock") + private @Nullable StateListenerInfo getStateListenerInfo( + OnSpatializerStateChangedListener listener) { + if (mStateListeners == null) { + return null; + } + for (StateListenerInfo info : mStateListeners) { + if (info.mListener == listener) { + return info; + } + } + return null; + } + + @GuardedBy("mStateListenerLock") + /** + * @return true if the listener was removed from the list + */ + private boolean removeStateListener(OnSpatializerStateChangedListener listener) { + final StateListenerInfo infoToRemove = getStateListenerInfo(listener); + if (infoToRemove != null) { + mStateListeners.remove(infoToRemove); + return true; + } + return false; + } +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 8d9479cc9895c..c167c75cb61f4 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -93,11 +93,13 @@ import android.media.ICommunicationDeviceDispatcher; import android.media.IPlaybackConfigDispatcher; import android.media.IRecordingConfigDispatcher; import android.media.IRingtonePlayer; +import android.media.ISpatializerCallback; import android.media.IStrategyPreferredDevicesDispatcher; import android.media.IVolumeController; import android.media.MediaMetrics; import android.media.MediaRecorder.AudioSource; import android.media.PlayerBase; +import android.media.Spatializer; import android.media.VolumePolicy; import android.media.audiofx.AudioEffect; import android.media.audiopolicy.AudioMix; @@ -8224,6 +8226,80 @@ public class AudioService extends IAudioService.Stub return true; } + //========================================================================================== + private final SpatializerHelper mSpatializerHelper = new SpatializerHelper(); + + private void enforceModifyDefaultAudioEffectsPermission() { + if (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Missing MODIFY_DEFAULT_AUDIO_EFFECTS permission"); + } + } + + /** @see AudioManager#getSpatializerImmersiveAudioLevel() */ + public int getSpatializerImmersiveAudioLevel() { + return mSpatializerHelper.getImmersiveAudioLevel(); + } + + /** @see Spatializer#isEnabled() */ + public boolean isSpatializerEnabled() { + return mSpatializerHelper.isEnabled(); + } + + /** @see Spatializer#isAvailable() */ + public boolean isSpatializerAvailable() { + return mSpatializerHelper.isAvailable(); + } + + /** @see Spatializer#setSpatializerEnabled(boolean) */ + public void setSpatializerEnabled(boolean enabled) { + enforceModifyDefaultAudioEffectsPermission(); + mSpatializerHelper.setEnabled(enabled); + } + + /** @see Spatializer#canBeSpatialized() */ + public boolean canBeSpatialized( + @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { + Objects.requireNonNull(attributes); + Objects.requireNonNull(format); + return mSpatializerHelper.canBeSpatialized(attributes, format); + } + + /** @see Spatializer.SpatializerInfoDispatcherStub */ + public void registerSpatializerCallback( + @NonNull ISpatializerCallback dispatcher) { + Objects.requireNonNull(dispatcher); + mSpatializerHelper.registerStateCallback(dispatcher); + } + + /** @see Spatializer.SpatializerInfoDispatcherStub */ + public void unregisterSpatializerCallback( + @NonNull ISpatializerCallback dispatcher) { + Objects.requireNonNull(dispatcher); + mSpatializerHelper.unregisterStateCallback(dispatcher); + } + + /** @see Spatializer#getSpatializerCompatibleAudioDevices() */ + public @NonNull List getSpatializerCompatibleAudioDevices() { + enforceModifyAudioRoutingPermission(); + return mSpatializerHelper.getCompatibleAudioDevices(); + } + + /** @see Spatializer#addSpatializerCompatibleAudioDevice(AudioDeviceAttributes) */ + public void addSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + enforceModifyDefaultAudioEffectsPermission(); + Objects.requireNonNull(ada); + mSpatializerHelper.addCompatibleAudioDevice(ada); + } + + /** @see Spatializer#removeSpatializerCompatibleAudioDevice(AudioDeviceAttributes) */ + public void removeSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + enforceModifyDefaultAudioEffectsPermission(); + Objects.requireNonNull(ada); + mSpatializerHelper.removeCompatibleAudioDevice(ada); + } + //========================================================================================== 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 new file mode 100644 index 0000000000000..708d9e10ad737 --- /dev/null +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -0,0 +1,128 @@ +/* + * 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 com.android.server.audio; + +import android.annotation.NonNull; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioFormat; +import android.media.ISpatializerCallback; +import android.media.Spatializer; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * A helper class to manage Spatializer related functionality + */ +public class SpatializerHelper { + + private static final String TAG = "AS.SpatializerHelper"; + + //--------------------------------------------------------------- + // audio device compatibility / enabled + + private final ArrayList mCompatibleAudioDevices = new ArrayList<>(0); + + /** + * @return a shallow copy of the list of compatible audio devices + */ + synchronized @NonNull List getCompatibleAudioDevices() { + return (List) mCompatibleAudioDevices.clone(); + } + + synchronized void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + if (!mCompatibleAudioDevices.contains(ada)) { + mCompatibleAudioDevices.add(ada); + } + } + + synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + mCompatibleAudioDevices.remove(ada); + } + + //------------------------------------------------------ + // enabled state + + // global state of feature + boolean mFeatureEnabled = false; + // initialized state, checked after each audio_server start + boolean mInitialized = false; + + synchronized boolean isEnabled() { + return mFeatureEnabled; + } + + synchronized boolean isAvailable() { + if (!mInitialized) { + return false; + } + // TODO check device compatibility + // ... + return true; + } + + synchronized void setEnabled(boolean enabled) { + final boolean oldState = mFeatureEnabled; + mFeatureEnabled = enabled; + if (oldState != enabled) { + dispatchEnabledState(); + } + } + + public int getImmersiveAudioLevel() { + // TODO replace placeholder code with actual effect discovery + return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + } + + final RemoteCallbackList mStateCallbacks = + new RemoteCallbackList(); + + synchronized void registerStateCallback( + @NonNull ISpatializerCallback callback) { + mStateCallbacks.register(callback); + } + + synchronized void unregisterStateCallback( + @NonNull ISpatializerCallback callback) { + mStateCallbacks.unregister(callback); + } + + private synchronized void dispatchEnabledState() { + final int nbCallbacks = mStateCallbacks.beginBroadcast(); + for (int i = 0; i < nbCallbacks; i++) { + try { + mStateCallbacks.getBroadcastItem(i) + .dispatchSpatializerEnabledChanged(mFeatureEnabled); + } catch (RemoteException e) { + Log.e(TAG, "Error in dispatchSpatializerEnabledChanged", e); + } + } + mStateCallbacks.finishBroadcast(); + } + + //------------------------------------------------------ + // virtualization capabilities + synchronized boolean canBeSpatialized( + @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { + // TODO hook up to spatializer effect for query + return false; + } +}