diff --git a/audio/aidl/Android.bp b/audio/aidl/Android.bp index 05c7649c90..8f0286f397 100644 --- a/audio/aidl/Android.bp +++ b/audio/aidl/Android.bp @@ -113,6 +113,7 @@ aidl_interface { "android/hardware/audio/core/AudioRoute.aidl", "android/hardware/audio/core/IConfig.aidl", "android/hardware/audio/core/IModule.aidl", + "android/hardware/audio/core/ISoundDose.aidl", "android/hardware/audio/core/IStreamCallback.aidl", "android/hardware/audio/core/IStreamIn.aidl", "android/hardware/audio/core/IStreamOut.aidl", diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl index 0c7ca27c61..42b12d94b3 100644 --- a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl +++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/IModule.aidl @@ -60,6 +60,7 @@ interface IModule { void updateAudioMode(android.hardware.audio.core.AudioMode mode); void updateScreenRotation(android.hardware.audio.core.IModule.ScreenRotation rotation); void updateScreenState(boolean isTurnedOn); + @nullable android.hardware.audio.core.ISoundDose getSoundDose(); @VintfStability parcelable OpenInputStreamArguments { int portConfigId; diff --git a/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/ISoundDose.aidl b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/ISoundDose.aidl new file mode 100644 index 0000000000..bc010caaf5 --- /dev/null +++ b/audio/aidl/aidl_api/android.hardware.audio.core/current/android/hardware/audio/core/ISoundDose.aidl @@ -0,0 +1,52 @@ +/* + * 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. + */ +/////////////////////////////////////////////////////////////////////////////// +// THIS FILE IS IMMUTABLE. DO NOT EDIT IN ANY CASE. // +/////////////////////////////////////////////////////////////////////////////// + +// This file is a snapshot of an AIDL file. Do not edit it manually. There are +// two cases: +// 1). this is a frozen version file - do not edit this in any case. +// 2). this is a 'current' file. If you make a backwards compatible change to +// the interface (from the latest frozen version), the build system will +// prompt you to update this file with `m -update-api`. +// +// You must not make a backward incompatible change to any AIDL file built +// with the aidl_interface module type with versions property set. The module +// type is used to build AIDL files in a way that they can be used across +// independently updatable components of the system. If a device is shipped +// with such a backward incompatible change, it has a high risk of breaking +// later when a module using the interface is updated, e.g., Mainline modules. + +package android.hardware.audio.core; +@VintfStability +interface ISoundDose { + void setOutputRs2(float rs2ValueDbA); + float getOutputRs2(); + void registerSoundDoseCallback(in android.hardware.audio.core.ISoundDose.IHalSoundDoseCallback callback); + const int DEFAULT_MAX_RS2 = 100; + const int MIN_RS2 = 80; + @VintfStability + interface IHalSoundDoseCallback { + oneway void onMomentaryExposureWarning(float currentDbA, in android.media.audio.common.AudioDevice audioDevice); + oneway void onNewMelValues(in android.hardware.audio.core.ISoundDose.IHalSoundDoseCallback.MelRecord melRecord, in android.media.audio.common.AudioDevice audioDevice); + @VintfStability + parcelable MelRecord { + float[] melValues; + long timestamp; + } + } +} diff --git a/audio/aidl/android/hardware/audio/core/IModule.aidl b/audio/aidl/android/hardware/audio/core/IModule.aidl index 786d5eecbe..2d34df6297 100644 --- a/audio/aidl/android/hardware/audio/core/IModule.aidl +++ b/audio/aidl/android/hardware/audio/core/IModule.aidl @@ -21,6 +21,7 @@ import android.hardware.audio.common.SourceMetadata; import android.hardware.audio.core.AudioMode; import android.hardware.audio.core.AudioPatch; import android.hardware.audio.core.AudioRoute; +import android.hardware.audio.core.ISoundDose; import android.hardware.audio.core.IStreamCallback; import android.hardware.audio.core.IStreamIn; import android.hardware.audio.core.IStreamOut; @@ -668,4 +669,19 @@ interface IModule { * @param isTurnedOn True if the screen is turned on. */ void updateScreenState(boolean isTurnedOn); + + /** + * Retrieve the sound dose interface. + * + * If a device must comply to IEC62368-1 3rd edition audio safety requirements and is + * implementing audio offload decoding or other direct playback paths where volume control + * happens below the audio HAL, it must return an instance of the ISoundDose interface. + * The same instance must be returned during the lifetime of the HAL module. + * If the HAL module does not support sound dose, null must be returned, without throwing + * any errors. + * + * @return An instance of the ISoundDose interface implementation. + * @throws EX_ILLEGAL_STATE If there was an error creating an instance. + */ + @nullable ISoundDose getSoundDose(); } diff --git a/audio/aidl/android/hardware/audio/core/ISoundDose.aidl b/audio/aidl/android/hardware/audio/core/ISoundDose.aidl new file mode 100644 index 0000000000..89fd69b80f --- /dev/null +++ b/audio/aidl/android/hardware/audio/core/ISoundDose.aidl @@ -0,0 +1,104 @@ +/* + * 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. + */ + +package android.hardware.audio.core; + +import android.media.audio.common.AudioDevice; + +/** + * This interface provides functions related to sound exposure control required for compliance to + * EN/IEC 62368-1 3rd edition. Implementing this interface is mandatory for devices for which + * compliance to this standard is mandated and implementing audio offload decoding or other direct + * playback paths where volume control happens below the audio HAL. + */ +@VintfStability +interface ISoundDose { + /** + * Max value in dBA used for momentary exposure warnings as defined by IEC62368-1 + * 3rd edition. This value represents the default RS2 value. + */ + const int DEFAULT_MAX_RS2 = 100; + /** Min value of the RS2 threshold in dBA as defined by IEC62368-1 3rd edition. */ + const int MIN_RS2 = 80; + + /** + * Sets the RS2 value used for momentary exposure warnings. Default value is + * DEFAULT_MAX_RS2 as specified in IEC62368-1 3rd edition. + * + * @param rs2ValueDbA custom RS2 value to use. Must not be higher than DEFAULT_MAX_RS2 + * @throws EX_ILLEGAL_ARGUMENT if rs2ValueDbA is greater than DEFAULT_MAX_RS2 or lower + * than 80dBA + */ + void setOutputRs2(float rs2ValueDbA); + + /** + * Gets the RS2 value used for momentary exposure warnings. + * + * @return the RS2 value in dBA + */ + float getOutputRs2(); + + /** + * Registers the HAL callback for sound dose computation. If sound dose is supported + * the MEL values and exposure notifications will be received through this callback + * only. The internal framework MEL computation will be disabled. + * It is not possible to unregister the callback. The HAL is responsible to provide + * the MEL values throughout its lifecycle. + * This method should only be called once (no updates allowed) with a valid callback. + * + * @param callback to use when new updates are available for sound dose + * @throws EX_ILLEGAL_STATE if the method is called more than once + * @throws EX_ILLEGAL_ARGUMENT if the passed callback is null + */ + void registerSoundDoseCallback(in IHalSoundDoseCallback callback); + + @VintfStability + oneway interface IHalSoundDoseCallback { + /** + * Called whenever the current MEL value exceeds the set RS2 value. + * + * @param currentDbA the current MEL value which exceeds the RS2 value + * @param audioDevice the audio device where the MEL exposure warning was recorded + */ + void onMomentaryExposureWarning(float currentDbA, in AudioDevice audioDevice); + + @VintfStability + parcelable MelRecord { + /** + * Array of continuously recorded MEL values >= RS1 (1 per second). + * First value in the array was recorded at 'timestamp'. + */ + float[] melValues; + /** + * Corresponds to the time in seconds when the first MEL entry in melValues + * was recorded. The timestamp values have to be consistent throughout all + * audio ports, equal timestamp values will be aggregated. + */ + long timestamp; + } + + /** + * Provides a MelRecord containing continuous MEL values sorted by timestamp. + * Note that all the MEL values originate from the audio device specified by audioDevice. + * In case values from multiple devices need to be reported, the caller should execute + * this callback once for every device. + * + * @param melRecord contains the MEL values used for CSD + * @param audioDevice the audio device where the MEL values were recorded + */ + void onNewMelValues(in MelRecord melRecord, in AudioDevice audioDevice); + } +} diff --git a/audio/aidl/default/Android.bp b/audio/aidl/default/Android.bp index f2cebbfe73..b9b8cd8ce2 100644 --- a/audio/aidl/default/Android.bp +++ b/audio/aidl/default/Android.bp @@ -43,6 +43,7 @@ cc_library_static { "Configuration.cpp", "EngineConfigXmlConverter.cpp", "Module.cpp", + "SoundDose.cpp", "Stream.cpp", "Telephony.cpp", ], diff --git a/audio/aidl/default/Module.cpp b/audio/aidl/default/Module.cpp index 47d6fa44be..86f0261d2a 100644 --- a/audio/aidl/default/Module.cpp +++ b/audio/aidl/default/Module.cpp @@ -26,6 +26,7 @@ #include #include "core-impl/Module.h" +#include "core-impl/SoundDose.h" #include "core-impl/Telephony.h" #include "core-impl/utils.h" @@ -931,4 +932,13 @@ ndk::ScopedAStatus Module::updateScreenState(bool in_isTurnedOn) { return ndk::ScopedAStatus::ok(); } +ndk::ScopedAStatus Module::getSoundDose(std::shared_ptr* _aidl_return) { + if (mSoundDose == nullptr) { + mSoundDose = ndk::SharedRefBase::make(); + } + *_aidl_return = mSoundDose; + LOG(DEBUG) << __func__ << ": returning instance of ISoundDose: " << _aidl_return->get(); + return ndk::ScopedAStatus::ok(); +} + } // namespace aidl::android::hardware::audio::core diff --git a/audio/aidl/default/SoundDose.cpp b/audio/aidl/default/SoundDose.cpp new file mode 100644 index 0000000000..3d222a8280 --- /dev/null +++ b/audio/aidl/default/SoundDose.cpp @@ -0,0 +1,57 @@ +/* + * 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_TAG "AHAL_SoundDose" + +#include "core-impl/SoundDose.h" + +#include + +namespace aidl::android::hardware::audio::core { + +ndk::ScopedAStatus SoundDose::setOutputRs2(float in_rs2ValueDbA) { + if (in_rs2ValueDbA < MIN_RS2 || in_rs2ValueDbA > DEFAULT_MAX_RS2) { + LOG(ERROR) << __func__ << ": RS2 value is invalid: " << in_rs2ValueDbA; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + + mRs2Value = in_rs2ValueDbA; + return ndk::ScopedAStatus::ok(); +} + +ndk::ScopedAStatus SoundDose::getOutputRs2(float* _aidl_return) { + *_aidl_return = mRs2Value; + LOG(DEBUG) << __func__ << ": returning " << *_aidl_return; + return ndk::ScopedAStatus::ok(); +} + +ndk::ScopedAStatus SoundDose::registerSoundDoseCallback( + const std::shared_ptr& in_callback) { + if (in_callback.get() == nullptr) { + LOG(ERROR) << __func__ << ": Callback is nullptr"; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + } + if (mCallback != nullptr) { + LOG(ERROR) << __func__ << ": Sound dose callback was already registered"; + return ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_STATE); + } + + mCallback = in_callback; + LOG(DEBUG) << __func__ << ": Registered sound dose callback "; + return ndk::ScopedAStatus::ok(); +} + +} // namespace aidl::android::hardware::audio::core diff --git a/audio/aidl/default/include/core-impl/Module.h b/audio/aidl/default/include/core-impl/Module.h index 52fb54ca9d..3cc31c5871 100644 --- a/audio/aidl/default/include/core-impl/Module.h +++ b/audio/aidl/default/include/core-impl/Module.h @@ -86,6 +86,7 @@ class Module : public BnModule { ndk::ScopedAStatus updateScreenRotation( ::aidl::android::hardware::audio::core::IModule::ScreenRotation in_rotation) override; ndk::ScopedAStatus updateScreenState(bool in_isTurnedOn) override; + ndk::ScopedAStatus getSoundDose(std::shared_ptr* _aidl_return) override; void cleanUpPatch(int32_t patchId); ndk::ScopedAStatus createStreamContext( @@ -123,6 +124,7 @@ class Module : public BnModule { bool mMasterMute = false; float mMasterVolume = 1.0f; bool mMicMute = false; + std::shared_ptr mSoundDose; }; } // namespace aidl::android::hardware::audio::core diff --git a/audio/aidl/default/include/core-impl/SoundDose.h b/audio/aidl/default/include/core-impl/SoundDose.h new file mode 100644 index 0000000000..54a6cbf1b8 --- /dev/null +++ b/audio/aidl/default/include/core-impl/SoundDose.h @@ -0,0 +1,42 @@ +/* + * 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. + */ + +#pragma once + +#include + +#include +#include + +using aidl::android::media::audio::common::AudioDevice; + +namespace aidl::android::hardware::audio::core { + +class SoundDose : public BnSoundDose { + public: + SoundDose() : mRs2Value(DEFAULT_MAX_RS2){}; + + ndk::ScopedAStatus setOutputRs2(float in_rs2ValueDbA) override; + ndk::ScopedAStatus getOutputRs2(float* _aidl_return) override; + ndk::ScopedAStatus registerSoundDoseCallback( + const std::shared_ptr& in_callback) override; + + private: + std::shared_ptr mCallback; + float mRs2Value; +}; + +} // namespace aidl::android::hardware::audio::core diff --git a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp index 99771e6cb1..5dadea384e 100644 --- a/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp +++ b/audio/aidl/vts/VtsHalAudioCoreModuleTargetTest.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,7 @@ using aidl::android::hardware::audio::core::AudioMode; using aidl::android::hardware::audio::core::AudioPatch; using aidl::android::hardware::audio::core::AudioRoute; using aidl::android::hardware::audio::core::IModule; +using aidl::android::hardware::audio::core::ISoundDose; using aidl::android::hardware::audio::core::IStreamIn; using aidl::android::hardware::audio::core::IStreamOut; using aidl::android::hardware::audio::core::ITelephony; @@ -2468,6 +2470,92 @@ TEST_P(AudioModulePatch, ResetInvalidPatchId) { } } +class AudioCoreSoundDose : public AudioCoreModuleBase, public testing::TestWithParam { + public: + class NoOpHalSoundDoseCallback : public ISoundDose::BnHalSoundDoseCallback { + public: + ndk::ScopedAStatus onMomentaryExposureWarning(float in_currentDbA, + const AudioDevice& in_audioDevice) override; + ndk::ScopedAStatus onNewMelValues( + const ISoundDose::IHalSoundDoseCallback::MelRecord& in_melRecord, + const AudioDevice& in_audioDevice) override; + }; + + void SetUp() override { + ASSERT_NO_FATAL_FAILURE(SetUpImpl(GetParam())); + ASSERT_IS_OK(module->getSoundDose(&soundDose)); + callback = ndk::SharedRefBase::make(); + } + + void TearDown() override { ASSERT_NO_FATAL_FAILURE(TearDownImpl()); } + + std::shared_ptr soundDose; + std::shared_ptr callback; +}; + +ndk::ScopedAStatus AudioCoreSoundDose::NoOpHalSoundDoseCallback::onMomentaryExposureWarning( + float in_currentDbA, const AudioDevice& in_audioDevice) { + // Do nothing + (void)in_currentDbA; + (void)in_audioDevice; + LOG(INFO) << "NoOpHalSoundDoseCallback::onMomentaryExposureWarning called"; + + return ndk::ScopedAStatus::ok(); +} + +ndk::ScopedAStatus AudioCoreSoundDose::NoOpHalSoundDoseCallback::onNewMelValues( + const ISoundDose::IHalSoundDoseCallback::MelRecord& in_melRecord, + const AudioDevice& in_audioDevice) { + // Do nothing + (void)in_melRecord; + (void)in_audioDevice; + LOG(INFO) << "NoOpHalSoundDoseCallback::onNewMelValues called"; + + return ndk::ScopedAStatus::ok(); +} + +TEST_P(AudioCoreSoundDose, GetSetOutputRs2) { + if (soundDose == nullptr) { + GTEST_SKIP() << "SoundDose is not supported"; + } + + bool isSupported = false; + EXPECT_NO_FATAL_FAILURE(TestAccessors(soundDose.get(), &ISoundDose::getOutputRs2, + &ISoundDose::setOutputRs2, + /*validValues=*/{80.f, 90.f, 100.f}, + /*invalidValues=*/{79.f, 101.f}, &isSupported)); + EXPECT_TRUE(isSupported) << "Getting/Setting RS2 must be supported"; +} + +TEST_P(AudioCoreSoundDose, CheckDefaultRs2Value) { + if (soundDose == nullptr) { + GTEST_SKIP() << "SoundDose is not supported"; + } + + float rs2Value; + ASSERT_IS_OK(soundDose->getOutputRs2(&rs2Value)); + EXPECT_EQ(rs2Value, ISoundDose::DEFAULT_MAX_RS2); +} + +TEST_P(AudioCoreSoundDose, RegisterSoundDoseCallbackTwiceThrowsException) { + if (soundDose == nullptr) { + GTEST_SKIP() << "SoundDose is not supported"; + } + + ASSERT_IS_OK(soundDose->registerSoundDoseCallback(callback)); + EXPECT_STATUS(EX_ILLEGAL_STATE, soundDose->registerSoundDoseCallback(callback)) + << "Registering sound dose callback twice should throw EX_ILLEGAL_STATE"; +} + +TEST_P(AudioCoreSoundDose, RegisterSoundDoseNullCallbackThrowsException) { + if (soundDose == nullptr) { + GTEST_SKIP() << "SoundDose is not supported"; + } + + EXPECT_STATUS(EX_ILLEGAL_ARGUMENT, soundDose->registerSoundDoseCallback(nullptr)) + << "Registering nullptr sound dose callback should throw EX_ILLEGAL_ARGUMENT"; +} + INSTANTIATE_TEST_SUITE_P(AudioCoreModuleTest, AudioCoreModule, testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), android::PrintInstanceNameToString); @@ -2484,6 +2572,10 @@ INSTANTIATE_TEST_SUITE_P(AudioStreamOutTest, AudioStreamOut, testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), android::PrintInstanceNameToString); GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioStreamOut); +INSTANTIATE_TEST_SUITE_P(AudioCoreSoundDoseTest, AudioCoreSoundDose, + testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), + android::PrintInstanceNameToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioCoreSoundDose); // This is the value used in test sequences for which the test needs to ensure // that the HAL stays in a transient state long enough to receive the next command.