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

Commit 1b8f65d4 authored by Mikhail Naganov's avatar Mikhail Naganov
Browse files

audio: Add initial support for clip transition reporting

For offloaded playback, provide separate reporting for
the moment when the HAL is ready to receive data for the next
clip, and the moment when the playback of the previous clip
has ended. See the updated state transition diagram for
details.

Enhance the stream state model with extra internal states
to support proper indication of the "previous clip has
finished playback" event.

HALs implementing Core API V3 (FRC 202504) can indicate
support for this behavior by exposing 'aosp.clipTransitionSupport'
vendor property. In Core API V4 this will be default behavior.

Bug: 373872271
Bug: 384431822
Test: VtsHalAudioCoreTargetTest
Change-Id: Ib912c507978eb6045d889d6d9cd27b5661b64f49
parent 2b6b7fea
Loading
Loading
Loading
Loading
+15 −1
Original line number Diff line number Diff line
@@ -188,6 +188,14 @@ parcelable StreamDescriptor {
         * In the 'DRAINING' state the producer is inactive, the consumer is
         * finishing up on the buffer contents, emptying it up. As soon as it
         * gets empty, the stream transfers itself into the next state.
         *
         * Note that "early notify" draining is a more complex procedure
         * intended for transitioning between two clips. Both 'DRAINING' and
         * 'DRAIN_PAUSED' states have "sub-states" not visible via the API. See
         * the details in the 'stream-out-async-sm.gv' state machine
         * description. In the HAL API V3 this behavior is enabled when the
         * HAL exposes "aosp.clipTransitionSupport" property, and in the HAL
         * API V4 it is the default behavior.
         */
        DRAINING = 5,
        /**
@@ -234,9 +242,15 @@ parcelable StreamDescriptor {
        /**
         * Used with output streams only, the HAL module indicates drain
         * completion shortly before all audio data has been consumed in order
         * to give the client an opportunity to provide data for the next track
         * to give the client an opportunity to provide data for the next clip
         * for gapless playback. The exact amount of provided time is specific
         * to the HAL implementation.
         *
         * In the HAL API V3, the HAL sends two 'onDrainReady' notifications:
         * one to indicate readiness to receive next clip data, and another when
         * the previous clip has finished playing. This behavior is enabled when
         * the HAL exposes "aosp.clipTransitionSupport" property, and in the HAL
         * API V4 it is the default behavior.
         */
        DRAIN_EARLY_NOTIFY = 2,
    }
+56 −5
Original line number Diff line number Diff line
@@ -32,27 +32,78 @@ digraph stream_out_async_state_machine {
    IDLE -> TRANSFERRING [label="burst"];             // producer -> active
    IDLE -> ACTIVE [label="burst"];                   // full write
    ACTIVE -> PAUSED [label="pause"];                 // consumer -> passive (not consuming)
    ACTIVE -> DRAINING [label="drain"];               // producer -> passive
    ACTIVE -> DRAINING [label="drain(ALL)"];          // producer -> passive
    ACTIVE -> DRAINING_en [label="drain(EARLY_NOTIFY)"];  // prepare for clip transition
    ACTIVE -> TRANSFERRING [label="burst"];           // early unblocking
    ACTIVE -> ACTIVE [label="burst"];                 // full write
    TRANSFERRING -> ACTIVE [label="←IStreamCallback.onTransferReady"];
    TRANSFERRING -> TRANSFER_PAUSED [label="pause"];  // consumer -> passive (not consuming)
    TRANSFERRING -> DRAINING [label="drain"];         // producer -> passive
    TRANSFERRING -> DRAINING [label="drain(ALL)"];    // producer -> passive
    TRANSFERRING -> DRAINING_en [label="drain(EARLY_NOTIFY)"]; // prepare for clip transition
    TRANSFER_PAUSED -> TRANSFERRING [label="start"];  // consumer -> active
    TRANSFER_PAUSED -> DRAIN_PAUSED [label="drain"];  // producer -> passive
    TRANSFER_PAUSED -> DRAIN_PAUSED [label="drain(ALL)"];  // producer -> passive
    TRANSFER_PAUSED -> IDLE [label="flush"];          // buffer is cleared
    PAUSED -> PAUSED [label="burst"];
    PAUSED -> ACTIVE [label="start"];                 // consumer -> active
    PAUSED -> IDLE [label="flush"];                   // producer -> passive, buffer is cleared
    DRAINING -> IDLE [label="←IStreamCallback.onDrainReady"];
    DRAINING -> DRAINING [label="←IStreamCallback.onDrainReady"];  // allowed for `DRAIN_EARLY_NOTIFY`
    DRAINING -> IDLE [label="<empty buffer>"];        // allowed for `DRAIN_EARLY_NOTIFY`
    DRAINING -> TRANSFERRING [label="burst"];         // producer -> active
    DRAINING -> ACTIVE [label="burst"];               // full write
    DRAINING -> DRAIN_PAUSED [label="pause"];         // consumer -> passive (not consuming)
    DRAIN_PAUSED -> DRAINING [label="start"];         // consumer -> active
    DRAIN_PAUSED -> TRANSFER_PAUSED [label="burst"];  // producer -> active
    DRAIN_PAUSED -> IDLE [label="flush"];             // buffer is cleared
    // Note that the states in both clusters are combined with 'DRAINING' and 'DRAIN_PAUSED'
    // state at the API level. The 'en' and 'en_sent' attributes only belong to the internal
    // state of the stream and are not observable outside.
    subgraph cluster_early_notify_entering {
        // The stream is preparing for a transition between two clips. After
        // receiving 'drain(EARLY_NOTIFY)' command, the stream continues playing
        // the current clip, and at some point notifies the client that it is
        // ready for the next clip data by issuing the first 'onDrainReady'
        // callback.
        label="EARLY_NOTIFY (entering)";
        color=gray;
        // Getting 'burst' or 'flush' command in these states resets the "clip
        // transition" mode.
        DRAINING_en;
        DRAIN_PAUSED_en;
    }
    subgraph cluster_early_notify_notification_sent {
        // After the stream has sent "onDrainReady", the client can now send
        // 'burst' commands with the data of the next clip. These 'bursts' are
        // always "early unblocking" because the previous clip is still playing
        // thus the stream is unable to play any of the received data
        // synchronously (in other words, it can not do a "full write"). To
        // indicate readiness to accept the next burst the stream uses the usual
        // 'onTransferReady' callback.
        label="EARLY_NOTIFY (notification sent)";
        color=gray;
        // The state machine remains in these states until the current clip ends
        // playing. When it ends, the stream sends 'onDrainReady' (note that
        // it's the second 'onDrainReady' for the same 'drain(EARLY_NOTIFY)'),
        // and transitions either to 'IDLE' if there is no data for the next
        // clip, or to 'TRANSFERRING' otherwise. Note that it can not transition
        // to 'ACTIVE' because that transition is associated with
        // 'onTransferReady' callback.
        DRAINING_en_sent;
        DRAIN_PAUSED_en_sent;
    }
    DRAINING_en -> TRANSFERRING [label="burst"];                  // producer -> active
    DRAINING_en -> ACTIVE [label="burst"];                        // full write
    DRAINING_en -> DRAIN_PAUSED_en [label="pause"];               // consumer -> passive (not consuming)
    DRAINING_en -> DRAINING_en_sent [label="←IStreamCallback.onDrainReady"];
    DRAIN_PAUSED_en -> DRAINING_en [label="start"];               // consumer -> active
    DRAIN_PAUSED_en -> TRANSFER_PAUSED [label="burst"];           // producer -> active
    DRAIN_PAUSED_en -> IDLE [label="flush"];                      // buffer is cleared
    DRAINING_en_sent -> DRAINING_en_sent [label="burst"];
    DRAINING_en_sent -> DRAINING_en_sent [label="←IStreamCallback.onTransferReady"];
    DRAINING_en_sent -> DRAIN_PAUSED_en_sent [label="pause"];     // consumer -> passive (not consuming)
    DRAINING_en_sent -> TRANSFERRING [label="←IStreamCallback.onDrainReady"];
    DRAINING_en_sent -> IDLE [label="←IStreamCallback.onDrainReady"];
    DRAIN_PAUSED_en_sent -> DRAINING_en_sent [label="start"];     // consumer -> active
    DRAIN_PAUSED_en_sent -> DRAIN_PAUSED_en_sent [label="burst"]; // producer -> active
    DRAIN_PAUSED_en_sent -> IDLE [label="flush"];                 // buffer is cleared
    ANY_STATE -> ERROR [label="←IStreamCallback.onError"];
    ANY_STATE -> CLOSED [label="→IStream*.close"];
    CLOSED -> F;
+5 −0
Original line number Diff line number Diff line
@@ -1546,6 +1546,7 @@ ndk::ScopedAStatus Module::generateHwAvSyncId(int32_t* _aidl_return) {

const std::string Module::VendorDebug::kForceTransientBurstName = "aosp.forceTransientBurst";
const std::string Module::VendorDebug::kForceSynchronousDrainName = "aosp.forceSynchronousDrain";
const std::string Module::kClipTransitionSupportName = "aosp.clipTransitionSupport";

ndk::ScopedAStatus Module::getVendorParameters(const std::vector<std::string>& in_ids,
                                               std::vector<VendorParameter>* _aidl_return) {
@@ -1560,6 +1561,10 @@ ndk::ScopedAStatus Module::getVendorParameters(const std::vector<std::string>& i
            VendorParameter forceSynchronousDrain{.id = id};
            forceSynchronousDrain.ext.setParcelable(Boolean{mVendorDebug.forceSynchronousDrain});
            _aidl_return->push_back(std::move(forceSynchronousDrain));
        } else if (id == kClipTransitionSupportName) {
            VendorParameter clipTransitionSupport{.id = id};
            clipTransitionSupport.ext.setParcelable(Boolean{true});
            _aidl_return->push_back(std::move(clipTransitionSupport));
        } else {
            allParametersKnown = false;
            LOG(VERBOSE) << __func__ << ": " << mType << ": unrecognized parameter \"" << id << "\"";
+16 −6
Original line number Diff line number Diff line
@@ -387,12 +387,16 @@ const std::string StreamOutWorkerLogic::kThreadName = "writer";

void StreamOutWorkerLogic::onBufferStateChange(size_t bufferFramesLeft) {
    const StreamDescriptor::State state = mState;
    LOG(DEBUG) << __func__ << ": state: " << toString(state)
    const DrainState drainState = mDrainState;
    LOG(DEBUG) << __func__ << ": state: " << toString(state) << ", drainState: " << drainState
               << ", bufferFramesLeft: " << bufferFramesLeft;
    if (state == StreamDescriptor::State::TRANSFERRING || drainState == DrainState::EN_SENT) {
        if (state == StreamDescriptor::State::TRANSFERRING) {
            mState = StreamDescriptor::State::ACTIVE;
        }
        std::shared_ptr<IStreamCallback> asyncCallback = mContext->getAsyncCallback();
        if (asyncCallback != nullptr) {
            LOG(VERBOSE) << __func__ << ": sending onTransferReady";
            ndk::ScopedAStatus status = asyncCallback->onTransferReady();
            if (!status.isOk()) {
                LOG(ERROR) << __func__ << ": error from onTransferReady: " << status;
@@ -411,8 +415,10 @@ void StreamOutWorkerLogic::onClipStateChange(size_t clipFramesLeft, bool hasNext
        mState =
                hasNextClip ? StreamDescriptor::State::TRANSFERRING : StreamDescriptor::State::IDLE;
        mDrainState = DrainState::NONE;
        if (drainState == DrainState::ALL && asyncCallback != nullptr) {
        if ((drainState == DrainState::ALL || drainState == DrainState::EN_SENT) &&
            asyncCallback != nullptr) {
            LOG(DEBUG) << __func__ << ": sending onDrainReady";
            // For EN_SENT, this is the second onDrainReady which notifies about clip transition.
            ndk::ScopedAStatus status = asyncCallback->onDrainReady();
            if (!status.isOk()) {
                LOG(ERROR) << __func__ << ": error from onDrainReady: " << status;
@@ -539,13 +545,17 @@ StreamOutWorkerLogic::Status StreamOutWorkerLogic::cycle() {
                            mState = StreamDescriptor::State::TRANSFER_PAUSED;
                        }
                    } else if (mState == StreamDescriptor::State::IDLE ||
                               mState == StreamDescriptor::State::DRAINING ||
                               mState == StreamDescriptor::State::ACTIVE) {
                               mState == StreamDescriptor::State::ACTIVE ||
                               (mState == StreamDescriptor::State::DRAINING &&
                                mDrainState != DrainState::EN_SENT)) {
                        if (asyncCallback == nullptr || reply.fmqByteCount == fmqByteCount) {
                            mState = StreamDescriptor::State::ACTIVE;
                        } else {
                            switchToTransientState(StreamDescriptor::State::TRANSFERRING);
                        }
                    } else if (mState == StreamDescriptor::State::DRAINING &&
                               mDrainState == DrainState::EN_SENT) {
                        // keep mState
                    }
                } else {
                    populateReplyWrongState(&reply, command);
+1 −0
Original line number Diff line number Diff line
@@ -159,6 +159,7 @@ class Module : public BnModule {
    // Multimap because both ports and configs can be used by multiple patches.
    using Patches = std::multimap<int32_t, int32_t>;

    static const std::string kClipTransitionSupportName;
    const Type mType;
    std::unique_ptr<Configuration> mConfig;
    ModuleDebug mDebug;
Loading