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