From f474524ab4d66496ee9681d996ecd49309aa8c55 Mon Sep 17 00:00:00 2001 From: Jean-Michel Trivi Date: Sun, 6 Feb 2022 21:55:16 -0800 Subject: [PATCH] Spatializer: more SysApi for availability, head tracker Add @SystemApi to check if the Spatializer will be available for a given device, if it has a head tracker, if enabled, and change its tracker enabled state. Implement checking device compatibility with transaural and binaural modes. Create list of compatible devices, list of devices that have been enabled, and list of disabled. This is to support the V3 UI where add/removeCompatibleDevice is used to control Spatial Audio state for a subset of output devices. Bug: 210803914 Test: atest SpatializerTest Change-Id: I0acf0c0310dbd503de2ea87328fca7e5597ae32d --- core/api/system-current.txt | 4 + media/java/android/media/IAudioService.aidl | 8 + media/java/android/media/Spatializer.java | 77 +++ .../android/server/audio/AudioService.java | 24 + .../server/audio/SpatializerHelper.java | 471 +++++++++++++++++- 5 files changed, 566 insertions(+), 18 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1adaf4b82ea78..97491ac5145e7 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -6266,6 +6266,9 @@ package android.media { method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getHeadTrackingMode(); method @IntRange(from=0) @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public int getOutput(); method @NonNull @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public java.util.List getSupportedHeadTrackingModes(); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public boolean hasHeadTracker(@NonNull android.media.AudioDeviceAttributes); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public boolean isAvailableForDevice(@NonNull android.media.AudioDeviceAttributes); + method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public boolean isHeadTrackerEnabled(@NonNull android.media.AudioDeviceAttributes); 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); @@ -6273,6 +6276,7 @@ package android.media { method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setEffectParameter(int, @NonNull byte[]); 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 setHeadTrackerEnabled(boolean, @NonNull android.media.AudioDeviceAttributes); method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnHeadToSoundstagePoseUpdatedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnHeadToSoundstagePoseUpdatedListener); method @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public void setOnSpatializerOutputChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.Spatializer.OnSpatializerOutputChangedListener); field @RequiresPermission("android.permission.MODIFY_DEFAULT_AUDIO_EFFECTS") public static final int HEAD_TRACKING_MODE_DISABLED = -1; // 0xffffffff diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 8ba3539edc584..a6fdf6cedd55d 100755 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -402,6 +402,14 @@ interface IAudioService { boolean isSpatializerAvailable(); + boolean isSpatializerAvailableForDevice(in AudioDeviceAttributes device); + + boolean hasHeadTracker(in AudioDeviceAttributes device); + + void setHeadTrackerEnabled(boolean enabled, in AudioDeviceAttributes device); + + boolean isHeadTrackerEnabled(in AudioDeviceAttributes device); + void setSpatializerEnabled(boolean enabled); boolean canBeSpatialized(in AudioAttributes aa, in AudioFormat af); diff --git a/media/java/android/media/Spatializer.java b/media/java/android/media/Spatializer.java index 030d212825ee2..be0ef37345ed3 100644 --- a/media/java/android/media/Spatializer.java +++ b/media/java/android/media/Spatializer.java @@ -108,6 +108,83 @@ public class Spatializer { } } + /** + * @hide + * Returns whether spatialization is available for a given audio device + * Reasons for spatialization being unavailable include situations where audio output is + * incompatible with sound spatialization, such as the device being a monophonic speaker, or + * the spatializer effect not supporting transaural processing when querying for speaker. + * @param device the audio device for which spatializer availability is queried + * @return {@code true} if the spatializer effect is available and capable + * of processing the audio over the given audio device, + * {@code false} otherwise. + * @see #isEnabled() + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public boolean isAvailableForDevice(@NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(device); + try { + return mAm.getService().isSpatializerAvailableForDevice(device); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return false; + } + + /** + * @hide + * Returns whether the given device has an associated headtracker + * @param device the audio device to query + * @return true if the device has a head tracker, false otherwise + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public boolean hasHeadTracker(@NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(device); + try { + return mAm.getService().hasHeadTracker(device); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return false; + } + + /** + * @hide + * Enables or disables the head tracker of the given device + * @param enabled true to enable, false to disable + * @param device the device whose head tracker state is changed + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public void setHeadTrackerEnabled(boolean enabled, @NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(device); + try { + mAm.getService().setHeadTrackerEnabled(enabled, device); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * @hide + * Returns whether the head tracker of the device is enabled + * @param device the device to query + * @return true if the head tracker is enabled, false if disabled or if there isn't one + */ + @SystemApi(client = SystemApi.Client.PRIVILEGED_APPS) + @RequiresPermission(android.Manifest.permission.MODIFY_DEFAULT_AUDIO_EFFECTS) + public boolean isHeadTrackerEnabled(@NonNull AudioDeviceAttributes device) { + Objects.requireNonNull(device); + try { + return mAm.getService().isHeadTrackerEnabled(device); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return false; + } + /** @hide */ @IntDef(flag = false, value = { SPATIALIZER_IMMERSIVE_LEVEL_OTHER, diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index cedc7dbd727af..53e5494542f2b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -8570,6 +8570,30 @@ public class AudioService extends IAudioService.Stub return mSpatializerHelper.isAvailable(); } + /** @see Spatializer#isAvailableForDevice(AudioDeviceAttributes) */ + public boolean isSpatializerAvailableForDevice(@NonNull AudioDeviceAttributes device) { + enforceModifyDefaultAudioEffectsPermission(); + return mSpatializerHelper.isAvailableForDevice(Objects.requireNonNull(device)); + } + + /** @see Spatializer#hasHeadTracker(AudioDeviceAttributes) */ + public boolean hasHeadTracker(@NonNull AudioDeviceAttributes device) { + enforceModifyDefaultAudioEffectsPermission(); + return mSpatializerHelper.hasHeadTracker(Objects.requireNonNull(device)); + } + + /** @see Spatializer#setHeadTrackerEnabled(boolean, AudioDeviceAttributes) */ + public void setHeadTrackerEnabled(boolean enabled, @NonNull AudioDeviceAttributes device) { + enforceModifyDefaultAudioEffectsPermission(); + mSpatializerHelper.setHeadTrackerEnabled(enabled, Objects.requireNonNull(device)); + } + + /** @see Spatializer#isHeadTrackerEnabled(AudioDeviceAttributes) */ + public boolean isHeadTrackerEnabled(@NonNull AudioDeviceAttributes device) { + enforceModifyDefaultAudioEffectsPermission(); + return mSpatializerHelper.isHeadTrackerEnabled(Objects.requireNonNull(device)); + } + /** @see Spatializer#setSpatializerEnabled(boolean) */ public void setSpatializerEnabled(boolean enabled) { enforceModifyDefaultAudioEffectsPermission(); diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java index 106cbba913d62..63b27d82dab9b 100644 --- a/services/core/java/com/android/server/audio/SpatializerHelper.java +++ b/services/core/java/com/android/server/audio/SpatializerHelper.java @@ -23,6 +23,7 @@ import android.hardware.Sensor; import android.hardware.SensorManager; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; +import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioSystem; import android.media.INativeSpatializerCallback; @@ -33,11 +34,14 @@ import android.media.ISpatializerHeadTrackingCallback; import android.media.ISpatializerHeadTrackingModeCallback; import android.media.ISpatializerOutputCallback; import android.media.SpatializationLevel; +import android.media.SpatializationMode; import android.media.Spatializer; import android.media.SpatializerHeadTrackingMode; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; +import android.util.Pair; +import android.util.SparseIntArray; import java.io.PrintWriter; import java.util.ArrayList; @@ -69,10 +73,42 @@ public class SpatializerHelper { // TODO: replace with generic head tracker sensor name. // the current implementation refers to the "google" namespace but will be replaced // by an android name at the next API level revision, it is not Google-specific. - // Also see "TODO-HT" in onInitSensors() method private static final String HEADTRACKER_SENSOR = "com.google.hardware.sensor.hid_dynamic.headtracker"; + private static final SparseIntArray SPAT_MODE_FOR_DEVICE_TYPE = new SparseIntArray(15) { + { + append(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_WIRED_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL); + append(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, SpatializationMode.SPATIALIZER_BINAURAL); + // assumption for A2DP: mostly headsets + append(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, SpatializationMode.SPATIALIZER_BINAURAL); + append(AudioDeviceInfo.TYPE_DOCK, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_USB_ACCESSORY, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_USB_DEVICE, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_USB_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL); + append(AudioDeviceInfo.TYPE_LINE_ANALOG, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_LINE_DIGITAL, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_AUX_LINE, SpatializationMode.SPATIALIZER_TRANSAURAL); + append(AudioDeviceInfo.TYPE_HEARING_AID, SpatializationMode.SPATIALIZER_BINAURAL); + append(AudioDeviceInfo.TYPE_BLE_HEADSET, SpatializationMode.SPATIALIZER_BINAURAL); + append(AudioDeviceInfo.TYPE_BLE_SPEAKER, SpatializationMode.SPATIALIZER_TRANSAURAL); + // assumption that BLE broadcast would be mostly consumed on headsets + append(AudioDeviceInfo.TYPE_BLE_BROADCAST, SpatializationMode.SPATIALIZER_BINAURAL); + } + }; + + private static final int[] WIRELESS_TYPES = { AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + AudioDeviceInfo.TYPE_BLE_HEADSET, + AudioDeviceInfo.TYPE_BLE_SPEAKER, + AudioDeviceInfo.TYPE_BLE_BROADCAST + }; + + private static final int[] WIRELESS_SPEAKER_TYPES = { + AudioDeviceInfo.TYPE_BLE_SPEAKER, + }; + // Spatializer state machine private static final int STATE_UNINITIALIZED = 0; private static final int STATE_NOT_SUPPORTED = 1; @@ -82,11 +118,20 @@ public class SpatializerHelper { private static final int STATE_DISABLED_AVAILABLE = 6; private int mState = STATE_UNINITIALIZED; + private boolean mFeatureEnabled = false; /** 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 boolean mTransauralSupported = false; + private boolean mBinauralSupported = false; private int mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; private int mDesiredHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD; + /** + * The desired head tracking mode when enabling head tracking, tracks mDesiredHeadTrackingMode, + * except when head tracking gets disabled through setting the desired mode to + * {@link Spatializer#HEAD_TRACKING_MODE_DISABLED}. + */ + private int mDesiredHeadTrackingModeWhenEnabled = Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD; private int mSpatOutput = 0; private @Nullable ISpatializer mSpat; private @Nullable SpatializerCallback mSpatCallback; @@ -108,8 +153,18 @@ public class SpatializerHelper { //--------------------------------------------------------------- // audio device compatibility / enabled + /** + * List of device types that can be used on this device with Spatial Audio. + * It is initialized based on the transaural/binaural capabilities + * of the effect. + */ + private final ArrayList mSACapableDeviceTypes = new ArrayList<>(0); - private final ArrayList mCompatibleAudioDevices = new ArrayList<>(0); + /** + * List of devices where Spatial Audio is possible. Each device can be enabled or disabled + * (== user choice to use or not) + */ + private final ArrayList mSADevices = new ArrayList<>(0); //------------------------------------------------------ // initialization @@ -155,6 +210,46 @@ public class SpatializerHelper { break; } } + byte[] spatModes = spat.getSupportedModes(); + for (byte mode : spatModes) { + switch (mode) { + case SpatializationMode.SPATIALIZER_BINAURAL: + mBinauralSupported = true; + break; + case SpatializationMode.SPATIALIZER_TRANSAURAL: + mTransauralSupported = true; + break; + default: + Log.e(TAG, "Spatializer reports unknown supported mode:" + mode); + break; + } + } + // if neither transaural nor binaural is supported, bail + if (!mBinauralSupported && !mTransauralSupported) { + mState = STATE_NOT_SUPPORTED; + return; + } + + // initialize list of compatible devices + for (int i = 0; i < SPAT_MODE_FOR_DEVICE_TYPE.size(); i++) { + int mode = SPAT_MODE_FOR_DEVICE_TYPE.valueAt(i); + if ((mode == (int) SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported) + || (mode == (int) SpatializationMode.SPATIALIZER_TRANSAURAL + && mTransauralSupported)) { + mSACapableDeviceTypes.add(SPAT_MODE_FOR_DEVICE_TYPE.keyAt(i)); + } + } + if (mTransauralSupported) { + // TODO deal with persisted values + mSADevices.add( + new SADeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, null)); + } + if (mBinauralSupported) { + // TODO deal with persisted values + mSADevices.add( + new SADeviceState(AudioDeviceInfo.TYPE_WIRED_HEADPHONES, null)); + } + // TODO read persisted states } catch (RemoteException e) { /* capable level remains at NONE*/ } finally { @@ -184,12 +279,15 @@ public class SpatializerHelper { mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE; mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED; init(true); - setFeatureEnabled(featureEnabled); + setSpatializerEnabledInt(featureEnabled); } //------------------------------------------------------ // routing monitoring - void onRoutingUpdated() { + synchronized void onRoutingUpdated() { + if (!mFeatureEnabled) { + return; + } switch (mState) { case STATE_UNINITIALIZED: case STATE_NOT_SUPPORTED: @@ -201,11 +299,36 @@ public class SpatializerHelper { break; } mASA.getDevicesForAttributes(DEFAULT_ATTRIBUTES).toArray(ROUTING_DEVICES); - final boolean able = - AudioSystem.canBeSpatialized(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, ROUTING_DEVICES); - logd("onRoutingUpdated: can spatialize media 5.1:" + able - + " on device:" + ROUTING_DEVICES[0]); - setDispatchAvailableState(able); + + // is media routed to a new device? + if (isWireless(ROUTING_DEVICES[0].getType())) { + addWirelessDeviceIfNew(ROUTING_DEVICES[0]); + } + + // find if media device enabled / available + final Pair enabledAvailable = evaluateState(ROUTING_DEVICES[0]); + + boolean able = false; + if (enabledAvailable.second) { + // available for Spatial audio, check w/ effect + able = canBeSpatializedOnDevice(DEFAULT_ATTRIBUTES, DEFAULT_FORMAT, ROUTING_DEVICES); + Log.i(TAG, "onRoutingUpdated: can spatialize media 5.1:" + able + + " on device:" + ROUTING_DEVICES[0]); + setDispatchAvailableState(able); + } else { + Log.i(TAG, "onRoutingUpdated: device:" + ROUTING_DEVICES[0] + + " not available for Spatial Audio"); + setDispatchAvailableState(false); + } + + if (able && enabledAvailable.first) { + Log.i(TAG, "Enabling Spatial Audio since enabled for media device:" + + ROUTING_DEVICES[0]); + } else { + Log.i(TAG, "Disabling Spatial Audio since disabled for media device:" + + ROUTING_DEVICES[0]); + } + setDispatchFeatureEnabledState(able && enabledAvailable.first); if (mDesiredHeadTrackingMode != Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED && mDesiredHeadTrackingMode != Spatializer.HEAD_TRACKING_MODE_DISABLED) { @@ -298,20 +421,145 @@ public class SpatializerHelper { //------------------------------------------------------ // compatible devices /** - * @return a shallow copy of the list of compatible audio devices + * Return the list of compatible devices, which reflects the device compatible with the + * spatializer effect, and those that have been explicitly enabled or disabled + * @return the list of compatible audio devices */ synchronized @NonNull List getCompatibleAudioDevices() { - return (List) mCompatibleAudioDevices.clone(); + // build unionOf(mCompatibleAudioDevices, mEnabledDevice) - mDisabledAudioDevices + ArrayList compatList = new ArrayList<>(); + for (SADeviceState dev : mSADevices) { + if (dev.mEnabled) { + compatList.add(new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_OUTPUT, + dev.mDeviceType, dev.mDeviceAddress == null ? "" : dev.mDeviceAddress)); + } + } + return compatList; } synchronized void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { - if (!mCompatibleAudioDevices.contains(ada)) { - mCompatibleAudioDevices.add(ada); + // TODO add log + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + boolean updateRouting = false; + boolean isInList = false; + + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + isInList = true; + // state change? + updateRouting = !deviceState.mEnabled; + deviceState.mEnabled = true; + break; + } + } + if (!isInList) { + final SADeviceState dev = new SADeviceState(deviceType, + wireless ? ada.getAddress() : null); + dev.mEnabled = true; + mSADevices.add(dev); + updateRouting = true; + } + if (updateRouting) { + onRoutingUpdated(); } } synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) { - mCompatibleAudioDevices.remove(ada); + // TODO add log + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + boolean updateRouting = false; + + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + // state change? + updateRouting = deviceState.mEnabled; + deviceState.mEnabled = false; + break; + } + } + if (updateRouting) { + onRoutingUpdated(); + } + } + + /** + * Return if Spatial Audio is enabled and available for the given device + * @param ada + * @return a pair of boolean, 1/ enabled? 2/ available? + */ + private synchronized Pair evaluateState(AudioDeviceAttributes ada) { + // if not a wireless device, this value will be overwritten to map the type + // to TYPE_BUILTIN_SPEAKER or TYPE_WIRED_HEADPHONES + int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + + // if not a wireless device: find if media device is in the speaker, wired headphones + if (!wireless) { + // is the device type capable of doing SA? + if (!mSACapableDeviceTypes.contains(deviceType)) { + Log.i(TAG, "Device incompatible with Spatial Audio dev:" + ada); + return new Pair<>(false, false); + } + // what spatialization mode to use for this device? + final int spatMode = SPAT_MODE_FOR_DEVICE_TYPE.get(deviceType, Integer.MIN_VALUE); + if (spatMode == Integer.MIN_VALUE) { + // error case, device not found + Log.e(TAG, "no spatialization mode found for device type:" + deviceType); + return new Pair<>(false, false); + } + // map the spatialization mode to the SPEAKER or HEADPHONES device + if (spatMode == SpatializationMode.SPATIALIZER_TRANSAURAL) { + deviceType = AudioDeviceInfo.TYPE_BUILTIN_SPEAKER; + } else { + deviceType = AudioDeviceInfo.TYPE_WIRED_HEADPHONES; + } + } else { // wireless device + if (isWirelessSpeaker(deviceType) && !mTransauralSupported) { + Log.i(TAG, "Device incompatible with Spatial Audio (no transaural) dev:" + + ada); + return new Pair<>(false, false); + } + if (!mBinauralSupported) { + Log.i(TAG, "Device incompatible with Spatial Audio (no binaural) dev:" + + ada); + return new Pair<>(false, false); + } + } + + boolean enabled = false; + boolean available = false; + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + available = true; + enabled = deviceState.mEnabled; + break; + } + } + return new Pair<>(enabled, available); + } + + private synchronized void addWirelessDeviceIfNew(@NonNull AudioDeviceAttributes ada) { + boolean knownDevice = false; + for (SADeviceState deviceState : mSADevices) { + // wireless device so always check address + if (ada.getType() == deviceState.mDeviceType + && ada.getAddress().equals(deviceState.mDeviceAddress)) { + knownDevice = true; + break; + } + } + if (!knownDevice) { + mSADevices.add(new SADeviceState(ada.getType(), ada.getAddress())); + //### TODO persist list + } } //------------------------------------------------------ @@ -345,7 +593,56 @@ public class SpatializerHelper { } } + synchronized boolean isAvailableForDevice(@NonNull AudioDeviceAttributes ada) { + if (ada.getRole() != AudioDeviceAttributes.ROLE_OUTPUT) { + return false; + } + + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + return true; + } + } + return false; + } + + private synchronized boolean canBeSpatializedOnDevice(@NonNull AudioAttributes attributes, + @NonNull AudioFormat format, @NonNull AudioDeviceAttributes[] devices) { + final byte modeForDevice = (byte) SPAT_MODE_FOR_DEVICE_TYPE.get(devices[0].getType(), + /*default when type not found*/ SpatializationMode.SPATIALIZER_BINAURAL); + if ((modeForDevice == SpatializationMode.SPATIALIZER_BINAURAL && mBinauralSupported) + || (modeForDevice == SpatializationMode.SPATIALIZER_TRANSAURAL + && mTransauralSupported)) { + return AudioSystem.canBeSpatialized(attributes, format, devices); + } + return false; + } + synchronized void setFeatureEnabled(boolean enabled) { + if (mFeatureEnabled == enabled) { + return; + } + mFeatureEnabled = enabled; + if (mFeatureEnabled) { + if (mState == STATE_NOT_SUPPORTED) { + Log.e(TAG, "Can't enabled Spatial Audio, unsupported"); + return; + } + if (mState == STATE_UNINITIALIZED) { + init(true); + } + setSpatializerEnabledInt(true); + onRoutingUpdated(); + } else { + setSpatializerEnabledInt(false); + } + } + + synchronized void setSpatializerEnabledInt(boolean enabled) { switch (mState) { case STATE_UNINITIALIZED: if (enabled) { @@ -397,8 +694,7 @@ public class SpatializerHelper { } /** - * precondition: mState = STATE_* - * isFeatureEnabled() != featureEnabled + * Update the feature state, no-op if no change * @param featureEnabled */ private synchronized void setDispatchFeatureEnabledState(boolean featureEnabled) { @@ -410,6 +706,10 @@ public class SpatializerHelper { case STATE_DISABLED_AVAILABLE: mState = STATE_ENABLED_AVAILABLE; break; + case STATE_ENABLED_AVAILABLE: + case STATE_ENABLED_UNAVAILABLE: + // already enabled: no-op + return; default: throw(new IllegalStateException("Invalid mState:" + mState + " for enabled true")); @@ -422,6 +722,10 @@ public class SpatializerHelper { case STATE_ENABLED_AVAILABLE: mState = STATE_DISABLED_AVAILABLE; break; + case STATE_DISABLED_AVAILABLE: + case STATE_DISABLED_UNAVAILABLE: + // already disabled: no-op + return; default: throw (new IllegalStateException("Invalid mState:" + mState + " for enabled false")); @@ -562,7 +866,7 @@ public class SpatializerHelper { AudioDeviceAttributes[] devices = new AudioDeviceAttributes[1]; // going through adapter to take advantage of routing cache mASA.getDevicesForAttributes(attributes).toArray(devices); - final boolean able = AudioSystem.canBeSpatialized(attributes, format, devices); + final boolean able = canBeSpatializedOnDevice(attributes, format, devices); logd("canBeSpatialized returning " + able); return able; } @@ -688,6 +992,9 @@ public class SpatializerHelper { if (!checkSpatForHeadTracking("setDesiredHeadTrackingMode")) { return; } + if (mode != Spatializer.HEAD_TRACKING_MODE_DISABLED) { + mDesiredHeadTrackingModeWhenEnabled = mode; + } try { if (mDesiredHeadTrackingMode != mode) { mDesiredHeadTrackingMode = mode; @@ -701,6 +1008,84 @@ public class SpatializerHelper { } } + synchronized void setHeadTrackerEnabled(boolean enabled, @NonNull AudioDeviceAttributes ada) { + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + if (!deviceState.mHasHeadTracker) { + Log.e(TAG, "Called setHeadTrackerEnabled enabled:" + enabled + + " device:" + ada + " on a device without headtracker"); + return; + } + Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada); + deviceState.mHeadTrackerEnabled = enabled; + break; + } + } + // check current routing to see if it affects the headtracking mode + if (ROUTING_DEVICES[0].getType() == deviceType + && ROUTING_DEVICES[0].getAddress().equals(ada.getAddress())) { + setDesiredHeadTrackingMode(enabled ? mDesiredHeadTrackingModeWhenEnabled + : Spatializer.HEAD_TRACKING_MODE_DISABLED); + } + } + + synchronized boolean hasHeadTracker(@NonNull AudioDeviceAttributes ada) { + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + return deviceState.mHasHeadTracker; + } + } + return false; + } + + /** + * Configures device in list as having a head tracker + * @param ada + * @return true if the head tracker is enabled, false otherwise or if device not found + */ + synchronized boolean setHasHeadTracker(@NonNull AudioDeviceAttributes ada) { + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + deviceState.mHasHeadTracker = true; + return deviceState.mHeadTrackerEnabled; + } + } + Log.e(TAG, "setHasHeadTracker: device not found for:" + ada); + return false; + } + + synchronized boolean isHeadTrackerEnabled(@NonNull AudioDeviceAttributes ada) { + final int deviceType = ada.getType(); + final boolean wireless = isWireless(deviceType); + + for (SADeviceState deviceState : mSADevices) { + if (deviceType == deviceState.mDeviceType + && (wireless && ada.getAddress().equals(deviceState.mDeviceAddress)) + || !wireless) { + if (!deviceState.mHasHeadTracker) { + return false; + } + return deviceState.mHeadTrackerEnabled; + } + } + return false; + } + private boolean checkSpatForHeadTracking(String funcName) { switch (mState) { case STATE_UNINITIALIZED: @@ -884,7 +1269,7 @@ public class SpatializerHelper { } synchronized void onInitSensors() { - final boolean init = (mSpatLevel != SpatializationLevel.NONE); + final boolean init = mFeatureEnabled && (mSpatLevel != SpatializationLevel.NONE); final String action = init ? "initializing" : "releasing"; if (mSpat == null) { Log.e(TAG, "not " + action + " sensors, null spatializer"); @@ -926,6 +1311,12 @@ public class SpatializerHelper { UUID uuid = sensor.getUuid(); if (uuid.equals(routingDeviceUuid)) { headHandle = sensor.getHandle(); + // TODO check risk of race condition: + // does this happen before routing is updated? + // avoid by supporting adding device here AND in onRoutingUpdated() + if (!setHasHeadTracker(ROUTING_DEVICES[0])) { + headHandle = -1; + } break; } if (uuid.equals(UuidUtils.STANDALONE_UUID)) { @@ -933,6 +1324,7 @@ public class SpatializerHelper { } } } + Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); screenHandle = screenSensor.getHandle(); } else { @@ -1017,7 +1409,50 @@ public class SpatializerHelper { for (int mode : modes) { modesString += Spatializer.headtrackingModeToString(mode) + " "; } + pw.println("\tsupports binaural:" + mBinauralSupported + " / transaural" + + mTransauralSupported); pw.println("\tsupported head tracking modes:" + modesString); pw.println("\tmSpatOutput:" + mSpatOutput); + pw.println("\tdevices:\n"); + for (SADeviceState device : mSADevices) { + pw.println("\t\t" + device + "\n"); + } + } + + private static final class SADeviceState { + final int mDeviceType; + final @Nullable String mDeviceAddress; // non-null for wireless devices + boolean mEnabled = true; // by default, SA is enabled on any device + boolean mHasHeadTracker = false; + boolean mHeadTrackerEnabled = true; // by default, if head tracker is present, use it + + SADeviceState(int deviceType, @Nullable String address) { + mDeviceType = deviceType; + mDeviceAddress = address; + } + + @Override + public String toString() { + return "type:" + mDeviceType + " addr:" + mDeviceAddress + " enabled:" + mEnabled + + " HT:" + mHasHeadTracker + " HTenabled:" + mHeadTrackerEnabled; + } + } + + private static boolean isWireless(int deviceType) { + for (int type : WIRELESS_TYPES) { + if (type == deviceType) { + return true; + } + } + return false; + } + + private static boolean isWirelessSpeaker(int deviceType) { + for (int type: WIRELESS_SPEAKER_TYPES) { + if (type == deviceType) { + return true; + } + } + return false; } }