397 lines
17 KiB
Java
397 lines
17 KiB
Java
/*
|
|
* 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;
|
|
|
|
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;
|
|
import android.annotation.FlaggedApi;
|
|
import android.media.permission.SafeCloseable;
|
|
import android.os.Bundle;
|
|
import android.util.Log;
|
|
|
|
import androidx.annotation.GuardedBy;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Map.Entry;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.function.Consumer;
|
|
|
|
/**
|
|
* Class for getting recommended loudness parameter updates for audio decoders as they are used
|
|
* to play back media content according to the encoded format and current audio routing. These
|
|
* audio decoder updates leverage loudness metadata present in compressed audio streams. They
|
|
* ensure the loudness and dynamic range of the content is optimized to the physical
|
|
* characteristics of the audio output device (e.g. phone microspeakers vs headphones vs TV
|
|
* speakers).Those updates can be automatically applied to the {@link MediaCodec} instance(s), or
|
|
* be provided to the user. The codec loudness management parameter updates are computed in
|
|
* accordance to the CTA-2075 standard.
|
|
* <p>A new object should be instantiated for each audio session
|
|
* (see {@link AudioManager#generateAudioSessionId()}) using creator methods {@link #create(int)} or
|
|
* {@link #create(int, Executor, OnLoudnessCodecUpdateListener)}.
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
public class LoudnessCodecController implements SafeCloseable {
|
|
private static final String TAG = "LoudnessCodecController";
|
|
|
|
/**
|
|
* Listener used for receiving asynchronous loudness metadata updates.
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
public interface OnLoudnessCodecUpdateListener {
|
|
/**
|
|
* Contains the MediaCodec key/values that can be set directly to
|
|
* configure the loudness of the handle's corresponding decoder (see
|
|
* {@link MediaCodec#setParameters(Bundle)}).
|
|
*
|
|
* @param mediaCodec the mediaCodec that will receive the new parameters
|
|
* @param codecValues contains loudness key/value pairs that can be set
|
|
* directly on the mediaCodec. The listener can modify
|
|
* these values with their own edits which will be
|
|
* returned for the mediaCodec configuration
|
|
*
|
|
* @return a Bundle which contains the original computed codecValues
|
|
* aggregated with user edits. The platform will configure the associated
|
|
* MediaCodecs with the returned Bundle params.
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
@NonNull
|
|
default Bundle onLoudnessCodecUpdate(@NonNull MediaCodec mediaCodec,
|
|
@NonNull Bundle codecValues) {
|
|
return codecValues;
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
private final LoudnessCodecDispatcher mLcDispatcher;
|
|
|
|
private final Object mControllerLock = new Object();
|
|
|
|
private final int mSessionId;
|
|
|
|
@GuardedBy("mControllerLock")
|
|
private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>();
|
|
|
|
/**
|
|
* Creates a new instance of {@link LoudnessCodecController}
|
|
*
|
|
* <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(int, Executor, OnLoudnessCodecUpdateListener)}.
|
|
*
|
|
* @param sessionId the session ID of the track that will receive data
|
|
* from the added {@link MediaCodec}'s
|
|
*
|
|
* @return the {@link LoudnessCodecController} instance
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
public static @NonNull LoudnessCodecController create(int sessionId) {
|
|
final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(
|
|
AudioManager.getService());
|
|
final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
|
|
sessionId);
|
|
dispatcher.addLoudnessCodecListener(controller, Executors.newSingleThreadExecutor(),
|
|
new OnLoudnessCodecUpdateListener() {});
|
|
dispatcher.startLoudnessCodecUpdates(sessionId);
|
|
return controller;
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance of {@link LoudnessCodecController}
|
|
*
|
|
* <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( int)}.
|
|
*
|
|
* @param sessionId the session ID of the track that will receive data
|
|
* from the added {@link MediaCodec}'s
|
|
* @param executor {@link Executor} to handle the callbacks
|
|
* @param listener used for receiving updates
|
|
*
|
|
* @return the {@link LoudnessCodecController} instance
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
public static @NonNull LoudnessCodecController create(
|
|
int sessionId,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull OnLoudnessCodecUpdateListener listener) {
|
|
Objects.requireNonNull(executor, "Executor cannot be null");
|
|
Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
|
|
|
|
final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(
|
|
AudioManager.getService());
|
|
final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
|
|
sessionId);
|
|
dispatcher.addLoudnessCodecListener(controller, executor, listener);
|
|
dispatcher.startLoudnessCodecUpdates(sessionId);
|
|
return controller;
|
|
}
|
|
|
|
/**
|
|
* Creates a new instance of {@link LoudnessCodecController}
|
|
*
|
|
* <p>This method should be used only in testing
|
|
*
|
|
* @param sessionId the session ID of the track that will receive data
|
|
* from the added {@link MediaCodec}'s
|
|
* @param executor {@link Executor} to handle the callbacks
|
|
* @param listener used for receiving updates
|
|
* @param service interface for communicating with AudioService
|
|
*
|
|
* @return the {@link LoudnessCodecController} instance
|
|
* @hide
|
|
*/
|
|
public static @NonNull LoudnessCodecController createForTesting(
|
|
int sessionId,
|
|
@NonNull @CallbackExecutor Executor executor,
|
|
@NonNull OnLoudnessCodecUpdateListener listener,
|
|
@NonNull IAudioService service) {
|
|
Objects.requireNonNull(service, "IAudioService cannot be null");
|
|
Objects.requireNonNull(executor, "Executor cannot be null");
|
|
Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
|
|
|
|
final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(service);
|
|
final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
|
|
sessionId);
|
|
dispatcher.addLoudnessCodecListener(controller, executor, listener);
|
|
dispatcher.startLoudnessCodecUpdates(sessionId);
|
|
return controller;
|
|
}
|
|
|
|
/** @hide */
|
|
private LoudnessCodecController(@NonNull LoudnessCodecDispatcher lcDispatcher, int sessionId) {
|
|
mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null");
|
|
mSessionId = sessionId;
|
|
}
|
|
|
|
/**
|
|
* Adds a new {@link MediaCodec} that will stream data to a player
|
|
* which uses {@link #mSessionId}.
|
|
*
|
|
* <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. The codec has to be in a configured or started
|
|
* state in order to add it for loudness updates.
|
|
* @return {@code false} if the {@code mediaCodec} was not configured or does
|
|
* not contain loudness metadata, {@code true} otherwise.
|
|
* @throws IllegalArgumentException if the same {@code mediaCodec} was already
|
|
* added before.
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
public boolean addMediaCodec(@NonNull MediaCodec mediaCodec) {
|
|
final MediaCodec mc = Objects.requireNonNull(mediaCodec,
|
|
"MediaCodec for addMediaCodec cannot be null");
|
|
final LoudnessCodecInfo mcInfo = getCodecInfo(mc);
|
|
|
|
if (mcInfo == null) {
|
|
Log.v(TAG, "Could not extract codec loudness information");
|
|
return false;
|
|
}
|
|
synchronized (mControllerLock) {
|
|
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()) {
|
|
throw new IllegalArgumentException(
|
|
"Loudness controller already added " + mediaCodec);
|
|
}
|
|
}
|
|
|
|
mLcDispatcher.addLoudnessCodecInfo(mSessionId, mediaCodec.hashCode(),
|
|
mcInfo);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @throws IllegalArgumentException if the {@code mediaCodec} was not configured,
|
|
* does not contain loudness metadata or if it
|
|
* was not added before
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
|
|
LoudnessCodecInfo mcInfo;
|
|
AtomicBoolean removedMc = new AtomicBoolean(false);
|
|
AtomicBoolean removeInfo = new AtomicBoolean(false);
|
|
|
|
mcInfo = getCodecInfo(Objects.requireNonNull(mediaCodec,
|
|
"MediaCodec for removeMediaCodec cannot be null"));
|
|
|
|
if (mcInfo == null) {
|
|
throw new IllegalArgumentException("Could not extract codec loudness information");
|
|
}
|
|
synchronized (mControllerLock) {
|
|
mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> {
|
|
removedMc.set(mcs.remove(mediaCodec));
|
|
if (mcs.isEmpty()) {
|
|
// remove the entry
|
|
removeInfo.set(true);
|
|
return null;
|
|
}
|
|
return mcs;
|
|
});
|
|
if (!removedMc.get()) {
|
|
throw new IllegalArgumentException(
|
|
"Loudness controller does not contain " + mediaCodec);
|
|
}
|
|
}
|
|
|
|
if (removeInfo.get()) {
|
|
mLcDispatcher.removeLoudnessCodecInfo(mSessionId, mcInfo);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the loudness parameters of the registered audio decoders
|
|
*
|
|
* <p>Those parameters may have been automatically applied if the
|
|
* {@code LoudnessCodecController} was created with {@link #create(int)}, or they are the
|
|
* parameters that have been sent to the {@link OnLoudnessCodecUpdateListener} if using a
|
|
* codec update listener.
|
|
*
|
|
* @param mediaCodec codec that decodes loudness annotated data. Has to be added
|
|
* with {@link #addMediaCodec(MediaCodec)} before calling this
|
|
* method
|
|
* @throws IllegalArgumentException if the passed {@link MediaCodec} was not
|
|
* added before calling this method
|
|
*
|
|
* @return the {@link Bundle} containing the current loudness parameters.
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
@NonNull
|
|
public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
|
|
Objects.requireNonNull(mediaCodec, "MediaCodec cannot be null");
|
|
|
|
LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec);
|
|
if (codecInfo == null) {
|
|
throw new IllegalArgumentException("MediaCodec does not have valid codec information");
|
|
}
|
|
|
|
synchronized (mControllerLock) {
|
|
final Set<MediaCodec> codecs = mMediaCodecs.get(codecInfo);
|
|
if (codecs == null || !codecs.contains(mediaCodec)) {
|
|
throw new IllegalArgumentException(
|
|
"MediaCodec was not added for loudness annotation");
|
|
}
|
|
}
|
|
|
|
return mLcDispatcher.getLoudnessCodecParams(codecInfo);
|
|
}
|
|
|
|
/**
|
|
* Stops any loudness updates and frees up the resources.
|
|
*/
|
|
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
|
|
@Override
|
|
public void close() {
|
|
synchronized (mControllerLock) {
|
|
mMediaCodecs.clear();
|
|
}
|
|
mLcDispatcher.stopLoudnessCodecUpdates(mSessionId);
|
|
}
|
|
|
|
/** @hide */
|
|
/*package*/ int getSessionId() {
|
|
return mSessionId;
|
|
}
|
|
|
|
/** @hide */
|
|
/*package*/ void mediaCodecsConsume(
|
|
Consumer<Entry<LoudnessCodecInfo, Set<MediaCodec>>> consumer) {
|
|
synchronized (mControllerLock) {
|
|
for (Entry<LoudnessCodecInfo, Set<MediaCodec>> entry : mMediaCodecs.entrySet()) {
|
|
consumer.accept(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
try {
|
|
final MediaFormat inputFormat = mediaCodec.getInputFormat();
|
|
final String mimeType = inputFormat.getString(MediaFormat.KEY_MIME);
|
|
if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mimeType)) {
|
|
// check both KEY_AAC_PROFILE and KEY_PROFILE as some codecs may only recognize
|
|
// one of these two keys
|
|
int aacProfile = -1;
|
|
int profile = -1;
|
|
try {
|
|
aacProfile = inputFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
|
|
} catch (NullPointerException e) {
|
|
// does not contain KEY_AAC_PROFILE. do nothing
|
|
}
|
|
try {
|
|
profile = inputFormat.getInteger(MediaFormat.KEY_PROFILE);
|
|
} catch (NullPointerException e) {
|
|
// does not contain KEY_PROFILE. do nothing
|
|
}
|
|
if (aacProfile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE
|
|
|| profile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE) {
|
|
lci.metadataType = CODEC_METADATA_TYPE_MPEG_D;
|
|
} else {
|
|
lci.metadataType = CODEC_METADATA_TYPE_MPEG_4;
|
|
}
|
|
} else {
|
|
Log.w(TAG, "MediaCodec mime type not supported for loudness annotation");
|
|
return null;
|
|
}
|
|
|
|
final MediaFormat outputFormat = mediaCodec.getOutputFormat();
|
|
lci.isDownmixing = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
|
|
< inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
|
|
} catch (IllegalStateException e) {
|
|
Log.e(TAG, "MediaCodec is not configured", e);
|
|
return null;
|
|
}
|
|
|
|
return lci;
|
|
}
|
|
}
|