blob: e6789d5e1d492ea09eb4cc87ba12ab5075d09bcd [file] [log] [blame]
/*
* 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.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.media.AudioAttributes;
import android.media.AudioDeviceAttributes;
import android.media.AudioFormat;
import android.media.AudioSystem;
import android.media.INativeSpatializerCallback;
import android.media.ISpatializer;
import android.media.ISpatializerCallback;
import android.media.ISpatializerHeadToSoundStagePoseCallback;
import android.media.ISpatializerHeadTrackingCallback;
import android.media.ISpatializerHeadTrackingModeCallback;
import android.media.ISpatializerOutputCallback;
import android.media.SpatializationLevel;
import android.media.Spatializer;
import android.media.SpatializerHeadTrackingMode;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* A helper class to manage Spatializer related functionality
*/
public class SpatializerHelper {
private static final String TAG = "AS.SpatializerHelper";
private static final boolean DEBUG = true;
private static final boolean DEBUG_MORE = false;
private static void logd(String s) {
if (DEBUG) {
Log.i(TAG, s);
}
}
private final @NonNull AudioSystemAdapter mASA;
private final @NonNull AudioService mAudioService;
private @Nullable SensorManager mSensorManager;
//------------------------------------------------------------
/** head tracker sensor name */
// 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";
// Spatializer state machine
private static final int STATE_UNINITIALIZED = 0;
private static final int STATE_NOT_SUPPORTED = 1;
private static final int STATE_DISABLED_UNAVAILABLE = 3;
private static final int STATE_ENABLED_UNAVAILABLE = 4;
private static final int STATE_ENABLED_AVAILABLE = 5;
private static final int STATE_DISABLED_AVAILABLE = 6;
private int mState = STATE_UNINITIALIZED;
/** current level as reported by native Spatializer in callback */
private int mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
private int mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
private int mDesiredHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
private int mSpatOutput = 0;
private @Nullable ISpatializer mSpat;
private @Nullable SpatializerCallback mSpatCallback;
private @Nullable SpatializerHeadTrackingCallback mSpatHeadTrackingCallback;
private @Nullable HelperDynamicSensorCallback mDynSensorCallback;
// default attributes and format that determine basic availability of spatialization
private static final AudioAttributes DEFAULT_ATTRIBUTES = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build();
private static final AudioFormat DEFAULT_FORMAT = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(48000)
.setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
.build();
// device array to store the routing for the default attributes and format, size 1 because
// media is never expected to be duplicated
private static final AudioDeviceAttributes[] ROUTING_DEVICES = new AudioDeviceAttributes[1];
//---------------------------------------------------------------
// audio device compatibility / enabled
private final ArrayList<AudioDeviceAttributes> mCompatibleAudioDevices = new ArrayList<>(0);
//------------------------------------------------------
// initialization
SpatializerHelper(@NonNull AudioService mother, @NonNull AudioSystemAdapter asa) {
mAudioService = mother;
mASA = asa;
}
synchronized void init(boolean effectExpected) {
Log.i(TAG, "Initializing");
if (!effectExpected) {
Log.i(TAG, "Setting state to STATE_NOT_SUPPORTED due to effect not expected");
mState = STATE_NOT_SUPPORTED;
return;
}
if (mState != STATE_UNINITIALIZED) {
throw new IllegalStateException(("init() called in state:" + mState));
}
// is there a spatializer?
mSpatCallback = new SpatializerCallback();
final ISpatializer spat = AudioSystem.getSpatializer(mSpatCallback);
if (spat == null) {
Log.i(TAG, "init(): No Spatializer found");
mState = STATE_NOT_SUPPORTED;
return;
}
// capabilities of spatializer?
try {
byte[] levels = spat.getSupportedLevels();
if (levels == null
|| levels.length == 0
|| (levels.length == 1
&& levels[0] == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE)) {
Log.e(TAG, "Spatializer is useless");
mState = STATE_NOT_SUPPORTED;
return;
}
for (byte level : levels) {
logd("found support for level: " + level);
if (level == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL) {
logd("Setting capable level to LEVEL_MULTICHANNEL");
mCapableSpatLevel = level;
break;
}
}
} catch (RemoteException e) {
/* capable level remains at NONE*/
} finally {
if (spat != null) {
try {
spat.release();
} catch (RemoteException e) { /* capable level remains at NONE*/ }
}
}
if (mCapableSpatLevel == Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE) {
mState = STATE_NOT_SUPPORTED;
return;
}
mState = STATE_DISABLED_UNAVAILABLE;
// note at this point mSpat is still not instantiated
}
/**
* Like init() but resets the state and spatializer levels
* @param featureEnabled
*/
synchronized void reset(boolean featureEnabled) {
Log.i(TAG, "Resetting");
mState = STATE_UNINITIALIZED;
mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
mActualHeadTrackingMode = Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
init(true);
setFeatureEnabled(featureEnabled);
}
//------------------------------------------------------
// routing monitoring
void onRoutingUpdated() {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
return;
case STATE_DISABLED_UNAVAILABLE:
case STATE_ENABLED_UNAVAILABLE:
case STATE_ENABLED_AVAILABLE:
case STATE_DISABLED_AVAILABLE:
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);
}
//------------------------------------------------------
// spatializer callback from native
private final class SpatializerCallback extends INativeSpatializerCallback.Stub {
public void onLevelChanged(byte level) {
logd("SpatializerCallback.onLevelChanged level:" + level);
synchronized (SpatializerHelper.this) {
mSpatLevel = spatializationLevelToSpatializerInt(level);
}
// TODO use reported spat level to change state
// init sensors
postInitSensors();
}
public void onOutputChanged(int output) {
logd("SpatializerCallback.onOutputChanged output:" + output);
int oldOutput;
synchronized (SpatializerHelper.this) {
oldOutput = mSpatOutput;
mSpatOutput = output;
}
if (oldOutput != output) {
dispatchOutputUpdate(output);
}
}
};
//------------------------------------------------------
// spatializer head tracking callback from native
private final class SpatializerHeadTrackingCallback
extends ISpatializerHeadTrackingCallback.Stub {
public void onHeadTrackingModeChanged(byte mode) {
logd("SpatializerHeadTrackingCallback.onHeadTrackingModeChanged mode:" + mode);
int oldMode, newMode;
synchronized (this) {
oldMode = mActualHeadTrackingMode;
mActualHeadTrackingMode = headTrackingModeTypeToSpatializerInt(mode);
newMode = mActualHeadTrackingMode;
}
if (oldMode != newMode) {
dispatchActualHeadTrackingMode(newMode);
}
}
public void onHeadToSoundStagePoseUpdated(float[] headToStage) {
if (headToStage == null) {
Log.e(TAG, "SpatializerHeadTrackingCallback.onHeadToStagePoseUpdated"
+ "null transform");
return;
}
if (headToStage.length != 6) {
Log.e(TAG, "SpatializerHeadTrackingCallback.onHeadToStagePoseUpdated"
+ " invalid transform length" + headToStage.length);
return;
}
if (DEBUG_MORE) {
// 6 values * (4 digits + 1 dot + 2 brackets) = 42 characters
StringBuilder t = new StringBuilder(42);
for (float val : headToStage) {
t.append("[").append(String.format(Locale.ENGLISH, "%.3f", val)).append("]");
}
logd("SpatializerHeadTrackingCallback.onHeadToStagePoseUpdated headToStage:" + t);
}
dispatchPoseUpdate(headToStage);
}
};
//------------------------------------------------------
// dynamic sensor callback
private final class HelperDynamicSensorCallback extends SensorManager.DynamicSensorCallback {
@Override
public void onDynamicSensorConnected(Sensor sensor) {
postInitSensors();
}
@Override
public void onDynamicSensorDisconnected(Sensor sensor) {
postInitSensors();
}
}
//------------------------------------------------------
// compatible devices
/**
* @return a shallow copy of the list of compatible audio devices
*/
synchronized @NonNull List<AudioDeviceAttributes> getCompatibleAudioDevices() {
return (List<AudioDeviceAttributes>) mCompatibleAudioDevices.clone();
}
synchronized void addCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
if (!mCompatibleAudioDevices.contains(ada)) {
mCompatibleAudioDevices.add(ada);
}
}
synchronized void removeCompatibleAudioDevice(@NonNull AudioDeviceAttributes ada) {
mCompatibleAudioDevices.remove(ada);
}
//------------------------------------------------------
// states
synchronized boolean isEnabled() {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
return false;
case STATE_ENABLED_UNAVAILABLE:
case STATE_ENABLED_AVAILABLE:
default:
return true;
}
}
synchronized boolean isAvailable() {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
return false;
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
default:
return true;
}
}
synchronized void setFeatureEnabled(boolean enabled) {
switch (mState) {
case STATE_UNINITIALIZED:
if (enabled) {
throw(new IllegalStateException("Can't enable when uninitialized"));
}
return;
case STATE_NOT_SUPPORTED:
if (enabled) {
Log.e(TAG, "Can't enable when unsupported");
}
return;
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
if (enabled) {
createSpat();
break;
} else {
// already in disabled state
return;
}
case STATE_ENABLED_UNAVAILABLE:
case STATE_ENABLED_AVAILABLE:
if (!enabled) {
releaseSpat();
break;
} else {
// already in enabled state
return;
}
}
setDispatchFeatureEnabledState(enabled);
}
synchronized int getCapableImmersiveAudioLevel() {
return mCapableSpatLevel;
}
final RemoteCallbackList<ISpatializerCallback> mStateCallbacks =
new RemoteCallbackList<ISpatializerCallback>();
synchronized void registerStateCallback(
@NonNull ISpatializerCallback callback) {
mStateCallbacks.register(callback);
}
synchronized void unregisterStateCallback(
@NonNull ISpatializerCallback callback) {
mStateCallbacks.unregister(callback);
}
/**
* precondition: mState = STATE_*
* isFeatureEnabled() != featureEnabled
* @param featureEnabled
*/
private synchronized void setDispatchFeatureEnabledState(boolean featureEnabled) {
if (featureEnabled) {
switch (mState) {
case STATE_DISABLED_UNAVAILABLE:
mState = STATE_ENABLED_UNAVAILABLE;
break;
case STATE_DISABLED_AVAILABLE:
mState = STATE_ENABLED_AVAILABLE;
break;
default:
throw(new IllegalStateException("Invalid mState:" + mState
+ " for enabled true"));
}
} else {
switch (mState) {
case STATE_ENABLED_UNAVAILABLE:
mState = STATE_DISABLED_UNAVAILABLE;
break;
case STATE_ENABLED_AVAILABLE:
mState = STATE_DISABLED_AVAILABLE;
break;
default:
throw (new IllegalStateException("Invalid mState:" + mState
+ " for enabled false"));
}
}
final int nbCallbacks = mStateCallbacks.beginBroadcast();
for (int i = 0; i < nbCallbacks; i++) {
try {
mStateCallbacks.getBroadcastItem(i)
.dispatchSpatializerEnabledChanged(featureEnabled);
} catch (RemoteException e) {
Log.e(TAG, "Error in dispatchSpatializerEnabledChanged", e);
}
}
mStateCallbacks.finishBroadcast();
mAudioService.persistSpatialAudioEnabled(featureEnabled);
}
private synchronized void setDispatchAvailableState(boolean available) {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
throw(new IllegalStateException(
"Should not update available state in state:" + mState));
case STATE_DISABLED_UNAVAILABLE:
if (available) {
mState = STATE_DISABLED_AVAILABLE;
break;
} else {
// already in unavailable state
return;
}
case STATE_ENABLED_UNAVAILABLE:
if (available) {
mState = STATE_ENABLED_AVAILABLE;
break;
} else {
// already in unavailable state
return;
}
case STATE_DISABLED_AVAILABLE:
if (available) {
// already in available state
return;
} else {
mState = STATE_DISABLED_UNAVAILABLE;
break;
}
case STATE_ENABLED_AVAILABLE:
if (available) {
// already in available state
return;
} else {
mState = STATE_ENABLED_UNAVAILABLE;
break;
}
}
final int nbCallbacks = mStateCallbacks.beginBroadcast();
for (int i = 0; i < nbCallbacks; i++) {
try {
mStateCallbacks.getBroadcastItem(i)
.dispatchSpatializerAvailableChanged(available);
} catch (RemoteException e) {
Log.e(TAG, "Error in dispatchSpatializerEnabledChanged", e);
}
}
mStateCallbacks.finishBroadcast();
}
//------------------------------------------------------
// native Spatializer management
/**
* precondition: mState == STATE_DISABLED_*
*/
private void createSpat() {
if (mSpat == null) {
mSpatCallback = new SpatializerCallback();
mSpatHeadTrackingCallback = new SpatializerHeadTrackingCallback();
mSpat = AudioSystem.getSpatializer(mSpatCallback);
try {
mSpat.setLevel((byte) Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL);
//TODO: register heatracking callback only when sensors are registered
if (mSpat.isHeadTrackingSupported()) {
mSpat.registerHeadTrackingCallback(mSpatHeadTrackingCallback);
}
} catch (RemoteException e) {
Log.e(TAG, "Can't set spatializer level", e);
mState = STATE_NOT_SUPPORTED;
mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
}
}
}
/**
* precondition: mState == STATE_ENABLED_*
*/
private void releaseSpat() {
if (mSpat != null) {
mSpatCallback = null;
try {
mSpat.registerHeadTrackingCallback(null);
mSpat.release();
mSpat = null;
} catch (RemoteException e) {
Log.e(TAG, "Can't set release spatializer cleanly", e);
}
}
}
//------------------------------------------------------
// virtualization capabilities
synchronized boolean canBeSpatialized(
@NonNull AudioAttributes attributes, @NonNull AudioFormat format) {
logd("canBeSpatialized usage:" + attributes.getUsage()
+ " format:" + format.toLogFriendlyString());
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
logd("canBeSpatialized false due to state:" + mState);
return false;
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
break;
}
// filter on AudioAttributes usage
switch (attributes.getUsage()) {
case AudioAttributes.USAGE_MEDIA:
case AudioAttributes.USAGE_GAME:
break;
default:
logd("canBeSpatialized false due to usage:" + attributes.getUsage());
return false;
}
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);
logd("canBeSpatialized returning " + able);
return able;
}
//------------------------------------------------------
// head tracking
final RemoteCallbackList<ISpatializerHeadTrackingModeCallback> mHeadTrackingModeCallbacks =
new RemoteCallbackList<ISpatializerHeadTrackingModeCallback>();
synchronized void registerHeadTrackingModeCallback(
@NonNull ISpatializerHeadTrackingModeCallback callback) {
mHeadTrackingModeCallbacks.register(callback);
}
synchronized void unregisterHeadTrackingModeCallback(
@NonNull ISpatializerHeadTrackingModeCallback callback) {
mHeadTrackingModeCallbacks.unregister(callback);
}
synchronized int[] getSupportedHeadTrackingModes() {
switch (mState) {
case STATE_UNINITIALIZED:
return new int[0];
case STATE_NOT_SUPPORTED:
// return an empty list when Spatializer functionality is not supported
// because the list of head tracking modes you can set is actually empty
// as defined in {@link Spatializer#getSupportedHeadTrackingModes()}
return new int[0];
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
if (mSpat == null) {
return new int[0];
}
break;
}
// mSpat != null
try {
final byte[] values = mSpat.getSupportedHeadTrackingModes();
ArrayList<Integer> list = new ArrayList<>(0);
for (byte value : values) {
switch (value) {
case SpatializerHeadTrackingMode.OTHER:
case SpatializerHeadTrackingMode.DISABLED:
// not expected here, skip
break;
case SpatializerHeadTrackingMode.RELATIVE_WORLD:
case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
list.add(headTrackingModeTypeToSpatializerInt(value));
break;
default:
Log.e(TAG, "Unexpected head tracking mode:" + value,
new IllegalArgumentException("invalid mode"));
break;
}
}
int[] modes = new int[list.size()];
for (int i = 0; i < list.size(); i++) {
modes[i] = list.get(i);
}
return modes;
} catch (RemoteException e) {
Log.e(TAG, "Error calling getSupportedHeadTrackingModes", e);
return new int[] { Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED };
}
}
synchronized int getActualHeadTrackingMode() {
switch (mState) {
case STATE_UNINITIALIZED:
return Spatializer.HEAD_TRACKING_MODE_DISABLED;
case STATE_NOT_SUPPORTED:
return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
if (mSpat == null) {
return Spatializer.HEAD_TRACKING_MODE_DISABLED;
}
break;
}
// mSpat != null
try {
return headTrackingModeTypeToSpatializerInt(mSpat.getActualHeadTrackingMode());
} catch (RemoteException e) {
Log.e(TAG, "Error calling getActualHeadTrackingMode", e);
return Spatializer.HEAD_TRACKING_MODE_UNSUPPORTED;
}
}
synchronized int getDesiredHeadTrackingMode() {
return mDesiredHeadTrackingMode;
}
synchronized void setGlobalTransform(@NonNull float[] transform) {
if (transform.length != 6) {
throw new IllegalArgumentException("invalid array size" + transform.length);
}
if (!checkSpatForHeadTracking("setGlobalTransform")) {
return;
}
try {
mSpat.setGlobalTransform(transform);
} catch (RemoteException e) {
Log.e(TAG, "Error calling setGlobalTransform", e);
}
}
synchronized void recenterHeadTracker() {
if (!checkSpatForHeadTracking("recenterHeadTracker")) {
return;
}
try {
mSpat.recenterHeadTracker();
} catch (RemoteException e) {
Log.e(TAG, "Error calling recenterHeadTracker", e);
}
}
synchronized void setDesiredHeadTrackingMode(@Spatializer.HeadTrackingModeSet int mode) {
if (!checkSpatForHeadTracking("setDesiredHeadTrackingMode")) {
return;
}
try {
if (mDesiredHeadTrackingMode != mode) {
mDesiredHeadTrackingMode = mode;
dispatchDesiredHeadTrackingMode(mode);
}
if (mode != headTrackingModeTypeToSpatializerInt(mSpat.getActualHeadTrackingMode())) {
mSpat.setDesiredHeadTrackingMode(spatializerIntToHeadTrackingModeType(mode));
}
} catch (RemoteException e) {
Log.e(TAG, "Error calling setDesiredHeadTrackingMode", e);
}
}
private boolean checkSpatForHeadTracking(String funcName) {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
return false;
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
if (mSpat == null) {
throw (new IllegalStateException(
"null Spatializer when calling " + funcName));
}
break;
}
return true;
}
private void dispatchActualHeadTrackingMode(int newMode) {
final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast();
for (int i = 0; i < nbCallbacks; i++) {
try {
mHeadTrackingModeCallbacks.getBroadcastItem(i)
.dispatchSpatializerActualHeadTrackingModeChanged(newMode);
} catch (RemoteException e) {
Log.e(TAG, "Error in dispatchSpatializerActualHeadTrackingModeChanged", e);
}
}
mHeadTrackingModeCallbacks.finishBroadcast();
}
private void dispatchDesiredHeadTrackingMode(int newMode) {
final int nbCallbacks = mHeadTrackingModeCallbacks.beginBroadcast();
for (int i = 0; i < nbCallbacks; i++) {
try {
mHeadTrackingModeCallbacks.getBroadcastItem(i)
.dispatchSpatializerDesiredHeadTrackingModeChanged(newMode);
} catch (RemoteException e) {
Log.e(TAG, "Error in dispatchSpatializerDesiredHeadTrackingModeChanged", e);
}
}
mHeadTrackingModeCallbacks.finishBroadcast();
}
//------------------------------------------------------
// head pose
final RemoteCallbackList<ISpatializerHeadToSoundStagePoseCallback> mHeadPoseCallbacks =
new RemoteCallbackList<ISpatializerHeadToSoundStagePoseCallback>();
synchronized void registerHeadToSoundstagePoseCallback(
@NonNull ISpatializerHeadToSoundStagePoseCallback callback) {
mHeadPoseCallbacks.register(callback);
}
synchronized void unregisterHeadToSoundstagePoseCallback(
@NonNull ISpatializerHeadToSoundStagePoseCallback callback) {
mHeadPoseCallbacks.unregister(callback);
}
private void dispatchPoseUpdate(float[] pose) {
final int nbCallbacks = mHeadPoseCallbacks.beginBroadcast();
for (int i = 0; i < nbCallbacks; i++) {
try {
mHeadPoseCallbacks.getBroadcastItem(i)
.dispatchPoseChanged(pose);
} catch (RemoteException e) {
Log.e(TAG, "Error in dispatchPoseChanged", e);
}
}
mHeadPoseCallbacks.finishBroadcast();
}
//------------------------------------------------------
// vendor parameters
synchronized void setEffectParameter(int key, @NonNull byte[] value) {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
throw (new IllegalStateException(
"Can't set parameter key:" + key + " without a spatializer"));
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
if (mSpat == null) {
throw (new IllegalStateException(
"null Spatializer for setParameter for key:" + key));
}
break;
}
// mSpat != null
try {
mSpat.setParameter(key, value);
} catch (RemoteException e) {
Log.e(TAG, "Error in setParameter for key:" + key, e);
}
}
synchronized void getEffectParameter(int key, @NonNull byte[] value) {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
throw (new IllegalStateException(
"Can't get parameter key:" + key + " without a spatializer"));
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
if (mSpat == null) {
throw (new IllegalStateException(
"null Spatializer for getParameter for key:" + key));
}
break;
}
// mSpat != null
try {
mSpat.getParameter(key, value);
} catch (RemoteException e) {
Log.e(TAG, "Error in getParameter for key:" + key, e);
}
}
//------------------------------------------------------
// output
/** @see Spatializer#getOutput */
synchronized int getOutput() {
switch (mState) {
case STATE_UNINITIALIZED:
case STATE_NOT_SUPPORTED:
throw (new IllegalStateException(
"Can't get output without a spatializer"));
case STATE_ENABLED_UNAVAILABLE:
case STATE_DISABLED_UNAVAILABLE:
case STATE_DISABLED_AVAILABLE:
case STATE_ENABLED_AVAILABLE:
if (mSpat == null) {
throw (new IllegalStateException(
"null Spatializer for getOutput"));
}
break;
}
// mSpat != null
try {
return mSpat.getOutput();
} catch (RemoteException e) {
Log.e(TAG, "Error in getOutput", e);
return 0;
}
}
final RemoteCallbackList<ISpatializerOutputCallback> mOutputCallbacks =
new RemoteCallbackList<ISpatializerOutputCallback>();
synchronized void registerSpatializerOutputCallback(
@NonNull ISpatializerOutputCallback callback) {
mOutputCallbacks.register(callback);
}
synchronized void unregisterSpatializerOutputCallback(
@NonNull ISpatializerOutputCallback callback) {
mOutputCallbacks.unregister(callback);
}
private void dispatchOutputUpdate(int output) {
final int nbCallbacks = mOutputCallbacks.beginBroadcast();
for (int i = 0; i < nbCallbacks; i++) {
try {
mOutputCallbacks.getBroadcastItem(i).dispatchSpatializerOutputChanged(output);
} catch (RemoteException e) {
Log.e(TAG, "Error in dispatchOutputUpdate", e);
}
}
mOutputCallbacks.finishBroadcast();
}
//------------------------------------------------------
// sensors
private void postInitSensors() {
mAudioService.postInitSpatializerHeadTrackingSensors();
}
synchronized void onInitSensors() {
final boolean init = (mSpatLevel != SpatializationLevel.NONE);
final String action = init ? "initializing" : "releasing";
if (mSpat == null) {
Log.e(TAG, "not " + action + " sensors, null spatializer");
return;
}
try {
if (!mSpat.isHeadTrackingSupported()) {
Log.e(TAG, "not " + action + " sensors, spatializer doesn't support headtracking");
return;
}
} catch (RemoteException e) {
Log.e(TAG, "not " + action + " sensors, error querying headtracking", e);
return;
}
int headHandle = -1;
int screenHandle = -1;
if (init) {
if (mSensorManager == null) {
try {
mSensorManager = (SensorManager)
mAudioService.mContext.getSystemService(Context.SENSOR_SERVICE);
mDynSensorCallback = new HelperDynamicSensorCallback();
mSensorManager.registerDynamicSensorCallback(mDynSensorCallback);
} catch (Exception e) {
Log.e(TAG, "Error with SensorManager, can't initialize sensors", e);
mSensorManager = null;
mDynSensorCallback = null;
return;
}
}
// initialize sensor handles
// TODO-HT update to non-private sensor once head tracker sensor is defined
for (Sensor sensor : mSensorManager.getDynamicSensorList(
Sensor.TYPE_DEVICE_PRIVATE_BASE)) {
if (sensor.getStringType().equals(HEADTRACKER_SENSOR)) {
headHandle = sensor.getHandle();
break;
}
}
Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
screenHandle = screenSensor.getHandle();
} else {
if (mSensorManager != null && mDynSensorCallback != null) {
mSensorManager.unregisterDynamicSensorCallback(mDynSensorCallback);
mSensorManager = null;
mDynSensorCallback = null;
}
// -1 is disable value for both screen and head tracker handles
}
try {
Log.i(TAG, "setScreenSensor:" + screenHandle);
mSpat.setScreenSensor(screenHandle);
} catch (Exception e) {
Log.e(TAG, "Error calling setScreenSensor:" + screenHandle, e);
}
try {
Log.i(TAG, "setHeadSensor:" + headHandle);
mSpat.setHeadSensor(headHandle);
} catch (Exception e) {
Log.e(TAG, "Error calling setHeadSensor:" + headHandle, e);
}
setDesiredHeadTrackingMode(mDesiredHeadTrackingMode);
}
//------------------------------------------------------
// SDK <-> AIDL converters
private static int headTrackingModeTypeToSpatializerInt(byte mode) {
switch (mode) {
case SpatializerHeadTrackingMode.OTHER:
return Spatializer.HEAD_TRACKING_MODE_OTHER;
case SpatializerHeadTrackingMode.DISABLED:
return Spatializer.HEAD_TRACKING_MODE_DISABLED;
case SpatializerHeadTrackingMode.RELATIVE_WORLD:
return Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD;
case SpatializerHeadTrackingMode.RELATIVE_SCREEN:
return Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE;
default:
throw(new IllegalArgumentException("Unexpected head tracking mode:" + mode));
}
}
private static byte spatializerIntToHeadTrackingModeType(int sdkMode) {
switch (sdkMode) {
case Spatializer.HEAD_TRACKING_MODE_OTHER:
return SpatializerHeadTrackingMode.OTHER;
case Spatializer.HEAD_TRACKING_MODE_DISABLED:
return SpatializerHeadTrackingMode.DISABLED;
case Spatializer.HEAD_TRACKING_MODE_RELATIVE_WORLD:
return SpatializerHeadTrackingMode.RELATIVE_WORLD;
case Spatializer.HEAD_TRACKING_MODE_RELATIVE_DEVICE:
return SpatializerHeadTrackingMode.RELATIVE_SCREEN;
default:
throw(new IllegalArgumentException("Unexpected head tracking mode:" + sdkMode));
}
}
private static int spatializationLevelToSpatializerInt(byte level) {
switch (level) {
case SpatializationLevel.NONE:
return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
case SpatializationLevel.SPATIALIZER_MULTICHANNEL:
return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MULTICHANNEL;
case SpatializationLevel.SPATIALIZER_MCHAN_BED_PLUS_OBJECTS:
return Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_MCHAN_BED_PLUS_OBJECTS;
default:
throw(new IllegalArgumentException("Unexpected spatializer level:" + level));
}
}
void dump(PrintWriter pw) {
pw.println("SpatializerHelper:");
pw.println("\tmState:" + mState);
pw.println("\tmSpatLevel:" + mSpatLevel);
pw.println("\tmCapableSpatLevel:" + mCapableSpatLevel);
pw.println("\tmActualHeadTrackingMode:"
+ Spatializer.headtrackingModeToString(mActualHeadTrackingMode));
pw.println("\tmDesiredHeadTrackingMode:"
+ Spatializer.headtrackingModeToString(mDesiredHeadTrackingMode));
String modesString = "";
int[] modes = getSupportedHeadTrackingModes();
for (int mode : modes) {
modesString += Spatializer.headtrackingModeToString(mode) + " ";
}
pw.println("\tsupported head tracking modes:" + modesString);
pw.println("\tmSpatOutput:" + mSpatOutput);
}
}