Merge "audio: Add initial support for clip transition reporting" into main am: d3bd8f9a8a

Original change: https://android-review.googlesource.com/c/platform/hardware/interfaces/+/3474372

Change-Id: Ia62f1ff49aef995b8dcb6d0142f48466cf801835
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/audio/aidl/android/hardware/audio/core/StreamDescriptor.aidl b/audio/aidl/android/hardware/audio/core/StreamDescriptor.aidl
index cfe001e..9b7fea2 100644
--- a/audio/aidl/android/hardware/audio/core/StreamDescriptor.aidl
+++ b/audio/aidl/android/hardware/audio/core/StreamDescriptor.aidl
@@ -188,6 +188,14 @@
          * 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 @@
         /**
          * 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,
     }
diff --git a/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv b/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv
index e2da90d..bf75594 100644
--- a/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv
+++ b/audio/aidl/android/hardware/audio/core/stream-out-async-sm.gv
@@ -32,27 +32,78 @@
     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;
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index 6a92481..123a5ec 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -1555,6 +1555,7 @@
 
 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) {
@@ -1569,6 +1570,10 @@
             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 << "\"";
diff --git a/audio/aidl/default/Stream.cpp b/audio/aidl/default/Stream.cpp
index 762c75e..2800bed 100644
--- a/audio/aidl/default/Stream.cpp
+++ b/audio/aidl/default/Stream.cpp
@@ -406,12 +406,16 @@
 
 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) {
-        mState = StreamDescriptor::State::ACTIVE;
+    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;
@@ -430,8 +434,10 @@
         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;
@@ -559,13 +565,17 @@
                             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);
diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h
index 6d59028..0661015 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -161,6 +161,7 @@
     // 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;
diff --git a/audio/aidl/default/include/core-impl/StreamOffloadStub.h b/audio/aidl/default/include/core-impl/StreamOffloadStub.h
index 67abe95..24e98c2 100644
--- a/audio/aidl/default/include/core-impl/StreamOffloadStub.h
+++ b/audio/aidl/default/include/core-impl/StreamOffloadStub.h
@@ -26,14 +26,16 @@
 namespace aidl::android::hardware::audio::core {
 
 struct DspSimulatorState {
+    static constexpr int64_t kSkipBufferNotifyFrames = -1;
+
     const std::string formatEncoding;
     const int sampleRate;
     const int64_t earlyNotifyFrames;
-    const int64_t bufferNotifyFrames;
     DriverCallbackInterface* callback = nullptr;  // set before starting DSP worker
     std::mutex lock;
     std::vector<int64_t> clipFramesLeft GUARDED_BY(lock);
-    int64_t bufferFramesLeft GUARDED_BY(lock);
+    int64_t bufferFramesLeft GUARDED_BY(lock) = 0;
+    int64_t bufferNotifyFrames GUARDED_BY(lock) = kSkipBufferNotifyFrames;
 };
 
 class DspSimulatorLogic : public ::android::hardware::audio::common::StreamLogic {
@@ -68,6 +70,7 @@
   private:
     ::android::status_t startWorkerIfNeeded();
 
+    const int64_t mBufferNotifyFrames;
     DspSimulatorState mState;
     DspSimulatorWorker mDspWorker;
     bool mDspWorkerStarted = false;
diff --git a/audio/aidl/default/stub/StreamOffloadStub.cpp b/audio/aidl/default/stub/StreamOffloadStub.cpp
index 95cef35..155f76d 100644
--- a/audio/aidl/default/stub/StreamOffloadStub.cpp
+++ b/audio/aidl/default/stub/StreamOffloadStub.cpp
@@ -42,14 +42,13 @@
     const int64_t clipFramesPlayed =
             (::android::uptimeNanos() - timeBeginNs) * mSharedState.sampleRate / NANOS_PER_SECOND;
     const int64_t bufferFramesConsumed = clipFramesPlayed / 2;  // assume 1:2 compression ratio
-    int64_t bufferFramesLeft = 0;
+    int64_t bufferFramesLeft = 0, bufferNotifyFrames = DspSimulatorState::kSkipBufferNotifyFrames;
     {
         std::lock_guard l(mSharedState.lock);
         mSharedState.bufferFramesLeft =
                 mSharedState.bufferFramesLeft > bufferFramesConsumed
                         ? mSharedState.bufferFramesLeft - bufferFramesConsumed
                         : 0;
-        bufferFramesLeft = mSharedState.bufferFramesLeft;
         int64_t framesPlayed = clipFramesPlayed;
         while (framesPlayed > 0 && !mSharedState.clipFramesLeft.empty()) {
             LOG(VERBOSE) << __func__ << ": clips: "
@@ -65,10 +64,21 @@
                 clipNotifies.emplace_back(0 /*clipFramesLeft*/, hasNextClip);
                 framesPlayed -= mSharedState.clipFramesLeft[0];
                 mSharedState.clipFramesLeft.erase(mSharedState.clipFramesLeft.begin());
+                if (!hasNextClip) {
+                    // Since it's a simulation, the buffer consumption rate it not real,
+                    // thus 'bufferFramesLeft' might still have something, need to erase it.
+                    mSharedState.bufferFramesLeft = 0;
+                }
             }
         }
+        bufferFramesLeft = mSharedState.bufferFramesLeft;
+        bufferNotifyFrames = mSharedState.bufferNotifyFrames;
+        if (bufferFramesLeft <= bufferNotifyFrames) {
+            // Suppress further notifications.
+            mSharedState.bufferNotifyFrames = DspSimulatorState::kSkipBufferNotifyFrames;
+        }
     }
-    if (bufferFramesLeft <= mSharedState.bufferNotifyFrames) {
+    if (bufferFramesLeft <= bufferNotifyFrames) {
         LOG(DEBUG) << __func__ << ": sending onBufferStateChange: " << bufferFramesLeft;
         mSharedState.callback->onBufferStateChange(bufferFramesLeft);
     }
@@ -82,9 +92,9 @@
 
 DriverOffloadStubImpl::DriverOffloadStubImpl(const StreamContext& context)
     : DriverStubImpl(context, 0 /*asyncSleepTimeUs*/),
+      mBufferNotifyFrames(static_cast<int64_t>(context.getBufferSizeInFrames()) / 2),
       mState{context.getFormat().encoding, context.getSampleRate(),
-             250 /*earlyNotifyMs*/ * context.getSampleRate() / MILLIS_PER_SECOND,
-             static_cast<int64_t>(context.getBufferSizeInFrames()) / 2},
+             250 /*earlyNotifyMs*/ * context.getSampleRate() / MILLIS_PER_SECOND},
       mDspWorker(mState) {
     LOG_IF(FATAL, !mIsAsynchronous) << "The steam must be used in asynchronous mode";
 }
@@ -111,6 +121,7 @@
             mState.clipFramesLeft.resize(1);
         }
     }
+    mState.bufferNotifyFrames = DspSimulatorState::kSkipBufferNotifyFrames;
     return ::android::OK;
 }
 
@@ -121,6 +132,7 @@
         std::lock_guard l(mState.lock);
         mState.clipFramesLeft.clear();
         mState.bufferFramesLeft = 0;
+        mState.bufferNotifyFrames = DspSimulatorState::kSkipBufferNotifyFrames;
     }
     return ::android::OK;
 }
@@ -128,6 +140,10 @@
 ::android::status_t DriverOffloadStubImpl::pause() {
     RETURN_STATUS_IF_ERROR(DriverStubImpl::pause());
     mDspWorker.pause();
+    {
+        std::lock_guard l(mState.lock);
+        mState.bufferNotifyFrames = DspSimulatorState::kSkipBufferNotifyFrames;
+    }
     return ::android::OK;
 }
 
@@ -140,6 +156,7 @@
         hasClips = !mState.clipFramesLeft.empty();
         LOG(DEBUG) << __func__
                    << ": clipFramesLeft: " << ::android::internal::ToString(mState.clipFramesLeft);
+        mState.bufferNotifyFrames = DspSimulatorState::kSkipBufferNotifyFrames;
     }
     if (hasClips) {
         mDspWorker.resume();
@@ -184,6 +201,7 @@
     {
         std::lock_guard l(mState.lock);
         mState.bufferFramesLeft = *actualFrameCount;
+        mState.bufferNotifyFrames = mBufferNotifyFrames;
     }
     mDspWorker.resume();
     return ::android::OK;
diff --git a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
index 21b7aff..806c93f 100644
--- a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
@@ -1025,6 +1025,7 @@
             auto [eventSeq, event] = mEventReceiver->waitForEvent(mLastEventSeq);
             mLastEventSeq = eventSeq;
             if (event != *expEvent) {
+                // TODO: Make available as an error so it can be displayed by GTest
                 LOG(ERROR) << __func__ << ": expected event " << toString(*expEvent) << ", got "
                            << toString(event);
                 return {};
@@ -1327,7 +1328,8 @@
   public:
     // To avoid timing out the whole test suite in case no event is received
     // from the HAL, use a local timeout for event waiting.
-    static constexpr auto kEventTimeoutMs = std::chrono::milliseconds(1000);
+    // TODO: The timeout for 'onTransferReady' should depend on the buffer size.
+    static constexpr auto kEventTimeoutMs = std::chrono::milliseconds(3000);
 
     StreamEventReceiver* getEventReceiver() { return this; }
     std::tuple<int, Event> getLastEvent() const override {
@@ -4236,15 +4238,17 @@
 enum {
     NAMED_CMD_NAME,
     NAMED_CMD_MIN_INTERFACE_VERSION,
+    NAMED_CMD_FEATURE_PROPERTY,
     NAMED_CMD_DELAY_MS,
     NAMED_CMD_STREAM_TYPE,
     NAMED_CMD_CMDS,
     NAMED_CMD_VALIDATE_POS_INCREASE
 };
-enum class StreamTypeFilter { ANY, SYNC, ASYNC };
+enum class StreamTypeFilter { ANY, SYNC, ASYNC, OFFLOAD };
 using NamedCommandSequence =
-        std::tuple<std::string, int /*minInterfaceVersion*/, int /*cmdDelayMs*/, StreamTypeFilter,
-                   std::shared_ptr<StateSequence>, bool /*validatePositionIncrease*/>;
+        std::tuple<std::string, int /*minInterfaceVersion*/, std::string /*featureProperty*/,
+                   int /*cmdDelayMs*/, StreamTypeFilter, std::shared_ptr<StateSequence>,
+                   bool /*validatePositionIncrease*/>;
 enum { PARAM_MODULE_NAME, PARAM_CMD_SEQ, PARAM_SETUP_SEQ };
 using StreamIoTestParameters =
         std::tuple<std::string /*moduleName*/, NamedCommandSequence, bool /*useSetupSequence2*/>;
@@ -4255,11 +4259,24 @@
     void SetUp() override {
         ASSERT_NO_FATAL_FAILURE(SetUpImpl(std::get<PARAM_MODULE_NAME>(GetParam())));
         ASSERT_GE(aidlVersion, kAidlVersion1);
-        if (const int minVersion =
-                    std::get<NAMED_CMD_MIN_INTERFACE_VERSION>(std::get<PARAM_CMD_SEQ>(GetParam()));
-            aidlVersion < minVersion) {
+        const int minVersion =
+                std::get<NAMED_CMD_MIN_INTERFACE_VERSION>(std::get<PARAM_CMD_SEQ>(GetParam()));
+        if (aidlVersion < minVersion) {
             GTEST_SKIP() << "Skip for audio HAL version lower than " << minVersion;
         }
+        // When an associated feature property is defined, need to check that either that the HAL
+        // exposes this property, or it's of the version 'NAMED_CMD_MIN_INTERFACE_VERSION' + 1
+        // which must have this functionality implemented by default.
+        if (const std::string featureProperty =
+                    std::get<NAMED_CMD_FEATURE_PROPERTY>(std::get<PARAM_CMD_SEQ>(GetParam()));
+            !featureProperty.empty() && aidlVersion < (minVersion + 1)) {
+            std::vector<VendorParameter> parameters;
+            ScopedAStatus result = module->getVendorParameters({featureProperty}, &parameters);
+            if (!result.isOk() || parameters.size() != 1) {
+                GTEST_SKIP() << "Skip as audio HAL does not support feature \"" << featureProperty
+                             << "\"";
+            }
+        }
         ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig());
     }
 
@@ -4300,10 +4317,18 @@
                             isBitPositionFlagSet(portConfig.flags.value()
                                                          .template get<AudioIoFlags::Tag::output>(),
                                                  AudioOutputFlags::NON_BLOCKING);
+            const bool isOffload =
+                    IOTraits<Stream>::is_input
+                            ? false
+                            : isBitPositionFlagSet(
+                                      portConfig.flags.value()
+                                              .template get<AudioIoFlags::Tag::output>(),
+                                      AudioOutputFlags::COMPRESS_OFFLOAD);
             if (auto streamType =
                         std::get<NAMED_CMD_STREAM_TYPE>(std::get<PARAM_CMD_SEQ>(GetParam()));
                 (isNonBlocking && streamType == StreamTypeFilter::SYNC) ||
-                (!isNonBlocking && streamType == StreamTypeFilter::ASYNC)) {
+                (!isNonBlocking && streamType == StreamTypeFilter::ASYNC) ||
+                (!isOffload && streamType == StreamTypeFilter::OFFLOAD)) {
                 continue;
             }
             WithDebugFlags delayTransientStates = WithDebugFlags::createNested(*debug);
@@ -4760,6 +4785,18 @@
 
 // TODO: Add async test cases for input once it is implemented.
 
+// Allow optional routing via the TRANSFERRING state on bursts.
+StateDag::Node makeAsyncBurstCommands(StateDag* d, size_t burstCount, StateDag::Node last) {
+    using State = StreamDescriptor::State;
+    std::reference_wrapper<std::remove_reference_t<StateDag::Node>> prev = last;
+    for (size_t i = 0; i < burstCount; ++i) {
+        StateDag::Node active = d->makeNode(State::ACTIVE, kBurstCommand, prev);
+        active.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, prev));
+        prev = active;
+    }
+    return prev;
+}
+
 std::shared_ptr<StateSequence> makeBurstCommands(bool isSync, size_t burstCount,
                                                  bool standbyInputWhenDone) {
     using State = StreamDescriptor::State;
@@ -4776,25 +4813,21 @@
                 d->makeNodes(State::ACTIVE, kBurstCommand, burstCount, last));
         d->makeNode(State::STANDBY, kStartCommand, idle);
     } else {
-        StateDag::Node active2 = d->makeNode(State::ACTIVE, kBurstCommand, last);
-        StateDag::Node active = d->makeNode(State::ACTIVE, kBurstCommand, active2);
+        StateDag::Node active = makeAsyncBurstCommands(d.get(), burstCount, last);
         StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
-        // Allow optional routing via the TRANSFERRING state on bursts.
-        active2.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, last));
-        active.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, active2));
         idle.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, active));
         d->makeNode(State::STANDBY, kStartCommand, idle);
     }
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kReadSeq =
-        std::make_tuple(std::string("Read"), kAidlVersion1, 0, StreamTypeFilter::ANY,
+        std::make_tuple(std::string("Read"), kAidlVersion1, "", 0, StreamTypeFilter::ANY,
                         makeBurstCommands(true), true /*validatePositionIncrease*/);
 static const NamedCommandSequence kWriteSyncSeq =
-        std::make_tuple(std::string("Write"), kAidlVersion1, 0, StreamTypeFilter::SYNC,
+        std::make_tuple(std::string("Write"), kAidlVersion1, "", 0, StreamTypeFilter::SYNC,
                         makeBurstCommands(true), true /*validatePositionIncrease*/);
 static const NamedCommandSequence kWriteAsyncSeq =
-        std::make_tuple(std::string("Write"), kAidlVersion1, 0, StreamTypeFilter::ASYNC,
+        std::make_tuple(std::string("Write"), kAidlVersion1, "", 0, StreamTypeFilter::ASYNC,
                         makeBurstCommands(false), true /*validatePositionIncrease*/);
 
 std::shared_ptr<StateSequence> makeAsyncDrainCommands(bool isInput) {
@@ -4824,10 +4857,10 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kWriteDrainAsyncSeq = std::make_tuple(
-        std::string("WriteDrain"), kAidlVersion1, kStreamTransientStateTransitionDelayMs,
+        std::string("WriteDrain"), kAidlVersion1, "", kStreamTransientStateTransitionDelayMs,
         StreamTypeFilter::ASYNC, makeAsyncDrainCommands(false), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kDrainInSeq =
-        std::make_tuple(std::string("Drain"), kAidlVersion1, 0, StreamTypeFilter::ANY,
+        std::make_tuple(std::string("Drain"), kAidlVersion1, "", 0, StreamTypeFilter::ANY,
                         makeAsyncDrainCommands(true), false /*validatePositionIncrease*/);
 
 std::shared_ptr<StateSequence> makeDrainOutCommands(bool isSync) {
@@ -4849,10 +4882,10 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainOutSyncSeq =
-        std::make_tuple(std::string("Drain"), kAidlVersion1, 0, StreamTypeFilter::SYNC,
+        std::make_tuple(std::string("Drain"), kAidlVersion1, "", 0, StreamTypeFilter::SYNC,
                         makeDrainOutCommands(true), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kDrainOutAsyncSeq =
-        std::make_tuple(std::string("Drain"), kAidlVersion3, 0, StreamTypeFilter::ASYNC,
+        std::make_tuple(std::string("Drain"), kAidlVersion3, "", 0, StreamTypeFilter::ASYNC,
                         makeDrainOutCommands(false), false /*validatePositionIncrease*/);
 
 std::shared_ptr<StateSequence> makeDrainEarlyOutCommands() {
@@ -4873,9 +4906,32 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainEarlyOutAsyncSeq =
-        std::make_tuple(std::string("DrainEarly"), kAidlVersion3, 0, StreamTypeFilter::ASYNC,
+        std::make_tuple(std::string("DrainEarly"), kAidlVersion3, "", 0, StreamTypeFilter::ASYNC,
                         makeDrainEarlyOutCommands(), false /*validatePositionIncrease*/);
 
+// DRAINING_en ->(onDrainReady) DRAINING_en_sent ->(onDrainReady) IDLE | TRANSFERRING
+std::shared_ptr<StateSequence> makeDrainEarlyOffloadCommands() {
+    using State = StreamDescriptor::State;
+    auto d = std::make_unique<StateDag>();
+    StateDag::Node lastIdle = d->makeFinalNode(State::IDLE);
+    StateDag::Node lastTransferring = d->makeFinalNode(State::TRANSFERRING);
+    // The second onDrainReady event.
+    StateDag::Node continueDraining =
+            d->makeNode(State::DRAINING, kDrainReadyEvent, lastIdle, lastTransferring);
+    // The first onDrainReady event.
+    StateDag::Node draining = d->makeNode(State::DRAINING, kDrainReadyEvent, continueDraining);
+    StateDag::Node drain = d->makeNode(State::ACTIVE, kDrainOutEarlyCommand, draining);
+    StateDag::Node active = makeAsyncBurstCommands(d.get(), 10, drain);
+    StateDag::Node idle = d->makeNode(State::IDLE, kBurstCommand, active);
+    idle.children().push_back(d->makeNode(State::TRANSFERRING, kTransferReadyEvent, active));
+    d->makeNode(State::STANDBY, kStartCommand, idle);
+    return std::make_shared<StateSequenceFollower>(std::move(d));
+}
+static const NamedCommandSequence kDrainEarlyOffloadSeq =
+        std::make_tuple(std::string("DrainEarly"), kAidlVersion3, "aosp.clipTransitionSupport", 0,
+                        StreamTypeFilter::OFFLOAD, makeDrainEarlyOffloadCommands(),
+                        true /*validatePositionIncrease*/);
+
 std::shared_ptr<StateSequence> makeDrainPauseOutCommands(bool isSync) {
     using State = StreamDescriptor::State;
     auto d = std::make_unique<StateDag>();
@@ -4896,11 +4952,11 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainPauseOutSyncSeq =
-        std::make_tuple(std::string("DrainPause"), kAidlVersion1,
+        std::make_tuple(std::string("DrainPause"), kAidlVersion1, "",
                         kStreamTransientStateTransitionDelayMs, StreamTypeFilter::SYNC,
                         makeDrainPauseOutCommands(true), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kDrainPauseOutAsyncSeq =
-        std::make_tuple(std::string("DrainPause"), kAidlVersion1,
+        std::make_tuple(std::string("DrainPause"), kAidlVersion1, "",
                         kStreamTransientStateTransitionDelayMs, StreamTypeFilter::ASYNC,
                         makeDrainPauseOutCommands(false), false /*validatePositionIncrease*/);
 
@@ -4919,7 +4975,7 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainEarlyPauseOutAsyncSeq =
-        std::make_tuple(std::string("DrainEarlyPause"), kAidlVersion3,
+        std::make_tuple(std::string("DrainEarlyPause"), kAidlVersion3, "",
                         kStreamTransientStateTransitionDelayMs, StreamTypeFilter::ASYNC,
                         makeDrainEarlyPauseOutCommands(), false /*validatePositionIncrease*/);
 
@@ -4963,13 +5019,13 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kStandbyInSeq =
-        std::make_tuple(std::string("Standby"), kAidlVersion1, 0, StreamTypeFilter::ANY,
+        std::make_tuple(std::string("Standby"), kAidlVersion1, "", 0, StreamTypeFilter::ANY,
                         makeStandbyCommands(true, false), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kStandbyOutSyncSeq =
-        std::make_tuple(std::string("Standby"), kAidlVersion1, 0, StreamTypeFilter::SYNC,
+        std::make_tuple(std::string("Standby"), kAidlVersion1, "", 0, StreamTypeFilter::SYNC,
                         makeStandbyCommands(false, true), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kStandbyOutAsyncSeq =
-        std::make_tuple(std::string("Standby"), kAidlVersion1,
+        std::make_tuple(std::string("Standby"), kAidlVersion1, "",
                         kStreamTransientStateTransitionDelayMs, StreamTypeFilter::ASYNC,
                         makeStandbyCommands(false, false), false /*validatePositionIncrease*/);
 
@@ -5006,15 +5062,15 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kPauseInSeq =
-        std::make_tuple(std::string("Pause"), kAidlVersion1, 0, StreamTypeFilter::ANY,
+        std::make_tuple(std::string("Pause"), kAidlVersion1, "", 0, StreamTypeFilter::ANY,
                         makePauseCommands(true, false), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kPauseOutSyncSeq =
-        std::make_tuple(std::string("Pause"), kAidlVersion1, 0, StreamTypeFilter::SYNC,
+        std::make_tuple(std::string("Pause"), kAidlVersion1, "", 0, StreamTypeFilter::SYNC,
                         makePauseCommands(false, true), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kPauseOutAsyncSeq =
-        std::make_tuple(std::string("Pause"), kAidlVersion3, kStreamTransientStateTransitionDelayMs,
-                        StreamTypeFilter::ASYNC, makePauseCommands(false, false),
-                        false /*validatePositionIncrease*/);
+        std::make_tuple(std::string("Pause"), kAidlVersion3, "",
+                        kStreamTransientStateTransitionDelayMs, StreamTypeFilter::ASYNC,
+                        makePauseCommands(false, false), false /*validatePositionIncrease*/);
 
 std::shared_ptr<StateSequence> makeFlushCommands(bool isInput, bool isSync) {
     using State = StreamDescriptor::State;
@@ -5042,15 +5098,15 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kFlushInSeq =
-        std::make_tuple(std::string("Flush"), kAidlVersion1, 0, StreamTypeFilter::ANY,
+        std::make_tuple(std::string("Flush"), kAidlVersion1, "", 0, StreamTypeFilter::ANY,
                         makeFlushCommands(true, false), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kFlushOutSyncSeq =
-        std::make_tuple(std::string("Flush"), kAidlVersion1, 0, StreamTypeFilter::SYNC,
+        std::make_tuple(std::string("Flush"), kAidlVersion1, "", 0, StreamTypeFilter::SYNC,
                         makeFlushCommands(false, true), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kFlushOutAsyncSeq =
-        std::make_tuple(std::string("Flush"), kAidlVersion1, kStreamTransientStateTransitionDelayMs,
-                        StreamTypeFilter::ASYNC, makeFlushCommands(false, false),
-                        false /*validatePositionIncrease*/);
+        std::make_tuple(std::string("Flush"), kAidlVersion1, "",
+                        kStreamTransientStateTransitionDelayMs, StreamTypeFilter::ASYNC,
+                        makeFlushCommands(false, false), false /*validatePositionIncrease*/);
 
 std::shared_ptr<StateSequence> makeDrainPauseFlushOutCommands(bool isSync) {
     using State = StreamDescriptor::State;
@@ -5070,11 +5126,11 @@
     return std::make_shared<StateSequenceFollower>(std::move(d));
 }
 static const NamedCommandSequence kDrainPauseFlushOutSyncSeq =
-        std::make_tuple(std::string("DrainPauseFlush"), kAidlVersion1,
+        std::make_tuple(std::string("DrainPauseFlush"), kAidlVersion1, "",
                         kStreamTransientStateTransitionDelayMs, StreamTypeFilter::SYNC,
                         makeDrainPauseFlushOutCommands(true), false /*validatePositionIncrease*/);
 static const NamedCommandSequence kDrainPauseFlushOutAsyncSeq =
-        std::make_tuple(std::string("DrainPauseFlush"), kAidlVersion1,
+        std::make_tuple(std::string("DrainPauseFlush"), kAidlVersion1, "",
                         kStreamTransientStateTransitionDelayMs, StreamTypeFilter::ASYNC,
                         makeDrainPauseFlushOutCommands(false), false /*validatePositionIncrease*/);
 
@@ -5087,6 +5143,8 @@
             return "Sync";
         case StreamTypeFilter::ASYNC:
             return "Async";
+        case StreamTypeFilter::OFFLOAD:
+            return "Offload";
     }
     return std::string("Unknown").append(std::to_string(static_cast<int32_t>(filter)));
 }
@@ -5119,7 +5177,8 @@
                                          kDrainPauseOutAsyncSeq, kDrainEarlyPauseOutAsyncSeq,
                                          kStandbyOutSyncSeq, kStandbyOutAsyncSeq, kPauseOutSyncSeq,
                                          kPauseOutAsyncSeq, kFlushOutSyncSeq, kFlushOutAsyncSeq,
-                                         kDrainPauseFlushOutSyncSeq, kDrainPauseFlushOutAsyncSeq),
+                                         kDrainPauseFlushOutSyncSeq, kDrainPauseFlushOutAsyncSeq,
+                                         kDrainEarlyOffloadSeq),
                          testing::Values(false, true)),
         GetStreamIoTestName);
 GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioStreamIoOut);