CTA2075: add client/server logic for codec loudness management
Changed the LoudnessCodecFormat name into LoudnessCodecInfo and removed
the setAudioTrack method. The new API will provide the corresponding
AudioTrack in the startLoudness updates methods.
Test: adb shell device_config put media_audio android.media.audio.loudness_configurator_api true
Test: atest LoundessCodecHelperTest
Test: atest LoudnessCodecConfiguratorTest
Bug: 298463873
Change-Id: Ia9d1b0c5ed523389d949154bf465a87f71f2f463
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index a9da832..8f5f1f6 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -49,6 +49,18 @@
{"exclude-annotation": "org.junit.Ignore"}
]
}
+ ],
+ "postsubmit": [
+ {
+ "file_patterns": [
+ "[^/]*(LoudnessCodec)[^/]*\\.java"
+ ],
+ "name": "LoudnessCodecApiTest",
+ "options": [
+ {
+ "include-annotation": "android.platform.test.annotations.Presubmit"
+ }
+ ]
+ }
]
}
-
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 9f63dfd..9ae6f8d 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,8 +19,6 @@
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
import static android.content.Context.DEVICE_ID_DEFAULT;
-import static android.media.audio.Flags.autoPublicVolumeApiHardening;
-import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
import android.Manifest;
@@ -2924,33 +2922,6 @@
}
//====================================================================
- // Loudness management
- private final Object mLoudnessCodecLock = new Object();
-
- @GuardedBy("mLoudnessCodecLock")
- private LoudnessCodecDispatcher mLoudnessCodecDispatcher = null;
-
- /**
- * Creates a new instance of {@link LoudnessCodecConfigurator}.
- * @return the {@link LoudnessCodecConfigurator} instance
- *
- * TODO: remove hide once API is final
- * @hide
- */
- @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public @NonNull LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
- LoudnessCodecConfigurator configurator;
- synchronized (mLoudnessCodecLock) {
- // initialize lazily
- if (mLoudnessCodecDispatcher == null) {
- mLoudnessCodecDispatcher = new LoudnessCodecDispatcher(this);
- }
- configurator = mLoudnessCodecDispatcher.createLoudnessCodecConfigurator();
- }
- return configurator;
- }
-
- //====================================================================
// Bluetooth SCO control
/**
* Sticky broadcast intent action indicating that the Bluetooth SCO audio
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 61b5fd5..367b38a 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2462,6 +2462,8 @@
public static final int PLATFORM_VOICE = 1;
/** @hide The platform is a television or a set-top box */
public static final int PLATFORM_TELEVISION = 2;
+ /** @hide The platform is automotive */
+ public static final int PLATFORM_AUTOMOTIVE = 3;
/**
* @hide
@@ -2478,6 +2480,9 @@
return PLATFORM_VOICE;
} else if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return PLATFORM_TELEVISION;
+ } else if (context.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_AUTOMOTIVE)) {
+ return PLATFORM_AUTOMOTIVE;
} else {
return PLATFORM_DEFAULT;
}
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index b4ca485..42400d1 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -52,7 +52,7 @@
import android.media.ISpatializerOutputCallback;
import android.media.IStreamAliasingDispatcher;
import android.media.IVolumeController;
-import android.media.LoudnessCodecFormat;
+import android.media.LoudnessCodecInfo;
import android.media.PlayerBase;
import android.media.VolumeInfo;
import android.media.VolumePolicy;
@@ -731,15 +731,13 @@
void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
- oneway void startLoudnessCodecUpdates(in int piid);
+ oneway void startLoudnessCodecUpdates(int piid, in List<LoudnessCodecInfo> codecInfoSet);
- oneway void stopLoudnessCodecUpdates(in int piid);
+ oneway void stopLoudnessCodecUpdates(int piid);
- oneway void addLoudnesssCodecFormat(in int piid, in LoudnessCodecFormat format);
+ oneway void addLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
- oneway void addLoudnesssCodecFormatList(in int piid, in List<LoudnessCodecFormat> format);
+ oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
- oneway void removeLoudnessCodecFormat(in int piid, in LoudnessCodecFormat format);
-
- PersistableBundle getLoudnessParams(in int piid, in LoudnessCodecFormat format);
+ PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo);
}
diff --git a/media/java/android/media/LoudnessCodecConfigurator.java b/media/java/android/media/LoudnessCodecConfigurator.java
index 409abc2..92f3372 100644
--- a/media/java/android/media/LoudnessCodecConfigurator.java
+++ b/media/java/android/media/LoudnessCodecConfigurator.java
@@ -16,6 +16,9 @@
package android.media;
+import static android.media.AudioPlaybackConfiguration.PLAYER_PIID_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.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
import android.annotation.CallbackExecutor;
@@ -23,21 +26,27 @@
import android.os.Bundle;
import android.util.Log;
+import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class for getting recommended loudness parameter updates for audio decoders, according to the
* encoded format and current audio routing. Those updates can be automatically applied to the
* {@link MediaCodec} instance(s), or be provided to the user. The codec loudness management
- * updates are defined by the CTA-2075 standard.
+ * parameter updates are defined by the CTA-2075 standard.
* <p>A new object should be instantiated for each {@link AudioTrack} with the help
- * of {@link AudioManager#createLoudnessCodecConfigurator()}.
+ * of {@link #create()} or {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
*
* TODO: remove hide once API is final
* @hide
@@ -81,120 +90,255 @@
@NonNull private final LoudnessCodecDispatcher mLcDispatcher;
+ private final Object mConfiguratorLock = new Object();
+
+ @GuardedBy("mConfiguratorLock")
private AudioTrack mAudioTrack;
- private final List<MediaCodec> mMediaCodecs = new ArrayList<>();
+ @GuardedBy("mConfiguratorLock")
+ private final Executor mExecutor;
- /** @hide */
- protected LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher) {
- mLcDispatcher = Objects.requireNonNull(lcDispatcher);
- }
+ @GuardedBy("mConfiguratorLock")
+ private final OnLoudnessCodecUpdateListener mListener;
+ @GuardedBy("mConfiguratorLock")
+ private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>();
/**
- * Starts receiving asynchronous loudness updates and registers the listener for
- * receiving {@link MediaCodec} loudness parameter updates.
- * <p>This method should be called before {@link #startLoudnessCodecUpdates()} or
- * after {@link #stopLoudnessCodecUpdates()}.
+ * Creates a new instance of {@link LoudnessCodecConfigurator}
+ *
+ * <p>This method should be used when the client does not need to alter the
+ * codec loudness parameters before they are applied to the audio decoders.
+ * Otherwise, use {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
+ *
+ * @return the {@link LoudnessCodecConfigurator} instance
+ *
+ * TODO: remove hide once API is final
+ * @hide
+ */
+ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public static @NonNull LoudnessCodecConfigurator create() {
+ return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+ Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
+ }
+
+ /**
+ * Creates a new instance of {@link LoudnessCodecConfigurator}
+ *
+ * <p>This method should be used when the client wants to alter the codec
+ * loudness parameters before they are applied to the audio decoders.
+ * Otherwise, use {@link #create()}.
*
* @param executor {@link Executor} to handle the callbacks
- * @param listener used to receive updates
+ * @param listener used for receiving updates
*
- * @return {@code true} if there is at least one {@link MediaCodec} and
- * {@link AudioTrack} set and the user can expect receiving updates.
+ * @return the {@link LoudnessCodecConfigurator} instance
*
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public boolean startLoudnessCodecUpdates(@NonNull @CallbackExecutor Executor executor,
- @NonNull OnLoudnessCodecUpdateListener listener) {
- Objects.requireNonNull(executor,
- "Executor must not be null");
- Objects.requireNonNull(listener,
- "OnLoudnessCodecUpdateListener must not be null");
- mLcDispatcher.addLoudnessCodecListener(this, executor, listener);
+ public static @NonNull LoudnessCodecConfigurator create(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(executor, "Executor cannot be null");
+ Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
- return checkStartLoudnessConfigurator();
+ return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+ executor, listener);
}
/**
- * Starts receiving asynchronous loudness updates.
- * <p>The registered MediaCodecs will be updated automatically without any client
- * callbacks.
+ * Creates a new instance of {@link LoudnessCodecConfigurator}
*
- * @return {@code true} if there is at least one MediaCodec and AudioTrack set
- * (see {@link #setAudioTrack(AudioTrack)}, {@link #addMediaCodec(MediaCodec)})
- * and the user can expect receiving updates.
+ * <p>This method should be used only in testing
*
- * TODO: remove hide once API is final
+ * @param service interface for communicating with AudioService
+ * @param executor {@link Executor} to handle the callbacks
+ * @param listener used for receiving updates
+ *
+ * @return the {@link LoudnessCodecConfigurator} instance
+ *
* @hide
*/
- @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public boolean startLoudnessCodecUpdates() {
- mLcDispatcher.addLoudnessCodecListener(this,
- Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
- return checkStartLoudnessConfigurator();
+ public static @NonNull LoudnessCodecConfigurator createForTesting(
+ @NonNull IAudioService service,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(service, "IAudioService cannot be null");
+ Objects.requireNonNull(executor, "Executor cannot be null");
+ Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
+
+ return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(service),
+ executor, listener);
+ }
+
+ /** @hide */
+ private LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null");
+ mExecutor = Objects.requireNonNull(executor, "Executor cannot be null");
+ mListener = Objects.requireNonNull(listener,
+ "OnLoudnessCodecUpdateListener cannot be null");
}
/**
- * Stops receiving asynchronous loudness updates.
+ * Sets the {@link AudioTrack} and starts receiving asynchronous updates for
+ * the registered {@link MediaCodec}s (see {@link #addMediaCodec(MediaCodec)})
+ *
+ * <p>The AudioTrack should be the one that receives audio data from the
+ * added audio decoders and is used to determine the device routing on which
+ * the audio streaming will take place. This will directly influence the
+ * loudness parameters.
+ * <p>After calling this method the framework will compute the initial set of
+ * parameters which will be applied to the registered codecs/returned to the
+ * listener for modification.
+ *
+ * @param audioTrack the track that will receive audio data from the provided
+ * audio decoders. In case this is {@code null} this
+ * method will have the effect of clearing the existing set
+ * {@link AudioTrack} and will stop receiving asynchronous
+ * loudness updates
*
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public void stopLoudnessCodecUpdates() {
- mLcDispatcher.removeLoudnessCodecListener(this);
+ public void setAudioTrack(AudioTrack audioTrack) {
+ List<LoudnessCodecInfo> codecInfos;
+ int piid = PLAYER_PIID_INVALID;
+ int oldPiid = PLAYER_PIID_INVALID;
+ synchronized (mConfiguratorLock) {
+ if (mAudioTrack != null && mAudioTrack == audioTrack) {
+ Log.v(TAG, "Loudness configurator already started for piid: "
+ + mAudioTrack.getPlayerIId());
+ return;
+ }
+
+ codecInfos = getLoudnessCodecInfoList_l();
+ if (mAudioTrack != null) {
+ oldPiid = mAudioTrack.getPlayerIId();
+ mLcDispatcher.removeLoudnessCodecListener(this);
+ }
+ if (audioTrack != null) {
+ piid = audioTrack.getPlayerIId();
+ mLcDispatcher.addLoudnessCodecListener(this, mExecutor, mListener);
+ }
+
+ mAudioTrack = audioTrack;
+ }
+
+ if (oldPiid != PLAYER_PIID_INVALID) {
+ Log.v(TAG, "Loudness configurator stopping updates for piid: " + oldPiid);
+ mLcDispatcher.stopLoudnessCodecUpdates(oldPiid);
+ }
+ if (piid != PLAYER_PIID_INVALID) {
+ Log.v(TAG, "Loudness configurator starting updates for piid: " + piid);
+ mLcDispatcher.startLoudnessCodecUpdates(piid, codecInfos);
+ }
}
/**
* Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack}
- * which is registered through {@link #setAudioTrack(AudioTrack)}.
+ * which the client sets
+ * (see {@link LoudnessCodecConfigurator#setAudioTrack(AudioTrack)}).
+ *
+ * <p>This method can be called while asynchronous updates are live.
+ *
+ * <p>No new element will be added if the passed {@code mediaCodec} was
+ * previously added.
+ *
+ * @param mediaCodec the codec to start receiving asynchronous loudness
+ * updates
*
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
public void addMediaCodec(@NonNull MediaCodec mediaCodec) {
- mMediaCodecs.add(Objects.requireNonNull(mediaCodec,
- "MediaCodec for addMediaCodec must not be null"));
+ final MediaCodec mc = Objects.requireNonNull(mediaCodec,
+ "MediaCodec for addMediaCodec cannot be null");
+ int piid = PLAYER_PIID_INVALID;
+ final LoudnessCodecInfo mcInfo = getCodecInfo(mc);
+
+ if (mcInfo != null) {
+ synchronized (mConfiguratorLock) {
+ final AtomicBoolean containsCodec = new AtomicBoolean(false);
+ Set<MediaCodec> newSet = mMediaCodecs.computeIfPresent(mcInfo, (info, codecSet) -> {
+ containsCodec.set(!codecSet.add(mc));
+ return codecSet;
+ });
+ if (newSet == null) {
+ newSet = new HashSet<>();
+ newSet.add(mc);
+ mMediaCodecs.put(mcInfo, newSet);
+ }
+ if (containsCodec.get()) {
+ Log.v(TAG, "Loudness configurator already added media codec " + mediaCodec);
+ return;
+ }
+ if (mAudioTrack != null) {
+ piid = mAudioTrack.getPlayerIId();
+ }
+ }
+
+ if (piid != PLAYER_PIID_INVALID) {
+ mLcDispatcher.addLoudnessCodecInfo(piid, mcInfo);
+ }
+ }
}
/**
* Removes the {@link MediaCodec} from receiving loudness updates.
*
+ * <p>This method can be called while asynchronous updates are live.
+ *
+ * <p>No elements will be removed if the passed mediaCodec was not added before.
+ *
+ * @param mediaCodec the element to remove for receiving asynchronous updates
+ *
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
- mMediaCodecs.remove(Objects.requireNonNull(mediaCodec,
- "MediaCodec for removeMediaCodec must not be null"));
+ int piid = PLAYER_PIID_INVALID;
+ LoudnessCodecInfo mcInfo;
+ AtomicBoolean removed = new AtomicBoolean(false);
+
+ mcInfo = getCodecInfo(Objects.requireNonNull(mediaCodec,
+ "MediaCodec for removeMediaCodec cannot be null"));
+
+ if (mcInfo != null) {
+ synchronized (mConfiguratorLock) {
+ if (mAudioTrack != null) {
+ piid = mAudioTrack.getPlayerIId();
+ }
+ mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> {
+ removed.set(mcs.remove(mediaCodec));
+ if (mcs.isEmpty()) {
+ // remove the entry
+ return null;
+ }
+ return mcs;
+ });
+ }
+
+ if (piid != PLAYER_PIID_INVALID && removed.get()) {
+ mLcDispatcher.removeLoudnessCodecInfo(piid, mcInfo);
+ }
+ }
}
/**
- * Sets the {@link AudioTrack} that can receive audio data from the added
- * {@link MediaCodec}'s. The {@link AudioTrack} is used to determine the devices
- * on which the streaming will take place and hence will directly influence the
- * loudness params.
- * <p>Should be called before starting the loudness updates
- * (see {@link #startLoudnessCodecUpdates()},
- * {@link #startLoudnessCodecUpdates(Executor, OnLoudnessCodecUpdateListener)})
+ * Gets synchronous loudness updates when no listener is required. The provided
+ * {@link MediaCodec} streams audio data to the passed {@link AudioTrack}.
*
- * TODO: remove hide once API is final
- * @hide
- */
- @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public void setAudioTrack(@NonNull AudioTrack audioTrack) {
- mAudioTrack = Objects.requireNonNull(audioTrack,
- "AudioTrack for setAudioTrack must not be null");
- }
-
- /**
- * Gets synchronous loudness updates when no listener is required and at least one
- * {@link MediaCodec} which streams to a registered {@link AudioTrack} is set.
- * Otherwise, an empty {@link Bundle} will be returned.
+ * @param audioTrack track that receives audio data from the passed
+ * {@link MediaCodec}
+ * @param mediaCodec codec that decodes loudness annotated data for the passed
+ * {@link AudioTrack}
*
* @return the {@link Bundle} containing the current loudness parameters. Caller is
* responsible to update the {@link MediaCodec}
@@ -204,22 +348,89 @@
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
@NonNull
- public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
- // TODO: implement synchronous loudness params updates
- return new Bundle();
+ public Bundle getLoudnessCodecParams(@NonNull AudioTrack audioTrack,
+ @NonNull MediaCodec mediaCodec) {
+ Objects.requireNonNull(audioTrack, "Passed audio track cannot be null");
+
+ LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec);
+ if (codecInfo == null) {
+ return new Bundle();
+ }
+
+ return mLcDispatcher.getLoudnessCodecParams(audioTrack.getPlayerIId(), codecInfo);
}
- private boolean checkStartLoudnessConfigurator() {
- if (mAudioTrack == null) {
- Log.w(TAG, "Cannot start loudness configurator without an AudioTrack");
- return false;
+ /** @hide */
+ /*package*/ int getAssignedTrackPiid() {
+ int piid = PLAYER_PIID_INVALID;
+
+ synchronized (mConfiguratorLock) {
+ if (mAudioTrack == null) {
+ return piid;
+ }
+ piid = mAudioTrack.getPlayerIId();
}
- if (mMediaCodecs.isEmpty()) {
- Log.w(TAG, "Cannot start loudness configurator without at least one MediaCodec");
- return false;
+ return piid;
+ }
+
+ /** @hide */
+ /*package*/ List<MediaCodec> getRegisteredMediaCodecList() {
+ synchronized (mConfiguratorLock) {
+ return mMediaCodecs.values().stream().flatMap(Collection::stream).toList();
+ }
+ }
+
+ @GuardedBy("mConfiguratorLock")
+ private List<LoudnessCodecInfo> getLoudnessCodecInfoList_l() {
+ return mMediaCodecs.values().stream().flatMap(listMc -> listMc.stream().map(
+ LoudnessCodecConfigurator::getCodecInfo)).toList();
+ }
+
+ @Nullable
+ private static LoudnessCodecInfo getCodecInfo(@NonNull MediaCodec mediaCodec) {
+ LoudnessCodecInfo lci = new LoudnessCodecInfo();
+ final MediaCodecInfo codecInfo = mediaCodec.getCodecInfo();
+ if (codecInfo.isEncoder()) {
+ // loudness info only for decoders
+ Log.w(TAG, "MediaCodec used for encoding does not support loudness annotation");
+ return null;
}
- return true;
+ final MediaFormat inputFormat = mediaCodec.getInputFormat();
+ final String mimeType = inputFormat.getString(MediaFormat.KEY_MIME);
+ if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mimeType)) {
+ // check both KEY_AAC_PROFILE and KEY_PROFILE as some codecs may only recognize one of
+ // these two keys
+ int aacProfile = -1;
+ int profile = -1;
+ try {
+ aacProfile = inputFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
+ } catch (NullPointerException e) {
+ // does not contain KEY_AAC_PROFILE. do nothing
+ }
+ try {
+ profile = inputFormat.getInteger(MediaFormat.KEY_PROFILE);
+ } catch (NullPointerException e) {
+ // does not contain KEY_PROFILE. do nothing
+ }
+ if (aacProfile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE
+ || profile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE) {
+ lci.metadataType = CODEC_METADATA_TYPE_MPEG_D;
+ } else {
+ lci.metadataType = CODEC_METADATA_TYPE_MPEG_4;
+ }
+ } else {
+ Log.w(TAG, "MediaCodec mime type not supported for loudness annotation");
+ return null;
+ }
+
+ final MediaFormat outputFormat = mediaCodec.getOutputFormat();
+ lci.isDownmixing = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
+ < inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+
+ lci.mediaCodecHashCode = mediaCodec.hashCode();
+
+ return lci;
}
}
diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java
index fc5c354..be881b1 100644
--- a/media/java/android/media/LoudnessCodecDispatcher.java
+++ b/media/java/android/media/LoudnessCodecDispatcher.java
@@ -16,94 +16,217 @@
package android.media;
+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.CallbackExecutor;
import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.os.Bundle;
import android.os.PersistableBundle;
import android.os.RemoteException;
+import android.util.Log;
import androidx.annotation.NonNull;
import java.util.HashMap;
+import java.util.List;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* Class used to handle the loudness related communication with the audio service.
+ *
* @hide
*/
-public class LoudnessCodecDispatcher {
- private final class LoudnessCodecUpdatesDispatcherStub
- extends ILoudnessCodecUpdatesDispatcher.Stub
- implements CallbackUtil.DispatcherStub {
+public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub {
+ private static final String TAG = "LoudnessCodecDispatcher";
+
+ private static final boolean DEBUG = false;
+
+ private static final class LoudnessCodecUpdatesDispatcherStub
+ extends ILoudnessCodecUpdatesDispatcher.Stub {
+ private static LoudnessCodecUpdatesDispatcherStub sLoudnessCodecStub;
+
+ private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
+ mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
+
+ private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
+ mConfiguratorListener = new HashMap<>();
+
+ public static synchronized LoudnessCodecUpdatesDispatcherStub getInstance() {
+ if (sLoudnessCodecStub == null) {
+ sLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
+ }
+ return sLoudnessCodecStub;
+ }
+
+ private LoudnessCodecUpdatesDispatcherStub() {}
+
@Override
public void dispatchLoudnessCodecParameterChange(int piid, PersistableBundle params) {
mLoudnessListenerMgr.callListeners(listener ->
- mConfiguratorListener.computeIfPresent(listener, (l, c) -> {
- // TODO: send the bundle for the user to update
- return c;
+ mConfiguratorListener.computeIfPresent(listener, (l, lcConfig) -> {
+ // send the appropriate bundle for the user to update
+ if (lcConfig.getAssignedTrackPiid() == piid) {
+ final List<MediaCodec> mediaCodecs =
+ lcConfig.getRegisteredMediaCodecList();
+ for (MediaCodec mediaCodec : mediaCodecs) {
+ final String infoKey = Integer.toString(mediaCodec.hashCode());
+ if (params.containsKey(infoKey)) {
+ Bundle bundle = new Bundle(
+ params.getPersistableBundle(infoKey));
+ if (DEBUG) {
+ Log.d(TAG,
+ "Received for piid " + piid + " bundle: " + bundle);
+ }
+ bundle =
+ LoudnessCodecUpdatesDispatcherStub.filterLoudnessParams(
+ l.onLoudnessCodecUpdate(mediaCodec, bundle));
+ if (DEBUG) {
+ Log.d(TAG, "User changed for piid " + piid
+ + " to filtered bundle: " + bundle);
+ }
+
+ if (!bundle.isDefinitelyEmpty()) {
+ mediaCodec.setParameters(bundle);
+ }
+ }
+ }
+ }
+
+ return lcConfig;
}));
}
- @Override
- public void register(boolean register) {
- try {
- if (register) {
- mAm.getService().registerLoudnessCodecUpdatesDispatcher(this);
- } else {
- mAm.getService().unregisterLoudnessCodecUpdatesDispatcher(this);
+ private static Bundle filterLoudnessParams(Bundle bundle) {
+ Bundle filteredBundle = new Bundle();
+
+ if (bundle.containsKey(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL)) {
+ filteredBundle.putInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL,
+ bundle.getInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL));
+ }
+ if (bundle.containsKey(KEY_AAC_DRC_HEAVY_COMPRESSION)) {
+ filteredBundle.putInt(KEY_AAC_DRC_HEAVY_COMPRESSION,
+ bundle.getInt(KEY_AAC_DRC_HEAVY_COMPRESSION));
+ }
+ if (bundle.containsKey(KEY_AAC_DRC_EFFECT_TYPE)) {
+ filteredBundle.putInt(KEY_AAC_DRC_EFFECT_TYPE,
+ bundle.getInt(KEY_AAC_DRC_EFFECT_TYPE));
+ }
+
+ return filteredBundle;
+ }
+
+ void addLoudnessCodecListener(@NonNull CallbackUtil.DispatcherStub dispatcher,
+ @NonNull LoudnessCodecConfigurator configurator,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnLoudnessCodecUpdateListener listener) {
+ Objects.requireNonNull(configurator);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+
+ mLoudnessListenerMgr.addListener(
+ executor, listener, "addLoudnessCodecListener",
+ () -> dispatcher);
+ mConfiguratorListener.put(listener, configurator);
+ }
+
+ void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
+ Objects.requireNonNull(configurator);
+
+ for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
+ mConfiguratorListener.entrySet()) {
+ if (e.getValue() == configurator) {
+ final OnLoudnessCodecUpdateListener listener = e.getKey();
+ mConfiguratorListener.remove(listener);
+ mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
+ break;
}
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
}
}
}
- private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
- mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
-
- @NonNull private final LoudnessCodecUpdatesDispatcherStub mLoudnessCodecStub;
-
- private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
- mConfiguratorListener = new HashMap<>();
-
- @NonNull private final AudioManager mAm;
-
- protected LoudnessCodecDispatcher(@NonNull AudioManager am) {
- mAm = Objects.requireNonNull(am);
- mLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
- }
+ @NonNull private final IAudioService mAudioService;
/** @hide */
- public LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
- return new LoudnessCodecConfigurator(this);
+ public LoudnessCodecDispatcher(@NonNull IAudioService audioService) {
+ mAudioService = Objects.requireNonNull(audioService);
+ }
+
+ @Override
+ public void register(boolean register) {
+ try {
+ if (register) {
+ mAudioService.registerLoudnessCodecUpdatesDispatcher(
+ LoudnessCodecUpdatesDispatcherStub.getInstance());
+ } else {
+ mAudioService.unregisterLoudnessCodecUpdatesDispatcher(
+ LoudnessCodecUpdatesDispatcherStub.getInstance());
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
}
/** @hide */
public void addLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator,
@NonNull @CallbackExecutor Executor executor,
@NonNull OnLoudnessCodecUpdateListener listener) {
- Objects.requireNonNull(configurator);
- Objects.requireNonNull(executor);
- Objects.requireNonNull(listener);
-
- mConfiguratorListener.put(listener, configurator);
- mLoudnessListenerMgr.addListener(
- executor, listener, "addLoudnessCodecListener", () -> mLoudnessCodecStub);
+ LoudnessCodecUpdatesDispatcherStub.getInstance().addLoudnessCodecListener(this,
+ configurator, executor, listener);
}
/** @hide */
public void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
- Objects.requireNonNull(configurator);
+ LoudnessCodecUpdatesDispatcherStub.getInstance().removeLoudnessCodecListener(configurator);
+ }
- for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
- mConfiguratorListener.entrySet()) {
- if (e.getValue() == configurator) {
- final OnLoudnessCodecUpdateListener listener = e.getKey();
- mConfiguratorListener.remove(listener);
- mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
- break;
- }
+ /** @hide */
+ public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+ try {
+ mAudioService.startLoudnessCodecUpdates(piid, codecInfoList);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
}
}
+
+ /** @hide */
+ public void stopLoudnessCodecUpdates(int piid) {
+ try {
+ mAudioService.stopLoudnessCodecUpdates(piid);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void addLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+ try {
+ mAudioService.addLoudnessCodecInfo(piid, mcInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public void removeLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+ try {
+ mAudioService.removeLoudnessCodecInfo(piid, mcInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ public Bundle getLoudnessCodecParams(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+ Bundle loudnessParams = null;
+ try {
+ loudnessParams = new Bundle(mAudioService.getLoudnessParams(piid, mcInfo));
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ return loudnessParams;
+ }
}
diff --git a/media/java/android/media/LoudnessCodecFormat.aidl b/media/java/android/media/LoudnessCodecFormat.aidl
deleted file mode 100644
index 75c9060..0000000
--- a/media/java/android/media/LoudnessCodecFormat.aidl
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.media;
-
-
-/**
- * Loudness format which specifies the input attributes used for measuring
- * the parameters required to perform loudness alignment as specified by the
- * CTA2075 standard.
- *
- * {@hide}
- */
-parcelable LoudnessCodecFormat {
- String metadataType;
- boolean isDownmixing;
-}
\ No newline at end of file
diff --git a/media/java/android/media/LoudnessCodecInfo.aidl b/media/java/android/media/LoudnessCodecInfo.aidl
new file mode 100644
index 0000000..fd69517
--- /dev/null
+++ b/media/java/android/media/LoudnessCodecInfo.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.media;
+
+/**
+ * Loudness information for a {@link MediaCodec} object which specifies the
+ * input attributes used for measuring the parameters required to perform
+ * loudness alignment as specified by the CTA2075 standard.
+ *
+ * {@hide}
+ */
+@JavaDerive(equals = true)
+parcelable LoudnessCodecInfo {
+ /** Supported codec metadata types for loudness updates. */
+ @Backing(type="int")
+ enum CodecMetadataType {
+ CODEC_METADATA_TYPE_INVALID = 0,
+ CODEC_METADATA_TYPE_MPEG_4 = 1,
+ CODEC_METADATA_TYPE_MPEG_D = 2,
+ CODEC_METADATA_TYPE_AC_3 = 3,
+ CODEC_METADATA_TYPE_AC_4 = 4,
+ CODEC_METADATA_TYPE_DTS_HD = 5,
+ CODEC_METADATA_TYPE_DTS_UHD = 6
+ }
+
+ int mediaCodecHashCode;
+ CodecMetadataType metadataType;
+ boolean isDownmixing;
+}
\ No newline at end of file
diff --git a/media/tests/LoudnessCodecApiTest/Android.bp b/media/tests/LoudnessCodecApiTest/Android.bp
new file mode 100644
index 0000000..5ca0fc9
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/Android.bp
@@ -0,0 +1,27 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+ name: "LoudnessCodecApiTest",
+ srcs: ["**/*.java"],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "junit-params",
+ "mockito-target-minus-junit4",
+ "flag-junit",
+ "hamcrest-library",
+ "platform-test-annotations",
+ ],
+ platform_apis: true,
+ certificate: "platform",
+ resource_dirs: ["res"],
+ test_suites: ["device-tests"],
+}
diff --git a/media/tests/LoudnessCodecApiTest/AndroidManifest.xml b/media/tests/LoudnessCodecApiTest/AndroidManifest.xml
new file mode 100644
index 0000000..91a671f
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.loudnesscodecapitest">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.loudnesscodecapitest"
+ android:label="AudioManager loudness codec integration tests InstrumentationRunner">
+ </instrumentation>
+</manifest>
diff --git a/media/tests/LoudnessCodecApiTest/AndroidTest.xml b/media/tests/LoudnessCodecApiTest/AndroidTest.xml
new file mode 100644
index 0000000..0099d98
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+<configuration description="Runs Media Framework Tests">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="test-file-name" value="LoudnessCodecApiTest.apk" />
+ </target_preparer>
+
+ <option name="test-tag" value="LoudnessCodecApiTest" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.loudnesscodecapitest" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml b/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml
new file mode 100644
index 0000000..17fdba6
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical">
+</LinearLayout>
diff --git a/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a
new file mode 100644
index 0000000..acba4b3
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a
Binary files differ
diff --git a/media/tests/LoudnessCodecApiTest/res/values/strings.xml b/media/tests/LoudnessCodecApiTest/res/values/strings.xml
new file mode 100644
index 0000000..0c4227c
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- name of the app [CHAR LIMIT=25]-->
+ <string name="app_name">Loudness Codec API Tests</string>
+</resources>
diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
new file mode 100644
index 0000000..65a9799
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2023 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.loudnesscodecapitest;
+
+import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.media.IAudioService;
+import android.media.LoudnessCodecConfigurator;
+import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit tests for {@link LoudnessCodecConfigurator} checking the internal interactions with a mocked
+ * {@link IAudioService} without any real IPC interactions.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LoudnessCodecConfiguratorTest {
+ private static final String TAG = "LoudnessCodecConfiguratorTest";
+
+ private static final String TEST_MEDIA_AUDIO_CODEC_PREFIX = "audio/";
+ private static final int TEST_AUDIO_TRACK_BUFFER_SIZE = 2048;
+ private static final int TEST_AUDIO_TRACK_SAMPLERATE = 48000;
+ private static final int TEST_AUDIO_TRACK_CHANNELS = 2;
+
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Mock
+ private IAudioService mAudioService;
+
+ private LoudnessCodecConfigurator mLcc;
+
+ @Before
+ public void setUp() {
+ mLcc = LoudnessCodecConfigurator.createForTesting(mAudioService,
+ Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setAudioTrack_callsAudioServiceStart() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+ anyList());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception {
+ when(mAudioService.getLoudnessParams(anyInt(), any())).thenReturn(new PersistableBundle());
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.getLoudnessCodecParams(track, createAndConfigureMediaCodec());
+
+ verify(mAudioService).getLoudnessParams(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setAudioTrack_addsAudioServicePiidCodecs() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setAudioTrackTwice_ignoresSecondCall() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService, times(1)).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+ anyList());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setTrackNull_stopCodecUpdates() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+
+ mLcc.setAudioTrack(null); // stops updates
+ verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void addMediaCodecTwice_ignoresSecondCall() throws Exception {
+ final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+
+ verify(mAudioService, times(1)).startLoudnessCodecUpdates(
+ eq(track.getPlayerIId()), argument.capture());
+ assertEquals(argument.getValue().size(), 1);
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void setClearTrack_removeAllAudioServicePiidCodecs() throws Exception {
+ final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
+
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+ argument.capture());
+ assertEquals(argument.getValue().size(), 1);
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(null);
+ verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+ mLcc.removeMediaCodec(mediaCodec);
+
+ verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void addMediaCodecAfterSetTrack_callsAudioServiceAdd() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ verify(mAudioService).addLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void removeMediaCodecAfterSetTrack_callsAudioServiceRemove() throws Exception {
+ final AudioTrack track = createAudioTrack();
+ final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+ mLcc.addMediaCodec(mediaCodec);
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+ mLcc.removeMediaCodec(mediaCodec);
+ verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ @Test
+ @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+ public void removeWrongMediaCodecAfterSetTrack_noAudioServiceRemoveCall() throws Exception {
+ final AudioTrack track = createAudioTrack();
+
+ mLcc.addMediaCodec(createAndConfigureMediaCodec());
+ mLcc.setAudioTrack(track);
+ verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+ mLcc.removeMediaCodec(createAndConfigureMediaCodec());
+ verify(mAudioService, times(0)).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+ }
+
+ private static AudioTrack createAudioTrack() {
+ return new AudioTrack.Builder()
+ .setAudioAttributes(new AudioAttributes.Builder().build())
+ .setBufferSizeInBytes(TEST_AUDIO_TRACK_BUFFER_SIZE)
+ .setAudioFormat(new AudioFormat.Builder()
+ .setChannelMask(TEST_AUDIO_TRACK_CHANNELS)
+ .setSampleRate(TEST_AUDIO_TRACK_SAMPLERATE).build())
+ .build();
+ }
+
+ private MediaCodec createAndConfigureMediaCodec() throws Exception {
+ AssetFileDescriptor testFd = InstrumentationRegistry.getInstrumentation().getContext()
+ .getResources()
+ .openRawResourceFd(R.raw.noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4);
+
+ MediaExtractor extractor;
+ extractor = new MediaExtractor();
+ extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
+ testFd.getLength());
+ testFd.close();
+
+ assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
+ MediaFormat format = extractor.getTrackFormat(0);
+ String mime = format.getString(MediaFormat.KEY_MIME);
+ assertTrue("not an audio file", mime.startsWith(TEST_MEDIA_AUDIO_CODEC_PREFIX));
+ final MediaCodec mediaCodec = MediaCodec.createDecoderByType(mime);
+
+ Log.v(TAG, "configuring with " + format);
+ mediaCodec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
+
+ return mediaCodec;
+ }
+}
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e7ea0be..9701fc8 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -107,6 +107,7 @@
import android.media.AudioRecordingConfiguration;
import android.media.AudioRoutesInfo;
import android.media.AudioSystem;
+import android.media.AudioTrack;
import android.media.BluetoothProfileConnectionInfo;
import android.media.IAudioDeviceVolumeDispatcher;
import android.media.IAudioFocusDispatcher;
@@ -133,7 +134,9 @@
import android.media.IStrategyPreferredDevicesDispatcher;
import android.media.IStreamAliasingDispatcher;
import android.media.IVolumeController;
-import android.media.LoudnessCodecFormat;
+import android.media.LoudnessCodecConfigurator;
+import android.media.LoudnessCodecInfo;
+import android.media.MediaCodec;
import android.media.MediaMetrics;
import android.media.MediaRecorder.AudioSource;
import android.media.PlayerBase;
@@ -347,7 +350,7 @@
}
/*package*/ boolean isPlatformAutomotive() {
- return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+ return mPlatformType == AudioSystem.PLATFORM_AUTOMOTIVE;
}
/** The controller for the volume UI. */
@@ -941,6 +944,8 @@
private final SoundDoseHelper mSoundDoseHelper;
+ private final LoudnessCodecHelper mLoudnessCodecHelper;
+
private final Object mSupportedSystemUsagesLock = new Object();
@GuardedBy("mSupportedSystemUsagesLock")
private @AttributeSystemUsage int[] mSupportedSystemUsages =
@@ -1275,6 +1280,8 @@
readPersistedSettings();
readUserRestrictions();
+ mLoudnessCodecHelper = new LoudnessCodecHelper(this);
+
mPlaybackMonitor =
new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM],
device -> onMuteAwaitConnectionTimeout(device));
@@ -4366,6 +4373,8 @@
mSoundDoseHelper.scheduleMusicActiveCheck();
}
+ mLoudnessCodecHelper.updateCodecParameters(configs);
+
// Update playback active state for all apps in audio mode stack.
// When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
// and request an audio mode update immediately. Upon any other change, queue the message
@@ -10562,44 +10571,43 @@
@Override
public void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
- // TODO: implement
+ mLoudnessCodecHelper.registerLoudnessCodecUpdatesDispatcher(dispatcher);
}
@Override
public void unregisterLoudnessCodecUpdatesDispatcher(
ILoudnessCodecUpdatesDispatcher dispatcher) {
- // TODO: implement
+ mLoudnessCodecHelper.unregisterLoudnessCodecUpdatesDispatcher(dispatcher);
}
+ /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
@Override
- public void startLoudnessCodecUpdates(int piid) {
- // TODO: implement
+ public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+ mLoudnessCodecHelper.startLoudnessCodecUpdates(piid, codecInfoList);
}
+ /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
@Override
public void stopLoudnessCodecUpdates(int piid) {
- // TODO: implement
+ mLoudnessCodecHelper.stopLoudnessCodecUpdates(piid);
}
+ /** @see LoudnessCodecConfigurator#addMediaCodec(MediaCodec) */
@Override
- public void addLoudnesssCodecFormat(int piid, LoudnessCodecFormat format) {
- // TODO: implement
+ public void addLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+ mLoudnessCodecHelper.addLoudnessCodecInfo(piid, codecInfo);
}
+ /** @see LoudnessCodecConfigurator#removeMediaCodec(MediaCodec) */
@Override
- public void addLoudnesssCodecFormatList(int piid, List<LoudnessCodecFormat> format) {
- // TODO: implement
+ public void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+ mLoudnessCodecHelper.removeLoudnessCodecInfo(piid, codecInfo);
}
+ /** @see LoudnessCodecConfigurator#getLoudnessCodecParams(AudioTrack, MediaCodec) */
@Override
- public void removeLoudnessCodecFormat(int piid, LoudnessCodecFormat format) {
- // TODO: implement
- }
-
- @Override
- public PersistableBundle getLoudnessParams(int piid, LoudnessCodecFormat format) {
- // TODO: implement
- return null;
+ public PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
+ return mLoudnessCodecHelper.getLoudnessParams(piid, codecInfo);
}
//==========================================================================================
diff --git a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
new file mode 100644
index 0000000..3c67e9d
--- /dev/null
+++ b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
@@ -0,0 +1,513 @@
+/*
+ * 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 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 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";
+
+ private static final int SPL_RANGE_UNKNOWN = 0;
+ private static final int SPL_RANGE_SMALL = 1;
+ private static final int SPL_RANGE_MEDIUM = 2;
+ private 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. */
+ private 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() {
+ // TODO: create bundle with new parameters
+ return new PersistableBundle();
+ }
+
+ }
+
+ @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;
+ }
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
new file mode 100644
index 0000000..749b07d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 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.GET_DEVICES_OUTPUTS;
+import static android.media.AudioPlaybackConfiguration.PLAYER_UPDATE_DEVICE_ID;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
+
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.media.ILoudnessCodecUpdatesDispatcher;
+import android.media.LoudnessCodecInfo;
+import android.media.PlayerBase;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class LoudnessCodecHelperTest {
+ private static final String TAG = "LoudnessCodecHelperTest";
+
+ @Rule
+ public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private LoudnessCodecHelper mLoudnessHelper;
+
+ @Mock
+ private AudioService mAudioService;
+ @Mock
+ private ILoudnessCodecUpdatesDispatcher.Default mDispatcher;
+
+ private final int mInitialApcPiid = 1;
+
+ @Before
+ public void setUp() throws Exception {
+ mLoudnessHelper = new LoudnessCodecHelper(mAudioService);
+
+ when(mAudioService.getActivePlaybackConfigurations()).thenReturn(
+ getApcListForPiids(mInitialApcPiid));
+
+ when(mDispatcher.asBinder()).thenReturn(Mockito.mock(IBinder.class));
+ }
+
+ @Test
+ public void registerDispatcher_sendsInitialUpdateOnStart() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+
+ verify(mDispatcher).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), any());
+ }
+
+ @Test
+ public void unregisterDispatcher_noInitialUpdateOnStart() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+ mLoudnessHelper.unregisterLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/false,
+ CODEC_METADATA_TYPE_MPEG_D)));
+
+ verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void addCodecInfo_sendsInitialUpdateAfterStart() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+ mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid,
+ getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D));
+
+ verify(mDispatcher, times(2)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void addCodecInfoForUnstartedPiid_noUpdateSent() throws Exception {
+ final int newPiid = 2;
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+ mLoudnessHelper.addLoudnessCodecInfo(newPiid,
+ getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D));
+
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void updateCodecParameters_updatesOnlyStartedPiids() throws Exception {
+ final int newPiid = 2;
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_4)));
+ //does not trigger dispatch since active apc list does not contain newPiid
+ mLoudnessHelper.startLoudnessCodecUpdates(newPiid,
+ List.of(getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D)));
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+
+ // triggers dispatch for new active apc with newPiid
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(newPiid));
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(newPiid), any());
+ }
+
+ @Test
+ public void updateCodecParameters_noStartedPiids_noDispatch() throws Exception {
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+ mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid,
+ getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+ CODEC_METADATA_TYPE_MPEG_D));
+
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+ // no dispatch since mInitialApcPiid was not started
+ verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void updateCodecParameters_removedCodecInfo_noDispatch() throws Exception {
+ final LoudnessCodecInfo info = getLoudnessInfo(/*mediaCodecHash=*/111,
+ /*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4);
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
+ mLoudnessHelper.removeLoudnessCodecInfo(mInitialApcPiid, info);
+
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+ // no second dispatch since codec info was removed for updates
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ @Test
+ public void updateCodecParameters_stoppedPiids_noDispatch() throws Exception {
+ final LoudnessCodecInfo info = getLoudnessInfo(/*mediaCodecHash=*/111,
+ /*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4);
+ mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+ mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
+ mLoudnessHelper.stopLoudnessCodecUpdates(mInitialApcPiid);
+
+ mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+ // no second dispatch since piid was removed for updates
+ verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+ any());
+ }
+
+ private List<AudioPlaybackConfiguration> getApcListForPiids(int... piids) {
+ final ArrayList<AudioPlaybackConfiguration> apcList = new ArrayList<>();
+
+ AudioDeviceInfo[] devicesStatic = AudioManager.getDevicesStatic(GET_DEVICES_OUTPUTS);
+ assumeTrue(devicesStatic.length > 0);
+ int index = new Random().nextInt(devicesStatic.length);
+ Log.d(TAG, "Out devices number " + devicesStatic.length + ". Picking index " + index);
+ int deviceId = devicesStatic[index].getId();
+
+ for (int piid : piids) {
+ PlayerBase.PlayerIdCard idCard = Mockito.mock(PlayerBase.PlayerIdCard.class);
+ AudioPlaybackConfiguration apc =
+ new AudioPlaybackConfiguration(idCard, piid, /*uid=*/1, /*pid=*/1);
+ apc.handleStateEvent(PLAYER_UPDATE_DEVICE_ID, deviceId);
+
+ apcList.add(apc);
+ }
+ return apcList;
+ }
+
+ private static LoudnessCodecInfo getLoudnessInfo(int mediaCodecHash, boolean isDownmixing,
+ int metadataType) {
+ LoudnessCodecInfo info = new LoudnessCodecInfo();
+ info.isDownmixing = isDownmixing;
+ info.mediaCodecHashCode = mediaCodecHash;
+ info.metadataType = metadataType;
+
+ return info;
+ }
+}