* 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/agc2/clipping_predictor.h"
#include <cstdint>
#include <limits>
#include <tuple>
#include "rtc_base/checks.h"
#include "test/gmock.h"
#include "test/gtest.h"
namespace webrtc {
namespace {
using ::testing::Eq;
using ::testing::Optional;
using ClippingPredictorConfig = AudioProcessing::Config::GainController1::
AnalogGainController::ClippingPredictor;
using ClippingPredictorMode = AudioProcessing::Config::GainController1::
AnalogGainController::ClippingPredictor::Mode;
constexpr int kSampleRateHz = 32000;
constexpr int kNumChannels = 1;
constexpr int kSamplesPerChannel = kSampleRateHz / 100;
constexpr int kMaxMicLevel = 255;
constexpr int kMinMicLevel = 12;
constexpr int kDefaultClippedLevelStep = 15;
constexpr float kMaxSampleS16 =
static_cast<float>(std::numeric_limits<int16_t>::max());
constexpr float kClippingThresholdDb = -0.08729610804900176f;
void CallAnalyze(int num_calls,
const AudioFrameView<const float>& frame,
ClippingPredictor& predictor) {
for (int i = 0; i < num_calls; ++i) {
predictor.Analyze(frame);
}
}
void AnalyzeNonZeroCrestFactorAudio(int num_calls,
int num_channels,
float peak_ratio,
ClippingPredictor& predictor) {
RTC_DCHECK_GT(num_calls, 0);
RTC_DCHECK_GT(num_channels, 0);
RTC_DCHECK_LE(peak_ratio, 1.0f);
std::vector<float*> audio(num_channels);
std::vector<float> audio_data(num_channels * kSamplesPerChannel, 0.0f);
for (int channel = 0; channel < num_channels; ++channel) {
audio[channel] = &audio_data[channel * kSamplesPerChannel];
for (int sample = 0; sample < kSamplesPerChannel; sample += 10) {
audio[channel][sample] = 0.1f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 1] = 0.2f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 2] = 0.3f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 3] = 0.4f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 4] = 0.5f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 5] = 0.6f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 6] = 0.7f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 7] = 0.8f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 8] = 0.9f * peak_ratio * kMaxSampleS16;
audio[channel][sample + 9] = 1.0f * peak_ratio * kMaxSampleS16;
}
}
AudioFrameView<const float> frame(audio.data(), num_channels,
kSamplesPerChannel);
CallAnalyze(num_calls, frame, predictor);
}
void CheckChannelEstimatesWithValue(int num_channels,
int level,
int default_step,
int min_mic_level,
int max_mic_level,
const ClippingPredictor& predictor,
int expected) {
for (int i = 0; i < num_channels; ++i) {
SCOPED_TRACE(i);
EXPECT_THAT(predictor.EstimateClippedLevelStep(
i, level, default_step, min_mic_level, max_mic_level),
Optional(Eq(expected)));
}
}
void CheckChannelEstimatesWithoutValue(int num_channels,
int level,
int default_step,
int min_mic_level,
int max_mic_level,
const ClippingPredictor& predictor) {
for (int i = 0; i < num_channels; ++i) {
SCOPED_TRACE(i);
EXPECT_EQ(predictor.EstimateClippedLevelStep(i, level, default_step,
min_mic_level, max_mic_level),
absl::nullopt);
}
}
void AnalyzeZeroCrestFactorAudio(int num_calls,
int num_channels,
float peak_ratio,
ClippingPredictor& predictor) {
RTC_DCHECK_GT(num_calls, 0);
RTC_DCHECK_GT(num_channels, 0);
RTC_DCHECK_LE(peak_ratio, 1.f);
std::vector<float*> audio(num_channels);
std::vector<float> audio_data(num_channels * kSamplesPerChannel, 0.f);
for (int channel = 0; channel < num_channels; ++channel) {
audio[channel] = &audio_data[channel * kSamplesPerChannel];
for (int sample = 0; sample < kSamplesPerChannel; ++sample) {
audio[channel][sample] = peak_ratio * kMaxSampleS16;
}
}
auto frame = AudioFrameView<const float>(audio.data(), num_channels,
kSamplesPerChannel);
CallAnalyze(num_calls, frame, predictor);
}
TEST(ClippingPeakPredictorTest, NoPredictorCreated) {
auto predictor =
CreateClippingPredictor(kNumChannels, {false});
EXPECT_FALSE(predictor);
}
TEST(ClippingPeakPredictorTest, ClippingEventPredictionCreated) {
auto predictor = CreateClippingPredictor(
kNumChannels,
{true,
ClippingPredictorMode::kClippingEventPrediction});
EXPECT_TRUE(predictor);
}
TEST(ClippingPeakPredictorTest, AdaptiveStepClippingPeakPredictionCreated) {
auto predictor = CreateClippingPredictor(
kNumChannels, {
true,
ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction});
EXPECT_TRUE(predictor);
}
TEST(ClippingPeakPredictorTest, FixedStepClippingPeakPredictionCreated) {
auto predictor = CreateClippingPredictor(
kNumChannels, {
true,
ClippingPredictorMode::kFixedStepClippingPeakPrediction});
EXPECT_TRUE(predictor);
}
class ClippingPredictorParameterization
: public ::testing::TestWithParam<std::tuple<int, int, int, int>> {
protected:
int num_channels() const { return std::get<0>(GetParam()); }
ClippingPredictorConfig GetConfig(ClippingPredictorMode mode) const {
return {true,
mode,
std::get<1>(GetParam()),
std::get<2>(GetParam()),
std::get<3>(GetParam()),
-1.0f,
0.5f};
}
};
TEST_P(ClippingPredictorParameterization,
CheckClippingEventPredictorEstimateAfterCrestFactorDrop) {
const ClippingPredictorConfig config =
GetConfig(ClippingPredictorMode::kClippingEventPrediction);
if (config.reference_window_length + config.reference_window_delay <=
config.window_length) {
return;
}
auto predictor = CreateClippingPredictor(num_channels(), config);
AnalyzeNonZeroCrestFactorAudio(
config.reference_window_length +
config.reference_window_delay - config.window_length,
num_channels(), 0.99f, *predictor);
CheckChannelEstimatesWithoutValue(num_channels(), 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeZeroCrestFactorAudio(config.window_length, num_channels(),
0.99f, *predictor);
CheckChannelEstimatesWithValue(
num_channels(), 255, kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
}
TEST_P(ClippingPredictorParameterization,
CheckClippingEventPredictorNoEstimateAfterConstantCrestFactor) {
const ClippingPredictorConfig config =
GetConfig(ClippingPredictorMode::kClippingEventPrediction);
if (config.reference_window_length + config.reference_window_delay <=
config.window_length) {
return;
}
auto predictor = CreateClippingPredictor(num_channels(), config);
AnalyzeNonZeroCrestFactorAudio(
config.reference_window_length +
config.reference_window_delay - config.window_length,
num_channels(), 0.99f, *predictor);
CheckChannelEstimatesWithoutValue(num_channels(), 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeNonZeroCrestFactorAudio(config.window_length,
num_channels(),
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(num_channels(), 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
}
TEST_P(ClippingPredictorParameterization,
CheckClippingPeakPredictorEstimateAfterHighCrestFactor) {
const ClippingPredictorConfig config =
GetConfig(ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction);
if (config.reference_window_length + config.reference_window_delay <=
config.window_length) {
return;
}
auto predictor = CreateClippingPredictor(num_channels(), config);
AnalyzeNonZeroCrestFactorAudio(
config.reference_window_length +
config.reference_window_delay - config.window_length,
num_channels(), 0.99f, *predictor);
CheckChannelEstimatesWithoutValue(num_channels(), 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeNonZeroCrestFactorAudio(config.window_length,
num_channels(),
0.99f, *predictor);
CheckChannelEstimatesWithValue(
num_channels(), 255, kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
}
TEST_P(ClippingPredictorParameterization,
CheckClippingPeakPredictorNoEstimateAfterLowCrestFactor) {
const ClippingPredictorConfig config =
GetConfig(ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction);
if (config.reference_window_length + config.reference_window_delay <=
config.window_length) {
return;
}
auto predictor = CreateClippingPredictor(num_channels(), config);
AnalyzeZeroCrestFactorAudio(
config.reference_window_length +
config.reference_window_delay - config.window_length,
num_channels(), 0.99f, *predictor);
CheckChannelEstimatesWithoutValue(num_channels(), 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeNonZeroCrestFactorAudio(config.window_length,
num_channels(),
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(num_channels(), 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
}
INSTANTIATE_TEST_SUITE_P(GainController1ClippingPredictor,
ClippingPredictorParameterization,
::testing::Combine(::testing::Values(1, 5),
::testing::Values(1, 5, 10),
::testing::Values(1, 5),
::testing::Values(0, 1, 5)));
class ClippingEventPredictorParameterization
: public ::testing::TestWithParam<std::tuple<float, float>> {
protected:
ClippingPredictorConfig GetConfig() const {
return {true,
ClippingPredictorMode::kClippingEventPrediction,
5,
5,
5,
std::get<0>(GetParam()),
std::get<1>(GetParam())};
}
};
TEST_P(ClippingEventPredictorParameterization,
CheckEstimateAfterCrestFactorDrop) {
const ClippingPredictorConfig config = GetConfig();
auto predictor = CreateClippingPredictor(kNumChannels, config);
AnalyzeNonZeroCrestFactorAudio(config.reference_window_length,
kNumChannels, 0.99f,
*predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeZeroCrestFactorAudio(config.window_length, kNumChannels,
0.99f, *predictor);
if (config.clipping_threshold < kClippingThresholdDb &&
config.crest_factor_margin < 4.15f) {
CheckChannelEstimatesWithValue(
kNumChannels, 255, kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
} else {
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
}
}
INSTANTIATE_TEST_SUITE_P(GainController1ClippingPredictor,
ClippingEventPredictorParameterization,
::testing::Combine(::testing::Values(-1.0f, 0.0f),
::testing::Values(3.0f, 4.16f)));
class ClippingPredictorModeParameterization
: public ::testing::TestWithParam<ClippingPredictorMode> {
protected:
ClippingPredictorConfig GetConfig(float clipping_threshold_dbfs) const {
return {true,
GetParam(),
5,
5,
5,
clipping_threshold_dbfs,
3.0f};
}
};
TEST_P(ClippingPredictorModeParameterization,
CheckEstimateAfterHighCrestFactorWithNoClippingMargin) {
const ClippingPredictorConfig config = GetConfig(
0.0f);
auto predictor = CreateClippingPredictor(kNumChannels, config);
AnalyzeNonZeroCrestFactorAudio(config.reference_window_length,
kNumChannels, 0.99f,
*predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeZeroCrestFactorAudio(config.window_length, kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
}
TEST_P(ClippingPredictorModeParameterization,
CheckEstimateAfterHighCrestFactorWithClippingMargin) {
const ClippingPredictorConfig config =
GetConfig(-1.0f);
auto predictor = CreateClippingPredictor(kNumChannels, config);
AnalyzeNonZeroCrestFactorAudio(config.reference_window_length,
kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeZeroCrestFactorAudio(config.window_length, kNumChannels,
0.99f, *predictor);
const float expected_step =
config.mode == ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction
? 17
: kDefaultClippedLevelStep;
CheckChannelEstimatesWithValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor, expected_step);
}
INSTANTIATE_TEST_SUITE_P(
GainController1ClippingPredictor,
ClippingPredictorModeParameterization,
::testing::Values(
ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction,
ClippingPredictorMode::kFixedStepClippingPeakPrediction));
TEST(ClippingEventPredictorTest, CheckEstimateAfterReset) {
constexpr ClippingPredictorConfig kConfig{
true,
ClippingPredictorMode::kClippingEventPrediction,
5,
5,
5,
-1.0f,
3.0f};
auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
AnalyzeNonZeroCrestFactorAudio(kConfig.reference_window_length,
kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
predictor->Reset();
AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
}
TEST(ClippingPeakPredictorTest, CheckNoEstimateAfterReset) {
constexpr ClippingPredictorConfig kConfig{
true,
ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction,
5,
5,
5,
-1.0f};
auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
AnalyzeNonZeroCrestFactorAudio(kConfig.reference_window_length,
kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
predictor->Reset();
AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
}
TEST(ClippingPeakPredictorTest, CheckAdaptiveStepEstimate) {
constexpr ClippingPredictorConfig kConfig{
true,
ClippingPredictorMode::kAdaptiveStepClippingPeakPrediction,
5,
5,
5,
-1.0f};
auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
AnalyzeNonZeroCrestFactorAudio(kConfig.reference_window_length,
kNumChannels, 0.99f,
*predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor, 17);
}
TEST(ClippingPeakPredictorTest, CheckFixedStepEstimate) {
constexpr ClippingPredictorConfig kConfig{
true,
ClippingPredictorMode::kFixedStepClippingPeakPrediction,
5,
5,
5,
-1.0f};
auto predictor = CreateClippingPredictor(kNumChannels, kConfig);
AnalyzeNonZeroCrestFactorAudio(kConfig.reference_window_length,
kNumChannels, 0.99f,
*predictor);
CheckChannelEstimatesWithoutValue(kNumChannels, 255,
kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor);
AnalyzeZeroCrestFactorAudio(kConfig.window_length, kNumChannels,
0.99f, *predictor);
CheckChannelEstimatesWithValue(
kNumChannels, 255, kDefaultClippedLevelStep, kMinMicLevel,
kMaxMicLevel, *predictor, kDefaultClippedLevelStep);
}
}
}