blob: caec95f0d3c1c5a3c6f271c63f773df1247814ce [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/media/audio/drivers/tests/position_test.h"
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/time.h>
#include <zircon/compiler.h>
#include <cstring>
#include <iomanip>
#include <sstream>
#include <gtest/gtest.h>
namespace media::audio::drivers::test {
// Start recording position/timestamps, set notifications to request another, and request the first
void PositionTest::EnablePositionNotifications() {
record_position_info_ = true;
request_next_position_notification_ = true;
RequestPositionNotification();
}
void PositionTest::RequestPositionNotification() {
ring_buffer()->WatchClockRecoveryPositionInfo(
[this](fuchsia::hardware::audio::RingBufferPositionInfo position_info) {
PositionNotificationCallback(position_info);
});
}
void PositionTest::PositionNotificationCallback(
fuchsia::hardware::audio::RingBufferPositionInfo position_info) {
zx::time now = zx::clock::get_monotonic();
zx::time position_time = zx::time(position_info.timestamp);
AdminTest::PositionNotificationCallback(position_info);
EXPECT_TRUE(position_notification_is_expected_);
EXPECT_LT(start_time(), now);
EXPECT_LT(position_time, now);
if (position_notification_count_) {
EXPECT_GT(position_time, start_time());
EXPECT_GT(position_time, zx::time(saved_position_.timestamp));
} else {
EXPECT_GE(position_time, start_time());
}
EXPECT_LT(position_info.position, ring_buffer_frames() * frame_size());
// If we want to continue to chain of position notifications, request the next one.
if (request_next_position_notification_) {
RequestPositionNotification();
}
// If we don't need to update our running stats on position, exit now.
if (!record_position_info_) {
return;
}
if constexpr (kLogDetailedPositionInfo) {
notifications_.push_back({
.position = position_info.position,
.timestamp = position_info.timestamp,
.arrival_time = now.get(),
});
}
++position_notification_count_;
// The `.position` reported by a position notification is a byte position within the ring buffer.
// For long-running byte position, we could maintain a `running_position_` (a uint64_t initialized
// to 0 upon Start()) that is updated by the algorithm below. This uses `.position` as a ring
// "modulo" and adds the buffer size when it detects rollover, so it does not account for "sparse"
// position notifications that occur more than a ring-buffer apart. For this technique to be
// accurate, the ring-buffer client must (1) set position notification frequency to 2/buffer or
// greater and (2) register for notifications actively enough that the position advanced between
// notifications never exceeds the ring-buffer size.
// running_position_ += position_info.position;
// running_position_ -= saved_position_.position;
// if (position_info.position <= saved_position_.position) {
// running_position_ += (ring_buffer_frames() * frame_size());
// }
saved_position_.timestamp = position_info.timestamp;
saved_position_.position = position_info.position;
}
// Wait for the specified number of position notifications, then stop recording timestamp data.
// ...but don't DisablePositionNotifications, in case later notifications surface other issues.
void PositionTest::ExpectPositionNotifyCount(uint32_t count) {
RunLoopUntil([this, count]() { return position_notification_count_ >= count || HasFailure(); });
record_position_info_ = false;
}
// What timestamp do we expect, for the final notification received? We know how many
// notifications we've received; we'll multiply this by the per-notification time duration.
void PositionTest::ValidatePositionInfo() {
zx::duration notification_timestamp = zx::time(saved_position_.timestamp) - start_time();
zx::duration arrived_timestamp = zx::clock::get_monotonic() - start_time();
ASSERT_GT(position_notification_count_, 0u) << "No position notifications received";
ASSERT_GT(ring_buffer_pcm_format().frame_rate, 0u) << "Frame rate cannot be zero";
// nsec/notification = nsec/sec * sec/frames * frames/ring * ring/notification
int64_t nsec_per_notif = zx::sec(1).to_nsecs() * ring_buffer_frames();
nsec_per_notif /=
static_cast<int64_t>(ring_buffer_pcm_format().frame_rate * notifications_per_ring());
// Upon enabling notifications, our first notification might arrive immediately. Thus, the average
// number of notification periods elapsed is (position_notification_count_ - 0.5).
auto expected_timestamp =
zx::duration(position_notification_count_ * nsec_per_notif - nsec_per_notif / 2);
// Delivery-time requirements for pos notifications are loose; include a tolerance of +/-2 notifs.
auto timestamp_tolerance = zx::duration(nsec_per_notif * 2);
auto min_allowed_timestamp = expected_timestamp - timestamp_tolerance;
auto max_allowed_timestamp = expected_timestamp + timestamp_tolerance;
if (notification_timestamp < min_allowed_timestamp ||
notification_timestamp > max_allowed_timestamp || arrived_timestamp < min_allowed_timestamp) {
std::ostringstream timestamps;
timestamps << "Expected [ min " << min_allowed_timestamp.to_nsecs() << ", ideal "
<< expected_timestamp.to_nsecs() << ", max " << max_allowed_timestamp.to_nsecs()
<< " ], actual " << notification_timestamp.to_nsecs() << " (arrived "
<< arrived_timestamp.to_nsecs() << ")";
EXPECT_GT(notification_timestamp.to_nsecs(), min_allowed_timestamp.to_nsecs())
<< timestamps.str() << "- notifications occurring too rapidly.";
EXPECT_LT(notification_timestamp.to_nsecs(), max_allowed_timestamp.to_nsecs())
<< timestamps.str() << " - notifications occurring too slowly.";
// Also validate when the notification was actually received (not just the timestamp).
EXPECT_GT(arrived_timestamp.to_nsecs(), min_allowed_timestamp.to_nsecs())
<< timestamps.str() << " - notification arrived too early.";
if constexpr (kLogDetailedPositionInfo) {
auto ring_buffer_bytes = ring_buffer_frames() * frame_size();
FX_LOGS(INFO) << "Start time " << start_time().get() << ", RingBuffer "
<< ring_buffer_frames() << " frames (" << ring_buffer_bytes << " bytes), "
<< ring_buffer_pcm_format().frame_rate << " Hz, " << nsec_per_notif
<< " nsec/notif, " << nsec_per_notif * notifications_per_ring()
<< " nsec/ring.";
FX_LOGS(INFO) << " Notif Position___Delta" << " Timestamp_____Delta "
<< " Arrival_____Delta";
for (auto idx = 0u; idx < notifications_.size(); ++idx) {
uint32_t position_delta = (ring_buffer_bytes + notifications_[idx].position -
(idx == 0u ? 0 : notifications_[idx - 1].position)) %
ring_buffer_bytes;
int64_t timestamp_delta =
notifications_[idx].timestamp -
(idx == 0 ? start_time().get() : notifications_[idx - 1].timestamp);
int64_t arrival_delta =
notifications_[idx].arrival_time -
(idx == 0 ? start_time().get() : notifications_[idx - 1].arrival_time);
FX_LOGS(INFO) << " [ " << std::setw(2) << idx << " ]" //
<< std::setw(12) << notifications_[idx].position //
<< std::setw(8) << position_delta //
<< std::setw(21) << notifications_[idx].timestamp //
<< std::setw(12) << timestamp_delta //
<< std::setw(21) << notifications_[idx].arrival_time //
<< std::setw(12) << arrival_delta;
}
}
}
}
#define DEFINE_POSITION_TEST_CLASS(CLASS_NAME, CODE) \
class CLASS_NAME : public PositionTest { \
public: \
explicit CLASS_NAME(const DeviceEntry& dev_entry) : PositionTest(dev_entry) {} \
void TestBody() override { CODE } \
}
//
// Test cases that target various position notification behaviors.
//
// Any case not ending in disconnect/error should WaitForError, in case the channel disconnects.
// Verify position notifications at fast rate (64/sec: 32 notifs/ring in a 0.5-second buffer).
DEFINE_POSITION_TEST_CLASS(PositionNotifyFast, {
constexpr auto kNotifsPerRingBuffer = 32u;
ASSERT_NO_FAILURE_OR_SKIP(RetrieveRingBufferFormats());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferChannelWithMaxFormat());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferProperties());
// Request a 0.5-second ring-buffer.
ASSERT_NO_FAILURE_OR_SKIP(
RequestBuffer(ring_buffer_pcm_format().frame_rate / 2, kNotifsPerRingBuffer));
ASSERT_NO_FAILURE_OR_SKIP(EnablePositionNotifications());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferStart());
// After numerous notifications (in this case, twice around the ring), stop updating position info
// (but let notifications continue). Ensure that the rate of advance is within acceptable range.
ExpectPositionNotifyCount(kNotifsPerRingBuffer * 2);
ValidatePositionInfo();
WaitForError();
});
// Verify position notifications at slow rate (1/sec: 2 notifs/ring in a 2-second buffer).
DEFINE_POSITION_TEST_CLASS(PositionNotifySlow, {
constexpr auto kNotifsPerRingBuffer = 2u;
ASSERT_NO_FAILURE_OR_SKIP(RetrieveRingBufferFormats());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferChannelWithMinFormat());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferProperties());
// Request a 2-second ring-buffer.
ASSERT_NO_FAILURE_OR_SKIP(
RequestBuffer(ring_buffer_pcm_format().frame_rate * 2, kNotifsPerRingBuffer));
ASSERT_NO_FAILURE_OR_SKIP(EnablePositionNotifications());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferStart());
// After numerous notifications (in this case, twice around the ring), stop updating position info
// (but let notifications continue). Ensure that the rate of advance is within acceptable range.
ExpectPositionNotifyCount(kNotifsPerRingBuffer * 2);
ValidatePositionInfo();
// Wait longer than the default (100 ms), as notifications are less frequent than that.
zx::duration time_per_notif = zx::sec(1) * ring_buffer_frames() /
ring_buffer_pcm_format().frame_rate / kNotifsPerRingBuffer;
WaitForError(time_per_notif);
});
// Verify that NO position notifications arrive after Stop is called.
DEFINE_POSITION_TEST_CLASS(NoPositionNotifyAfterStop, {
constexpr auto kNotifsPerRingBuffer = 32u;
ASSERT_NO_FAILURE_OR_SKIP(RetrieveRingBufferFormats());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferChannelWithMaxFormat());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferProperties());
// Set notifications to be rapid, with a small ring buffer and a large notifications-per-buffer.
// If the device supports 192 kHz and the driver supports a ring this small, the buffer will be
// 32 ms and notifications should arrive every 1 msec!
ASSERT_NO_FAILURE_OR_SKIP(RequestBuffer(6144, kNotifsPerRingBuffer));
ASSERT_NO_FAILURE_OR_SKIP(EnablePositionNotifications());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferStart());
// After just a few position notifications, stop the ring buffer. From the Stop callback itself,
// register a position callback that will fail the test if any further notification occurs.
ASSERT_NO_FAILURE_OR_SKIP(ExpectPositionNotifyCount(3u));
RequestRingBufferStopAndExpectNoPositionNotifications();
WaitForError();
});
// Verify no position notifications arrive if notifications_per_ring is 0.
DEFINE_POSITION_TEST_CLASS(PositionNotifyNone, {
ASSERT_NO_FAILURE_OR_SKIP(RetrieveRingBufferFormats());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferChannelWithMaxFormat());
ASSERT_NO_FAILURE_OR_SKIP(RequestRingBufferProperties());
ASSERT_NO_FAILURE_OR_SKIP(RequestBuffer(8000, 0));
ASSERT_NO_FAILURE_OR_SKIP(DisallowPositionNotifications());
ASSERT_NO_FAILURE_OR_SKIP(EnablePositionNotifications());
RequestRingBufferStart();
WaitForError();
});
// Register separate test case instances for each enumerated device
//
// See googletest/docs/advanced.md for details
#define REGISTER_POSITION_TEST(CLASS_NAME, DEVICE) \
testing::RegisterTest("PositionTest", TestNameForEntry(#CLASS_NAME, DEVICE).c_str(), nullptr, \
DevNameForEntry(DEVICE).c_str(), __FILE__, __LINE__, \
[&]() -> PositionTest* { return new CLASS_NAME(DEVICE); })
#define REGISTER_DISABLED_POSITION_TEST(CLASS_NAME, DEVICE) \
testing::RegisterTest( \
"PositionTest", (std::string("DISABLED_") + TestNameForEntry(#CLASS_NAME, DEVICE)).c_str(), \
nullptr, DevNameForEntry(DEVICE).c_str(), __FILE__, __LINE__, \
[&]() -> PositionTest* { return new CLASS_NAME(DEVICE); })
void RegisterPositionTestsForDevice(const DeviceEntry& device_entry,
bool expect_audio_core_not_connected,
bool enable_position_tests) {
if (device_entry.isCodec()) {
return;
}
// If audio_core is connected to the audio driver, admin tests will fail.
// We test a hermetic instance of the A2DP driver, so audio_core is never connected.
if (device_entry.isA2DP() || expect_audio_core_not_connected) {
if (enable_position_tests) {
REGISTER_POSITION_TEST(PositionNotifySlow, device_entry);
REGISTER_POSITION_TEST(PositionNotifyFast, device_entry);
REGISTER_POSITION_TEST(NoPositionNotifyAfterStop, device_entry);
REGISTER_POSITION_TEST(PositionNotifyNone, device_entry);
}
}
}
} // namespace media::audio::drivers::test