diff options
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 Binary files differnew file mode 100644 index 000000000000..acba4b354066 --- /dev/null +++ b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a 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; + } +} |