blob: c306e3c07ac0e22e6aeb0623b2bc8095de03f42f [file] [log] [blame]
/*
* Copyright (C) 2022 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 static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_CARKIT;
import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEARING_AID;
import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH;
import static android.media.AudioPlaybackConfiguration.PLAYER_DEVICEID_INVALID;
import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
import static android.media.MediaFormat.KEY_AAC_DRC_EFFECT_TYPE;
import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION;
import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.media.AudioDeviceInfo;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioSystem;
import android.media.ILoudnessCodecUpdatesDispatcher;
import android.media.LoudnessCodecInfo;
import android.media.permission.ClearCallingIdentityContext;
import android.media.permission.SafeCloseable;
import android.os.Binder;
import android.os.PersistableBundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* Class to handle the updates in loudness parameters and responsible to generate parameters that
* can be set directly on a MediaCodec.
*/
public class LoudnessCodecHelper {
private static final String TAG = "AS.LoudnessCodecHelper";
private static final boolean DEBUG = false;
/**
* Property containing a string to set for a custom built in speaker SPL range as defined by
* CTA2075. The options that can be set are:
* - "small": for max SPL with test signal < 75 dB,
* - "medium": for max SPL with test signal between 70 and 90 dB,
* - "large": for max SPL with test signal > 85 dB.
*/
private static final String SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE =
"audio.loudness.builtin-speaker-spl-range-size";
@VisibleForTesting
static final int SPL_RANGE_UNKNOWN = 0;
@VisibleForTesting
static final int SPL_RANGE_SMALL = 1;
@VisibleForTesting
static final int SPL_RANGE_MEDIUM = 2;
@VisibleForTesting
static final int SPL_RANGE_LARGE = 3;
/** The possible transducer SPL ranges as defined in CTA2075 */
@IntDef({
SPL_RANGE_UNKNOWN,
SPL_RANGE_SMALL,
SPL_RANGE_MEDIUM,
SPL_RANGE_LARGE
})
@Retention(RetentionPolicy.SOURCE)
public @interface DeviceSplRange {}
private static final class LoudnessRemoteCallbackList extends
RemoteCallbackList<ILoudnessCodecUpdatesDispatcher> {
private final LoudnessCodecHelper mLoudnessCodecHelper;
LoudnessRemoteCallbackList(LoudnessCodecHelper loudnessCodecHelper) {
mLoudnessCodecHelper = loudnessCodecHelper;
}
@Override
public void onCallbackDied(ILoudnessCodecUpdatesDispatcher callback, Object cookie) {
Integer pid = null;
if (cookie instanceof Integer) {
pid = (Integer) cookie;
}
if (pid != null) {
mLoudnessCodecHelper.removePid(pid);
}
super.onCallbackDied(callback, cookie);
}
}
private final LoudnessRemoteCallbackList mLoudnessUpdateDispatchers =
new LoudnessRemoteCallbackList(this);
private final Object mLock = new Object();
/** Contains for each started piid the set corresponding to unique registered audio codecs. */
@GuardedBy("mLock")
private final SparseArray<Set<LoudnessCodecInfo>> mStartedPiids = new SparseArray<>();
/** Contains the current device id assignment for each piid. */
@GuardedBy("mLock")
private final SparseIntArray mPiidToDeviceIdCache = new SparseIntArray();
/** Maps each piid to the owner process of the player. */
@GuardedBy("mLock")
private final SparseIntArray mPiidToPidCache = new SparseIntArray();
private final AudioService mAudioService;
/** Contains the properties necessary to compute the codec loudness related parameters. */
@VisibleForTesting
static final class LoudnessCodecInputProperties {
private final int mMetadataType;
private final boolean mIsDownmixing;
@DeviceSplRange
private final int mDeviceSplRange;
static final class Builder {
private int mMetadataType;
private boolean mIsDownmixing;
@DeviceSplRange
private int mDeviceSplRange;
Builder setMetadataType(int metadataType) {
mMetadataType = metadataType;
return this;
}
Builder setIsDownmixing(boolean isDownmixing) {
mIsDownmixing = isDownmixing;
return this;
}
Builder setDeviceSplRange(@DeviceSplRange int deviceSplRange) {
mDeviceSplRange = deviceSplRange;
return this;
}
LoudnessCodecInputProperties build() {
return new LoudnessCodecInputProperties(mMetadataType,
mIsDownmixing, mDeviceSplRange);
}
}
private LoudnessCodecInputProperties(int metadataType,
boolean isDownmixing,
@DeviceSplRange int deviceSplRange) {
mMetadataType = metadataType;
mIsDownmixing = isDownmixing;
mDeviceSplRange = deviceSplRange;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
// type check and cast
if (getClass() != obj.getClass()) {
return false;
}
final LoudnessCodecInputProperties lcip = (LoudnessCodecInputProperties) obj;
return mMetadataType == lcip.mMetadataType
&& mIsDownmixing == lcip.mIsDownmixing
&& mDeviceSplRange == lcip.mDeviceSplRange;
}
@Override
public int hashCode() {
return Objects.hash(mMetadataType, mIsDownmixing, mDeviceSplRange);
}
@Override
public String toString() {
return "Loudness properties:"
+ " device SPL range: " + splRangeToString(mDeviceSplRange)
+ " down-mixing: " + mIsDownmixing
+ " metadata type: " + mMetadataType;
}
PersistableBundle createLoudnessParameters() {
PersistableBundle loudnessParams = new PersistableBundle();
switch (mDeviceSplRange) {
case SPL_RANGE_LARGE:
// corresponds to -31dB attenuation
loudnessParams.putInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 124);
if (mMetadataType == CODEC_METADATA_TYPE_MPEG_4) {
loudnessParams.putInt(KEY_AAC_DRC_HEAVY_COMPRESSION, 0);
} else if (mMetadataType == CODEC_METADATA_TYPE_MPEG_D) {
// general compression
loudnessParams.putInt(KEY_AAC_DRC_EFFECT_TYPE, 6);
}
break;
case SPL_RANGE_MEDIUM:
// corresponds to -24dB attenuation
loudnessParams.putInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 96);
if (mMetadataType == CODEC_METADATA_TYPE_MPEG_4) {
loudnessParams.putInt(KEY_AAC_DRC_HEAVY_COMPRESSION, mIsDownmixing ? 1 : 0);
} else if (mMetadataType == CODEC_METADATA_TYPE_MPEG_D) {
// general compression
loudnessParams.putInt(KEY_AAC_DRC_EFFECT_TYPE, 6);
}
break;
case SPL_RANGE_SMALL:
// corresponds to -16dB attenuation
loudnessParams.putInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 64);
if (mMetadataType == CODEC_METADATA_TYPE_MPEG_4) {
loudnessParams.putInt(KEY_AAC_DRC_HEAVY_COMPRESSION, 1);
} else if (mMetadataType == CODEC_METADATA_TYPE_MPEG_D) {
// limited playback range compression
loudnessParams.putInt(KEY_AAC_DRC_EFFECT_TYPE, 3);
}
break;
default:
// corresponds to -24dB attenuation
loudnessParams.putInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL, 96);
if (mMetadataType == CODEC_METADATA_TYPE_MPEG_4) {
loudnessParams.putInt(KEY_AAC_DRC_HEAVY_COMPRESSION, mIsDownmixing ? 1 : 0);
} else if (mMetadataType == CODEC_METADATA_TYPE_MPEG_D) {
// general compression
loudnessParams.putInt(KEY_AAC_DRC_EFFECT_TYPE, 6);
}
break;
}
return loudnessParams;
}
}
@GuardedBy("mLock")
private final HashMap<LoudnessCodecInputProperties, PersistableBundle> mCachedProperties =
new HashMap<>();
LoudnessCodecHelper(@NonNull AudioService audioService) {
mAudioService = Objects.requireNonNull(audioService);
}
void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
mLoudnessUpdateDispatchers.register(dispatcher, Binder.getCallingPid());
}
void unregisterLoudnessCodecUpdatesDispatcher(
ILoudnessCodecUpdatesDispatcher dispatcher) {
mLoudnessUpdateDispatchers.unregister(dispatcher);
}
void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
if (DEBUG) {
Log.d(TAG, "startLoudnessCodecUpdates: piid " + piid + " codecInfos " + codecInfoList);
}
Set<LoudnessCodecInfo> infoSet;
synchronized (mLock) {
if (mStartedPiids.contains(piid)) {
Log.w(TAG, "Already started loudness updates for piid " + piid);
return;
}
infoSet = new HashSet<>(codecInfoList);
mStartedPiids.put(piid, infoSet);
mPiidToPidCache.put(piid, Binder.getCallingPid());
}
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mAudioService.getActivePlaybackConfigurations().stream().filter(
conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
apc -> updateCodecParametersForConfiguration(apc, infoSet));
}
}
void stopLoudnessCodecUpdates(int piid) {
if (DEBUG) {
Log.d(TAG, "stopLoudnessCodecUpdates: piid " + piid);
}
synchronized (mLock) {
if (!mStartedPiids.contains(piid)) {
Log.w(TAG, "Loudness updates are already stopped for piid " + piid);
return;
}
mStartedPiids.remove(piid);
mPiidToDeviceIdCache.delete(piid);
mPiidToPidCache.delete(piid);
}
}
void addLoudnessCodecInfo(int piid, LoudnessCodecInfo info) {
if (DEBUG) {
Log.d(TAG, "addLoudnessCodecInfo: piid " + piid + " info " + info);
}
Set<LoudnessCodecInfo> infoSet;
synchronized (mLock) {
if (!mStartedPiids.contains(piid)) {
Log.w(TAG, "Cannot add new loudness info for stopped piid " + piid);
return;
}
infoSet = mStartedPiids.get(piid);
infoSet.add(info);
}
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
mAudioService.getActivePlaybackConfigurations().stream().filter(
conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
apc -> updateCodecParametersForConfiguration(apc, Set.of(info)));
}
}
void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
if (DEBUG) {
Log.d(TAG, "removeLoudnessCodecInfo: piid " + piid + " info " + codecInfo);
}
synchronized (mLock) {
if (!mStartedPiids.contains(piid)) {
Log.w(TAG, "Cannot remove loudness info for stopped piid " + piid);
return;
}
final Set<LoudnessCodecInfo> infoSet = mStartedPiids.get(piid);
infoSet.remove(codecInfo);
}
}
void removePid(int pid) {
if (DEBUG) {
Log.d(TAG, "Removing pid " + pid + " from receiving updates");
}
synchronized (mLock) {
for (int i = 0; i < mPiidToPidCache.size(); ++i) {
int piid = mPiidToPidCache.keyAt(i);
if (mPiidToPidCache.get(piid) == pid) {
if (DEBUG) {
Log.d(TAG, "Removing piid " + piid);
}
mStartedPiids.delete(piid);
mPiidToDeviceIdCache.delete(piid);
}
}
}
}
PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
if (DEBUG) {
Log.d(TAG, "getLoudnessParams: piid " + piid + " codecInfo " + codecInfo);
}
try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
final List<AudioPlaybackConfiguration> configs =
mAudioService.getActivePlaybackConfigurations();
for (final AudioPlaybackConfiguration apc : configs) {
if (apc.getPlayerInterfaceId() == piid) {
final AudioDeviceInfo info = apc.getAudioDeviceInfo();
if (info == null) {
Log.i(TAG, "Player with piid " + piid + " is not assigned any device");
break;
}
synchronized (mLock) {
return getCodecBundle_l(info, codecInfo);
}
}
}
}
// return empty Bundle
return new PersistableBundle();
}
/** Method to be called whenever there is a changed in the active playback configurations. */
void updateCodecParameters(List<AudioPlaybackConfiguration> configs) {
if (DEBUG) {
Log.d(TAG, "updateCodecParameters: configs " + configs);
}
List<AudioPlaybackConfiguration> updateApcList = new ArrayList<>();
synchronized (mLock) {
for (final AudioPlaybackConfiguration apc : configs) {
int piid = apc.getPlayerInterfaceId();
int cachedDeviceId = mPiidToDeviceIdCache.get(piid, PLAYER_DEVICEID_INVALID);
AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
if (deviceInfo == null) {
if (DEBUG) {
Log.d(TAG, "No device info for piid: " + piid);
}
if (cachedDeviceId != PLAYER_DEVICEID_INVALID) {
mPiidToDeviceIdCache.delete(piid);
if (DEBUG) {
Log.d(TAG, "Remove cached device id for piid: " + piid);
}
}
continue;
}
if (cachedDeviceId == deviceInfo.getId()) {
// deviceId did not change
if (DEBUG) {
Log.d(TAG, "DeviceId " + cachedDeviceId + " for piid: " + piid
+ " did not change");
}
continue;
}
mPiidToDeviceIdCache.put(piid, deviceInfo.getId());
if (mStartedPiids.contains(piid)) {
updateApcList.add(apc);
}
}
}
updateApcList.forEach(apc -> updateCodecParametersForConfiguration(apc, null));
}
/** Updates and dispatches the new loudness parameters for the {@code codecInfos} set.
*
* @param apc the player configuration for which the loudness parameters are updated.
* @param codecInfos the codec info for which the parameters are updated. If {@code null},
* send updates for all the started codecs assigned to {@code apc}
*/
private void updateCodecParametersForConfiguration(AudioPlaybackConfiguration apc,
Set<LoudnessCodecInfo> codecInfos) {
if (DEBUG) {
Log.d(TAG, "updateCodecParametersForConfiguration apc:" + apc + " codecInfos: "
+ codecInfos);
}
final PersistableBundle allBundles = new PersistableBundle();
final int piid = apc.getPlayerInterfaceId();
synchronized (mLock) {
if (codecInfos == null) {
codecInfos = mStartedPiids.get(piid);
}
final AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
if (codecInfos != null && deviceInfo != null) {
for (LoudnessCodecInfo info : codecInfos) {
allBundles.putPersistableBundle(Integer.toString(info.mediaCodecHashCode),
getCodecBundle_l(deviceInfo, info));
}
}
}
if (!allBundles.isDefinitelyEmpty()) {
if (DEBUG) {
Log.d(TAG, "Dispatching for piid: " + piid + " bundle: " + allBundles);
}
dispatchNewLoudnessParameters(piid, allBundles);
}
}
private void dispatchNewLoudnessParameters(int piid, PersistableBundle bundle) {
if (DEBUG) {
Log.d(TAG, "dispatchNewLoudnessParameters: piid " + piid);
}
final int nbDispatchers = mLoudnessUpdateDispatchers.beginBroadcast();
for (int i = 0; i < nbDispatchers; ++i) {
try {
mLoudnessUpdateDispatchers.getBroadcastItem(i)
.dispatchLoudnessCodecParameterChange(piid, bundle);
} catch (RemoteException e) {
Log.e(TAG, "Error dispatching for piid: " + piid + " bundle: " + bundle , e);
}
}
mLoudnessUpdateDispatchers.finishBroadcast();
}
@GuardedBy("mLock")
private PersistableBundle getCodecBundle_l(AudioDeviceInfo deviceInfo,
LoudnessCodecInfo codecInfo) {
LoudnessCodecInputProperties.Builder builder = new LoudnessCodecInputProperties.Builder();
LoudnessCodecInputProperties prop = builder.setDeviceSplRange(getDeviceSplRange(deviceInfo))
.setIsDownmixing(codecInfo.isDownmixing)
.setMetadataType(codecInfo.metadataType)
.build();
if (mCachedProperties.containsKey(prop)) {
return mCachedProperties.get(prop);
}
final PersistableBundle codecBundle = prop.createLoudnessParameters();
mCachedProperties.put(prop, codecBundle);
return codecBundle;
}
@DeviceSplRange
private int getDeviceSplRange(AudioDeviceInfo deviceInfo) {
final int internalDeviceType = deviceInfo.getInternalType();
if (internalDeviceType == AudioSystem.DEVICE_OUT_SPEAKER) {
final String splRange = SystemProperties.get(
SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE, "unknown");
if (!splRange.equals("unknown")) {
return stringToSplRange(splRange);
}
@DeviceSplRange int result = SPL_RANGE_SMALL; // default for phone/tablet/watch
if (mAudioService.isPlatformAutomotive() || mAudioService.isPlatformTelevision()) {
result = SPL_RANGE_MEDIUM;
}
return result;
} else if (internalDeviceType == AudioSystem.DEVICE_OUT_USB_HEADSET
|| internalDeviceType == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE
|| internalDeviceType == AudioSystem.DEVICE_OUT_WIRED_HEADSET
|| (AudioSystem.isBluetoothDevice(internalDeviceType)
&& mAudioService.getBluetoothAudioDeviceCategory(deviceInfo.getAddress(),
AudioSystem.isBluetoothLeDevice(internalDeviceType))
== AUDIO_DEVICE_CATEGORY_HEADPHONES)) {
return SPL_RANGE_LARGE;
} else if (AudioSystem.isBluetoothDevice(internalDeviceType)) {
final int audioDeviceType = mAudioService.getBluetoothAudioDeviceCategory(
deviceInfo.getAddress(), AudioSystem.isBluetoothLeDevice(internalDeviceType));
if (audioDeviceType == AUDIO_DEVICE_CATEGORY_CARKIT) {
return SPL_RANGE_MEDIUM;
} else if (audioDeviceType == AUDIO_DEVICE_CATEGORY_WATCH) {
return SPL_RANGE_SMALL;
} else if (audioDeviceType == AUDIO_DEVICE_CATEGORY_HEARING_AID) {
return SPL_RANGE_SMALL;
}
}
return SPL_RANGE_UNKNOWN;
}
private static String splRangeToString(@DeviceSplRange int splRange) {
switch (splRange) {
case SPL_RANGE_LARGE: return "large";
case SPL_RANGE_MEDIUM: return "medium";
case SPL_RANGE_SMALL: return "small";
default: return "unknown";
}
}
@DeviceSplRange
private static int stringToSplRange(String splRange) {
switch (splRange) {
case "large": return SPL_RANGE_LARGE;
case "medium": return SPL_RANGE_MEDIUM;
case "small": return SPL_RANGE_SMALL;
default: return SPL_RANGE_UNKNOWN;
}
}
}