| /* |
| * Copyright 2021 HIMSA II K/S - www.himsa.com. |
| * Represented by EHIMA - www.ehima.com |
| * |
| * 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 "client_audio.h" |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include <chrono> |
| #include <future> |
| |
| #include "audio_hal_interface/le_audio_software.h" |
| #include "base/bind_helpers.h" |
| #include "common/message_loop_thread.h" |
| #include "hardware/bluetooth.h" |
| #include "osi/include/wakelock.h" |
| |
| using ::testing::_; |
| using ::testing::Assign; |
| using ::testing::AtLeast; |
| using ::testing::DoAll; |
| using ::testing::Invoke; |
| using ::testing::Mock; |
| using ::testing::Return; |
| using ::testing::ReturnPointee; |
| using ::testing::SaveArg; |
| using std::chrono_literals::operator""ms; |
| |
| bluetooth::common::MessageLoopThread message_loop_thread("test message loop"); |
| bluetooth::common::MessageLoopThread* get_main_thread() { |
| return &message_loop_thread; |
| } |
| bt_status_t do_in_main_thread(const base::Location& from_here, |
| base::OnceClosure task) { |
| if (!message_loop_thread.DoInThread(from_here, std::move(task))) { |
| LOG(ERROR) << __func__ << ": failed from " << from_here.ToString(); |
| return BT_STATUS_FAIL; |
| } |
| return BT_STATUS_SUCCESS; |
| } |
| |
| static base::MessageLoop* message_loop_; |
| base::MessageLoop* get_main_message_loop() { return message_loop_; } |
| |
| static void init_message_loop_thread() { |
| message_loop_thread.StartUp(); |
| if (!message_loop_thread.IsRunning()) { |
| FAIL() << "unable to create message loop thread."; |
| } |
| |
| if (!message_loop_thread.EnableRealTimeScheduling()) |
| LOG(ERROR) << "Unable to set real time scheduling"; |
| |
| message_loop_ = message_loop_thread.message_loop(); |
| if (message_loop_ == nullptr) FAIL() << "unable to get message loop."; |
| } |
| |
| static void cleanup_message_loop_thread() { |
| message_loop_ = nullptr; |
| message_loop_thread.ShutDown(); |
| } |
| |
| using bluetooth::audio::le_audio::LeAudioClientInterface; |
| |
| class MockLeAudioClientInterfaceSink : public LeAudioClientInterface::Sink { |
| public: |
| MOCK_METHOD((void), Cleanup, (), (override)); |
| MOCK_METHOD((void), SetPcmParameters, |
| (const LeAudioClientInterface::PcmParameters& params), |
| (override)); |
| MOCK_METHOD((void), SetRemoteDelay, (uint16_t delay_report_ms), (override)); |
| MOCK_METHOD((void), StartSession, (), (override)); |
| MOCK_METHOD((void), StopSession, (), (override)); |
| MOCK_METHOD((void), ConfirmStreamingRequest, (), (override)); |
| MOCK_METHOD((void), CancelStreamingRequest, (), (override)); |
| MOCK_METHOD((size_t), Read, (uint8_t * p_buf, uint32_t len)); |
| }; |
| |
| class MockLeAudioClientInterfaceSource : public LeAudioClientInterface::Source { |
| public: |
| MOCK_METHOD((void), Cleanup, (), (override)); |
| MOCK_METHOD((void), SetPcmParameters, |
| (const LeAudioClientInterface::PcmParameters& params), |
| (override)); |
| MOCK_METHOD((void), SetRemoteDelay, (uint16_t delay_report_ms), (override)); |
| MOCK_METHOD((void), StartSession, (), (override)); |
| MOCK_METHOD((void), StopSession, (), (override)); |
| MOCK_METHOD((void), ConfirmStreamingRequest, (), (override)); |
| MOCK_METHOD((void), CancelStreamingRequest, (), (override)); |
| MOCK_METHOD((size_t), Write, (const uint8_t* p_buf, uint32_t len)); |
| }; |
| |
| class MockLeAudioClientInterface : public LeAudioClientInterface { |
| public: |
| MockLeAudioClientInterface() = default; |
| ~MockLeAudioClientInterface() = default; |
| |
| MOCK_METHOD((Sink*), GetSink, |
| (bluetooth::audio::le_audio::StreamCallbacks stream_cb, |
| bluetooth::common::MessageLoopThread* message_loop)); |
| MOCK_METHOD((Source*), GetSource, |
| (bluetooth::audio::le_audio::StreamCallbacks stream_cb, |
| bluetooth::common::MessageLoopThread* message_loop)); |
| }; |
| |
| LeAudioClientInterface* mockInterface; |
| |
| namespace bluetooth { |
| namespace audio { |
| namespace le_audio { |
| MockLeAudioClientInterface* interface_mock; |
| MockLeAudioClientInterfaceSink* sink_mock; |
| MockLeAudioClientInterfaceSource* source_mock; |
| |
| LeAudioClientInterface* LeAudioClientInterface::Get() { return interface_mock; } |
| |
| LeAudioClientInterface::Sink* LeAudioClientInterface::GetSink( |
| StreamCallbacks stream_cb, |
| bluetooth::common::MessageLoopThread* message_loop) { |
| return interface_mock->GetSink(stream_cb, message_loop); |
| } |
| |
| LeAudioClientInterface::Source* LeAudioClientInterface::GetSource( |
| StreamCallbacks stream_cb, |
| bluetooth::common::MessageLoopThread* message_loop) { |
| return interface_mock->GetSource(stream_cb, message_loop); |
| } |
| |
| bool LeAudioClientInterface::ReleaseSink(LeAudioClientInterface::Sink* sink) { |
| return true; |
| } |
| bool LeAudioClientInterface::ReleaseSource( |
| LeAudioClientInterface::Source* source) { |
| return true; |
| } |
| |
| void LeAudioClientInterface::Sink::Cleanup() {} |
| void LeAudioClientInterface::Sink::SetPcmParameters( |
| const PcmParameters& params) {} |
| void LeAudioClientInterface::Sink::SetRemoteDelay(uint16_t delay_report_ms) {} |
| void LeAudioClientInterface::Sink::StartSession() {} |
| void LeAudioClientInterface::Sink::StopSession() {} |
| void LeAudioClientInterface::Sink::ConfirmStreamingRequest(){}; |
| void LeAudioClientInterface::Sink::CancelStreamingRequest(){}; |
| |
| void LeAudioClientInterface::Source::Cleanup() {} |
| void LeAudioClientInterface::Source::SetPcmParameters( |
| const PcmParameters& params) {} |
| void LeAudioClientInterface::Source::SetRemoteDelay(uint16_t delay_report_ms) {} |
| void LeAudioClientInterface::Source::StartSession() {} |
| void LeAudioClientInterface::Source::StopSession() {} |
| void LeAudioClientInterface::Source::ConfirmStreamingRequest(){}; |
| void LeAudioClientInterface::Source::CancelStreamingRequest(){}; |
| |
| size_t LeAudioClientInterface::Source::Write(const uint8_t* p_buf, |
| uint32_t len) { |
| return source_mock->Write(p_buf, len); |
| } |
| |
| size_t LeAudioClientInterface::Sink::Read(uint8_t* p_buf, uint32_t len) { |
| return sink_mock->Read(p_buf, len); |
| } |
| } // namespace le_audio |
| } // namespace audio |
| } // namespace bluetooth |
| |
| class MockLeAudioClientAudioSinkEventReceiver |
| : public LeAudioClientAudioSinkReceiver { |
| public: |
| MOCK_METHOD((void), OnAudioDataReady, (const std::vector<uint8_t>& data), |
| (override)); |
| MOCK_METHOD((void), OnAudioSuspend, (std::promise<void> do_suspend_promise), |
| (override)); |
| MOCK_METHOD((void), OnAudioResume, (std::promise<void> do_resume_promise), |
| (override)); |
| MOCK_METHOD((void), OnAudioMetadataUpdate, |
| (std::promise<void> do_update_metadata_promise, |
| const source_metadata_t& source_metadata), |
| (override)); |
| }; |
| |
| class MockLeAudioClientAudioSourceEventReceiver |
| : public LeAudioClientAudioSourceReceiver { |
| public: |
| MOCK_METHOD((void), OnAudioSuspend, (std::promise<void> do_suspend_promise), |
| (override)); |
| MOCK_METHOD((void), OnAudioResume, (std::promise<void> do_resume_promise), |
| (override)); |
| }; |
| |
| class LeAudioClientAudioTest : public ::testing::Test { |
| public: |
| void SetUp(void) override { |
| init_message_loop_thread(); |
| bluetooth::audio::le_audio::interface_mock = &mock_client_interface_; |
| bluetooth::audio::le_audio::sink_mock = &mock_client_interface_sink_; |
| bluetooth::audio::le_audio::source_mock = &mock_client_interface_source_; |
| |
| // Init sink Audio HAL mock |
| is_sink_acquired = false; |
| hal_sink_stream_cb = {.on_suspend_ = nullptr, .on_resume_ = nullptr}; |
| |
| ON_CALL(mock_client_interface_, GetSink(_, _)) |
| .WillByDefault(DoAll(SaveArg<0>(&hal_sink_stream_cb), |
| Assign(&is_sink_acquired, true), |
| Return(bluetooth::audio::le_audio::sink_mock))); |
| ON_CALL(mock_client_interface_sink_, Cleanup()) |
| .WillByDefault(Assign(&is_sink_acquired, false)); |
| |
| // Init source Audio HAL mock |
| is_source_acquired = false; |
| hal_source_stream_cb = {.on_suspend_ = nullptr, .on_resume_ = nullptr}; |
| |
| ON_CALL(mock_client_interface_, GetSource(_, _)) |
| .WillByDefault(DoAll(SaveArg<0>(&hal_source_stream_cb), |
| Assign(&is_source_acquired, true), |
| Return(bluetooth::audio::le_audio::source_mock))); |
| ON_CALL(mock_client_interface_source_, Cleanup()) |
| .WillByDefault(Assign(&is_source_acquired, false)); |
| } |
| |
| void AcquireAudioSink(void) { |
| audio_sink_instance_ = LeAudioClientAudioSink::Acquire(); |
| } |
| |
| void ReleaseAudioSink(void) { |
| LeAudioClientAudioSink::Release(audio_sink_instance_); |
| audio_sink_instance_ = nullptr; |
| } |
| |
| void AcquireAudioSource(void) { |
| audio_source_instance_ = LeAudioClientAudioSource::Acquire(); |
| } |
| |
| void ReleaseAudioSource(void) { |
| LeAudioClientAudioSource::Release(audio_source_instance_); |
| audio_source_instance_ = nullptr; |
| } |
| |
| void TearDown(void) override { |
| /* We have to call Cleanup to tidy up some static variables. |
| * If on the HAL end Source is running it means we are running the Sink |
| * on our end, and vice versa. |
| */ |
| if (is_source_acquired == true) ReleaseAudioSink(); |
| if (is_sink_acquired == true) ReleaseAudioSource(); |
| |
| cleanup_message_loop_thread(); |
| |
| bluetooth::audio::le_audio::sink_mock = nullptr; |
| bluetooth::audio::le_audio::source_mock = nullptr; |
| } |
| |
| MockLeAudioClientInterface mock_client_interface_; |
| MockLeAudioClientInterfaceSink mock_client_interface_sink_; |
| MockLeAudioClientInterfaceSource mock_client_interface_source_; |
| |
| MockLeAudioClientAudioSinkEventReceiver mock_hal_sink_event_receiver_; |
| MockLeAudioClientAudioSourceEventReceiver mock_hal_source_event_receiver_; |
| |
| bool is_source_acquired = false; |
| bool is_sink_acquired = false; |
| const void* audio_sink_instance_ = nullptr; |
| const void* audio_source_instance_ = nullptr; |
| |
| bluetooth::audio::le_audio::StreamCallbacks hal_source_stream_cb; |
| bluetooth::audio::le_audio::StreamCallbacks hal_sink_stream_cb; |
| |
| const LeAudioCodecConfiguration default_codec_conf{ |
| .num_channels = LeAudioCodecConfiguration::kChannelNumberMono, |
| .sample_rate = LeAudioCodecConfiguration::kSampleRate44100, |
| .bits_per_sample = LeAudioCodecConfiguration::kBitsPerSample24, |
| .data_interval_us = LeAudioCodecConfiguration::kInterval10000Us, |
| }; |
| }; |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkInitializeCleanup) { |
| EXPECT_CALL(mock_client_interface_, GetSource(_, _)) |
| .WillOnce(DoAll(Assign(&is_source_acquired, true), |
| Return(bluetooth::audio::le_audio::source_mock))); |
| AcquireAudioSink(); |
| |
| EXPECT_CALL(mock_client_interface_source_, Cleanup()) |
| .WillOnce(Assign(&is_source_acquired, false)); |
| ReleaseAudioSink(); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceInitializeCleanup) { |
| EXPECT_CALL(mock_client_interface_, GetSink(_, _)) |
| .WillOnce(DoAll(Assign(&is_sink_acquired, true), |
| Return(bluetooth::audio::le_audio::sink_mock))); |
| AcquireAudioSource(); |
| |
| EXPECT_CALL(mock_client_interface_sink_, Cleanup()) |
| .WillOnce(Assign(&is_sink_acquired, false)); |
| ReleaseAudioSource(); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkStartStop) { |
| LeAudioClientInterface::PcmParameters params; |
| EXPECT_CALL(mock_client_interface_source_, SetPcmParameters(_)) |
| .Times(1) |
| .WillOnce(SaveArg<0>(¶ms)); |
| EXPECT_CALL(mock_client_interface_source_, StartSession()).Times(1); |
| |
| AcquireAudioSink(); |
| ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf, |
| &mock_hal_source_event_receiver_)); |
| |
| ASSERT_EQ(params.channels_count, |
| bluetooth::audio::le_audio::kChannelNumberMono); |
| ASSERT_EQ(params.sample_rate, bluetooth::audio::le_audio::kSampleRate44100); |
| ASSERT_EQ(params.bits_per_sample, |
| bluetooth::audio::le_audio::kBitsPerSample24); |
| ASSERT_EQ(params.data_interval_us, 10000u); |
| |
| EXPECT_CALL(mock_client_interface_source_, StopSession()).Times(1); |
| |
| LeAudioClientAudioSink::Stop(); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceStartStop) { |
| LeAudioClientInterface::PcmParameters params; |
| EXPECT_CALL(mock_client_interface_sink_, SetPcmParameters(_)) |
| .Times(1) |
| .WillOnce(SaveArg<0>(¶ms)); |
| EXPECT_CALL(mock_client_interface_sink_, StartSession()).Times(1); |
| |
| AcquireAudioSource(); |
| ASSERT_TRUE(LeAudioClientAudioSource::Start(default_codec_conf, |
| &mock_hal_sink_event_receiver_)); |
| |
| ASSERT_EQ(params.channels_count, |
| bluetooth::audio::le_audio::kChannelNumberMono); |
| ASSERT_EQ(params.sample_rate, bluetooth::audio::le_audio::kSampleRate44100); |
| ASSERT_EQ(params.bits_per_sample, |
| bluetooth::audio::le_audio::kBitsPerSample24); |
| ASSERT_EQ(params.data_interval_us, 10000u); |
| |
| EXPECT_CALL(mock_client_interface_sink_, StopSession()).Times(1); |
| |
| LeAudioClientAudioSource::Stop(); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkSendData) { |
| AcquireAudioSink(); |
| ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf, |
| &mock_hal_source_event_receiver_)); |
| |
| const uint8_t* exp_p = nullptr; |
| uint32_t exp_len = 0; |
| uint8_t input_buf[] = { |
| 0x02, |
| 0x03, |
| 0x05, |
| 0x19, |
| }; |
| ON_CALL(mock_client_interface_source_, Write(_, _)) |
| .WillByDefault(DoAll(SaveArg<0>(&exp_p), SaveArg<1>(&exp_len), |
| ReturnPointee(&exp_len))); |
| |
| ASSERT_EQ(LeAudioClientAudioSink::SendData(input_buf, sizeof(input_buf)), |
| sizeof(input_buf)); |
| ASSERT_EQ(exp_len, sizeof(input_buf)); |
| ASSERT_EQ(exp_p, input_buf); |
| |
| LeAudioClientAudioSource::Stop(); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkSuspend) { |
| AcquireAudioSink(); |
| ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf, |
| &mock_hal_source_event_receiver_)); |
| |
| ASSERT_NE(hal_source_stream_cb.on_suspend_, nullptr); |
| |
| /* Expect LeAudio registered event listener to get called when HAL calls the |
| * client_audio's internal suspend callback. |
| */ |
| EXPECT_CALL(mock_hal_source_event_receiver_, OnAudioSuspend(_)).Times(1); |
| ASSERT_TRUE(hal_source_stream_cb.on_suspend_()); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceSuspend) { |
| AcquireAudioSource(); |
| ASSERT_TRUE(LeAudioClientAudioSource::Start(default_codec_conf, |
| &mock_hal_sink_event_receiver_)); |
| |
| ASSERT_NE(hal_sink_stream_cb.on_suspend_, nullptr); |
| |
| /* Expect LeAudio registered event listener to get called when HAL calls the |
| * client_audio's internal suspend callback. |
| */ |
| EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioSuspend(_)).Times(1); |
| ASSERT_TRUE(hal_sink_stream_cb.on_suspend_()); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSinkResume) { |
| AcquireAudioSink(); |
| ASSERT_TRUE(LeAudioClientAudioSink::Start(default_codec_conf, |
| &mock_hal_source_event_receiver_)); |
| |
| ASSERT_NE(hal_source_stream_cb.on_resume_, nullptr); |
| |
| /* Expect LeAudio registered event listener to get called when HAL calls the |
| * client_audio's internal resume callback. |
| */ |
| EXPECT_CALL(mock_hal_source_event_receiver_, OnAudioResume(_)).Times(1); |
| bool start_media_task = false; |
| ASSERT_TRUE(hal_source_stream_cb.on_resume_(start_media_task)); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, |
| testLeAudioClientAudioSourceResumeStartSourceTask) { |
| const LeAudioCodecConfiguration codec_conf{ |
| .num_channels = LeAudioCodecConfiguration::kChannelNumberStereo, |
| .sample_rate = LeAudioCodecConfiguration::kSampleRate16000, |
| .bits_per_sample = LeAudioCodecConfiguration::kBitsPerSample24, |
| .data_interval_us = LeAudioCodecConfiguration::kInterval10000Us, |
| }; |
| AcquireAudioSource(); |
| ASSERT_TRUE(LeAudioClientAudioSource::Start(codec_conf, |
| &mock_hal_sink_event_receiver_)); |
| |
| std::chrono::time_point<std::chrono::system_clock> resumed_ts; |
| std::chrono::time_point<std::chrono::system_clock> executed_ts; |
| std::promise<void> promise; |
| auto future = promise.get_future(); |
| |
| uint32_t calculated_bytes_per_tick = 0; |
| EXPECT_CALL(mock_client_interface_sink_, Read(_, _)) |
| .Times(AtLeast(1)) |
| .WillOnce(Invoke([&](uint8_t* p_buf, uint32_t len) -> uint32_t { |
| executed_ts = std::chrono::system_clock::now(); |
| calculated_bytes_per_tick = len; |
| |
| // fake some data from audio framework |
| for (uint32_t i = 0u; i < len; ++i) { |
| p_buf[i] = i; |
| } |
| |
| // Return exactly as much data as requested |
| promise.set_value(); |
| return len; |
| })); |
| |
| std::promise<void> data_promise; |
| auto data_future = data_promise.get_future(); |
| |
| /* Expect this callback to be called to Client by the HAL glue layer */ |
| std::vector<uint8_t> media_data_to_send; |
| EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioDataReady(_)) |
| .WillOnce(Invoke([&](const std::vector<uint8_t>& data) -> void { |
| media_data_to_send = std::move(data); |
| data_promise.set_value(); |
| })); |
| |
| /* Expect LeAudio registered event listener to get called when HAL calls the |
| * client_audio's internal resume callback. |
| */ |
| ASSERT_NE(hal_sink_stream_cb.on_resume_, nullptr); |
| EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioResume(_)).Times(1); |
| resumed_ts = std::chrono::system_clock::now(); |
| bool start_media_task = true; |
| ASSERT_TRUE(hal_sink_stream_cb.on_resume_(start_media_task)); |
| LeAudioClientAudioSource::ConfirmStreamingRequest(); |
| |
| ASSERT_EQ(future.wait_for(std::chrono::seconds(1)), |
| std::future_status::ready); |
| |
| ASSERT_EQ(data_future.wait_for(std::chrono::seconds(1)), |
| std::future_status::ready); |
| |
| // Check agains expected payload size |
| const uint32_t channel_bytes_per_sample = (24 /*bps*/ / 8); |
| const uint32_t channel_bytes_per_10ms_at_16000Hz = |
| ((10ms).count() * channel_bytes_per_sample * 16000 /*Hz*/) / |
| (1000ms).count(); |
| |
| // Expect 2 channel (stereo) data |
| ASSERT_EQ(calculated_bytes_per_tick, 2 * channel_bytes_per_10ms_at_16000Hz); |
| |
| // Verify callback call interval for the requested 10ms (+2ms error margin) |
| auto delta = std::chrono::duration_cast<std::chrono::milliseconds>( |
| executed_ts - resumed_ts); |
| EXPECT_TRUE((delta >= 10ms) && (delta <= 12ms)); |
| |
| // Verify if we got just right amount of data in the callback call |
| ASSERT_EQ(media_data_to_send.size(), calculated_bytes_per_tick); |
| } |
| |
| TEST_F(LeAudioClientAudioTest, testLeAudioClientAudioSourceResume) { |
| AcquireAudioSource(); |
| ASSERT_TRUE(LeAudioClientAudioSource::Start(default_codec_conf, |
| &mock_hal_sink_event_receiver_)); |
| |
| ASSERT_NE(hal_sink_stream_cb.on_resume_, nullptr); |
| |
| /* Expect LeAudio registered event listener to get called when HAL calls the |
| * client_audio's internal resume callback. |
| */ |
| EXPECT_CALL(mock_hal_sink_event_receiver_, OnAudioResume(_)).Times(1); |
| bool start_media_task = false; |
| ASSERT_TRUE(hal_sink_stream_cb.on_resume_(start_media_task)); |
| } |