Merge changes from topic "fix-b-274456992-mmap-status" into main am: 5e9fcfa538 am: 872d5453fc

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

Change-Id: I493cf3b9c9af7a759fb6134c607a1b515081b127
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/audio/aidl/default/Android.bp b/audio/aidl/default/Android.bp
index 230c717..a9ecdc2 100644
--- a/audio/aidl/default/Android.bp
+++ b/audio/aidl/default/Android.bp
@@ -80,6 +80,7 @@
         "stub/ApeHeader.cpp",
         "stub/DriverStubImpl.cpp",
         "stub/ModuleStub.cpp",
+        "stub/StreamMmapStub.cpp",
         "stub/StreamOffloadStub.cpp",
         "stub/StreamStub.cpp",
         "usb/ModuleUsb.cpp",
diff --git a/audio/aidl/default/ModulePrimary.cpp b/audio/aidl/default/ModulePrimary.cpp
index 2a1dba9..6cb9251 100644
--- a/audio/aidl/default/ModulePrimary.cpp
+++ b/audio/aidl/default/ModulePrimary.cpp
@@ -21,18 +21,23 @@
 #include <android-base/logging.h>
 
 #include "core-impl/ModulePrimary.h"
+#include "core-impl/StreamMmapStub.h"
 #include "core-impl/StreamOffloadStub.h"
 #include "core-impl/StreamPrimary.h"
 #include "core-impl/Telephony.h"
 
 using aidl::android::hardware::audio::common::areAllBitPositionFlagsSet;
+using aidl::android::hardware::audio::common::hasMmapFlag;
 using aidl::android::hardware::audio::common::SinkMetadata;
 using aidl::android::hardware::audio::common::SourceMetadata;
+using aidl::android::hardware::audio::core::StreamDescriptor;
+using aidl::android::media::audio::common::AudioInputFlags;
 using aidl::android::media::audio::common::AudioIoFlags;
 using aidl::android::media::audio::common::AudioOffloadInfo;
 using aidl::android::media::audio::common::AudioOutputFlags;
 using aidl::android::media::audio::common::AudioPort;
 using aidl::android::media::audio::common::AudioPortConfig;
+using aidl::android::media::audio::common::AudioPortExt;
 using aidl::android::media::audio::common::MicrophoneInfo;
 
 namespace aidl::android::hardware::audio::core {
@@ -62,6 +67,11 @@
                                                     const SinkMetadata& sinkMetadata,
                                                     const std::vector<MicrophoneInfo>& microphones,
                                                     std::shared_ptr<StreamIn>* result) {
+    if (context.isMmap()) {
+        // "Stub" is used because there is no support for MMAP audio I/O on CVD.
+        return createStreamInstance<StreamInMmapStub>(result, std::move(context), sinkMetadata,
+                                                      microphones);
+    }
     return createStreamInstance<StreamInPrimary>(result, std::move(context), sinkMetadata,
                                                  microphones);
 }
@@ -69,26 +79,54 @@
 ndk::ScopedAStatus ModulePrimary::createOutputStream(
         StreamContext&& context, const SourceMetadata& sourceMetadata,
         const std::optional<AudioOffloadInfo>& offloadInfo, std::shared_ptr<StreamOut>* result) {
-    if (!areAllBitPositionFlagsSet(
-                context.getFlags().get<AudioIoFlags::output>(),
-                {AudioOutputFlags::COMPRESS_OFFLOAD, AudioOutputFlags::NON_BLOCKING})) {
-        return createStreamInstance<StreamOutPrimary>(result, std::move(context), sourceMetadata,
-                                                      offloadInfo);
-    } else {
+    if (context.isMmap()) {
+        // "Stub" is used because there is no support for MMAP audio I/O on CVD.
+        return createStreamInstance<StreamOutMmapStub>(result, std::move(context), sourceMetadata,
+                                                       offloadInfo);
+    } else if (areAllBitPositionFlagsSet(
+                       context.getFlags().get<AudioIoFlags::output>(),
+                       {AudioOutputFlags::COMPRESS_OFFLOAD, AudioOutputFlags::NON_BLOCKING})) {
         // "Stub" is used because there is no actual decoder. The stream just
         // extracts the clip duration from the media file header and simulates
         // playback over time.
         return createStreamInstance<StreamOutOffloadStub>(result, std::move(context),
                                                           sourceMetadata, offloadInfo);
     }
+    return createStreamInstance<StreamOutPrimary>(result, std::move(context), sourceMetadata,
+                                                  offloadInfo);
 }
 
-int32_t ModulePrimary::getNominalLatencyMs(const AudioPortConfig&) {
+ndk::ScopedAStatus ModulePrimary::createMmapBuffer(const AudioPortConfig& portConfig,
+                                                   int32_t bufferSizeFrames, int32_t frameSizeBytes,
+                                                   MmapBufferDescriptor* desc) {
+    const size_t bufferSizeBytes = static_cast<size_t>(bufferSizeFrames) * frameSizeBytes;
+    // The actual mmap buffer for I/O is created after the stream exits standby, via
+    // 'IStreamCommon.createMmapBuffer'. But we must return a valid file descriptor here because
+    // 'MmapBufferDescriptor' can not contain a "null" fd.
+    const std::string regionName =
+            std::string("mmap-sim-o-") +
+            std::to_string(portConfig.ext.get<AudioPortExt::Tag::mix>().handle);
+    int fd = ashmem_create_region(regionName.c_str(), bufferSizeBytes);
+    if (fd < 0) {
+        PLOG(ERROR) << __func__ << ": failed to create shared memory region of " << bufferSizeBytes
+                    << " bytes";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    desc->sharedMemory.fd = ndk::ScopedFileDescriptor(fd);
+    desc->sharedMemory.size = bufferSizeBytes;
+    desc->burstSizeFrames = bufferSizeFrames / 2;
+    desc->flags = 0;
+    LOG(DEBUG) << __func__ << ": " << desc->toString();
+    return ndk::ScopedAStatus::ok();
+}
+
+int32_t ModulePrimary::getNominalLatencyMs(const AudioPortConfig& portConfig) {
+    static constexpr int32_t kLowLatencyMs = 5;
     // 85 ms is chosen considering 4096 frames @ 48 kHz. This is the value which allows
     // the virtual Android device implementation to pass CTS. Hardware implementations
     // should have significantly lower latency.
-    static constexpr int32_t kLatencyMs = 85;
-    return kLatencyMs;
+    static constexpr int32_t kStandardLatencyMs = 85;
+    return hasMmapFlag(portConfig.flags.value()) ? kLowLatencyMs : kStandardLatencyMs;
 }
 
 }  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/include/core-impl/ModulePrimary.h b/audio/aidl/default/include/core-impl/ModulePrimary.h
index a657dc5..c93deed 100644
--- a/audio/aidl/default/include/core-impl/ModulePrimary.h
+++ b/audio/aidl/default/include/core-impl/ModulePrimary.h
@@ -42,6 +42,9 @@
             const std::optional<::aidl::android::media::audio::common::AudioOffloadInfo>&
                     offloadInfo,
             std::shared_ptr<StreamOut>* result) override;
+    ndk::ScopedAStatus createMmapBuffer(
+            const ::aidl::android::media::audio::common::AudioPortConfig& portConfig,
+            int32_t bufferSizeFrames, int32_t frameSizeBytes, MmapBufferDescriptor* desc) override;
     int32_t getNominalLatencyMs(
             const ::aidl::android::media::audio::common::AudioPortConfig& portConfig) override;
 
diff --git a/audio/aidl/default/include/core-impl/StreamMmapStub.h b/audio/aidl/default/include/core-impl/StreamMmapStub.h
new file mode 100644
index 0000000..0332007
--- /dev/null
+++ b/audio/aidl/default/include/core-impl/StreamMmapStub.h
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+#pragma once
+
+#include <mutex>
+#include <string>
+
+#include "core-impl/DriverStubImpl.h"
+#include "core-impl/Stream.h"
+
+namespace aidl::android::hardware::audio::core {
+
+namespace mmap {
+
+struct DspSimulatorState {
+    const bool isInput;
+    const int sampleRate;
+    const int frameSizeBytes;
+    const size_t bufferSizeBytes;
+    std::mutex lock;
+    // The lock is also used to prevent un-mapping while the memory is in use.
+    uint8_t* sharedMemory GUARDED_BY(lock) = nullptr;
+    StreamDescriptor::Position mmapPos GUARDED_BY(lock);
+};
+
+class DspSimulatorLogic : public ::android::hardware::audio::common::StreamLogic {
+  protected:
+    explicit DspSimulatorLogic(DspSimulatorState& sharedState) : mSharedState(sharedState) {}
+    std::string init() override;
+    Status cycle() override;
+
+  private:
+    DspSimulatorState& mSharedState;
+    uint32_t mCycleDurationUs = 0;
+    uint8_t* mMemBegin = nullptr;
+    uint8_t* mMemPos = nullptr;
+    int64_t mLastFrames = 0;
+};
+
+class DspSimulatorWorker
+    : public ::android::hardware::audio::common::StreamWorker<DspSimulatorLogic> {
+  public:
+    explicit DspSimulatorWorker(DspSimulatorState& sharedState)
+        : ::android::hardware::audio::common::StreamWorker<DspSimulatorLogic>(sharedState) {}
+};
+
+}  // namespace mmap
+
+class DriverMmapStubImpl : public DriverStubImpl {
+  public:
+    explicit DriverMmapStubImpl(const StreamContext& context);
+    ::android::status_t init(DriverCallbackInterface* callback) override;
+    ::android::status_t drain(StreamDescriptor::DrainMode drainMode) override;
+    ::android::status_t pause() override;
+    ::android::status_t start() override;
+    ::android::status_t transfer(void* buffer, size_t frameCount, size_t* actualFrameCount,
+                                 int32_t* latencyMs) override;
+    void shutdown() override;
+    ::android::status_t refinePosition(StreamDescriptor::Position* position) override;
+    ::android::status_t getMmapPositionAndLatency(StreamDescriptor::Position* position,
+                                                  int32_t* latency) override;
+
+  protected:
+    ::android::status_t initSharedMemory(int ashmemFd);
+
+  private:
+    ::android::status_t releaseSharedMemory() REQUIRES(mState.lock);
+    ::android::status_t startWorkerIfNeeded();
+
+    mmap::DspSimulatorState mState;
+    mmap::DspSimulatorWorker mDspWorker;
+    bool mDspWorkerStarted = false;
+};
+
+class StreamMmapStub : public StreamCommonImpl, public DriverMmapStubImpl {
+  public:
+    static const std::string kCreateMmapBufferName;
+
+    StreamMmapStub(StreamContext* context, const Metadata& metadata);
+    ~StreamMmapStub();
+
+    ndk::ScopedAStatus getVendorParameters(const std::vector<std::string>& in_ids,
+                                           std::vector<VendorParameter>* _aidl_return) override;
+    ndk::ScopedAStatus setVendorParameters(const std::vector<VendorParameter>& in_parameters,
+                                           bool in_async) override;
+
+  private:
+    ndk::ScopedAStatus createMmapBuffer(MmapBufferDescriptor* desc);
+
+    ndk::ScopedFileDescriptor mSharedMemoryFd;
+};
+
+class StreamInMmapStub final : public StreamIn, public StreamMmapStub {
+  public:
+    friend class ndk::SharedRefBase;
+    StreamInMmapStub(
+            StreamContext&& context,
+            const ::aidl::android::hardware::audio::common::SinkMetadata& sinkMetadata,
+            const std::vector<::aidl::android::media::audio::common::MicrophoneInfo>& microphones);
+
+  private:
+    void onClose(StreamDescriptor::State) override { defaultOnClose(); }
+};
+
+class StreamOutMmapStub final : public StreamOut, public StreamMmapStub {
+  public:
+    friend class ndk::SharedRefBase;
+    StreamOutMmapStub(
+            StreamContext&& context,
+            const ::aidl::android::hardware::audio::common::SourceMetadata& sourceMetadata,
+            const std::optional<::aidl::android::media::audio::common::AudioOffloadInfo>&
+                    offloadInfo);
+
+  private:
+    void onClose(StreamDescriptor::State) override { defaultOnClose(); }
+};
+
+}  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/include/core-impl/StreamOffloadStub.h b/audio/aidl/default/include/core-impl/StreamOffloadStub.h
index 24e98c2..09b88aa 100644
--- a/audio/aidl/default/include/core-impl/StreamOffloadStub.h
+++ b/audio/aidl/default/include/core-impl/StreamOffloadStub.h
@@ -19,12 +19,15 @@
 #include <mutex>
 #include <set>
 #include <string>
+#include <vector>
 
 #include "core-impl/DriverStubImpl.h"
 #include "core-impl/Stream.h"
 
 namespace aidl::android::hardware::audio::core {
 
+namespace offload {
+
 struct DspSimulatorState {
     static constexpr int64_t kSkipBufferNotifyFrames = -1;
 
@@ -55,9 +58,11 @@
         : ::android::hardware::audio::common::StreamWorker<DspSimulatorLogic>(sharedState) {}
 };
 
+}  // namespace offload
+
 class DriverOffloadStubImpl : public DriverStubImpl {
   public:
-    DriverOffloadStubImpl(const StreamContext& context);
+    explicit DriverOffloadStubImpl(const StreamContext& context);
     ::android::status_t init(DriverCallbackInterface* callback) override;
     ::android::status_t drain(StreamDescriptor::DrainMode drainMode) override;
     ::android::status_t flush() override;
@@ -71,8 +76,8 @@
     ::android::status_t startWorkerIfNeeded();
 
     const int64_t mBufferNotifyFrames;
-    DspSimulatorState mState;
-    DspSimulatorWorker mDspWorker;
+    offload::DspSimulatorState mState;
+    offload::DspSimulatorWorker mDspWorker;
     bool mDspWorkerStarted = false;
 };
 
diff --git a/audio/aidl/default/stub/DriverStubImpl.cpp b/audio/aidl/default/stub/DriverStubImpl.cpp
index 0d129e6..cb8ee70 100644
--- a/audio/aidl/default/stub/DriverStubImpl.cpp
+++ b/audio/aidl/default/stub/DriverStubImpl.cpp
@@ -15,6 +15,7 @@
  */
 
 #include <cmath>
+#include <cstdlib>
 
 #define LOG_TAG "AHAL_Stream"
 #include <android-base/logging.h>
diff --git a/audio/aidl/default/stub/StreamMmapStub.cpp b/audio/aidl/default/stub/StreamMmapStub.cpp
new file mode 100644
index 0000000..f48aea4
--- /dev/null
+++ b/audio/aidl/default/stub/StreamMmapStub.cpp
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2025 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.
+ */
+
+#include <unistd.h>
+#include <cstdlib>
+
+#define LOG_TAG "AHAL_MmapStream"
+#include <android-base/logging.h>
+#include <audio_utils/clock.h>
+#include <error/Result.h>
+#include <utils/SystemClock.h>
+
+#include "core-impl/StreamMmapStub.h"
+
+using aidl::android::hardware::audio::common::SinkMetadata;
+using aidl::android::hardware::audio::common::SourceMetadata;
+using aidl::android::media::audio::common::AudioOffloadInfo;
+using aidl::android::media::audio::common::MicrophoneInfo;
+
+namespace aidl::android::hardware::audio::core {
+
+namespace mmap {
+
+std::string DspSimulatorLogic::init() {
+    {
+        std::lock_guard l(mSharedState.lock);
+        mSharedState.mmapPos.timeNs = StreamDescriptor::Position::UNKNOWN;
+        mSharedState.mmapPos.frames = StreamDescriptor::Position::UNKNOWN;
+    }
+    // Progress in buffer size chunks to make sure that VTS tolerates infrequent position updates
+    // (see b/350998390).
+    mCycleDurationUs = (mSharedState.bufferSizeBytes / mSharedState.frameSizeBytes) *
+                       MICROS_PER_SECOND / mSharedState.sampleRate;
+    return "";
+}
+
+DspSimulatorLogic::Status DspSimulatorLogic::cycle() {
+    // Simulate DSP moving along in real time.
+    const int64_t timeBeginNs = ::android::uptimeNanos();
+    usleep(mCycleDurationUs);
+    int64_t newFrames;
+    std::lock_guard l(mSharedState.lock);
+    if (mMemBegin != mSharedState.sharedMemory) {
+        mMemBegin = mSharedState.sharedMemory;
+        if (mMemBegin != nullptr) mMemPos = mMemBegin;
+    }
+    if (mMemBegin != nullptr) {
+        mSharedState.mmapPos.timeNs = ::android::uptimeNanos();
+        newFrames = (mSharedState.mmapPos.timeNs - timeBeginNs) * mSharedState.sampleRate /
+                    NANOS_PER_SECOND;
+        // Restore the reported frames position to ensure continuity.
+        if (mSharedState.mmapPos.frames == StreamDescriptor::Position::UNKNOWN) {
+            mSharedState.mmapPos.frames = mLastFrames;
+        }
+        mSharedState.mmapPos.frames += newFrames;
+        mLastFrames = mSharedState.mmapPos.frames;
+        if (mSharedState.isInput) {
+            for (size_t i = 0; i < static_cast<size_t>(newFrames) * mSharedState.frameSizeBytes;
+                 ++i) {
+                *mMemPos++ = std::rand() % 255;
+                if (mMemPos >= mMemBegin + mSharedState.bufferSizeBytes) mMemPos = mMemBegin;
+            }
+        }
+    } else {
+        LOG(WARNING) << "No shared memory but the DSP is active";
+        mSharedState.mmapPos.timeNs = StreamDescriptor::Position::UNKNOWN;
+        mSharedState.mmapPos.frames = StreamDescriptor::Position::UNKNOWN;
+    }
+    return Status::CONTINUE;
+}
+
+}  // namespace mmap
+
+using mmap::DspSimulatorState;
+
+DriverMmapStubImpl::DriverMmapStubImpl(const StreamContext& context)
+    : DriverStubImpl(context, 0 /*asyncSleepTimeUs*/),
+      mState{mIsInput, mSampleRate, static_cast<int>(mFrameSizeBytes),
+             mBufferSizeFrames * mFrameSizeBytes},
+      mDspWorker(mState) {
+    LOG_IF(FATAL, !context.isMmap()) << "The steam must be used in MMAP mode";
+}
+
+::android::status_t DriverMmapStubImpl::init(DriverCallbackInterface* callback) {
+    RETURN_STATUS_IF_ERROR(DriverStubImpl::init(callback));
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::drain(StreamDescriptor::DrainMode drainMode) {
+    RETURN_STATUS_IF_ERROR(DriverStubImpl::drain(drainMode));
+    mDspWorker.pause();
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::pause() {
+    RETURN_STATUS_IF_ERROR(DriverStubImpl::pause());
+    mDspWorker.pause();
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::start() {
+    RETURN_STATUS_IF_ERROR(DriverStubImpl::start());
+    RETURN_STATUS_IF_ERROR(startWorkerIfNeeded());
+    mDspWorker.resume();
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::transfer(void*, size_t, size_t*, int32_t*) {
+    // Do not call into DriverStubImpl::transfer
+    if (!mIsInitialized) {
+        LOG(FATAL) << __func__ << ": must not happen for an uninitialized driver";
+    }
+    if (mIsStandby) {
+        LOG(FATAL) << __func__ << ": must not happen while in standby";
+    }
+    RETURN_STATUS_IF_ERROR(startWorkerIfNeeded());
+    mDspWorker.resume();
+    return ::android::OK;
+}
+
+void DriverMmapStubImpl::shutdown() {
+    LOG(DEBUG) << __func__ << ": stopping the DSP simulator worker";
+    mDspWorker.stop();
+    std::lock_guard l(mState.lock);
+    releaseSharedMemory();
+    DriverStubImpl::shutdown();
+}
+
+::android::status_t DriverMmapStubImpl::initSharedMemory(int ashmemFd) {
+    {
+        std::lock_guard l(mState.lock);
+        if (ashmemFd == -1) {
+            mState.sharedMemory = nullptr;
+            return ::android::BAD_VALUE;
+        }
+        RETURN_STATUS_IF_ERROR(releaseSharedMemory());
+    }
+    uint8_t* sharedMemory = static_cast<uint8_t*>(::mmap(
+            nullptr, mState.bufferSizeBytes, PROT_READ | PROT_WRITE, MAP_SHARED, ashmemFd, 0));
+    if (sharedMemory == reinterpret_cast<uint8_t*>(MAP_FAILED) || sharedMemory == nullptr) {
+        PLOG(ERROR) << "mmap failed for size " << mState.bufferSizeBytes << ", fd " << ashmemFd;
+        return ::android::NO_INIT;
+    }
+    std::lock_guard l(mState.lock);
+    mState.sharedMemory = sharedMemory;
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::releaseSharedMemory() {
+    if (mState.sharedMemory != nullptr) {
+        LOG(DEBUG) << __func__ << ": unmapping shared memory";
+        if (munmap(mState.sharedMemory, mState.bufferSizeBytes) != 0) {
+            PLOG(ERROR) << "munmap failed for size " << mState.bufferSizeBytes;
+            return ::android::INVALID_OPERATION;
+        }
+        mState.sharedMemory = nullptr;
+    }
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::startWorkerIfNeeded() {
+    if (!mDspWorkerStarted) {
+        // This is an "audio service thread," must have elevated priority.
+        if (!mDspWorker.start("dsp_sim", ANDROID_PRIORITY_URGENT_AUDIO)) {
+            return ::android::NO_INIT;
+        }
+        mDspWorkerStarted = true;
+    }
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::refinePosition(StreamDescriptor::Position* position) {
+    std::lock_guard l(mState.lock);
+    *position = mState.mmapPos;
+    return ::android::OK;
+}
+
+::android::status_t DriverMmapStubImpl::getMmapPositionAndLatency(
+        StreamDescriptor::Position* position, int32_t* latencyMs) {
+    {
+        std::lock_guard l(mState.lock);
+        *position = mState.mmapPos;
+    }
+    const size_t latencyFrames = mBufferSizeFrames / 2;
+    if (position->frames != StreamDescriptor::Position::UNKNOWN) {
+        position->frames += latencyFrames;
+    }
+    *latencyMs = latencyFrames * MILLIS_PER_SECOND / mSampleRate;
+    return ::android::OK;
+}
+
+const std::string StreamMmapStub::kCreateMmapBufferName = "aosp.createMmapBuffer";
+
+StreamMmapStub::StreamMmapStub(StreamContext* context, const Metadata& metadata)
+    : StreamCommonImpl(context, metadata), DriverMmapStubImpl(getContext()) {}
+
+StreamMmapStub::~StreamMmapStub() {
+    cleanupWorker();
+}
+
+ndk::ScopedAStatus StreamMmapStub::getVendorParameters(const std::vector<std::string>& in_ids,
+                                                       std::vector<VendorParameter>* _aidl_return) {
+    std::vector<std::string> unprocessedIds;
+    for (const auto& id : in_ids) {
+        if (id == kCreateMmapBufferName) {
+            LOG(DEBUG) << __func__ << ": " << id;
+            MmapBufferDescriptor mmapDesc;
+            RETURN_STATUS_IF_ERROR(createMmapBuffer(&mmapDesc));
+            VendorParameter createMmapBuffer{.id = id};
+            createMmapBuffer.ext.setParcelable(mmapDesc);
+            LOG(DEBUG) << __func__ << ": returning " << mmapDesc.toString();
+            _aidl_return->push_back(std::move(createMmapBuffer));
+        } else {
+            unprocessedIds.push_back(id);
+        }
+    }
+    if (!unprocessedIds.empty()) {
+        return StreamCommonImpl::getVendorParameters(unprocessedIds, _aidl_return);
+    }
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus StreamMmapStub::setVendorParameters(
+        const std::vector<VendorParameter>& in_parameters, bool in_async) {
+    std::vector<VendorParameter> unprocessedParameters;
+    for (const auto& param : in_parameters) {
+        if (param.id == kCreateMmapBufferName) {
+            LOG(DEBUG) << __func__ << ": " << param.id;
+            // The value is irrelevant. The fact that this parameter can be "set" is an
+            // indication that the method can be used by the client via 'getVendorParameters'.
+        } else {
+            unprocessedParameters.push_back(param);
+        }
+    }
+    if (!unprocessedParameters.empty()) {
+        return StreamCommonImpl::setVendorParameters(unprocessedParameters, in_async);
+    }
+    return ndk::ScopedAStatus::ok();
+}
+
+ndk::ScopedAStatus StreamMmapStub::createMmapBuffer(MmapBufferDescriptor* desc) {
+    const size_t bufferSizeFrames = mContext.getBufferSizeInFrames();
+    const size_t bufferSizeBytes = static_cast<size_t>(bufferSizeFrames) * mContext.getFrameSize();
+    const std::string regionName =
+            std::string("mmap-sim-") + std::to_string(mContext.getMixPortHandle());
+    int fd = ashmem_create_region(regionName.c_str(), bufferSizeBytes);
+    if (fd < 0) {
+        PLOG(ERROR) << __func__ << ": failed to create shared memory region of " << bufferSizeBytes
+                    << " bytes";
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    mSharedMemoryFd = ndk::ScopedFileDescriptor(fd);
+    if (initSharedMemory(mSharedMemoryFd.get()) != ::android::OK) {
+        return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE);
+    }
+    desc->sharedMemory.fd = mSharedMemoryFd.dup();
+    desc->sharedMemory.size = bufferSizeBytes;
+    desc->burstSizeFrames = bufferSizeFrames / 2;
+    desc->flags = 0;
+    LOG(DEBUG) << __func__ << ": " << desc->toString();
+    return ndk::ScopedAStatus::ok();
+}
+
+StreamInMmapStub::StreamInMmapStub(StreamContext&& context, const SinkMetadata& sinkMetadata,
+                                   const std::vector<MicrophoneInfo>& microphones)
+    : StreamIn(std::move(context), microphones), StreamMmapStub(&mContextInstance, sinkMetadata) {}
+
+StreamOutMmapStub::StreamOutMmapStub(StreamContext&& context, const SourceMetadata& sourceMetadata,
+                                     const std::optional<AudioOffloadInfo>& offloadInfo)
+    : StreamOut(std::move(context), offloadInfo),
+      StreamMmapStub(&mContextInstance, sourceMetadata) {}
+
+}  // namespace aidl::android::hardware::audio::core
diff --git a/audio/aidl/default/stub/StreamOffloadStub.cpp b/audio/aidl/default/stub/StreamOffloadStub.cpp
index 155f76d..5f5f741 100644
--- a/audio/aidl/default/stub/StreamOffloadStub.cpp
+++ b/audio/aidl/default/stub/StreamOffloadStub.cpp
@@ -30,6 +30,8 @@
 
 namespace aidl::android::hardware::audio::core {
 
+namespace offload {
+
 std::string DspSimulatorLogic::init() {
     return "";
 }
@@ -90,6 +92,10 @@
     return Status::CONTINUE;
 }
 
+}  // namespace offload
+
+using offload::DspSimulatorState;
+
 DriverOffloadStubImpl::DriverOffloadStubImpl(const StreamContext& context)
     : DriverStubImpl(context, 0 /*asyncSleepTimeUs*/),
       mBufferNotifyFrames(static_cast<int64_t>(context.getBufferSizeInFrames()) / 2),
diff --git a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
index 806c93f..2c692f5 100644
--- a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
+++ b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp
@@ -27,6 +27,7 @@
 #include <set>
 #include <string>
 #include <string_view>
+#include <thread>
 #include <variant>
 #include <vector>
 
@@ -76,6 +77,7 @@
 using aidl::android::hardware::audio::core::IStreamIn;
 using aidl::android::hardware::audio::core::IStreamOut;
 using aidl::android::hardware::audio::core::ITelephony;
+using aidl::android::hardware::audio::core::MmapBufferDescriptor;
 using aidl::android::hardware::audio::core::ModuleDebug;
 using aidl::android::hardware::audio::core::StreamDescriptor;
 using aidl::android::hardware::audio::core::VendorParameter;
@@ -720,21 +722,8 @@
           mFlags(flags),
           mDataMQ(maybeCreateDataMQ(descriptor)),
           mIsMmapped(isMmapped(descriptor)),
-          mSharedMemoryFd(maybeGetMmapFd(descriptor)) {
-        if (isMmapped()) {
-            mSharedMemory = (int8_t*)mmap(nullptr, getBufferSizeBytes(), PROT_READ | PROT_WRITE,
-                                          MAP_SHARED, mSharedMemoryFd, 0);
-            if (mSharedMemory == MAP_FAILED) {
-                PLOG(ERROR) << __func__ << ": mmap() failed.";
-                mSharedMemory = nullptr;
-            }
-        }
-    }
-    ~StreamContext() {
-        if (mSharedMemory != nullptr) {
-            munmap(mSharedMemory, getBufferSizeBytes());
-        }
-    }
+          mMmapBurstSizeFrames(getMmapBurstSizeFrames(descriptor)),
+          mSharedMemoryFd(maybeGetMmapFd(descriptor)) {}
     void checkIsValid() const {
         EXPECT_NE(0UL, mFrameSizeBytes);
         ASSERT_NE(nullptr, mCommandMQ);
@@ -742,15 +731,14 @@
         ASSERT_NE(nullptr, mReplyMQ);
         EXPECT_TRUE(mReplyMQ->isValid());
         if (isMmapped()) {
-            ASSERT_NE(nullptr, mSharedMemory);
+            EXPECT_NE(0, mMmapBurstSizeFrames) << "MMAP burst size must not be zero";
         } else {
-            if (mDataMQ != nullptr) {
-                EXPECT_TRUE(mDataMQ->isValid());
-                EXPECT_GE(mDataMQ->getQuantumCount() * mDataMQ->getQuantumSize(),
-                          mFrameSizeBytes * mBufferSizeFrames)
-                        << "Data MQ actual buffer size is "
-                           "less than the buffer size as specified by the descriptor";
-            }
+            ASSERT_NE(nullptr, mDataMQ);
+            EXPECT_TRUE(mDataMQ->isValid());
+            EXPECT_GE(mDataMQ->getQuantumCount() * mDataMQ->getQuantumSize(),
+                      mFrameSizeBytes * mBufferSizeFrames)
+                    << "Data MQ actual buffer size is "
+                       "less than the buffer size as specified by the descriptor";
         }
     }
     size_t getBufferSizeBytes() const { return mFrameSizeBytes * mBufferSizeFrames; }
@@ -763,7 +751,8 @@
     ReplyMQ* getReplyMQ() const { return mReplyMQ.get(); }
     int getSampleRate() const { return mConfig.sampleRate; }
     bool isMmapped() const { return mIsMmapped; }
-    int8_t* getMmapMemory() const { return mSharedMemory; }
+    int32_t getMmapBurstSizeFrames() const { return mMmapBurstSizeFrames; }
+    int getMmapFd() const { return mSharedMemoryFd; }
 
   private:
     static std::unique_ptr<DataMQ> maybeCreateDataMQ(const StreamDescriptor& descriptor) {
@@ -773,6 +762,13 @@
         }
         return nullptr;
     }
+    static int32_t getMmapBurstSizeFrames(const StreamDescriptor& descriptor) {
+        using Tag = StreamDescriptor::AudioBuffer::Tag;
+        if (descriptor.audio.getTag() == Tag::mmap) {
+            return descriptor.audio.get<Tag::mmap>().burstSizeFrames;
+        }
+        return -1;
+    }
     static bool isMmapped(const StreamDescriptor& descriptor) {
         using Tag = StreamDescriptor::AudioBuffer::Tag;
         return descriptor.audio.getTag() == Tag::mmap;
@@ -793,8 +789,75 @@
     const AudioIoFlags mFlags;
     std::unique_ptr<DataMQ> mDataMQ;
     const bool mIsMmapped;
-    const int32_t mSharedMemoryFd;
-    int8_t* mSharedMemory = nullptr;
+    const int32_t mMmapBurstSizeFrames;
+    const int32_t mSharedMemoryFd;  // owned by StreamDescriptor
+};
+
+struct StreamWorkerMethods {
+    virtual ~StreamWorkerMethods() = default;
+    virtual bool createMmapBuffer(MmapBufferDescriptor* desc) = 0;
+    virtual bool supportsCreateMmapBuffer() = 0;
+};
+
+class MmapSharedMemory {
+  public:
+    explicit MmapSharedMemory(const StreamContext& context, StreamWorkerMethods* stream)
+        : mStream(stream),
+          mBufferSizeBytes(context.getBufferSizeBytes()),
+          mSharedMemoryFd(::dup(context.getMmapFd())) {}
+    ~MmapSharedMemory() { releaseSharedMemory(); }
+
+    int8_t* getMmapMemory() {
+        if (mSharedMemory != nullptr) return mSharedMemory;
+        if (mSharedMemoryFd.get() != -1) {
+            int8_t* sharedMemory = (int8_t*)mmap(nullptr, mBufferSizeBytes, PROT_READ | PROT_WRITE,
+                                                 MAP_SHARED, mSharedMemoryFd.get(), 0);
+            if (sharedMemory != MAP_FAILED && sharedMemory != nullptr) {
+                mSharedMemory = sharedMemory;
+            } else {
+                PLOG(ERROR) << __func__ << ": mmap() failed, fd " << mSharedMemoryFd.get()
+                            << ", size " << mBufferSizeBytes;
+            }
+        } else {
+            LOG(WARNING) << __func__ << ": shared memory FD has not been set yet";
+        }
+        return mSharedMemory;
+    }
+    bool updateMmapSharedMemoryIfNeeded(StreamDescriptor::State state) {
+        if (mPreviousState == StreamDescriptor::State::STANDBY &&
+            state != StreamDescriptor::State::STANDBY && state != StreamDescriptor::State::ERROR) {
+            LOG(INFO) << "Mmap stream exited standby, update Mmap buffer";
+            MmapBufferDescriptor desc;
+            if (!mStream->createMmapBuffer(&desc)) return false;
+            updateMmapSharedMemoryFd(desc);
+        }
+        mPreviousState = state;
+        return true;
+    }
+
+  private:
+    static ndk::ScopedFileDescriptor getMmapFd(const MmapBufferDescriptor& desc) {
+        return desc.sharedMemory.fd.get() != -1 ? desc.sharedMemory.fd.dup()
+                                                : ndk::ScopedFileDescriptor{};
+    }
+    void releaseSharedMemory() {
+        if (mSharedMemory != nullptr) {
+            munmap(mSharedMemory, mBufferSizeBytes);
+        }
+        mSharedMemory = nullptr;
+    }
+    void updateMmapSharedMemoryFd(const MmapBufferDescriptor& desc) {
+        mSharedMemoryFd = getMmapFd(desc);
+        releaseSharedMemory();
+    }
+
+    StreamWorkerMethods* const mStream;
+    const size_t mBufferSizeBytes;
+    ndk::ScopedFileDescriptor mSharedMemoryFd;
+    // Maps on the worker thread, may unmap in the destructor on the main thread.
+    std::atomic<int8_t*> mSharedMemory = nullptr;
+    // 'STANDBY' is always the starting state for a stream.
+    StreamDescriptor::State mPreviousState = StreamDescriptor::State::STANDBY;
 };
 
 struct StreamEventReceiver {
@@ -984,15 +1047,18 @@
 class StreamCommonLogic : public StreamLogic {
   protected:
     StreamCommonLogic(const StreamContext& context, StreamLogicDriver* driver,
-                      StreamEventReceiver* eventReceiver)
+                      StreamWorkerMethods* stream, StreamEventReceiver* eventReceiver)
         : mCommandMQ(context.getCommandMQ()),
           mReplyMQ(context.getReplyMQ()),
           mDataMQ(context.getDataMQ()),
+          mMmap(context, stream),
           mData(context.getBufferSizeBytes()),
           mDriver(driver),
           mEventReceiver(eventReceiver),
           mIsMmapped(context.isMmapped()),
-          mSharedMemory(context.getMmapMemory()),
+          mMmapBurstSleep(mIsMmapped ? static_cast<double>(context.getMmapBurstSizeFrames()) /
+                                               context.getSampleRate()
+                                     : 0.0),
           mIsCompressOffload(context.getFlags().getTag() == AudioIoFlags::output &&
                              isBitPositionFlagSet(context.getFlags().get<AudioIoFlags::output>(),
                                                   AudioOutputFlags::COMPRESS_OFFLOAD)),
@@ -1008,7 +1074,9 @@
     bool isMmapped() const { return mIsMmapped; }
 
     std::string init() override {
-        LOG(DEBUG) << __func__;
+        LOG(DEBUG) << __func__ << ": isMmapped? " << mIsMmapped << ", MmapBurstSleep "
+                   << mMmapBurstSleep << ", isCompressOffload? " << mIsCompressOffload << ", "
+                   << mConfig.toString();
         return "";
     }
     const std::vector<int8_t>& getData() const { return mData; }
@@ -1054,32 +1122,40 @@
         return false;
     }
     bool readDataFromMmap(size_t readCount) {
-        if (mSharedMemory != nullptr) {
-            std::memcpy(mData.data(), mSharedMemory, readCount);
+        if (auto memory = mMmap.getMmapMemory(); memory != nullptr) {
+            std::memcpy(mData.data(), memory, readCount);
+            // Since MMap `burst` does not block, need to sleep here to get an updated position.
+            std::this_thread::sleep_for(mMmapBurstSleep);
             return true;
         }
-        LOG(ERROR) << __func__ << ": reading of " << readCount << " bytes from mmap failed";
+        LOG(ERROR) << __func__ << ": reading of " << readCount << " bytes from MMap failed";
         return false;
     }
     bool writeDataToMmap() {
-        if (mSharedMemory != nullptr) {
-            std::memcpy(mSharedMemory, mData.data(), mData.size());
+        if (auto memory = mMmap.getMmapMemory(); memory != nullptr) {
+            std::memcpy(memory, mData.data(), mData.size());
+            // Since MMap `burst` does not block, need to sleep here to get an updated position.
+            std::this_thread::sleep_for(mMmapBurstSleep);
             return true;
         }
-        LOG(ERROR) << __func__ << ": writing of " << mData.size() << " bytes to mmap failed";
+        LOG(ERROR) << __func__ << ": writing of " << mData.size() << " bytes to MMap failed";
         return false;
     }
+    bool updateMmapSharedMemoryIfNeeded(StreamDescriptor::State state) {
+        return isMmapped() ? mMmap.updateMmapSharedMemoryIfNeeded(state) : true;
+    }
 
   private:
     StreamContext::CommandMQ* mCommandMQ;
     StreamContext::ReplyMQ* mReplyMQ;
     StreamContext::DataMQ* mDataMQ;
+    MmapSharedMemory mMmap;
     std::vector<int8_t> mData;
     StreamLogicDriver* const mDriver;
     StreamEventReceiver* const mEventReceiver;
     int mLastEventSeq = StreamEventReceiver::kEventSeqInit;
     const bool mIsMmapped;
-    int8_t* mSharedMemory = nullptr;
+    const std::chrono::duration<double> mMmapBurstSleep;
     const bool mIsCompressOffload;
     const AudioConfigBase mConfig;
 };
@@ -1087,8 +1163,9 @@
 class StreamReaderLogic : public StreamCommonLogic {
   public:
     StreamReaderLogic(const StreamContext& context, StreamLogicDriver* driver,
-                      StreamEventReceiver* eventReceiver)
-        : StreamCommonLogic(context, driver, eventReceiver) {}
+                      StreamWorkerMethods* stream, StreamEventReceiver* eventReceiver)
+        : StreamCommonLogic(context, driver, stream, eventReceiver),
+          mMmapBurstSizeFrames(context.getMmapBurstSizeFrames()) {}
     // Should only be called after the worker has joined.
     const std::vector<int8_t>& getData() const { return StreamCommonLogic::getData(); }
 
@@ -1154,30 +1231,35 @@
         }
         const bool acceptedReply = getDriver()->processValidReply(reply);
         if (const size_t readCount =
-                    !isMmapped() ? getDataMQ()->availableToRead() : reply.fmqByteCount;
+                    !isMmapped() ? getDataMQ()->availableToRead()
+                                 : (command.getTag() == StreamDescriptor::Command::Tag::burst
+                                            ? mMmapBurstSizeFrames
+                                            : 0);
             readCount > 0) {
             fillData(-1);
             if (isMmapped() ? readDataFromMmap(readCount) : readDataFromMQ(readCount)) {
                 goto checkAcceptedReply;
             }
-            LOG(ERROR) << __func__ << ": reading of " << readCount << " data bytes from MQ failed";
+            LOG(ERROR) << __func__ << ": reading of " << readCount << " data bytes failed";
             return Status::ABORT;
         }  // readCount == 0
     checkAcceptedReply:
         if (acceptedReply) {
-            return Status::CONTINUE;
+            return updateMmapSharedMemoryIfNeeded(reply.state) ? Status::CONTINUE : Status::ABORT;
         }
         LOG(ERROR) << __func__ << ": unacceptable reply: " << reply.toString();
         return Status::ABORT;
     }
+
+    const int32_t mMmapBurstSizeFrames;
 };
 using StreamReader = StreamWorker<StreamReaderLogic>;
 
 class StreamWriterLogic : public StreamCommonLogic {
   public:
     StreamWriterLogic(const StreamContext& context, StreamLogicDriver* driver,
-                      StreamEventReceiver* eventReceiver)
-        : StreamCommonLogic(context, driver, eventReceiver) {}
+                      StreamWorkerMethods* stream, StreamEventReceiver* eventReceiver)
+        : StreamCommonLogic(context, driver, stream, eventReceiver) {}
     // Should only be called after the worker has joined.
     const std::vector<int8_t>& getData() const { return StreamCommonLogic::getData(); }
 
@@ -1293,7 +1375,7 @@
             return Status::ABORT;
         }
         if (getDriver()->processValidReply(reply)) {
-            return Status::CONTINUE;
+            return updateMmapSharedMemoryIfNeeded(reply.state) ? Status::CONTINUE : Status::ABORT;
         }
         LOG(ERROR) << __func__ << ": unacceptable reply: " << reply.toString();
         return Status::ABORT;
@@ -1381,7 +1463,7 @@
 };
 
 template <typename Stream>
-class WithStream {
+class WithStream : public StreamWorkerMethods {
   public:
     static ndk::ScopedAStatus callClose(std::shared_ptr<Stream> stream) {
         std::shared_ptr<IStreamCommon> common;
@@ -1421,6 +1503,7 @@
         const AudioConfigBase cfg{config.sampleRate->value, *config.channelMask, *config.format};
         mContext.emplace(mDescriptor, cfg, config.flags.value());
         ASSERT_NO_FATAL_FAILURE(mContext.value().checkIsValid());
+        ASSERT_IS_OK(mStream->getInterfaceVersion(&mInterfaceVersion));
     }
     void SetUp(IModule* module, long bufferSizeFrames) {
         ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module));
@@ -1432,13 +1515,66 @@
     std::shared_ptr<Stream> getSharedPointer() const { return mStream; }
     const AudioPortConfig& getPortConfig() const { return mPortConfig.get(); }
     int32_t getPortId() const { return mPortConfig.getId(); }
+    // StreamWorkerMethods
+    bool createMmapBuffer(MmapBufferDescriptor* desc) override {
+        std::shared_ptr<IStreamCommon> common;
+        ndk::ScopedAStatus status = mStream->getStreamCommon(&common);
+        if (!status.isOk()) {
+            LOG(ERROR) << __func__ << ": getStreamCommon failed: " << status.getMessage();
+            return false;
+        }
+        if (mInterfaceVersion <= kAidlVersion3) {
+            std::vector<VendorParameter> parameters;
+            ScopedAStatus result = common->getVendorParameters({kCreateMmapBuffer}, &parameters);
+            if (result.isOk() && parameters.size() == 1) {
+                std::optional<MmapBufferDescriptor> result;
+                binder_status_t status = parameters[0].ext.getParcelable(&result);
+                if (status == ::android::OK) {
+                    *desc = std::move(*result);
+                    return true;
+                } else {
+                    LOG(ERROR) << __func__ << ": failed to extract parcelable: " << status;
+                }
+            } else {
+                LOG(ERROR) << __func__
+                           << ": failed to call 'createMmapBuffer' via 'getVendorParameter': "
+                           << result.getMessage();
+            }
+        } else {
+            // TODO: Use common->createMmapBuffer after interface update.
+        }
+        return false;
+    }
+    bool supportsCreateMmapBuffer() override {
+        if (!mHasCreateMmapBuffer.has_value()) {
+            if (mInterfaceVersion > kAidlVersion3) {
+                mHasCreateMmapBuffer = true;
+            } else {
+                std::shared_ptr<IStreamCommon> common;
+                ndk::ScopedAStatus status = mStream->getStreamCommon(&common);
+                if (status.isOk()) {
+                    VendorParameter createMmapBuffer{.id = kCreateMmapBuffer};
+                    mHasCreateMmapBuffer =
+                            common->setVendorParameters({createMmapBuffer}, false).isOk();
+                } else {
+                    LOG(ERROR) << __func__ << ": getStreamCommon failed: " << status.getMessage();
+                    return false;
+                }
+            }
+        }
+        return mHasCreateMmapBuffer.value();
+    }
 
   private:
+    static constexpr const char* kCreateMmapBuffer = "aosp.createMmapBuffer";
+
     WithAudioPortConfig mPortConfig;
     std::shared_ptr<Stream> mStream;
     StreamDescriptor mDescriptor;
     std::optional<StreamContext> mContext;
     std::shared_ptr<DefaultStreamCallback> mStreamCallback;
+    int32_t mInterfaceVersion = -1;
+    std::optional<bool> mHasCreateMmapBuffer;
 };
 
 SinkMetadata GenerateSinkMetadata(const AudioPortConfig& portConfig) {
@@ -3103,6 +3239,7 @@
     const StreamContext* getStreamContext() const { return mStream->getContext(); }
     StreamEventReceiver* getStreamEventReceiver() { return mStream->getEventReceiver(); }
     std::shared_ptr<Stream> getStreamSharedPointer() const { return mStream->getSharedPointer(); }
+    StreamWorkerMethods* getStreamWorkerMethods() const { return mStream.get(); }
     const std::string& skipTestReason() const { return mSkipTestReason; }
 
   private:
@@ -3315,12 +3452,11 @@
 static bool skipStreamIoTestForMixPortConfig(const AudioPortConfig& portConfig) {
     return (portConfig.flags.value().getTag() == AudioIoFlags::input &&
             isAnyBitPositionFlagSet(portConfig.flags.value().template get<AudioIoFlags::input>(),
-                                    {AudioInputFlags::MMAP_NOIRQ, AudioInputFlags::VOIP_TX,
-                                     AudioInputFlags::HW_HOTWORD, AudioInputFlags::HOTWORD_TAP})) ||
+                                    {AudioInputFlags::VOIP_TX, AudioInputFlags::HW_HOTWORD,
+                                     AudioInputFlags::HOTWORD_TAP})) ||
            (portConfig.flags.value().getTag() == AudioIoFlags::output &&
             (isAnyBitPositionFlagSet(portConfig.flags.value().template get<AudioIoFlags::output>(),
-                                     {AudioOutputFlags::MMAP_NOIRQ, AudioOutputFlags::VOIP_RX,
-                                      AudioOutputFlags::INCALL_MUSIC}) ||
+                                     {AudioOutputFlags::VOIP_RX, AudioOutputFlags::INCALL_MUSIC}) ||
              (isBitPositionFlagSet(portConfig.flags.value().template get<AudioIoFlags::output>(),
                                    AudioOutputFlags::COMPRESS_OFFLOAD) &&
               !getMediaFileInfoForConfig(portConfig))));
@@ -3331,6 +3467,12 @@
     return device.type.type == AudioDeviceType::IN_ECHO_REFERENCE;
 }
 
+// MMap implementation on the HAL version <= 3 was not test compliant,
+// unless the stream provides 'createMmapBuffer'
+static bool skipStreamIoTestForStream(const StreamContext* context, StreamWorkerMethods* stream) {
+    return context->isMmapped() && !stream->supportsCreateMmapBuffer();
+}
+
 template <typename Stream>
 class StreamFixtureWithWorker {
   public:
@@ -3376,7 +3518,8 @@
                 makeBurstCommands(mIsSync, burstCount, standbyInputWhenDone),
                 context->getFrameSizeBytes(), context->isMmapped());
         mWorker = std::make_unique<typename IOTraits<Stream>::Worker>(
-                *context, mWorkerDriver.get(), mStream->getStreamEventReceiver());
+                *context, mWorkerDriver.get(), mStream->getStreamWorkerMethods(),
+                mStream->getStreamEventReceiver());
         LOG(DEBUG) << __func__ << ": starting " << IOTraits<Stream>::directionStr << " worker...";
         ASSERT_TRUE(mWorker->start());
     }
@@ -3419,6 +3562,10 @@
         if (skipStreamIoTestForMixPortConfig(mStream->getPortConfig())) {
             mSkipTestReason = "Mix port config is not supported for stream I/O tests";
         }
+        if (skipStreamIoTestForStream(mStream->getStreamContext(),
+                                      mStream->getStreamWorkerMethods())) {
+            mSkipTestReason = "Stream can not be used in I/O tests";
+        }
     }
 
     const bool mIsSync;
@@ -3774,6 +3921,7 @@
             ASSERT_EQ("", stream.skipTestReason());
             StreamLogicDriverInvalidCommand driver(seq.second);
             typename IOTraits<Stream>::Worker worker(*stream.getStreamContext(), &driver,
+                                                     stream.getStreamWorkerMethods(),
                                                      stream.getStreamEventReceiver());
             LOG(DEBUG) << __func__ << ": starting worker...";
             ASSERT_TRUE(worker.start());
@@ -4373,11 +4521,15 @@
         ASSERT_NO_FATAL_FAILURE(
                 stream.SetUpStreamForMixPortConfig(module.get(), moduleConfig.get(), portConfig));
         if (skipStreamIoTestForDevice(stream.getDevice())) return;
+        if (skipStreamIoTestForStream(stream.getStreamContext(), stream.getStreamWorkerMethods())) {
+            return;
+        }
         ASSERT_EQ("", stream.skipTestReason());
         StreamLogicDefaultDriver driver(commandsAndStates,
                                         stream.getStreamContext()->getFrameSizeBytes(),
                                         stream.getStreamContext()->isMmapped());
         typename IOTraits<Stream>::Worker worker(*stream.getStreamContext(), &driver,
+                                                 stream.getStreamWorkerMethods(),
                                                  stream.getStreamEventReceiver());
 
         LOG(DEBUG) << __func__ << ": starting worker...";
@@ -4407,10 +4559,14 @@
         if (skipStreamIoTestForDevice(stream.getDevice())) return;
         ASSERT_EQ("", stream.skipTestReason());
         ASSERT_NO_FATAL_FAILURE(stream.TeardownPatchSetUpStream(module.get()));
+        if (skipStreamIoTestForStream(stream.getStreamContext(), stream.getStreamWorkerMethods())) {
+            return;
+        }
         StreamLogicDefaultDriver driver(commandsAndStates,
                                         stream.getStreamContext()->getFrameSizeBytes(),
                                         stream.getStreamContext()->isMmapped());
         typename IOTraits<Stream>::Worker worker(*stream.getStreamContext(), &driver,
+                                                 stream.getStreamWorkerMethods(),
                                                  stream.getStreamEventReceiver());
         ASSERT_NO_FATAL_FAILURE(stream.ReconnectPatch(module.get()));
 
@@ -4788,7 +4944,7 @@
 // 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;
+    std::reference_wrapper<StateDag::value_type> 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));