summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--media/TEST_MAPPING14
-rw-r--r--media/java/android/media/AudioManager.java29
-rw-r--r--media/java/android/media/AudioSystem.java5
-rw-r--r--media/java/android/media/IAudioService.aidl14
-rw-r--r--media/java/android/media/LoudnessCodecConfigurator.java357
-rw-r--r--media/java/android/media/LoudnessCodecDispatcher.java217
-rw-r--r--media/java/android/media/LoudnessCodecFormat.aidl30
-rw-r--r--media/java/android/media/LoudnessCodecInfo.aidl43
-rw-r--r--media/tests/LoudnessCodecApiTest/Android.bp27
-rw-r--r--media/tests/LoudnessCodecApiTest/AndroidManifest.xml28
-rw-r--r--media/tests/LoudnessCodecApiTest/AndroidTest.xml27
-rw-r--r--media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml21
-rw-r--r--media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4abin0 -> 17333 bytes
-rw-r--r--media/tests/LoudnessCodecApiTest/res/values/strings.xml5
-rw-r--r--media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java273
-rw-r--r--services/core/java/com/android/server/audio/AudioService.java46
-rw-r--r--services/core/java/com/android/server/audio/LoudnessCodecHelper.java513
-rw-r--r--services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java232
18 files changed, 1674 insertions, 207 deletions
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index a9da832b2a5a..8f5f1f6a4794 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 9f63dfdc0ccb..9ae6f8deb98b 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,8 +19,6 @@ package android.media;
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 @@ public class AudioManager {
}
//====================================================================
- // 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 61b5fd5fb0ec..367b38a152fc 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2462,6 +2462,8 @@ public class AudioSystem
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 @@ public class AudioSystem
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 b4ca485eb764..42400d1d5d82 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -52,7 +52,7 @@ import android.media.ISpatializerHeadToSoundStagePoseCallback;
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 @@ interface IAudioService {
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 409abc211cb6..92f337244daf 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.annotation.FlaggedApi;
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 @@ public class LoudnessCodecConfigurator {
@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}
*
- * @param executor {@link Executor} to handle the callbacks
- * @param listener used to receive updates
+ * <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 {@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);
-
- return checkStartLoudnessConfigurator();
+ public static @NonNull LoudnessCodecConfigurator create() {
+ return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+ Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
}
/**
- * 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 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 for receiving updates
+ *
+ * @return the {@link LoudnessCodecConfigurator} instance
*
* TODO: remove hide once API is final
* @hide
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public boolean startLoudnessCodecUpdates() {
- mLcDispatcher.addLoudnessCodecListener(this,
- Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
- return checkStartLoudnessConfigurator();
+ 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 new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+ executor, listener);
}
/**
- * Stops receiving asynchronous loudness updates.
+ * Creates a new instance of {@link LoudnessCodecConfigurator}
+ *
+ * <p>This method should be used only in testing
+ *
+ * @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
*
- * TODO: remove hide once API is final
* @hide
*/
- @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public void stopLoudnessCodecUpdates() {
- mLcDispatcher.removeLoudnessCodecListener(this);
+ 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");
}
/**
- * Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack}
- * which is registered through {@link #setAudioTrack(AudioTrack)}.
+ * 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 addMediaCodec(@NonNull MediaCodec mediaCodec) {
- mMediaCodecs.add(Objects.requireNonNull(mediaCodec,
- "MediaCodec for addMediaCodec must not be null"));
+ 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);
+ }
}
/**
- * Removes the {@link MediaCodec} from receiving loudness updates.
+ * Adds a new {@link MediaCodec} that will stream data to an {@link 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 removeMediaCodec(@NonNull MediaCodec mediaCodec) {
- mMediaCodecs.remove(Objects.requireNonNull(mediaCodec,
- "MediaCodec for removeMediaCodec must not be null"));
+ public void addMediaCodec(@NonNull MediaCodec mediaCodec) {
+ 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);
+ }
+ }
}
/**
- * 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)})
+ * 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 setAudioTrack(@NonNull AudioTrack audioTrack) {
- mAudioTrack = Objects.requireNonNull(audioTrack,
- "AudioTrack for setAudioTrack must not be null");
+ public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
+ 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);
+ }
+ }
}
/**
- * 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.
+ * Gets synchronous loudness updates when no listener is required. The provided
+ * {@link MediaCodec} streams audio data to the passed {@link AudioTrack}.
+ *
+ * @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 @@ public class LoudnessCodecConfigurator {
*/
@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);
+ }
+
+ /** @hide */
+ /*package*/ int getAssignedTrackPiid() {
+ int piid = PLAYER_PIID_INVALID;
+
+ synchronized (mConfiguratorLock) {
+ if (mAudioTrack == null) {
+ return piid;
+ }
+ piid = mAudioTrack.getPlayerIId();
+ }
+
+ return piid;
}
- private boolean checkStartLoudnessConfigurator() {
- if (mAudioTrack == null) {
- Log.w(TAG, "Cannot start loudness configurator without an AudioTrack");
- return false;
+ /** @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;
}
- if (mMediaCodecs.isEmpty()) {
- Log.w(TAG, "Cannot start loudness configurator without at least one MediaCodec");
- return false;
+ 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;
}
- return true;
+ 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 fc5c354b98f5..be881b11e545 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);
- }
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
+ 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;
}
- }
- private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
- mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
+ 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);
+ }
- @NonNull private final LoudnessCodecUpdatesDispatcherStub mLoudnessCodecStub;
+ void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
+ Objects.requireNonNull(configurator);
- private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
- mConfiguratorListener = new HashMap<>();
+ for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
+ mConfiguratorListener.entrySet()) {
+ if (e.getValue() == configurator) {
+ final OnLoudnessCodecUpdateListener listener = e.getKey();
+ mConfiguratorListener.remove(listener);
+ mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
+ break;
+ }
+ }
+ }
+ }
- @NonNull private final AudioManager mAm;
+ @NonNull private final IAudioService mAudioService;
- protected LoudnessCodecDispatcher(@NonNull AudioManager am) {
- mAm = Objects.requireNonNull(am);
- mLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
+ /** @hide */
+ public LoudnessCodecDispatcher(@NonNull IAudioService audioService) {
+ mAudioService = Objects.requireNonNull(audioService);
}
- /** @hide */
- public LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
- return new LoudnessCodecConfigurator(this);
+ @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);
-
- for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
- mConfiguratorListener.entrySet()) {
- if (e.getValue() == configurator) {
- final OnLoudnessCodecUpdateListener listener = e.getKey();
- mConfiguratorListener.remove(listener);
- mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
- break;
- }
+ LoudnessCodecUpdatesDispatcherStub.getInstance().removeLoudnessCodecListener(configurator);
+ }
+
+ /** @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 75c906060d43..000000000000
--- 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 000000000000..fd695179057d
--- /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 000000000000..5ca0fc9661c2
--- /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 000000000000..91a671fd6eef
--- /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 000000000000..0099d986ac75
--- /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 000000000000..17fdba6f7c15
--- /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 000000000000..acba4b354066
--- /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 000000000000..0c4227c364ca
--- /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 000000000000..65a9799431e7
--- /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 e7ea0bedb2cb..9701fc87b2a6 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.AudioPlaybackConfiguration;
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.IStrategyNonDefaultDevicesDispatcher;
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 @@ public class AudioService extends IAudioService.Stub
}
/*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 @@ public class AudioService extends IAudioService.Stub
private final SoundDoseHelper mSoundDoseHelper;
+ private final LoudnessCodecHelper mLoudnessCodecHelper;
+
private final Object mSupportedSystemUsagesLock = new Object();
@GuardedBy("mSupportedSystemUsagesLock")
private @AttributeSystemUsage int[] mSupportedSystemUsages =
@@ -1275,6 +1280,8 @@ public class AudioService extends IAudioService.Stub
readPersistedSettings();
readUserRestrictions();
+ mLoudnessCodecHelper = new LoudnessCodecHelper(this);
+
mPlaybackMonitor =
new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM],
device -> onMuteAwaitConnectionTimeout(device));
@@ -4366,6 +4373,8 @@ public class AudioService extends IAudioService.Stub
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 @@ public class AudioService extends IAudioService.Stub
@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 000000000000..3c67e9dd116b
--- /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 000000000000..749b07d16ebe
--- /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;
+ }
+}