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
diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp
index 077d80b..e67d6d7 100644
--- a/audio/aidl/default/Module.cpp
+++ b/audio/aidl/default/Module.cpp
@@ -1546,6 +1546,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) {
@@ -1560,6 +1561,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 c6c1b5d..850f83a 100644
--- a/audio/aidl/default/Stream.cpp
+++ b/audio/aidl/default/Stream.cpp
@@ -387,12 +387,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;
@@ -411,8 +415,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;
@@ -539,13 +545,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 6a43102..d424eb3 100644
--- a/audio/aidl/default/include/core-impl/Module.h
+++ b/audio/aidl/default/include/core-impl/Module.h
@@ -159,6 +159,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;