diff --git a/core/api/current.txt b/core/api/current.txt index 03d58350d498b..784450ef18505 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -20178,8 +20178,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 @@ -20193,6 +20195,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 @@ -20219,7 +20223,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); } @@ -20444,6 +20450,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); @@ -23840,6 +23847,17 @@ package android.media { method public void onLoadComplete(android.media.SoundPool, int, int); } + public class Spatializer { + method public void addOnSpatializerEnabledChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnSpatializerEnabledChangedListener); + method public boolean canBeSpatialized(@NonNull android.media.AudioAttributes, @NonNull android.media.AudioFormat); + method public boolean isEnabled(); + method public void removeOnSpatializerEnabledChangedListener(@NonNull android.media.Spatializer.OnSpatializerEnabledChangedListener); + } + + public static interface Spatializer.OnSpatializerEnabledChangedListener { + method public void onSpatializerEnabledChanged(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 2d73aa67ed1a2..ff7fe8d8c9d79 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -5370,6 +5370,12 @@ package android.media { field public static final android.media.RouteDiscoveryPreference EMPTY; } + public class Spatializer { + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void addCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes); + method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public java.util.List getCompatibleAudioDevices(); + method @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public void removeCompatibleAudioDevice(@NonNull android.media.AudioDeviceAttributes); + } + } 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..a31ad613bf48e 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,149 @@ public class AudioManager { return AudioSystem.getOffloadSupport(format, attributes); } + //==================================================================== + // Immersive audio + + /** + * @hide + * Returns the level of support for immersive audio from the {@link Spatializer} if + * available. + * @return the level of immersive audio support through spatialization + * @see Spatializer#getImmersiveAudioLevel() + */ + @Spatializer.ImmersiveAudioLevel int getSpatializerImmersiveAudioLevel() { + final IAudioService service = getService(); + try { + return service.getSpatializerImmersiveAudioLevel(); + } catch (RemoteException e) { + return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + } + } + + /** + * 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() { + if (getSpatializerImmersiveAudioLevel() == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) { + return null; + } + return new Spatializer(this); + } + + /** + * @hide + * @see Spatializer#isEnabled() + * @return {@code true} if spatialization is enabled + */ + boolean isSpatializerEnabled() { + final IAudioService service = getService(); + try { + return service.isSpatializerEnabled(); + } catch (RemoteException e) { + Log.e(TAG, "Error querying isSpatializerEnabled, returning false", e); + return false; + } + } + + /** + * @hide + * @see Spatializer#setEnabled(boolean) + * Enable/disable the spatialization wherever supported. + * @param enabled {@code true} to enable + */ + void setSpatializerFeatureEnabled(boolean enabled) { + final IAudioService service = getService(); + try { + service.setSpatializerFeatureEnabled(enabled); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setSpatializerFeatureEnabled", e); + } + } + + /** + * @hide + * @see Spatializer#setEnabledForDevice(boolean, AudioDeviceAttributes) + * @see Spatializer#setEnabled(boolean) + * @param enabled enable/disable for a specific device. + * @param device the device concerned with spatializer functionality. + */ + void setSpatializerEnabledForDevice(boolean enabled, + @NonNull AudioDeviceAttributes device) { + final IAudioService service = getService(); + try { + service.setSpatializerEnabledForDevice(enabled, device); + } catch (RemoteException e) { + Log.e(TAG, "Error calling setSpatializerEnabledForDevice", e); + } + } + + /** + * @hide + * @see Spatializer#canBeSpatialized(AudioAttributes, AudioFormat) + * @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 true if the device is capable of spatializing the combination of audio + * format and attributes. + */ + boolean canBeSpatialized( + @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { + final IAudioService service = getService(); + try { + return service.canBeSpatialized(attributes, format); + } catch (RemoteException e) { + Log.e(TAG, "Error querying canBeSpatialized for attr:" + attributes + + " format:" + format + " returning false", e); + return false; + } + } + + /** + * @hide + * @see Spatializer#getCompatibleAudioDevices() + * @return a non-null list of the spatialization-compatible audio devices + */ + @NonNull List getSpatializerCompatibleAudioDevices() { + final IAudioService service = getService(); + try { + return service.getSpatializerCompatibleAudioDevices(); + } catch (RemoteException e) { + Log.e(TAG, "Error querying getSpatializerCompatibleAudioDevices(), " + + " returning empty list", e); + return new ArrayList(0); + } + } + + /** + * @hide + * @see Spatializer#addCompatibleAudioDevice(AudioDeviceAttributes) + * @param ada the audio device compatible with spatialization + */ + void addSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + final IAudioService service = getService(); + try { + service.addSpatializerCompatibleAudioDevice(ada); + } catch (RemoteException e) { + Log.e(TAG, "Error calling addSpatializerCompatibleAudioDevice()", e); + } + } + + /** + * @hide + * @see Spatializer#removeCompatibleAudioDevice(AudioDeviceAttributes) + * @param ada the audio device incompatible with spatialization + */ + void removeSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + final IAudioService service = getService(); + try { + service.removeSpatializerCompatibleAudioDevice(ada); + } catch (RemoteException e) { + Log.e(TAG, "Error calling removeSpatializerCompatibleAudioDevice()", e); + } + } + + //==================================================================== // Bluetooth SCO control /** diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 0b35ebed966ae..d2c70e4a66168 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(); + + void setSpatializerFeatureEnabled(boolean enabled); + + void setSpatializerEnabledForDevice(boolean enabled, in AudioDeviceAttributes device); + + 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..50c5a6b8338f0 --- /dev/null +++ b/media/java/android/media/ISpatializerCallback.aidl @@ -0,0 +1,28 @@ +/* + * 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 dispatchSpatializerStateChanged(boolean enabled); + +} diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java new file mode 100644 index 0000000000000..a2267cc51c832 --- /dev/null +++ b/media/java/android/media/Spatializer.java @@ -0,0 +1,331 @@ +/* + * 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 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(); + + /** + * 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 from a number of sources, examples are the user electing to + * disable the feature, or the use of an audio device that is not compatible with multichannel + * audio spatialization (for instance playing audio over a monophonic speaker). + * @return {@code true} if spatialization is enabled + */ + public boolean isEnabled() { + return mAm.isSpatializerEnabled(); + } + + /** @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 + * @param enabled + * @param device + */ + //TODO make as API if needed for UX, remove otherwise + //@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + //@RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void setEnabledForDevice(boolean enabled, + @NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(device); + mAm.setSpatializerEnabledForDevice(enabled, device); + } + + /** + * @hide + * Enables / disables the spatializer effect + * @param enabled {@code true} for enabling the effect + */ + //TODO make as API if needed for UX, remove otherwise + //@SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public void setEnabled(boolean enabled) { + mAm.setSpatializerFeatureEnabled(enabled); + } + + /** + * An interface to be notified of changes to the state of the spatializer. + */ + public interface OnSpatializerEnabledChangedListener { + /** + * Called when the enabled state of the Spatializer changes + * @param enabled {@code true} if the Spatializer effect is enabled on the device, + * {@code false} otherwise + */ + void onSpatializerEnabledChanged(boolean enabled); + } + + /** + * 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 true if the device is capable of spatializing the combination of audio format and + * attributes. + */ + public boolean canBeSpatialized( + @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { + return mAm.canBeSpatialized( + Objects.requireNonNull(attributes), Objects.requireNonNull(format)); + } + + /** + * Adds a listener to be notified of changes to the enabled state of the + * {@code Spatializer}. + * @see #isEnabled() + * @param executor the {@code Executor} handling the callback + * @param listener the listener to receive enabled state updates + */ + public void addOnSpatializerEnabledChangedListener( + @NonNull @CallbackExecutor Executor executor, + @NonNull OnSpatializerEnabledChangedListener listener) { + Objects.requireNonNull(executor); + Objects.requireNonNull(listener); + synchronized (mStateListenerLock) { + if (hasSpatializerStateListener(listener)) { + throw new IllegalArgumentException( + "Called addOnSpatializerEnabledChangedListener() " + + "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}. + * @see #isEnabled() + * @param listener the listener to receive enabled state updates + */ + public void removeOnSpatializerEnabledChangedListener( + @NonNull OnSpatializerEnabledChangedListener listener) { + Objects.requireNonNull(listener); + synchronized (mStateListenerLock) { + if (!removeStateListener(listener)) { + throw new IllegalArgumentException( + "Called removeOnSpatializerEnabledChangedListener() " + + "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_AUDIO_ROUTING) + public @NonNull List getCompatibleAudioDevices() { + return mAm.getSpatializerCompatibleAudioDevices(); + } + + /** + * @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_AUDIO_ROUTING) + public void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + mAm.addSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); + } + + /** + * @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_AUDIO_ROUTING) + public void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + mAm.removeSpatializerCompatibleAudioDevice(Objects.requireNonNull(ada)); + } + + private final class SpatializerInfoDispatcherStub + extends ISpatializerCallback.Stub { + @Override + public void dispatchSpatializerStateChanged(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(enabled)); + } + } + } + } + + private static class StateListenerInfo { + final @NonNull OnSpatializerEnabledChangedListener mListener; + final @NonNull Executor mExecutor; + + StateListenerInfo(@NonNull OnSpatializerEnabledChangedListener listener, + @NonNull Executor exe) { + mListener = listener; + mExecutor = exe; + } + } + + @GuardedBy("mStateListenerLock") + private boolean hasSpatializerStateListener(OnSpatializerEnabledChangedListener listener) { + return getStateListenerInfo(listener) != null; + } + + @GuardedBy("mStateListenerLock") + private @Nullable StateListenerInfo getStateListenerInfo( + OnSpatializerEnabledChangedListener 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(OnSpatializerEnabledChangedListener 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 34e2578f7855f..50a758e66dcc4 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; @@ -8227,6 +8229,82 @@ public class AudioService extends IAudioService.Stub return true; } + //========================================================================================== + private final SpatializerHelper mSpatializerHelper = new SpatializerHelper(); + + /** @see AudioManager#getSpatializerImmersiveAudioLevel() */ + public int getSpatializerImmersiveAudioLevel() { + return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; + } + + /** @see Spatializer#isEnabled() */ + public boolean isSpatializerEnabled() { + return false; + } + + /** @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) { + enforceModifyAudioRoutingPermission(); + Objects.requireNonNull(ada); + mSpatializerHelper.addCompatibleAudioDevice(ada); + } + + /** @see Spatializer#removeSpatializerCompatibleAudioDevice(AudioDeviceAttributes) */ + public void removeSpatializerCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + enforceModifyAudioRoutingPermission(); + Objects.requireNonNull(ada); + mSpatializerHelper.removeCompatibleAudioDevice(ada); + } + + /** @see AudioManager#setSpatializerFeatureEnabled(boolean) */ + public void setSpatializerFeatureEnabled(boolean enabled) { + if (mContext.checkCallingOrSelfPermission( + android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("Missing MODIFY_DEFAULT_AUDIO_EFFECTS permission"); + } + mSpatializerHelper.setEnabled(enabled); + } + + /** @see Spatializer#setEnabledForDevice(boolean, AudioDeviceAttributes) + * @see Spatializer#setEnabled(boolean) + */ + public void setSpatializerEnabledForDevice(boolean enabled, + @NonNull AudioDeviceAttributes ada) { + enforceModifyAudioRoutingPermission(); + Objects.requireNonNull(ada); + mSpatializerHelper.setEnabledForDevice(enabled, 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..1b68f840e2986 --- /dev/null +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -0,0 +1,126 @@ +/* + * 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.annotation.Nullable; +import android.media.AudioAttributes; +import android.media.AudioDeviceAttributes; +import android.media.AudioFormat; +import android.media.ISpatializerCallback; +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.NotificationHelper"; + + //--------------------------------------------------------------- + // audio device compatibility / enabled + + private final ArrayList mCompatibleAudioDevices = new ArrayList<>(0); + private final ArrayList mEnabledAudioDevices = 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); + } + // by default, adding a compatible device enables it + if (!mEnabledAudioDevices.contains(ada)) { + mEnabledAudioDevices.add(ada); + } + } + + synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { + mCompatibleAudioDevices.remove(ada); + mEnabledAudioDevices.remove(ada); + } + + synchronized void setEnabledForDevice(boolean enabled, @NonNull AudioDeviceAttributes ada) { + if (enabled) { + if (!mEnabledAudioDevices.contains(ada)) { + mEnabledAudioDevices.add(ada); + } + } else { + mEnabledAudioDevices.remove(ada); + } + } + + //------------------------------------------------------ + // enabled state + + // global state of feature + boolean mFeatureEnabled = false; + + synchronized void setEnabled(boolean enabled) { + final boolean oldState = mFeatureEnabled; + mFeatureEnabled = enabled; + if (oldState != enabled) { + dispatchState(); + } + } + + 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 dispatchState() { + // TODO check enabled state based on available devices + // (current implementation takes state as-is and dispatches it to listeners + final int nbCallbacks = mStateCallbacks.beginBroadcast(); + for (int i = 0; i < nbCallbacks; i++) { + try { + mStateCallbacks.getBroadcastItem(i) + .dispatchSpatializerStateChanged(mFeatureEnabled); + } catch (RemoteException e) { + Log.e(TAG, "Error in dispatchSpatializerStateChanged", e); + } + } + mStateCallbacks.finishBroadcast(); + } + + //------------------------------------------------------ + // virtualization capabilities + synchronized boolean canBeSpatialized( + @NonNull AudioAttributes attributes, @NonNull AudioFormat format) { + // TODO hook up to spatializer effect for query + return false; + } +}