diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ab38d12e593bd..c0c8a0df6a6d7 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -6285,6 +6285,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); @@ -6292,6 +6295,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 d2c6c13653e72..3e5e4357b8f4b 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -8574,6 +8574,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; } }