1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
20 import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
21 import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
22 
23 import android.annotation.CallbackExecutor;
24 import android.annotation.FlaggedApi;
25 import android.media.permission.SafeCloseable;
26 import android.os.Bundle;
27 import android.util.Log;
28 
29 import androidx.annotation.GuardedBy;
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.Map.Entry;
36 import java.util.Objects;
37 import java.util.Set;
38 import java.util.concurrent.Executor;
39 import java.util.concurrent.Executors;
40 import java.util.concurrent.atomic.AtomicBoolean;
41 import java.util.function.Consumer;
42 
43 /**
44  * Class for getting recommended loudness parameter updates for audio decoders as they are used
45  * to play back media content according to the encoded format and current audio routing. These
46  * audio decoder updates leverage loudness metadata present in compressed audio streams. They
47  * ensure the loudness and dynamic range of the content is optimized to the physical
48  * characteristics of the audio output device (e.g. phone microspeakers vs headphones vs TV
49  * speakers).Those updates can be automatically applied to the {@link MediaCodec} instance(s), or
50  * be provided to the user. The codec loudness management parameter updates are computed in
51  * accordance to the CTA-2075 standard.
52  * <p>A new object should be instantiated for each audio session
53  * (see {@link AudioManager#generateAudioSessionId()}) using creator methods {@link #create(int)} or
54  * {@link #create(int, Executor, OnLoudnessCodecUpdateListener)}.
55  */
56 @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
57 public class LoudnessCodecController implements SafeCloseable {
58     private static final String TAG = "LoudnessCodecController";
59 
60     /**
61      * Listener used for receiving asynchronous loudness metadata updates.
62      */
63     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
64     public interface OnLoudnessCodecUpdateListener {
65         /**
66          * Contains the MediaCodec key/values that can be set directly to
67          * configure the loudness of the handle's corresponding decoder (see
68          * {@link MediaCodec#setParameters(Bundle)}).
69          *
70          * @param mediaCodec  the mediaCodec that will receive the new parameters
71          * @param codecValues contains loudness key/value pairs that can be set
72          *                    directly on the mediaCodec. The listener can modify
73          *                    these values with their own edits which will be
74          *                    returned for the mediaCodec configuration
75          *
76          * @return a Bundle which contains the original computed codecValues
77          * aggregated with user edits. The platform will configure the associated
78          * MediaCodecs with the returned Bundle params.
79          */
80         @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
81         @NonNull
onLoudnessCodecUpdate(@onNull MediaCodec mediaCodec, @NonNull Bundle codecValues)82         default Bundle onLoudnessCodecUpdate(@NonNull MediaCodec mediaCodec,
83                 @NonNull Bundle codecValues) {
84             return codecValues;
85         }
86     }
87 
88     @NonNull
89     private final LoudnessCodecDispatcher mLcDispatcher;
90 
91     private final Object mControllerLock = new Object();
92 
93     private final int mSessionId;
94 
95     @GuardedBy("mControllerLock")
96     private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>();
97 
98     /**
99      * Creates a new instance of {@link LoudnessCodecController}
100      *
101      * <p>This method should be used when the client does not need to alter the
102      * codec loudness parameters before they are applied to the audio decoders.
103      * Otherwise, use {@link #create(int, Executor, OnLoudnessCodecUpdateListener)}.
104      *
105      * @param sessionId  the session ID of the track that will receive data
106      *                        from the added {@link MediaCodec}'s
107      *
108      * @return the {@link LoudnessCodecController} instance
109      */
110     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
create(int sessionId)111     public static @NonNull LoudnessCodecController create(int sessionId) {
112         final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(
113                 AudioManager.getService());
114         final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
115                 sessionId);
116         dispatcher.addLoudnessCodecListener(controller, Executors.newSingleThreadExecutor(),
117                 new OnLoudnessCodecUpdateListener() {});
118         dispatcher.startLoudnessCodecUpdates(sessionId);
119         return controller;
120     }
121 
122     /**
123      * Creates a new instance of {@link LoudnessCodecController}
124      *
125      * <p>This method should be used when the client wants to alter the codec
126      * loudness parameters before they are applied to the audio decoders.
127      * Otherwise, use {@link #create( int)}.
128      *
129      * @param sessionId       the session ID of the track that will receive data
130      *                        from the added {@link MediaCodec}'s
131      * @param executor        {@link Executor} to handle the callbacks
132      * @param listener        used for receiving updates
133      *
134      * @return the {@link LoudnessCodecController} instance
135      */
136     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
create( int sessionId, @NonNull @CallbackExecutor Executor executor, @NonNull OnLoudnessCodecUpdateListener listener)137     public static @NonNull LoudnessCodecController create(
138             int sessionId,
139             @NonNull @CallbackExecutor Executor executor,
140             @NonNull OnLoudnessCodecUpdateListener listener) {
141         Objects.requireNonNull(executor, "Executor cannot be null");
142         Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
143 
144         final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(
145                 AudioManager.getService());
146         final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
147                 sessionId);
148         dispatcher.addLoudnessCodecListener(controller, executor, listener);
149         dispatcher.startLoudnessCodecUpdates(sessionId);
150         return controller;
151     }
152 
153     /**
154      * Creates a new instance of {@link LoudnessCodecController}
155      *
156      * <p>This method should be used only in testing
157      *
158      * @param sessionId  the session ID of the track that will receive data
159      *                        from the added {@link MediaCodec}'s
160      * @param executor {@link Executor} to handle the callbacks
161      * @param listener used for receiving updates
162      * @param service  interface for communicating with AudioService
163      *
164      * @return the {@link LoudnessCodecController} instance
165      * @hide
166      */
createForTesting( int sessionId, @NonNull @CallbackExecutor Executor executor, @NonNull OnLoudnessCodecUpdateListener listener, @NonNull IAudioService service)167     public static @NonNull LoudnessCodecController createForTesting(
168             int sessionId,
169             @NonNull @CallbackExecutor Executor executor,
170             @NonNull OnLoudnessCodecUpdateListener listener,
171             @NonNull IAudioService service) {
172         Objects.requireNonNull(service, "IAudioService cannot be null");
173         Objects.requireNonNull(executor, "Executor cannot be null");
174         Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
175 
176         final LoudnessCodecDispatcher dispatcher = new LoudnessCodecDispatcher(service);
177         final LoudnessCodecController controller = new LoudnessCodecController(dispatcher,
178                 sessionId);
179         dispatcher.addLoudnessCodecListener(controller, executor, listener);
180         dispatcher.startLoudnessCodecUpdates(sessionId);
181         return controller;
182     }
183 
184     /** @hide */
LoudnessCodecController(@onNull LoudnessCodecDispatcher lcDispatcher, int sessionId)185     private LoudnessCodecController(@NonNull LoudnessCodecDispatcher lcDispatcher, int sessionId) {
186         mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null");
187         mSessionId = sessionId;
188     }
189 
190     /**
191      * Adds a new {@link MediaCodec} that will stream data to a player
192      * which uses {@link #mSessionId}.
193      *
194      * <p>No new element will be added if the passed {@code mediaCodec} was
195      * previously added.
196      *
197      * @param mediaCodec the codec to start receiving asynchronous loudness
198      *                   updates. The codec has to be in a configured or started
199      *                   state in order to add it for loudness updates.
200      * @return {@code false} if the {@code mediaCodec} was not configured or does
201      * not contain loudness metadata, {@code true} otherwise.
202      * @throws IllegalArgumentException if the same {@code mediaCodec} was already
203      *                                  added before.
204      */
205     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
addMediaCodec(@onNull MediaCodec mediaCodec)206     public boolean addMediaCodec(@NonNull MediaCodec mediaCodec) {
207         final MediaCodec mc = Objects.requireNonNull(mediaCodec,
208                 "MediaCodec for addMediaCodec cannot be null");
209         final LoudnessCodecInfo mcInfo = getCodecInfo(mc);
210 
211         if (mcInfo == null) {
212             Log.v(TAG, "Could not extract codec loudness information");
213             return false;
214         }
215         synchronized (mControllerLock) {
216             final AtomicBoolean containsCodec = new AtomicBoolean(false);
217             Set<MediaCodec> newSet = mMediaCodecs.computeIfPresent(mcInfo, (info, codecSet) -> {
218                 containsCodec.set(!codecSet.add(mc));
219                 return codecSet;
220             });
221             if (newSet == null) {
222                 newSet = new HashSet<>();
223                 newSet.add(mc);
224                 mMediaCodecs.put(mcInfo, newSet);
225             }
226             if (containsCodec.get()) {
227                 throw new IllegalArgumentException(
228                         "Loudness controller already added " + mediaCodec);
229             }
230         }
231 
232         mLcDispatcher.addLoudnessCodecInfo(mSessionId, mediaCodec.hashCode(),
233                 mcInfo);
234 
235         return true;
236     }
237 
238     /**
239      * Removes the {@link MediaCodec} from receiving loudness updates.
240      *
241      * <p>This method can be called while asynchronous updates are live.
242      *
243      * <p>No elements will be removed if the passed mediaCodec was not added before.
244      *
245      * @param mediaCodec the element to remove for receiving asynchronous updates
246      * @throws IllegalArgumentException if the {@code mediaCodec} was not configured,
247      *                                  does not contain loudness metadata or if it
248      *                                  was not added before
249      */
250     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
removeMediaCodec(@onNull MediaCodec mediaCodec)251     public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
252         LoudnessCodecInfo mcInfo;
253         AtomicBoolean removedMc = new AtomicBoolean(false);
254         AtomicBoolean removeInfo = new AtomicBoolean(false);
255 
256         mcInfo = getCodecInfo(Objects.requireNonNull(mediaCodec,
257                 "MediaCodec for removeMediaCodec cannot be null"));
258 
259         if (mcInfo == null) {
260             throw new IllegalArgumentException("Could not extract codec loudness information");
261         }
262         synchronized (mControllerLock) {
263             mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> {
264                 removedMc.set(mcs.remove(mediaCodec));
265                 if (mcs.isEmpty()) {
266                     // remove the entry
267                     removeInfo.set(true);
268                     return null;
269                 }
270                 return mcs;
271             });
272             if (!removedMc.get()) {
273                 throw new IllegalArgumentException(
274                         "Loudness controller does not contain " + mediaCodec);
275             }
276         }
277 
278         if (removeInfo.get()) {
279             mLcDispatcher.removeLoudnessCodecInfo(mSessionId, mcInfo);
280         }
281     }
282 
283     /**
284      * Returns the loudness parameters of the registered audio decoders
285      *
286      * <p>Those parameters may have been automatically applied if the
287      * {@code LoudnessCodecController} was created with {@link #create(int)}, or they are the
288      * parameters that have been sent to the {@link OnLoudnessCodecUpdateListener} if using a
289      * codec update listener.
290      *
291      * @param mediaCodec codec that decodes loudness annotated data. Has to be added
292      *                   with {@link #addMediaCodec(MediaCodec)} before calling this
293      *                   method
294      * @throws IllegalArgumentException if the passed {@link MediaCodec} was not
295      *                                  added before calling this method
296      *
297      * @return the {@link Bundle} containing the current loudness parameters.
298      */
299     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
300     @NonNull
getLoudnessCodecParams(@onNull MediaCodec mediaCodec)301     public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
302         Objects.requireNonNull(mediaCodec, "MediaCodec cannot be null");
303 
304         LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec);
305         if (codecInfo == null) {
306             throw new IllegalArgumentException("MediaCodec does not have valid codec information");
307         }
308 
309         synchronized (mControllerLock) {
310             final Set<MediaCodec> codecs = mMediaCodecs.get(codecInfo);
311             if (codecs == null || !codecs.contains(mediaCodec)) {
312                 throw new IllegalArgumentException(
313                         "MediaCodec was not added for loudness annotation");
314             }
315         }
316 
317         return mLcDispatcher.getLoudnessCodecParams(codecInfo);
318     }
319 
320     /**
321      * Stops any loudness updates and frees up the resources.
322      */
323     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
324     @Override
close()325     public void close() {
326         synchronized (mControllerLock) {
327             mMediaCodecs.clear();
328         }
329         mLcDispatcher.stopLoudnessCodecUpdates(mSessionId);
330     }
331 
332     /** @hide */
getSessionId()333     /*package*/ int getSessionId() {
334         return mSessionId;
335     }
336 
337     /** @hide */
mediaCodecsConsume( Consumer<Entry<LoudnessCodecInfo, Set<MediaCodec>>> consumer)338     /*package*/ void mediaCodecsConsume(
339             Consumer<Entry<LoudnessCodecInfo, Set<MediaCodec>>> consumer) {
340         synchronized (mControllerLock) {
341             for (Entry<LoudnessCodecInfo, Set<MediaCodec>> entry : mMediaCodecs.entrySet()) {
342                 consumer.accept(entry);
343             }
344         }
345     }
346 
347     @Nullable
getCodecInfo(@onNull MediaCodec mediaCodec)348     private static LoudnessCodecInfo getCodecInfo(@NonNull MediaCodec mediaCodec) {
349         LoudnessCodecInfo lci = new LoudnessCodecInfo();
350         final MediaCodecInfo codecInfo = mediaCodec.getCodecInfo();
351         if (codecInfo.isEncoder()) {
352             // loudness info only for decoders
353             Log.w(TAG, "MediaCodec used for encoding does not support loudness annotation");
354             return null;
355         }
356 
357         try {
358             final MediaFormat inputFormat = mediaCodec.getInputFormat();
359             final String mimeType = inputFormat.getString(MediaFormat.KEY_MIME);
360             if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mimeType)) {
361                 // check both KEY_AAC_PROFILE and KEY_PROFILE as some codecs may only recognize
362                 // one of these two keys
363                 int aacProfile = -1;
364                 int profile = -1;
365                 try {
366                     aacProfile = inputFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
367                 } catch (NullPointerException e) {
368                     // does not contain KEY_AAC_PROFILE. do nothing
369                 }
370                 try {
371                     profile = inputFormat.getInteger(MediaFormat.KEY_PROFILE);
372                 } catch (NullPointerException e) {
373                     // does not contain KEY_PROFILE. do nothing
374                 }
375                 if (aacProfile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE
376                         || profile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE) {
377                     lci.metadataType = CODEC_METADATA_TYPE_MPEG_D;
378                 } else {
379                     lci.metadataType = CODEC_METADATA_TYPE_MPEG_4;
380                 }
381             } else {
382                 Log.w(TAG, "MediaCodec mime type not supported for loudness annotation");
383                 return null;
384             }
385 
386             final MediaFormat outputFormat = mediaCodec.getOutputFormat();
387             lci.isDownmixing = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
388                     < inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
389         } catch (IllegalStateException e) {
390             Log.e(TAG, "MediaCodec is not configured", e);
391             return null;
392         }
393 
394         return lci;
395     }
396 }
397