CSD: extract SoundDoseManager for MEL handling

The SoundDoseManager owns the aggregator which computes the sound dose.
This is a central place for passing the sound dose properties up to the
higher layers.

Test: UT and dumpsys media.audio_flinger
Bug: 255943034

Change-Id: I2d8139b9115bdd793d4ed418f2da6bc8782d32b3
diff --git a/services/audioflinger/Android.bp b/services/audioflinger/Android.bp
index 27ac212..48d3e6f 100644
--- a/services/audioflinger/Android.bp
+++ b/services/audioflinger/Android.bp
@@ -87,6 +87,7 @@
         "libmemunreachable",
         "libmedia_helper",
         "libshmemcompat",
+        "libsounddose",
         "libvibrator",
         "packagemanager_aidl-cpp",
     ],
@@ -103,6 +104,7 @@
         "libaudiohal_headers",
         "libaudioutils_headers",
         "libmedia_headers",
+        "libsounddose_headers",
     ],
 
     export_shared_lib_headers: [
diff --git a/services/audioflinger/AudioFlinger.h b/services/audioflinger/AudioFlinger.h
index 6a94164..d2a033e 100644
--- a/services/audioflinger/AudioFlinger.h
+++ b/services/audioflinger/AudioFlinger.h
@@ -87,6 +87,8 @@
 #include <audio_utils/SimpleLog.h>
 #include <audio_utils/TimestampVerifier.h>
 
+#include <sounddose/SoundDoseManager.h>
+
 #include "FastCapture.h"
 #include "FastMixer.h"
 #include <media/nbaio/NBAIO.h>
diff --git a/services/audioflinger/MelReporter.cpp b/services/audioflinger/MelReporter.cpp
index 9a579dd..88500b8 100644
--- a/services/audioflinger/MelReporter.cpp
+++ b/services/audioflinger/MelReporter.cpp
@@ -18,13 +18,11 @@
 // #define LOG_NDEBUG 0
 #define LOG_TAG "AudioFlinger::MelReporter"
 
-#include <cinttypes>
-#include <utils/Log.h>
-#include <android-base/stringprintf.h>
-#include <audio_utils/power.h>
-
 #include "AudioFlinger.h"
 
+#include <audio_utils/power.h>
+#include <utils/Log.h>
+
 namespace android {
 
 bool AudioFlinger::MelReporter::shouldComputeMelForDeviceType(audio_devices_t device) {
@@ -67,7 +65,7 @@
             std::lock_guard _lAf(mAudioFlinger.mLock);
             auto thread = mAudioFlinger.checkPlaybackThread_l(streamHandle);
             if (thread != nullptr) {
-                thread->startMelComputation(mMelAggregator.getOrCreateCallbackForDevice(
+                thread->startMelComputation(mSoundDoseManager.getOrCreateCallbackForDevice(
                     deviceId,
                     newPatch.streamHandle));
             }
@@ -103,24 +101,13 @@
     if (thread != nullptr) {
         thread->stopMelComputation();
     }
-    mMelAggregator.removeStreamCallback(melPatch.streamHandle);
+    mSoundDoseManager.removeStreamCallback(melPatch.streamHandle);
 }
 
 std::string AudioFlinger::MelReporter::dump() {
     std::lock_guard _l(mLock);
-    std::string output;
-
-    base::StringAppendF(&output, "\nMel Reporter:\n");
-    mMelAggregator.foreach([&output](const audio_utils::MelRecord& melRecord) {
-        base::StringAppendF(&output, "Continuous MELs for portId=%d, ", melRecord.portId);
-        base::StringAppendF(&output, "starting at timestamp %" PRId64 ": ", melRecord.timestamp);
-
-        for (const auto& mel : melRecord.mels) {
-            base::StringAppendF(&output, "%d ", mel);
-        }
-        base::StringAppendF(&output, "\n");
-    });
-
+    std::string output("\nSound Dose:\n");
+    output.append(mSoundDoseManager.dump());
     return output;
 }
 
diff --git a/services/audioflinger/MelReporter.h b/services/audioflinger/MelReporter.h
index 905a4cd..8a78f6e 100644
--- a/services/audioflinger/MelReporter.h
+++ b/services/audioflinger/MelReporter.h
@@ -19,8 +19,9 @@
     #error This header file should only be included from AudioFlinger.h
 #endif
 
-#include <unordered_map>
 #include <mutex>
+#include <sounddose/SoundDoseManager.h>
+#include <unordered_map>
 
 constexpr static int kMaxTimestampDeltaInSec = 120;
 
@@ -31,8 +32,7 @@
 class MelReporter : public PatchCommandThread::PatchCommandListener {
 public:
     explicit MelReporter(AudioFlinger& audioFlinger)
-        : mAudioFlinger(audioFlinger),
-          mMelAggregator(kMaxTimestampDeltaInSec) {}
+        : mAudioFlinger(audioFlinger) {}
 
     void onFirstRef() override {
         mAudioFlinger.mPatchCommandThread->addListener(this);
@@ -54,7 +54,7 @@
 private:
     AudioFlinger& mAudioFlinger;  // does not own the object
 
-    audio_utils::MelAggregator mMelAggregator;
+    SoundDoseManager mSoundDoseManager;
 
     struct ActiveMelPatch {
         audio_io_handle_t streamHandle{AUDIO_IO_HANDLE_NONE};
diff --git a/services/audioflinger/sounddose/Android.bp b/services/audioflinger/sounddose/Android.bp
new file mode 100644
index 0000000..eb9517e
--- /dev/null
+++ b/services/audioflinger/sounddose/Android.bp
@@ -0,0 +1,43 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_av_services_audioflinger_license"],
+}
+
+cc_library {
+    name: "libsounddose",
+
+    product_available: true,
+    double_loadable: true,
+    host_supported: true,
+
+    srcs: [
+        "SoundDoseManager.cpp",
+    ],
+
+    shared_libs: [
+        "libaudioutils",
+        "libbase",
+        "liblog",
+        "libutils",
+    ],
+
+    header_libs: [
+        "libaudioutils_headers",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+    ],
+}
+
+cc_library_headers {
+    name: "libsounddose_headers",
+    host_supported: true,
+    device_supported: true,
+    export_include_dirs: ["."],
+}
diff --git a/services/audioflinger/sounddose/SoundDoseManager.cpp b/services/audioflinger/sounddose/SoundDoseManager.cpp
new file mode 100644
index 0000000..780d796
--- /dev/null
+++ b/services/audioflinger/sounddose/SoundDoseManager.cpp
@@ -0,0 +1,122 @@
+/*
+**
+** Copyright 2022, 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.
+*/
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "SoundDoseManager"
+
+#include "SoundDoseManager.h"
+
+#include <android-base/stringprintf.h>
+#include <cinttypes>
+#include <utils/Log.h>
+#include <time.h>
+
+namespace android {
+
+namespace {
+
+int64_t getMonotonicSecond() {
+    struct timespec now_ts;
+    if (clock_gettime(CLOCK_MONOTONIC, &now_ts) != 0) {
+        ALOGE("%s: cannot get timestamp", __func__);
+        return -1;
+    }
+    return now_ts.tv_sec;
+}
+
+}  // namespace
+
+sp<audio_utils::MelProcessor::MelCallback> SoundDoseManager::getOrCreateCallbackForDevice(
+        audio_port_handle_t deviceId,
+        audio_io_handle_t streamHandle)
+{
+    std::lock_guard _l(mLock);
+
+    auto streamHandleCallback = mActiveCallbacks.find(streamHandle);
+    if (streamHandleCallback != mActiveCallbacks.end()) {
+        ALOGV("%s: found callback for stream %d", __func__, streamHandle);
+        auto callback = streamHandleCallback->second;
+        callback->mDeviceHandle = deviceId;
+        return callback;
+    } else {
+        ALOGV("%s: creating new callback for device %d", __func__, streamHandle);
+        sp<Callback> melCallback = sp<Callback>::make(*this, deviceId);
+        mActiveCallbacks[streamHandle] = melCallback;
+        return melCallback;
+    }
+}
+
+void SoundDoseManager::removeStreamCallback(audio_io_handle_t streamHandle)
+{
+    std::unordered_map<audio_io_handle_t, sp<Callback>>::iterator callbackToRemove;
+
+    std::lock_guard _l(mLock);
+    callbackToRemove = mActiveCallbacks.find(streamHandle);
+    if (callbackToRemove != mActiveCallbacks.end()) {
+        mActiveCallbacks.erase(callbackToRemove);
+    }
+}
+
+void SoundDoseManager::Callback::onNewMelValues(const std::vector<float>& mels,
+                                                size_t offset,
+                                                size_t length) const
+{
+    ALOGV("%s", __func__);
+    std::lock_guard _l(mSoundDoseManager.mLock);
+
+    int64_t timestampSec = getMonotonicSecond();
+
+    // only for internal callbacks
+    mSoundDoseManager.mMelAggregator.aggregateAndAddNewMelRecord(
+        audio_utils::MelRecord(mDeviceHandle, std::vector<float>(
+                                   mels.begin() + offset,
+                                   mels.begin() + offset + length),
+                               timestampSec - length));
+}
+
+std::string SoundDoseManager::dump() const
+{
+    std::string output;
+    mMelAggregator.foreachCsd([&output](audio_utils::CsdRecord csdRecord) {
+        base::StringAppendF(&output,
+                            "CSD %f with average MEL %f in interval [%" PRId64 ", %" PRId64 "]",
+                            csdRecord.value,
+                            csdRecord.averageMel,
+                            csdRecord.timestamp,
+                            csdRecord.timestamp + csdRecord.duration);
+        base::StringAppendF(&output, "\n");
+    });
+
+    base::StringAppendF(&output, "\nCached Mel Records:\n");
+    mMelAggregator.foreachCachedMel([&output](const audio_utils::MelRecord& melRecord) {
+        base::StringAppendF(&output, "Continuous MELs for portId=%d, ", melRecord.portId);
+        base::StringAppendF(&output, "starting at timestamp %" PRId64 ": ", melRecord.timestamp);
+
+        for (const auto& mel : melRecord.mels) {
+            base::StringAppendF(&output, "%.2f ", mel);
+        }
+        base::StringAppendF(&output, "\n");
+    });
+
+    return output;
+}
+
+size_t SoundDoseManager::getCachedMelRecordsSize() const {
+    return mMelAggregator.getCachedMelRecordsSize();
+}
+
+}  // namespace android
diff --git a/services/audioflinger/sounddose/SoundDoseManager.h b/services/audioflinger/sounddose/SoundDoseManager.h
new file mode 100644
index 0000000..fae5ba7
--- /dev/null
+++ b/services/audioflinger/sounddose/SoundDoseManager.h
@@ -0,0 +1,80 @@
+/*
+**
+** Copyright 2022, 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 <audio_utils/MelProcessor.h>
+#include <audio_utils/MelAggregator.h>
+#include <mutex>
+#include <unordered_map>
+
+namespace android {
+
+/** CSD is computed with a rolling window of 7 days. */
+constexpr int64_t kCsdWindowSeconds = 604800;  // 60 * 60 * 24 * 7
+
+class SoundDoseManager {
+public:
+    SoundDoseManager() : mMelAggregator(kCsdWindowSeconds) {};
+
+    /**
+     * \brief Creates or gets the callback assigned to the streamHandle
+     *
+     * \param deviceId          id for the devices where the stream is active.
+     * \param streanHandle      handle to the stream
+     */
+    sp<audio_utils::MelProcessor::MelCallback> getOrCreateCallbackForDevice(
+        audio_port_handle_t deviceId,
+        audio_io_handle_t streamHandle);
+
+    /**
+     * \brief Removes stream callback when MEL computation is not needed anymore
+     *
+     * \param streanHandle      handle to the stream
+     */
+    void removeStreamCallback(audio_io_handle_t streamHandle);
+
+    std::string dump() const;
+
+    // used for testing
+    size_t getCachedMelRecordsSize() const;
+private:
+    /**
+     * An implementation of the MelProcessor::MelCallback that is assigned to a
+     * specific device.
+     */
+    class Callback : public audio_utils::MelProcessor::MelCallback {
+    public:
+        Callback(SoundDoseManager& soundDoseManager, audio_port_handle_t deviceHandle)
+            : mSoundDoseManager(soundDoseManager), mDeviceHandle(deviceHandle) {}
+
+        void onNewMelValues(const std::vector<float>& mels,
+                            size_t offset,
+                            size_t length) const override;
+
+        SoundDoseManager& mSoundDoseManager;
+        audio_port_handle_t mDeviceHandle;
+    };
+
+    // no need for lock since MelAggregator is thread-safe
+    audio_utils::MelAggregator mMelAggregator;
+
+    std::mutex mLock;
+    std::unordered_map<audio_io_handle_t, sp<Callback>> mActiveCallbacks GUARDED_BY(mLock);
+};
+
+}  // namespace android
diff --git a/services/audioflinger/sounddose/tests/Android.bp b/services/audioflinger/sounddose/tests/Android.bp
new file mode 100644
index 0000000..cb10950
--- /dev/null
+++ b/services/audioflinger/sounddose/tests/Android.bp
@@ -0,0 +1,40 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_av_services_audioflinger_license"],
+}
+
+cc_test {
+    name: "sounddosemanager_tests",
+    host_supported: true,
+
+    srcs: [
+        "sounddosemanager_tests.cpp"
+    ],
+
+    shared_libs: [
+        "libaudioutils",
+        "libbase",
+        "liblog",
+        "libutils",
+    ],
+
+    static_libs: [
+        "libgmock",
+        "libsounddose",
+    ],
+
+    header_libs: [
+        "libaudioutils_headers",
+        "libsounddose_headers",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wextra",
+    ],
+}
\ No newline at end of file
diff --git a/services/audioflinger/sounddose/tests/sounddosemanager_tests.cpp b/services/audioflinger/sounddose/tests/sounddosemanager_tests.cpp
new file mode 100644
index 0000000..3fa7d6c
--- /dev/null
+++ b/services/audioflinger/sounddose/tests/sounddosemanager_tests.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "sounddosemanager_tests"
+
+#include <SoundDoseManager.h>
+
+#include <gtest/gtest.h>
+
+namespace android {
+namespace {
+
+TEST(SoundDoseManagerTest, GetCallbackForExistingStream) {
+    SoundDoseManager soundDoseManager;
+    sp<audio_utils::MelProcessor::MelCallback> callback1 =
+        soundDoseManager.getOrCreateCallbackForDevice(/*deviceId=*/1, /*streamHandle=*/1);
+    sp<audio_utils::MelProcessor::MelCallback> callback2 =
+        soundDoseManager.getOrCreateCallbackForDevice(/*deviceId=*/2, /*streamHandle=*/1);
+
+    EXPECT_EQ(callback1, callback2);
+}
+
+TEST(SoundDoseManagerTest, RemoveExistingStream) {
+    SoundDoseManager soundDoseManager;
+    sp<audio_utils::MelProcessor::MelCallback> callback1 =
+        soundDoseManager.getOrCreateCallbackForDevice(/*deviceId=*/1, /*streamHandle=*/1);
+
+    soundDoseManager.removeStreamCallback(1);
+    sp<audio_utils::MelProcessor::MelCallback> callback2 =
+        soundDoseManager.getOrCreateCallbackForDevice(/*deviceId=*/2, /*streamHandle=*/1);
+
+    EXPECT_NE(callback1, callback2);
+}
+
+TEST(SoundDoseManagerTest, NewMelValuesCacheNewRecord) {
+    SoundDoseManager soundDoseManager;
+    std::vector<float>mels{1, 1};
+    sp<audio_utils::MelProcessor::MelCallback> callback =
+        soundDoseManager.getOrCreateCallbackForDevice(/*deviceId=*/1, /*streamHandle=*/1);
+
+    callback->onNewMelValues(mels, 0, mels.size());
+
+    EXPECT_EQ(soundDoseManager.getCachedMelRecordsSize(), size_t{1});
+}
+
+}  // namespace
+}  // namespace android