Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cc485936 authored by Vlad Popa's avatar Vlad Popa
Browse files

CTA2075: add client loudness configuration interfaces

The client can instantiate LoudnessCodecConfigurator's which are used to
receive loudness updates as defined by CTA2075. They can subscribe to
receive asynchronous updates with the option to modify the MediaCodec
parameter so get a one time synchronous result for a given codec.

Test: adb shell device_config put media_audio android.media.audio.loudness_configurator_api true
Test: atest LoudnessCodecConfiguratorTest
Bug: 298463873
Change-Id: Id76063821211f786957ef8d7df97419f74d9ae53
parent 6947720e
Loading
Loading
Loading
Loading
+28 −1
Original line number Diff line number Diff line
@@ -19,8 +19,8 @@ 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;
@@ -2941,6 +2941,33 @@ public class AudioManager {
        return new Spatializer(this);
    }

    //====================================================================
    // 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
    /**
+18 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import android.media.ICapturePresetDevicesRoleDispatcher;
import android.media.ICommunicationDeviceDispatcher;
import android.media.IDeviceVolumeBehaviorDispatcher;
import android.media.IDevicesForAttributesCallback;
import android.media.ILoudnessCodecUpdatesDispatcher;
import android.media.IMuteAwaitConnectionCallback;
import android.media.IPlaybackConfigDispatcher;
import android.media.IPreferredMixerAttributesDispatcher;
@@ -51,6 +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.PlayerBase;
import android.media.VolumeInfo;
import android.media.VolumePolicy;
@@ -728,4 +730,20 @@ interface IAudioService {
    @EnforcePermission("MODIFY_AUDIO_ROUTING")
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING)")
    boolean isBluetoothVariableLatencyEnabled();

    void registerLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);

    void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);

    oneway void startLoudnessCodecUpdates(in int piid);

    oneway void stopLoudnessCodecUpdates(in int piid);

    oneway void addLoudnesssCodecFormat(in int piid, in LoudnessCodecFormat format);

    oneway void addLoudnesssCodecFormatList(in int piid, in List<LoudnessCodecFormat> format);

    oneway void removeLoudnessCodecFormat(in int piid, in LoudnessCodecFormat format);

    PersistableBundle getLoudnessParams(in int piid, in LoudnessCodecFormat format);
}
+31 −0
Original line number Diff line number Diff line
/*
 * 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 android.os.PersistableBundle;

/**
 * Interface which provides updates for the clients about MediaCodec loudness
 * parameter changes.
 *
 * {@hide}
 */
oneway interface ILoudnessCodecUpdatesDispatcher {

    void dispatchLoudnessCodecParameterChange(int piid, in PersistableBundle params);

}
 No newline at end of file
+225 −0
Original line number Diff line number Diff line
/*
 * 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.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;

import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.os.Bundle;
import android.util.Log;

import androidx.annotation.NonNull;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * 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.
 * <p>A new object should be instantiated for each {@link AudioTrack} with the help
 * of {@link AudioManager#createLoudnessCodecConfigurator()}.
 *
 * TODO: remove hide once API is final
 * @hide
 */
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
public class LoudnessCodecConfigurator {
    private static final String TAG = "LoudnessCodecConfigurator";

    /**
     * Listener used for receiving asynchronous loudness metadata updates.
     *
     * TODO: remove hide once API is final
     * @hide
     */
    @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.
         *
         * TODO: remove hide once API is final
         * @hide
         */
        @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
        @NonNull
        default Bundle onLoudnessCodecUpdate(@NonNull MediaCodec mediaCodec,
                                             @NonNull Bundle codecValues) {
            return codecValues;
        }
    }

    @NonNull private final LoudnessCodecDispatcher mLcDispatcher;

    private AudioTrack mAudioTrack;

    private final List<MediaCodec> mMediaCodecs = new ArrayList<>();

    /** @hide */
    protected LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher) {
        mLcDispatcher = Objects.requireNonNull(lcDispatcher);
    }


    /**
     * 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()}.
     *
     * @param executor {@link Executor} to handle the callbacks
     * @param listener used to receive updates
     *
     * @return {@code true} if there is at least one {@link MediaCodec} and
     * {@link AudioTrack} set and the user can expect receiving updates.
     *
     * 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();
    }

    /**
     * Starts receiving asynchronous loudness updates.
     * <p>The registered MediaCodecs will be updated automatically without any client
     * callbacks.
     *
     * @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.
     *
     * 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();
    }

    /**
     * Stops receiving asynchronous loudness updates.
     *
     * TODO: remove hide once API is final
     * @hide
     */
    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
    public void stopLoudnessCodecUpdates() {
        mLcDispatcher.removeLoudnessCodecListener(this);
    }

    /**
     * Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack}
     * which is registered through {@link #setAudioTrack(AudioTrack)}.
     *
     * 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"));
    }

    /**
     * Removes the {@link MediaCodec} from receiving 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"));
    }

    /**
     * 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)})
     *
     * 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");
    }

    /**
     * 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.
     *
     * @return the {@link Bundle} containing the current loudness parameters. Caller is
     * responsible to update the {@link MediaCodec}
     *
     * TODO: remove hide once API is final
     * @hide
     */
    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
    @NonNull
    public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
        // TODO: implement synchronous loudness params updates
        return new Bundle();
    }

    private boolean checkStartLoudnessConfigurator() {
        if (mAudioTrack == null) {
            Log.w(TAG, "Cannot start loudness configurator without an AudioTrack");
            return false;
        }

        if (mMediaCodecs.isEmpty()) {
            Log.w(TAG, "Cannot start loudness configurator without at least one MediaCodec");
            return false;
        }

        return true;
    }
}
+109 −0
Original line number Diff line number Diff line
/*
 * 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 android.annotation.CallbackExecutor;
import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
import android.os.PersistableBundle;
import android.os.RemoteException;

import androidx.annotation.NonNull;

import java.util.HashMap;
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 {
        @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;
                    }));
        }

        @Override
        public void register(boolean register) {
            try {
                if (register) {
                    mAm.getService().registerLoudnessCodecUpdatesDispatcher(this);
                } else {
                    mAm.getService().unregisterLoudnessCodecUpdatesDispatcher(this);
                }
            } catch (RemoteException e) {
                e.rethrowFromSystemServer();
            }
        }
    }

    private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
            mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();

    @NonNull private final LoudnessCodecUpdatesDispatcherStub mLoudnessCodecStub;

    private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
            mConfiguratorListener = new HashMap<>();

    @NonNull private final AudioManager mAm;

    protected LoudnessCodecDispatcher(@NonNull AudioManager am) {
        mAm = Objects.requireNonNull(am);
        mLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
    }

    /** @hide */
    public LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
        return new LoudnessCodecConfigurator(this);
    }

    /** @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);
    }

    /** @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;
            }
        }
    }
}
Loading