From 5b9d0c70c23bc2b6270b47702e9f558af30be131 Mon Sep 17 00:00:00 2001 From: Alessio Bazzica Date: Wed, 16 Jun 2021 14:55:40 +0200 Subject: [PATCH] AGC1 add clipping predictor evaluator Observes clipping predictions and detections and computes evaluation metrics for the predictor. Bug: webrtc:12774 Change-Id: I83f5942a3b6491de288510f2200f2f5c0e099bf2 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/221619 Reviewed-by: Hanna Silen Commit-Queue: Alessio Bazzica Cr-Commit-Position: refs/heads/master@{#34305} --- modules/audio_processing/agc/BUILD.gn | 18 + .../agc/clipping_predictor_evaluator.cc | 175 ++++++ .../agc/clipping_predictor_evaluator.h | 102 ++++ .../clipping_predictor_evaluator_unittest.cc | 568 ++++++++++++++++++ 4 files changed, 863 insertions(+) create mode 100644 modules/audio_processing/agc/clipping_predictor_evaluator.cc create mode 100644 modules/audio_processing/agc/clipping_predictor_evaluator.h create mode 100644 modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc diff --git a/modules/audio_processing/agc/BUILD.gn b/modules/audio_processing/agc/BUILD.gn index 3b2b205385..ca6d9bdb0f 100644 --- a/modules/audio_processing/agc/BUILD.gn +++ b/modules/audio_processing/agc/BUILD.gn @@ -58,6 +58,18 @@ rtc_library("clipping_predictor") { absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ] } +rtc_library("clipping_predictor_evaluator") { + sources = [ + "clipping_predictor_evaluator.cc", + "clipping_predictor_evaluator.h", + ] + deps = [ + "../../../rtc_base:checks", + "../../../rtc_base:logging", + ] + absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ] +} + rtc_library("clipping_predictor_level_buffer") { sources = [ "clipping_predictor_level_buffer.cc", @@ -66,6 +78,7 @@ rtc_library("clipping_predictor_level_buffer") { deps = [ "../../../rtc_base:checks", "../../../rtc_base:logging", + "../../../rtc_base:rtc_base_approved", ] absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ] } @@ -128,6 +141,7 @@ if (rtc_include_tests) { testonly = true sources = [ "agc_manager_direct_unittest.cc", + "clipping_predictor_evaluator_unittest.cc", "clipping_predictor_level_buffer_unittest.cc", "clipping_predictor_unittest.cc", "loudness_histogram_unittest.cc", @@ -138,15 +152,19 @@ if (rtc_include_tests) { deps = [ ":agc", ":clipping_predictor", + ":clipping_predictor_evaluator", ":clipping_predictor_level_buffer", ":gain_control_interface", ":level_estimation", "..:mocks", "../../../rtc_base:checks", + "../../../rtc_base:rtc_base_approved", + "../../../rtc_base:safe_conversions", "../../../test:field_trial", "../../../test:fileutils", "../../../test:test_support", "//testing/gtest", ] + absl_deps = [ "//third_party/abseil-cpp/absl/types:optional" ] } } diff --git a/modules/audio_processing/agc/clipping_predictor_evaluator.cc b/modules/audio_processing/agc/clipping_predictor_evaluator.cc new file mode 100644 index 0000000000..2a4ea922cf --- /dev/null +++ b/modules/audio_processing/agc/clipping_predictor_evaluator.cc @@ -0,0 +1,175 @@ +/* + * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "modules/audio_processing/agc/clipping_predictor_evaluator.h" + +#include + +#include "rtc_base/checks.h" +#include "rtc_base/logging.h" + +namespace webrtc { +namespace { + +// Returns the index of the oldest item in the ring buffer for a non-empty +// ring buffer with give `size`, `tail` index and `capacity`. +int OldestExpectedDetectionIndex(int size, int tail, int capacity) { + RTC_DCHECK_GT(size, 0); + return tail - size + (tail < size ? capacity : 0); +} + +} // namespace + +ClippingPredictorEvaluator::ClippingPredictorEvaluator(int history_size) + : history_size_(history_size), + ring_buffer_capacity_(history_size + 1), + ring_buffer_(ring_buffer_capacity_), + true_positives_(0), + true_negatives_(0), + false_positives_(0), + false_negatives_(0) { + RTC_DCHECK_GT(history_size_, 0); + Reset(); +} + +ClippingPredictorEvaluator::~ClippingPredictorEvaluator() = default; + +absl::optional ClippingPredictorEvaluator::Observe( + bool clipping_detected, + bool clipping_predicted) { + RTC_DCHECK_GE(ring_buffer_size_, 0); + RTC_DCHECK_LE(ring_buffer_size_, ring_buffer_capacity_); + RTC_DCHECK_GE(ring_buffer_tail_, 0); + RTC_DCHECK_LT(ring_buffer_tail_, ring_buffer_capacity_); + + DecreaseTimesToLive(); + if (clipping_predicted) { + // TODO(bugs.webrtc.org/12874): Use designated initializers one fixed. + Push(/*expected_detection=*/{/*ttl=*/history_size_, /*detected=*/false}); + } + // Clipping is expected if there are expected detections regardless of + // whether all the expected detections have been previously matched - i.e., + // `ExpectedDetection::detected` is true. + const bool clipping_expected = ring_buffer_size_ > 0; + + absl::optional prediction_interval; + if (clipping_expected && clipping_detected) { + prediction_interval = FindEarliestPredictionInterval(); + // Add a true positive for each unexpired expected detection. + const int num_modified_items = MarkExpectedDetectionAsDetected(); + true_positives_ += num_modified_items; + RTC_DCHECK(prediction_interval.has_value() || num_modified_items == 0); + RTC_DCHECK(!prediction_interval.has_value() || num_modified_items > 0); + } else if (clipping_expected && !clipping_detected) { + // Add a false positive if there is one expected detection that has expired + // and that has never been matched before. Note that there is at most one + // unmatched expired detection. + if (HasExpiredUnmatchedExpectedDetection()) { + false_positives_++; + } + } else if (!clipping_expected && clipping_detected) { + false_negatives_++; + } else { + RTC_DCHECK(!clipping_expected && !clipping_detected); + true_negatives_++; + } + return prediction_interval; +} + +void ClippingPredictorEvaluator::Reset() { + // Empty the ring buffer of expected detections. + ring_buffer_tail_ = 0; + ring_buffer_size_ = 0; +} + +// Cost: O(1). +void ClippingPredictorEvaluator::Push(ExpectedDetection value) { + ring_buffer_[ring_buffer_tail_] = value; + ring_buffer_tail_++; + if (ring_buffer_tail_ == ring_buffer_capacity_) { + ring_buffer_tail_ = 0; + } + ring_buffer_size_ = std::min(ring_buffer_capacity_, ring_buffer_size_ + 1); +} + +// Cost: O(N). +void ClippingPredictorEvaluator::DecreaseTimesToLive() { + bool expired_found = false; + for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_; + ++i) { + int index = i >= 0 ? i : ring_buffer_capacity_ + i; + RTC_DCHECK_GE(index, 0); + RTC_DCHECK_LT(index, ring_buffer_.size()); + RTC_DCHECK_GE(ring_buffer_[index].ttl, 0); + if (ring_buffer_[index].ttl == 0) { + RTC_DCHECK(!expired_found) + << "There must be at most one expired item in the ring buffer."; + expired_found = true; + RTC_DCHECK_EQ(index, OldestExpectedDetectionIndex(ring_buffer_size_, + ring_buffer_tail_, + ring_buffer_capacity_)) + << "The expired item must be the oldest in the ring buffer."; + } + ring_buffer_[index].ttl--; + } + if (expired_found) { + ring_buffer_size_--; + } +} + +// Cost: O(N). +absl::optional ClippingPredictorEvaluator::FindEarliestPredictionInterval() + const { + absl::optional prediction_interval; + for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_; + ++i) { + int index = i >= 0 ? i : ring_buffer_capacity_ + i; + RTC_DCHECK_GE(index, 0); + RTC_DCHECK_LT(index, ring_buffer_.size()); + if (!ring_buffer_[index].detected) { + prediction_interval = std::max(prediction_interval.value_or(0), + history_size_ - ring_buffer_[index].ttl); + } + } + return prediction_interval; +} + +// Cost: O(N). +int ClippingPredictorEvaluator::MarkExpectedDetectionAsDetected() { + int num_modified_items = 0; + for (int i = ring_buffer_tail_ - ring_buffer_size_; i < ring_buffer_tail_; + ++i) { + int index = i >= 0 ? i : ring_buffer_capacity_ + i; + RTC_DCHECK_GE(index, 0); + RTC_DCHECK_LT(index, ring_buffer_.size()); + if (!ring_buffer_[index].detected) { + num_modified_items++; + } + ring_buffer_[index].detected = true; + } + return num_modified_items; +} + +// Cost: O(1). +bool ClippingPredictorEvaluator::HasExpiredUnmatchedExpectedDetection() const { + if (ring_buffer_size_ == 0) { + return false; + } + // If an expired item, that is `ttl` equal to 0, exists, it must be the + // oldest. + const int oldest_index = OldestExpectedDetectionIndex( + ring_buffer_size_, ring_buffer_tail_, ring_buffer_capacity_); + RTC_DCHECK_GE(oldest_index, 0); + RTC_DCHECK_LT(oldest_index, ring_buffer_.size()); + return ring_buffer_[oldest_index].ttl == 0 && + !ring_buffer_[oldest_index].detected; +} + +} // namespace webrtc diff --git a/modules/audio_processing/agc/clipping_predictor_evaluator.h b/modules/audio_processing/agc/clipping_predictor_evaluator.h new file mode 100644 index 0000000000..e76f25d5e1 --- /dev/null +++ b/modules/audio_processing/agc/clipping_predictor_evaluator.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#ifndef MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_ +#define MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_ + +#include + +#include "absl/types/optional.h" + +namespace webrtc { + +// Counts true/false positives/negatives while observing sequences of flag pairs +// that indicate whether clipping has been detected and/or if clipping is +// predicted. When a true positive is found measures the time interval between +// prediction and detection events. +// From the time a prediction is observed and for a period equal to +// `history_size` calls to `Observe()`, one or more detections are expected. If +// the expectation is met, a true positives is added and the time interval +// between the earliest prediction and the detection is recorded; otherwise, +// when the deadline is reached, a false positive is added. Note that one +// detection matches all the expected detections that have not expired - i.e., +// one detection counts as multiple true positives. +// If a detection is observed, but no prediction has been observed over the past +// `history_size` calls to `Observe()`, then a false negative is added; +// otherwise, a true negative is added. +class ClippingPredictorEvaluator { + public: + // Ctor. `history_size` indicates how long to wait for a call to `Observe()` + // having `clipping_detected` set to true from the time clipping is predicted. + explicit ClippingPredictorEvaluator(int history_size); + ClippingPredictorEvaluator(const ClippingPredictorEvaluator&) = delete; + ClippingPredictorEvaluator& operator=(const ClippingPredictorEvaluator&) = + delete; + ~ClippingPredictorEvaluator(); + + // Observes whether clipping has been detected and/or if clipping is + // predicted. When predicted one or more detections are expected in the next + // `history_size_` calls of `Observe()`. When true positives are found returns + // the prediction interval between the earliest prediction and the detection. + absl::optional Observe(bool clipping_detected, bool clipping_predicted); + + // Removes any expectation recently set after a call to `Observe()` having + // `clipping_predicted` set to true. + void Reset(); + + // Metrics getters. + int true_positives() const { return true_positives_; } + int true_negatives() const { return true_negatives_; } + int false_positives() const { return false_positives_; } + int false_negatives() const { return false_negatives_; } + + private: + const int history_size_; + + // State of a detection expected to be observed after a prediction. + struct ExpectedDetection { + // Time to live (TTL); remaining number of `Observe()` calls to match a call + // having `clipping_detected` set to true. + int ttl; + // True if an `Observe()` call having `clipping_detected` set to true has + // been observed. + bool detected; + }; + // Ring buffer of expected detections. + const int ring_buffer_capacity_; + std::vector ring_buffer_; + int ring_buffer_tail_; + int ring_buffer_size_; + + // Pushes `expected_detection` into `expected_matches_ring_buffer_`. + void Push(ExpectedDetection expected_detection); + // Decreased the TTLs in `expected_matches_ring_buffer_` and removes expired + // items. + void DecreaseTimesToLive(); + // Returns the prediction interval for the earliest unexpired expected + // detection if any. + absl::optional FindEarliestPredictionInterval() const; + // Marks all the items in `expected_matches_ring_buffer_` as `detected` and + // returns the number of updated items. + int MarkExpectedDetectionAsDetected(); + // Returns true if `expected_matches_ring_buffer_` has an item having `ttl` + // equal to 0 (expired) and `detected` equal to false (unmatched). + bool HasExpiredUnmatchedExpectedDetection() const; + + // Metrics. + int true_positives_; + int true_negatives_; + int false_positives_; + int false_negatives_; +}; + +} // namespace webrtc + +#endif // MODULES_AUDIO_PROCESSING_AGC_CLIPPING_PREDICTOR_EVALUATOR_H_ diff --git a/modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc b/modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc new file mode 100644 index 0000000000..1eb83eae61 --- /dev/null +++ b/modules/audio_processing/agc/clipping_predictor_evaluator_unittest.cc @@ -0,0 +1,568 @@ +/* + * Copyright (c) 2021 The WebRTC project authors. All Rights Reserved. + * + * Use of this source code is governed by a BSD-style license + * that can be found in the LICENSE file in the root of the source + * tree. An additional intellectual property rights grant can be found + * in the file PATENTS. All contributing project authors may + * be found in the AUTHORS file in the root of the source tree. + */ + +#include "modules/audio_processing/agc/clipping_predictor_evaluator.h" + +#include +#include +#include +#include + +#include "absl/types/optional.h" +#include "rtc_base/numerics/safe_conversions.h" +#include "rtc_base/random.h" +#include "test/gmock.h" +#include "test/gtest.h" + +namespace webrtc { +namespace { + +using testing::Eq; +using testing::Optional; + +constexpr bool kDetected = true; +constexpr bool kNotDetected = false; + +constexpr bool kPredicted = true; +constexpr bool kNotPredicted = false; + +int SumTrueFalsePositivesNegatives( + const ClippingPredictorEvaluator& evaluator) { + return evaluator.true_positives() + evaluator.true_negatives() + + evaluator.false_positives() + evaluator.false_negatives(); +} + +// Checks the metrics after init - i.e., no call to `Observe()`. +TEST(ClippingPredictorEvaluatorTest, Init) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +class ClippingPredictorEvaluatorParameterization + : public ::testing::TestWithParam> { + protected: + uint64_t seed() const { + return rtc::checked_cast(std::get<0>(GetParam())); + } + int history_size() const { return std::get<1>(GetParam()); } +}; + +// Checks that after each call to `Observe()` at most one metric changes. +TEST_P(ClippingPredictorEvaluatorParameterization, AtMostOneMetricChanges) { + constexpr int kNumCalls = 123; + Random random_generator(seed()); + ClippingPredictorEvaluator evaluator(history_size()); + + for (int i = 0; i < kNumCalls; ++i) { + SCOPED_TRACE(i); + // Read metrics before `Observe()` is called. + const int last_tp = evaluator.true_positives(); + const int last_tn = evaluator.true_negatives(); + const int last_fp = evaluator.false_positives(); + const int last_fn = evaluator.false_negatives(); + // `Observe()` a random observation. + bool clipping_detected = random_generator.Rand(); + bool clipping_predicted = random_generator.Rand(); + evaluator.Observe(clipping_detected, clipping_predicted); + + // Check that at most one metric has changed. + int num_changes = 0; + num_changes += last_tp == evaluator.true_positives() ? 0 : 1; + num_changes += last_tn == evaluator.true_negatives() ? 0 : 1; + num_changes += last_fp == evaluator.false_positives() ? 0 : 1; + num_changes += last_fn == evaluator.false_negatives() ? 0 : 1; + EXPECT_GE(num_changes, 0); + EXPECT_LE(num_changes, 1); + } +} + +// Checks that after each call to `Observe()` each metric either remains +// unchanged or grows. +TEST_P(ClippingPredictorEvaluatorParameterization, MetricsAreWeaklyMonotonic) { + constexpr int kNumCalls = 123; + Random random_generator(seed()); + ClippingPredictorEvaluator evaluator(history_size()); + + for (int i = 0; i < kNumCalls; ++i) { + SCOPED_TRACE(i); + // Read metrics before `Observe()` is called. + const int last_tp = evaluator.true_positives(); + const int last_tn = evaluator.true_negatives(); + const int last_fp = evaluator.false_positives(); + const int last_fn = evaluator.false_negatives(); + // `Observe()` a random observation. + bool clipping_detected = random_generator.Rand(); + bool clipping_predicted = random_generator.Rand(); + evaluator.Observe(clipping_detected, clipping_predicted); + + // Check that metrics are weakly monotonic. + EXPECT_GE(evaluator.true_positives(), last_tp); + EXPECT_GE(evaluator.true_negatives(), last_tn); + EXPECT_GE(evaluator.false_positives(), last_fp); + EXPECT_GE(evaluator.false_negatives(), last_fn); + } +} + +// Checks that after each call to `Observe()` the growth speed of each metrics +// is bounded. +TEST_P(ClippingPredictorEvaluatorParameterization, BoundedMetricsGrowth) { + constexpr int kNumCalls = 123; + Random random_generator(seed()); + ClippingPredictorEvaluator evaluator(history_size()); + + for (int i = 0; i < kNumCalls; ++i) { + SCOPED_TRACE(i); + // Read metrics before `Observe()` is called. + const int last_tp = evaluator.true_positives(); + const int last_tn = evaluator.true_negatives(); + const int last_fp = evaluator.false_positives(); + const int last_fn = evaluator.false_negatives(); + // `Observe()` a random observation. + bool clipping_detected = random_generator.Rand(); + bool clipping_predicted = random_generator.Rand(); + evaluator.Observe(clipping_detected, clipping_predicted); + + // Check that TPs grow by at most `history_size() + 1`. Such an upper bound + // is reached when multiple predictions are matched by a single detection. + EXPECT_LE(evaluator.true_positives() - last_tp, history_size() + 1); + // Check that TNs, FPs and FNs grow by at most one. `max_growth`. + EXPECT_LE(evaluator.true_negatives() - last_tn, 1); + EXPECT_LE(evaluator.false_positives() - last_fp, 1); + EXPECT_LE(evaluator.false_negatives() - last_fn, 1); + } +} + +// Checks that `Observe()` returns a prediction interval if and only if one or +// more true positives are found. +TEST_P(ClippingPredictorEvaluatorParameterization, + PredictionIntervalIfAndOnlyIfTruePositives) { + constexpr int kNumCalls = 123; + Random random_generator(seed()); + ClippingPredictorEvaluator evaluator(history_size()); + + for (int i = 0; i < kNumCalls; ++i) { + SCOPED_TRACE(i); + // Read true positives before `Observe()` is called. + const int last_tp = evaluator.true_positives(); + // `Observe()` a random observation. + bool clipping_detected = random_generator.Rand(); + bool clipping_predicted = random_generator.Rand(); + absl::optional prediction_interval = + evaluator.Observe(clipping_detected, clipping_predicted); + + // Check that the prediction interval is returned when a true positive is + // found. + if (evaluator.true_positives() == last_tp) { + EXPECT_FALSE(prediction_interval.has_value()); + } else { + EXPECT_TRUE(prediction_interval.has_value()); + } + } +} + +INSTANTIATE_TEST_SUITE_P( + ClippingPredictorEvaluatorTest, + ClippingPredictorEvaluatorParameterization, + ::testing::Combine(::testing::Values(4, 8, 15, 16, 23, 42), + ::testing::Values(1, 10, 21))); + +// Checks that, observing a detection and a prediction after init, produces a +// true positive. +TEST(ClippingPredictorEvaluatorTest, OneTruePositiveAfterInit) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kDetected, kPredicted); + EXPECT_EQ(evaluator.true_positives(), 1); + + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that, observing a detection but no prediction after init, produces a +// false negative. +TEST(ClippingPredictorEvaluatorTest, OneFalseNegativeAfterInit) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_negatives(), 1); + + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); +} + +// Checks that, observing no detection but a prediction after init, produces a +// false positive after expiration. +TEST(ClippingPredictorEvaluatorTest, OneFalsePositiveAfterInit) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 1); + + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that, observing no detection and no prediction after init, produces a +// true negative. +TEST(ClippingPredictorEvaluatorTest, OneTrueNegativeAfterInit) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_negatives(), 1); + + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that the evaluator detects true negatives when clipping is neither +// predicted nor detected. +TEST(ClippingPredictorEvaluatorTest, NeverDetectedAndNotPredicted) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_negatives(), 4); + + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that the evaluator detects a false negative when clipping is detected +// but not predicted. +TEST(ClippingPredictorEvaluatorTest, DetectedButNotPredicted) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_negatives(), 1); + + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.true_negatives(), 3); + EXPECT_EQ(evaluator.false_positives(), 0); +} + +// Checks that the evaluator does not detect a false positive when clipping is +// predicted but not detected until the observation period expires. +TEST(ClippingPredictorEvaluatorTest, + PredictedOnceAndNeverDetectedBeforeDeadline) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 1); + + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that the evaluator detects a false positive when clipping is predicted +// but detected after the observation period expires. +TEST(ClippingPredictorEvaluatorTest, PredictedOnceButDetectedAfterDeadline) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 1); + + EXPECT_EQ(evaluator.true_positives(), 0); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 1); +} + +// Checks that a prediction followed by a detection counts as true positive. +TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndThenImmediatelyDetected) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_positives(), 1); + + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that a prediction followed by a delayed detection counts as true +// positive if the delay is within the observation period. +TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedBeforeDeadline) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_positives(), 1); + + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that a prediction followed by a delayed detection counts as true +// positive if the delay equals the observation period. +TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedAtDeadline) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_positives(), 0); + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_positives(), 1); + + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that a prediction followed by a multiple adjacent detections within +// the deadline counts as a single true positive and that, after the deadline, +// a detection counts as a false negative. +TEST(ClippingPredictorEvaluatorTest, PredictedOnceAndDetectedMultipleTimes) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + // Multiple detections. + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_positives(), 1); + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_positives(), 1); + // A detection outside of the observation period counts as false negative. + evaluator.Observe(kDetected, kNotPredicted); + EXPECT_EQ(evaluator.false_negatives(), 1); + EXPECT_EQ(SumTrueFalsePositivesNegatives(evaluator), 2); + + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); +} + +// Checks that a false positive is added when clipping is detected after a too +// early prediction. +TEST(ClippingPredictorEvaluatorTest, + PredictedMultipleTimesAndDetectedOnceAfterDeadline) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); // ---+ + evaluator.Observe(kNotDetected, kPredicted); // | + evaluator.Observe(kNotDetected, kPredicted); // | + evaluator.Observe(kNotDetected, kPredicted); // <--+ Not matched. + // The time to match a detection after the first prediction expired. + EXPECT_EQ(evaluator.false_positives(), 1); + evaluator.Observe(kDetected, kNotPredicted); + // The detection above does not match the first prediction because it happened + // after the deadline of the 1st prediction. + EXPECT_EQ(evaluator.false_positives(), 1); + + EXPECT_EQ(evaluator.true_positives(), 3); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that multiple consecutive predictions match the first detection +// observed before the expected detection deadline expires. +TEST(ClippingPredictorEvaluatorTest, PredictedMultipleTimesAndDetectedOnce) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); // --+ + evaluator.Observe(kNotDetected, kPredicted); // | --+ + evaluator.Observe(kNotDetected, kPredicted); // | | --+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+ + EXPECT_EQ(evaluator.true_positives(), 3); + // The following observations do not generate any true negatives as they + // belong to the observation period of the last prediction - for which a + // detection has already been matched. + const int true_negatives = evaluator.true_negatives(); + evaluator.Observe(kNotDetected, kNotPredicted); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_negatives(), true_negatives); + + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that multiple consecutive predictions match the multiple detections +// observed before the expected detection deadline expires. +TEST(ClippingPredictorEvaluatorTest, + PredictedMultipleTimesAndDetectedMultipleTimes) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); // --+ + evaluator.Observe(kNotDetected, kPredicted); // | --+ + evaluator.Observe(kNotDetected, kPredicted); // | | --+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ + EXPECT_EQ(evaluator.true_positives(), 3); + // The following observation does not generate a true negative as it belongs + // to the observation period of the last prediction - for which two detections + // have already been matched. + const int true_negatives = evaluator.true_negatives(); + evaluator.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(evaluator.true_negatives(), true_negatives); + + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that multiple consecutive predictions match all the detections +// observed before the expected detection deadline expires. +TEST(ClippingPredictorEvaluatorTest, PredictedMultipleTimesAndAllDetected) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); // --+ + evaluator.Observe(kNotDetected, kPredicted); // | --+ + evaluator.Observe(kNotDetected, kPredicted); // | | --+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ <-+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ + EXPECT_EQ(evaluator.true_positives(), 3); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +// Checks that multiple non-consecutive predictions match all the detections +// observed before the expected detection deadline expires. +TEST(ClippingPredictorEvaluatorTest, + PredictedMultipleTimesWithGapAndAllDetected) { + ClippingPredictorEvaluator evaluator(/*history_size=*/3); + evaluator.Observe(kNotDetected, kPredicted); // --+ + evaluator.Observe(kNotDetected, kNotPredicted); // | + evaluator.Observe(kNotDetected, kPredicted); // | --+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ <-+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ + evaluator.Observe(kDetected, kNotPredicted); // <-+ + EXPECT_EQ(evaluator.true_positives(), 2); + EXPECT_EQ(evaluator.true_negatives(), 0); + EXPECT_EQ(evaluator.false_positives(), 0); + EXPECT_EQ(evaluator.false_negatives(), 0); +} + +class ClippingPredictorEvaluatorPredictionIntervalParameterization + : public ::testing::TestWithParam> { + protected: + int num_extra_observe_calls() const { return std::get<0>(GetParam()); } + int history_size() const { return std::get<1>(GetParam()); } +}; + +// Checks that the minimum prediction interval is returned if clipping is +// correctly predicted as soon as detected - i.e., no anticipation. +TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization, + MinimumPredictionInterval) { + ClippingPredictorEvaluator evaluator(history_size()); + for (int i = 0; i < num_extra_observe_calls(); ++i) { + EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt); + } + absl::optional prediction_interval = + evaluator.Observe(kDetected, kPredicted); + EXPECT_THAT(prediction_interval, Optional(Eq(0))); +} + +// Checks that a prediction interval between the minimum and the maximum is +// returned if clipping is correctly predicted before it is detected but not as +// early as possible. +TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization, + IntermediatePredictionInterval) { + ClippingPredictorEvaluator evaluator(history_size()); + for (int i = 0; i < num_extra_observe_calls(); ++i) { + EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt); + } + EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt); + EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt); + EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt); + absl::optional prediction_interval = + evaluator.Observe(kDetected, kPredicted); + EXPECT_THAT(prediction_interval, Optional(Eq(3))); +} + +// Checks that the maximum prediction interval is returned if clipping is +// correctly predicted as early as possible. +TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization, + MaximumPredictionInterval) { + ClippingPredictorEvaluator evaluator(history_size()); + for (int i = 0; i < num_extra_observe_calls(); ++i) { + EXPECT_EQ(evaluator.Observe(kNotDetected, kNotPredicted), absl::nullopt); + } + for (int i = 0; i < history_size(); ++i) { + EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt); + } + absl::optional prediction_interval = + evaluator.Observe(kDetected, kPredicted); + EXPECT_THAT(prediction_interval, Optional(Eq(history_size()))); +} + +// Checks that `Observe()` returns the prediction interval as soon as a true +// positive is found and never again while ongoing detections are matched to a +// previously observed prediction. +TEST_P(ClippingPredictorEvaluatorPredictionIntervalParameterization, + PredictionIntervalReturnedOnce) { + ASSERT_LT(num_extra_observe_calls(), history_size()); + ClippingPredictorEvaluator evaluator(history_size()); + // Observe predictions before detection. + for (int i = 0; i < num_extra_observe_calls(); ++i) { + EXPECT_EQ(evaluator.Observe(kNotDetected, kPredicted), absl::nullopt); + } + // Observe a detection. + absl::optional prediction_interval = + evaluator.Observe(kDetected, kPredicted); + EXPECT_TRUE(prediction_interval.has_value()); + // `Observe()` does not return a prediction interval anymore during ongoing + // detections observed while a detection is still expected. + for (int i = 0; i < history_size(); ++i) { + EXPECT_EQ(evaluator.Observe(kDetected, kNotPredicted), absl::nullopt); + } +} + +INSTANTIATE_TEST_SUITE_P( + ClippingPredictorEvaluatorTest, + ClippingPredictorEvaluatorPredictionIntervalParameterization, + ::testing::Combine(::testing::Values(0, 3, 5), ::testing::Values(7, 11))); + +// Checks that, when a detection is expected, the expectation is removed if and +// only if `Reset()` is called after a prediction is observed. +TEST(ClippingPredictorEvaluatorTest, NoFalsePositivesAfterReset) { + constexpr int kHistorySize = 2; + + ClippingPredictorEvaluator with_reset(kHistorySize); + with_reset.Observe(kNotDetected, kPredicted); + with_reset.Reset(); + with_reset.Observe(kNotDetected, kNotPredicted); + with_reset.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(with_reset.true_positives(), 0); + EXPECT_EQ(with_reset.true_negatives(), 2); + EXPECT_EQ(with_reset.false_positives(), 0); + EXPECT_EQ(with_reset.false_negatives(), 0); + + ClippingPredictorEvaluator no_reset(kHistorySize); + no_reset.Observe(kNotDetected, kPredicted); + no_reset.Observe(kNotDetected, kNotPredicted); + no_reset.Observe(kNotDetected, kNotPredicted); + EXPECT_EQ(no_reset.true_positives(), 0); + EXPECT_EQ(no_reset.true_negatives(), 0); + EXPECT_EQ(no_reset.false_positives(), 1); + EXPECT_EQ(no_reset.false_negatives(), 0); +} + +} // namespace +} // namespace webrtc