blob: 0c376d919a0d707b498712bf14c2ebcb2e49497d [file] [log] [blame]
// Copyright 2019 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/audio_core/mixer/gain.h"
#include <lib/syslog/cpp/macros.h>
#include <iterator>
#include <fbl/algorithm.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/media/audio/lib/processing/gain.h"
#include "src/media/audio/lib/test/constants.h"
namespace media::audio {
namespace {
using ::testing::Each;
using ::testing::FloatEq;
using ::testing::Not;
using ::testing::Pointwise;
TEST(StaticGainTest, CombineGains) {
static_assert(-90.0 < 0.5f * media_audio::kMinGainDb);
EXPECT_FLOAT_EQ(Gain::CombineGains(-90.0f, -90.0f), media_audio::kMinGainDb);
EXPECT_FLOAT_EQ(Gain::CombineGains(-20.0f, 5.0f), -15.0f);
EXPECT_FLOAT_EQ(Gain::CombineGains(15.0f, 15.0f), 30.0f);
}
// Gain tests - how does the Gain object respond when given values close to its
// maximum or minimum; does it correctly cache; do values combine to form Unity
// gain. Is data scaling accurately performed, and is it adequately linear? Do
// our gains and accumulators behave as expected when they overflow?
//
// Gain tests using AScale and the Gain object only
//
class GainBase : public testing::Test {
protected:
void SetUp() override {
testing::Test::SetUp();
rate_1khz_output_ = TimelineRate(1000, ZX_SEC(1));
}
// Used for debugging purposes.
static void DisplayScaleVals(const Gain::AScale* scale_arr, int64_t buf_size) {
printf("\n ********************************************************");
printf("\n **************************************************************");
printf("\n *** Displaying raw scale array data for length %5ld ***", buf_size);
printf("\n **************************************************************");
for (auto idx = 0; idx < buf_size; ++idx) {
if (idx % 10 == 0) {
printf("\n [%d] ", idx);
}
printf("%.7f ", scale_arr[idx]);
}
printf("\n **************************************************************");
printf("\n ********************************************************");
printf("\n");
}
// Overridden by SourceDestGainControl and DestSourceGainControl
virtual void SetGain(float gain_db) = 0;
virtual void SetOtherGain(float gain_db) = 0;
virtual void SetGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) = 0;
virtual void SetOtherGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) = 0;
virtual float GetPartialGainDb() = 0;
virtual float GetOtherPartialGainDb() = 0;
virtual void CompleteRamp() = 0;
// Used by SourceDestGainTest and DestSourceGainTest
void TestUnityGain(float first_gain_db, float second_gain_db);
void UnityChecks();
void GainCachingChecks();
void VerifyMinGain(float first_gain_db, float second_gain_db);
void MinGainChecks();
void VerifyMaxGain(float first_gain_db, float second_gain_db);
void MaxGainChecks();
void SourceMuteChecks();
void TestRampWithNoDuration();
void TestRampWithDuration();
void TestRampIntoSilence();
void TestRampOutOfSilence();
void TestRampFromSilenceToSilence();
void TestRampsCombineForSilence();
void TestRampUnity();
void TestFlatRamp();
void TestRampingBelowMinGain();
void TestRampWithMute();
void TestAdvance();
void TestSetGainCancelsRamp();
void TestRampsForSilence();
void TestRampsForNonSilence();
// These precise scale tests produce a scale array from two controls.
// The returned max value ignores the "internal" control.
enum class AdjustmentControlPosition {
First,
Second,
None,
};
virtual AdjustmentControlPosition adjustment_control_position() const = 0;
void TestCalculateScaleArrayNoRamp();
void TestCalculateScaleArray();
void TestScaleArrayLongRamp();
void TestScaleArrayShortRamp();
void TestScaleArrayWithoutAdvance();
void TestScaleArrayBigAdvance();
void TestRampCompletion();
void TestAdvanceHalfwayThroughRamp();
void TestSuccessiveRamps();
void TestCombinedRamps();
void TestCrossFades();
void TestScaleArrayForMinScale();
Gain gain_;
// All tests use a 1 kHz frame rate, for easy 1-frame-per-msec observation.
TimelineRate rate_1khz_output_;
};
// Used so that identical testing is done on each pair of gain controls.
class SourceDestGainControl : public GainBase {
protected:
void SetGain(float gain_db) override { gain_.SetSourceGain(gain_db); }
void SetOtherGain(float gain_db) override { gain_.SetDestGain(gain_db); }
void SetGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetSourceGainWithRamp(gain_db, duration, ramp_type);
}
void SetOtherGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetDestGainWithRamp(gain_db, duration, ramp_type);
}
float GetPartialGainDb() override { return gain_.GetSourceGainDb(); }
float GetOtherPartialGainDb() override { return gain_.GetDestGainDb(); }
void CompleteRamp() override { gain_.CompleteSourceRamp(); }
AdjustmentControlPosition adjustment_control_position() const override {
return AdjustmentControlPosition::None;
}
};
class DestSourceGainControl : public GainBase {
protected:
void SetGain(float gain_db) override { gain_.SetDestGain(gain_db); }
void SetOtherGain(float gain_db) override { gain_.SetSourceGain(gain_db); }
void SetGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetDestGainWithRamp(gain_db, duration, ramp_type);
}
void SetOtherGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetSourceGainWithRamp(gain_db, duration, ramp_type);
}
float GetPartialGainDb() override { return gain_.GetDestGainDb(); }
float GetOtherPartialGainDb() override { return gain_.GetSourceGainDb(); }
void CompleteRamp() override { gain_.CompleteDestRamp(); }
AdjustmentControlPosition adjustment_control_position() const override {
return AdjustmentControlPosition::None;
}
};
class SourceAdjustmentGainControl : public GainBase {
protected:
void SetGain(float gain_db) override { gain_.SetSourceGain(gain_db); }
void SetOtherGain(float gain_db) override { gain_.SetGainAdjustment(gain_db); }
void SetGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetSourceGainWithRamp(gain_db, duration, ramp_type);
}
void SetOtherGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetGainAdjustmentWithRamp(gain_db, duration, ramp_type);
}
float GetPartialGainDb() override { return gain_.GetSourceGainDb(); }
float GetOtherPartialGainDb() override { return gain_.GetGainAdjustmentDb(); }
void CompleteRamp() override { gain_.CompleteSourceRamp(); }
AdjustmentControlPosition adjustment_control_position() const override {
return AdjustmentControlPosition::Second;
}
};
class AdjustmentSourceGainControl : public GainBase {
protected:
void SetGain(float gain_db) override { gain_.SetGainAdjustment(gain_db); }
void SetOtherGain(float gain_db) override { gain_.SetSourceGain(gain_db); }
void SetGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetGainAdjustmentWithRamp(gain_db, duration, ramp_type);
}
void SetOtherGainWithRamp(float gain_db, zx::duration duration,
fuchsia::media::audio::RampType ramp_type =
fuchsia::media::audio::RampType::SCALE_LINEAR) override {
gain_.SetSourceGainWithRamp(gain_db, duration, ramp_type);
}
float GetPartialGainDb() override { return gain_.GetGainAdjustmentDb(); }
float GetOtherPartialGainDb() override { return gain_.GetSourceGainDb(); }
void CompleteRamp() override { gain_.CompleteAdjustmentRamp(); }
AdjustmentControlPosition adjustment_control_position() const override {
return AdjustmentControlPosition::First;
}
};
// General (non-specific to source or dest) gain checks
class GainTest : public SourceDestGainControl {};
// Gain checks that can be source/dest inverted.
class SourceDestGainTest : public SourceDestGainControl {};
class DestSourceGainTest : public DestSourceGainControl {};
class SourceAdjustmentGainTest : public SourceAdjustmentGainControl {};
class AdjustmentSourceGainTest : public AdjustmentSourceGainControl {};
// Test the defaults upon construction
TEST_F(GainTest, Defaults) {
EXPECT_FLOAT_EQ(gain_.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_TRUE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
}
void GainBase::TestUnityGain(float first_gain_db, float second_gain_db) {
SetGain(first_gain_db);
SetOtherGain(second_gain_db);
EXPECT_FLOAT_EQ(media_audio::kUnityGainScale, gain_.GetGainScale());
EXPECT_FLOAT_EQ(media_audio::kUnityGainDb, GetPartialGainDb() + GetOtherPartialGainDb());
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsUnity());
}
void GainBase::UnityChecks() {
TestUnityGain(media_audio::kUnityGainDb, media_audio::kUnityGainDb);
// These positive/negative values should sum to 0.0: UNITY
TestUnityGain(24.0f, -24.0f);
TestUnityGain(-5.0f, 5.0f);
}
// Do source and destination gains correctly combine to produce unity scaling?
TEST_F(SourceDestGainTest, Unity) { UnityChecks(); }
TEST_F(DestSourceGainTest, Unity) { UnityChecks(); }
TEST_F(SourceAdjustmentGainTest, Unity) { UnityChecks(); }
TEST_F(AdjustmentSourceGainTest, Unity) { UnityChecks(); }
void GainBase::GainCachingChecks() {
Gain expect_gain;
Gain::AScale amplitude_scale, expect_amplitude_scale;
// Set expect_amplitude_scale to a value that represents -6.0 dB.
expect_gain.SetSourceGain(-6.0f);
expect_amplitude_scale = expect_gain.GetGainScale();
// Source gain defaults to 0.0, so this represents -6.0 dB too.
SetGain(0.0f);
SetOtherGain(-6.0f);
amplitude_scale = gain_.GetGainScale();
EXPECT_FLOAT_EQ(expect_amplitude_scale, amplitude_scale);
// Now set a different source gain that will be cached (+3.0).
SetGain(3.0f);
SetOtherGain(-3.0f);
amplitude_scale = gain_.GetGainScale();
EXPECT_FLOAT_EQ(media_audio::kUnityGainScale, amplitude_scale);
EXPECT_GT(GetPartialGainDb(), media_audio::kUnityGainDb);
EXPECT_LT(GetOtherPartialGainDb(), media_audio::kUnityGainDb);
SetOtherGain(-1.0f);
EXPECT_EQ(GetOtherPartialGainDb(), -1.0f);
// If source gain is cached val of +3, then combo should be greater than Unity.
amplitude_scale = gain_.GetGainScale();
EXPECT_GT(amplitude_scale, media_audio::kUnityGainScale);
// And now the previous SetOtherGain call has been incorporated into the cache.
EXPECT_EQ(GetOtherPartialGainDb(), -1.0f);
// Try another dest gain; with cached +3 this should equate to -6dB.
SetOtherGain(-9.0f);
EXPECT_FLOAT_EQ(expect_amplitude_scale, gain_.GetGainScale());
// source gain cached +3 and dest gain non-cached -3 should lead to Unity.
SetOtherGain(-3.0f);
EXPECT_FLOAT_EQ(media_audio::kUnityGainScale, gain_.GetGainScale());
}
// Gain caches any previously set source gain, using it if needed.
// This verifies the default and caching behavior of the Gain object
TEST_F(SourceDestGainTest, GainCaching) { GainCachingChecks(); }
TEST_F(DestSourceGainTest, GainCaching) { GainCachingChecks(); }
TEST_F(SourceAdjustmentGainTest, GainCaching) { GainCachingChecks(); }
TEST_F(AdjustmentSourceGainTest, GainCaching) { GainCachingChecks(); }
void GainBase::VerifyMinGain(float first_gain_db, float second_gain_db) {
SCOPED_TRACE("VerifyMinGain(" + std::to_string(first_gain_db) + ", " +
std::to_string(second_gain_db) + ")");
SetGain(first_gain_db);
SetOtherGain(second_gain_db);
EXPECT_FLOAT_EQ(Gain::kMuteScale, gain_.GetGainScale());
EXPECT_FLOAT_EQ(GetPartialGainDb(), std::max(first_gain_db, media_audio::kMinGainDb));
EXPECT_FLOAT_EQ(GetOtherPartialGainDb(), std::max(second_gain_db, media_audio::kMinGainDb));
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsSilent());
}
void GainBase::MinGainChecks() {
// First, test for source/dest interactions.
// if dest gain <= media_audio::kMinGainDb, scale must be 0, regardless of source gain.
VerifyMinGain(-2 * media_audio::kMinGainDb, media_audio::kMinGainDb);
// if source gain <= media_audio::kMinGainDb, scale must be 0, regardless of dest gain.
VerifyMinGain(media_audio::kMinGainDb, -2.0f * media_audio::kMinGainDb);
// if sum of source gain and dest gain <= media_audio::kMinGainDb, scale should be 0.
// dest gain is just slightly above MinGain; source gain takes us below it.
VerifyMinGain(-2.0f, media_audio::kMinGainDb + 1.0f);
// Next, test for source/dest interactions.
// Check if source alone mutes.
VerifyMinGain(media_audio::kMinGainDb, media_audio::kUnityGainDb);
VerifyMinGain(media_audio::kMinGainDb, media_audio::kUnityGainDb + 1);
// Check if dest alone mutes.
VerifyMinGain(media_audio::kUnityGainDb + 1, media_audio::kMinGainDb);
VerifyMinGain(media_audio::kUnityGainDb, media_audio::kMinGainDb);
// Check if the combination mutes.
VerifyMinGain(media_audio::kMinGainDb / 2, media_audio::kMinGainDb / 2);
}
// System independently limits source gain and dest gain to media_audio::kMinGainDb (-160dB).
// Assert scale is zero, if either (or combo) are media_audio::kMinGainDb or less.
TEST_F(SourceDestGainTest, GainIsLimitedToMin) { MinGainChecks(); }
TEST_F(DestSourceGainTest, GainIsLimitedToMin) { MinGainChecks(); }
TEST_F(SourceAdjustmentGainTest, GainIsLimitedToMin) { MinGainChecks(); }
TEST_F(AdjustmentSourceGainTest, GainIsLimitedToMin) { MinGainChecks(); }
void GainBase::SourceMuteChecks() {
SetGain(0.0f);
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_EQ(gain_.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_EQ(gain_.GetGainDb(), media_audio::kUnityGainDb);
gain_.SetSourceMute(false);
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_EQ(gain_.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_EQ(gain_.GetGainDb(), media_audio::kUnityGainDb);
gain_.SetSourceMute(true);
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_EQ(gain_.GetGainScale(), Gain::kMuteScale);
EXPECT_LE(gain_.GetGainDb(), media_audio::kMinGainDb);
gain_.SetSourceMute(false);
SetGainWithRamp(-10.0, zx::msec(25));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_EQ(gain_.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_EQ(gain_.GetGainDb(), media_audio::kUnityGainDb);
gain_.SetSourceMute(true);
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_EQ(gain_.GetGainScale(), Gain::kMuteScale);
EXPECT_LE(gain_.GetGainDb(), media_audio::kMinGainDb);
}
// source_mute control should affect IsSilent, IsUnity, IsRamping and GetGainScale appropriately.
TEST_F(SourceDestGainTest, SourceMuteOverridesGainAndRamp) { SourceMuteChecks(); }
TEST_F(DestSourceGainTest, SourceMuteOverridesGainAndRamp) { SourceMuteChecks(); }
TEST_F(SourceAdjustmentGainTest, SourceMuteOverridesGainAndRamp) { SourceMuteChecks(); }
TEST_F(AdjustmentSourceGainTest, SourceMuteOverridesGainAndRamp) { SourceMuteChecks(); }
// Ramp-related tests
//
void GainBase::TestRampWithNoDuration() {
SetGain(-11.0f);
SetOtherGain(-1.0f);
EXPECT_FALSE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
SetGainWithRamp(+1.0f, zx::nsec(0));
EXPECT_TRUE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsSilent());
}
// Setting a ramp with zero duration is the same as an immediate gain change.
TEST_F(SourceDestGainTest, SetRampWithNoDurationChangesCurrentGain) { TestRampWithNoDuration(); }
TEST_F(DestSourceGainTest, SetRampWithNoDurationChangesCurrentGain) { TestRampWithNoDuration(); }
TEST_F(SourceAdjustmentGainTest, SetRampWithNoDurationChangesCurrentGain) {
TestRampWithNoDuration();
}
TEST_F(AdjustmentSourceGainTest, SetRampWithNoDurationChangesCurrentGain) {
TestRampWithNoDuration();
}
// Setting a ramp with non-zero duration does not take effect until Advance.
void GainBase::TestRampWithDuration() {
SetGain(24.0f);
SetOtherGain(-24.0f);
EXPECT_TRUE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
SetGainWithRamp(media_audio::kMinGainDb, zx::nsec(1));
EXPECT_TRUE(gain_.GetGainScale() == media_audio::kUnityGainScale);
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
}
// Setting a ramp with non-zero duration does not take effect until Advance.
TEST_F(SourceDestGainTest, SetRampWithDurationDoesntChangeCurrentGain) { TestRampWithDuration(); }
TEST_F(DestSourceGainTest, SetRampWithDurationDoesntChangeCurrentGain) { TestRampWithDuration(); }
TEST_F(SourceAdjustmentGainTest, SetRampWithDurationDoesntChangeCurrentGain) {
TestRampWithDuration();
}
TEST_F(AdjustmentSourceGainTest, SetRampWithDurationDoesntChangeCurrentGain) {
TestRampWithDuration();
}
void GainBase::TestRampIntoSilence() {
SetGain(0.0f);
SetOtherGain(media_audio::kMinGainDb + 1.0f);
SetGainWithRamp(media_audio::kMinGainDb + 1.0f, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsUnity());
SetOtherGain(0.0f);
SetGainWithRamp(media_audio::kMinGainDb * 2, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsUnity());
}
// If we are ramping-down and already silent, IsSilent should remain true.
TEST_F(SourceDestGainTest, RampFromNonSilenceToSilenceIsNotSilent) { TestRampIntoSilence(); }
TEST_F(DestSourceGainTest, RampFromNonSilenceToSilenceIsNotSilent) { TestRampIntoSilence(); }
TEST_F(SourceAdjustmentGainTest, RampFromNonSilenceToSilenceIsNotSilent) { TestRampIntoSilence(); }
TEST_F(AdjustmentSourceGainTest, RampFromNonSilenceToSilenceIsNotSilent) { TestRampIntoSilence(); }
void GainBase::TestRampOutOfSilence() {
// Combined, we start in silence...
SetGain(media_audio::kMinGainDb + 10.f);
SetOtherGain(-22.0f);
EXPECT_TRUE(gain_.IsSilent());
// ... and ramp out of it
SetGainWithRamp(+22.0f, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
// The first stage, on its own, makes us silent...
SetGain(media_audio::kMinGainDb - 5.0f);
SetOtherGain(0.0f);
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
// ... but it ramps out of it.
SetGainWithRamp(media_audio::kMinGainDb + 1.0f, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsUnity());
}
// If we are ramping-down and already silent, IsSilent should remain true.
TEST_F(SourceDestGainTest, RampFromSilenceToNonSilenceIsNotSilent) { TestRampOutOfSilence(); }
TEST_F(DestSourceGainTest, RampFromSilenceToNonSilenceIsNotSilent) { TestRampOutOfSilence(); }
TEST_F(SourceAdjustmentGainTest, RampFromSilenceToNonSilenceIsNotSilent) { TestRampOutOfSilence(); }
TEST_F(AdjustmentSourceGainTest, RampFromSilenceToNonSilenceIsNotSilent) { TestRampOutOfSilence(); }
void GainBase::TestRampFromSilenceToSilence() {
// Both start and end are at/below media_audio::kMinGainDb -- ramping up
SetGain(media_audio::kMinGainDb - 1.0f);
SetGainWithRamp(media_audio::kMinGainDb, zx::sec(1));
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
// Both start and end are at/below media_audio::kMinGainDb -- ramping down
SetGainWithRamp(media_audio::kMinGainDb - 2.0f, zx::sec(1));
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
}
// If the beginning and end of a ramp are both at/below min gain, it isn't ramping.
TEST_F(SourceDestGainTest, RampFromSilenceToSilenceIsNotRamping) { TestRampFromSilenceToSilence(); }
TEST_F(DestSourceGainTest, RampFromSilenceToSilenceIsNotRamping) { TestRampFromSilenceToSilence(); }
TEST_F(SourceAdjustmentGainTest, RampFromSilenceToSilenceIsNotRamping) {
TestRampFromSilenceToSilence();
}
TEST_F(AdjustmentSourceGainTest, RampFromSilenceToSilenceIsNotRamping) {
TestRampFromSilenceToSilence();
}
void GainBase::TestRampsCombineForSilence() {
// Both start and end are at/below media_audio::kMinGainDb -- ramping up
SetGain(media_audio::kMinGainDb);
SetOtherGain(media_audio::kUnityGainDb);
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
// Because our scalelinear ramps are not equal-power, we "bulge" at the midpoint of fades, thus
// combined ramps may not be silent just because their endpoints are.
SetGainWithRamp(media_audio::kUnityGainDb, zx::sec(1));
SetOtherGainWithRamp(media_audio::kMinGainDb, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
}
// If the beginning and end of a ramp are both at/below min gain, it isn't ramping.
TEST_F(SourceDestGainTest, RampsCombineForSilenceIsNotSilent) { TestRampsCombineForSilence(); }
TEST_F(DestSourceGainTest, RampsCombineForSilenceIsNotSilent) { TestRampsCombineForSilence(); }
TEST_F(SourceAdjustmentGainTest, RampsCombineForSilenceIsNotSilent) {
TestRampsCombineForSilence();
}
TEST_F(AdjustmentSourceGainTest, RampsCombineForSilenceIsNotSilent) {
TestRampsCombineForSilence();
}
void GainBase::TestRampUnity() {
SetGain(media_audio::kUnityGainDb);
SetOtherGain(media_audio::kUnityGainDb);
EXPECT_TRUE(gain_.IsUnity());
SetGainWithRamp(-1.0f, zx::sec(1));
// Expect pre-ramp conditions
EXPECT_EQ(gain_.GetGainDb(), media_audio::kUnityGainDb);
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity()); // unity at this instant, but not _staying_ there
EXPECT_TRUE(gain_.IsRamping());
}
// If a ramp is active/pending, then IsUnity should never be true.
TEST_F(SourceDestGainTest, RampIsNeverUnity) { TestRampUnity(); }
TEST_F(DestSourceGainTest, RampIsNeverUnity) { TestRampUnity(); }
TEST_F(SourceAdjustmentGainTest, RampIsNeverUnity) { TestRampUnity(); }
TEST_F(AdjustmentSourceGainTest, RampIsNeverUnity) { TestRampUnity(); }
void GainBase::TestFlatRamp() {
SetGain(media_audio::kUnityGainDb);
SetOtherGain(-20.0f);
SetGainWithRamp(0.0f, zx::sec(1));
// Expect pre-ramp conditions
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
// ... and a flat ramp should combine with the other side to equal Unity.
SetOtherGain(0.0f);
EXPECT_TRUE(gain_.IsUnity());
}
// If the beginning and end of a ramp are the same, it isn't ramping.
TEST_F(SourceDestGainTest, FlatIsntRamping) { TestFlatRamp(); }
TEST_F(DestSourceGainTest, FlatIsntRamping) { TestFlatRamp(); }
TEST_F(SourceAdjustmentGainTest, FlatIsntRamping) { TestFlatRamp(); }
TEST_F(AdjustmentSourceGainTest, FlatIsntRamping) { TestFlatRamp(); }
void GainBase::TestRampWithMute() {
SetGain(0.0f);
SetGainWithRamp(-10.0, zx::msec(25));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
gain_.SetSourceMute(true);
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
// after clearing the mute, we should be seen as ramping.
gain_.SetSourceMute(false);
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
}
// If the beginning and end of a ramp are the same, it isn't ramping.
TEST_F(SourceDestGainTest, MuteOverridesRamp) { TestRampWithMute(); }
TEST_F(DestSourceGainTest, MuteOverridesRamp) { TestRampWithMute(); }
TEST_F(SourceAdjustmentGainTest, MuteOverridesRamp) { TestRampWithMute(); }
TEST_F(AdjustmentSourceGainTest, MuteOverridesRamp) { TestRampWithMute(); }
void GainBase::TestAdvance() {
SetGain(-150.0f);
SetOtherGain(-13.0f);
SetGainWithRamp(+13.0f, zx::nsec(1));
// Advance far beyond end of ramp -- 10 msec (10 frames@1kHz) vs. 1 nsec.
gain_.Advance(10, rate_1khz_output_);
// Expect post-ramp conditions
EXPECT_FALSE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
}
// Upon Advance, we should see a change in the instantaneous GetGainScale().
TEST_F(SourceDestGainTest, AdvanceChangesGain) { TestAdvance(); }
TEST_F(DestSourceGainTest, AdvanceChangesGain) { TestAdvance(); }
TEST_F(SourceAdjustmentGainTest, AdvanceChangesGain) { TestAdvance(); }
TEST_F(AdjustmentSourceGainTest, AdvanceChangesGain) { TestAdvance(); }
void GainBase::TestSetGainCancelsRamp() {
SetGain(-60.0f);
SetOtherGain(-20.0f);
SetGainWithRamp(-20.0f, zx::sec(1));
EXPECT_FLOAT_EQ(gain_.GetGainDb(), -80.0f);
EXPECT_TRUE(gain_.IsRamping());
// Advance halfway through the ramp (500 frames, which at 1kHz is 500 ms).
gain_.Advance(500, rate_1khz_output_);
EXPECT_TRUE(gain_.IsRamping());
SetGain(0.0f);
EXPECT_FALSE(gain_.IsRamping());
EXPECT_FLOAT_EQ(gain_.GetGainDb(), -20.0f);
}
// Setting a static gain during ramping should cancel the ramp
TEST_F(SourceDestGainTest, SetSourceGainCancelsRamp) { TestSetGainCancelsRamp(); }
TEST_F(DestSourceGainTest, SetSourceGainCancelsRamp) { TestSetGainCancelsRamp(); }
TEST_F(SourceAdjustmentGainTest, SetSourceGainCancelsRamp) { TestSetGainCancelsRamp(); }
TEST_F(AdjustmentSourceGainTest, SetSourceGainCancelsRamp) { TestSetGainCancelsRamp(); }
void GainBase::TestRampsForSilence() {
// Flat ramp reverts to static gain combination
SetGain(-80.0f);
SetOtherGain(-80.0f);
SetGainWithRamp(-80.0f, zx::sec(1));
EXPECT_TRUE(gain_.IsSilent());
// Already below the silence threshold and ramping downward
SetGainWithRamp(-90.0f, zx::sec(1));
EXPECT_TRUE(gain_.IsSilent());
// Ramping upward, but other stage is below mute threshold
SetGain(10.0f);
SetOtherGain(media_audio::kMinGainDb);
SetGainWithRamp(12.0f, zx::sec(1));
EXPECT_TRUE(gain_.IsSilent());
// Ramping upward, but to a target below mute threshold
SetGain(media_audio::kMinGainDb - 5.0f);
SetOtherGain(10.0f);
SetGainWithRamp(media_audio::kMinGainDb, zx::sec(1));
EXPECT_TRUE(gain_.IsSilent());
}
// Setting a static gain during ramping should cancel the ramp
TEST_F(SourceDestGainTest, WhenIsSilentShouldBeTrue) { TestRampsForSilence(); }
TEST_F(DestSourceGainTest, WhenIsSilentShouldBeTrue) { TestRampsForSilence(); }
TEST_F(SourceAdjustmentGainTest, WhenIsSilentShouldBeTrue) { TestRampsForSilence(); }
TEST_F(AdjustmentSourceGainTest, WhenIsSilentShouldBeTrue) { TestRampsForSilence(); }
void GainBase::TestRampsForNonSilence() {
// Above the silence threshold, ramping downward
SetGain(-79.0f);
SetOtherGain(-80.0f);
SetGainWithRamp(-90.0f, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
// Below the silence threshold, ramping upward
SetGain(-100.0f);
SetOtherGain(-65.0f);
SetGainWithRamp(-90.0f, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
// Ramping from below to above mute threshold
SetGain(media_audio::kMinGainDb - 5.0f);
SetOtherGain(10.0f);
SetGainWithRamp(media_audio::kMinGainDb + 1.0f, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
// The following is not considered silence, because we expect clients to advance the ramp
SetGain(-100.0f);
SetOtherGain(-120.0f);
SetGainWithRamp(-60.0f, zx::sec(1));
EXPECT_FALSE(gain_.IsSilent());
}
TEST_F(SourceDestGainTest, WhenIsSilentShouldBeFalse) { TestRampsForNonSilence(); }
TEST_F(DestSourceGainTest, WhenIsSilentShouldBeFalse) { TestRampsForNonSilence(); }
TEST_F(SourceAdjustmentGainTest, WhenIsSilentShouldBeFalse) { TestRampsForNonSilence(); }
TEST_F(AdjustmentSourceGainTest, WhenIsSilentShouldBeFalse) { TestRampsForNonSilence(); }
// ScaleArray-related tests
//
void GainBase::TestCalculateScaleArrayNoRamp() {
Gain::AScale scale_arr[3];
SetGain(-42.0f);
SetOtherGain(-68.0f);
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_THAT(scale_arr, Each(FloatEq(media_audio::DbToScale(-110.0f))));
switch (adjustment_control_position()) {
case AdjustmentControlPosition::None:
EXPECT_FLOAT_EQ(max_gain_scale, media_audio::DbToScale(-110.0f));
break;
case AdjustmentControlPosition::First:
EXPECT_FLOAT_EQ(max_gain_scale, media_audio::DbToScale(-68.0f));
break;
case AdjustmentControlPosition::Second:
EXPECT_FLOAT_EQ(max_gain_scale, media_audio::DbToScale(-42.0f));
break;
}
EXPECT_FALSE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsSilent());
}
// If no ramp, all vals returned by CalculateScaleArray should equal GetGainScale().
TEST_F(SourceDestGainTest, CalculateScaleArrayNoRampEqualsGetScale) {
TestCalculateScaleArrayNoRamp();
}
TEST_F(DestSourceGainTest, CalculateScaleArrayNoRampEqualsGetScale) {
TestCalculateScaleArrayNoRamp();
}
TEST_F(SourceAdjustmentGainTest, CalculateScaleArrayNoRampEqualsGetScale) {
TestCalculateScaleArrayNoRamp();
}
TEST_F(AdjustmentSourceGainTest, CalculateScaleArrayNoRampEqualsGetScale) {
TestCalculateScaleArrayNoRamp();
}
void GainBase::TestCalculateScaleArray() {
Gain::AScale scale_arr[6];
Gain::AScale expect_arr[6] = {0.5f, 0.42f, 0.34f, 0.26f, 0.18f, 0.10f};
SetGain(media_audio::ScaleToDb(0.5f));
SetGainWithRamp(media_audio::ScaleToDb(0.1f), zx::msec(5));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
switch (adjustment_control_position()) {
case AdjustmentControlPosition::None:
case AdjustmentControlPosition::Second:
EXPECT_FLOAT_EQ(max_gain_scale, 0.5f);
break;
case AdjustmentControlPosition::First:
// The internal control is ramping, while source and dest are unity.
EXPECT_FLOAT_EQ(max_gain_scale, 1.0f);
break;
}
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsSilent());
}
// Validate when ramp and CalculateScaleArray are identical length.
TEST_F(SourceDestGainTest, CalculateScaleArrayRamp) { TestCalculateScaleArray(); }
TEST_F(DestSourceGainTest, CalculateScaleArrayRamp) { TestCalculateScaleArray(); }
TEST_F(SourceAdjustmentGainTest, CalculateScaleArrayRamp) { TestCalculateScaleArray(); }
TEST_F(AdjustmentSourceGainTest, CalculateScaleArrayRamp) { TestCalculateScaleArray(); }
void GainBase::TestScaleArrayLongRamp() {
Gain::AScale scale_arr[4]; // At 1kHz this is less than the ramp duration.
Gain::AScale expect_arr[4] = {1.000f, 0.901f, 0.802f, 0.703f};
SetGainWithRamp(-40, zx::msec(10));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[0]);
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsSilent());
}
// Validate when ramp duration is greater than CalculateScaleArray.
TEST_F(SourceDestGainTest, CalculateScaleArrayLongRamp) { TestScaleArrayLongRamp(); }
TEST_F(DestSourceGainTest, CalculateScaleArrayLongRamp) { TestScaleArrayLongRamp(); }
void GainBase::TestScaleArrayShortRamp() {
Gain::AScale scale_arr[9]; // At 1kHz this is longer than the ramp duration.
Gain::AScale expect_arr[9] = {1.00f, 0.82f, 0.64f, 0.46f, 0.28f, 0.10f, 0.10f, 0.10f, 0.10f};
SetGainWithRamp(-20, zx::msec(5));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[0]);
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsSilent());
}
// Validate when ramp duration is shorter than CalculateScaleArray.
TEST_F(SourceDestGainTest, CalculateScaleArrayShortRamp) { TestScaleArrayShortRamp(); }
TEST_F(DestSourceGainTest, CalculateScaleArrayShortRamp) { TestScaleArrayShortRamp(); }
void GainBase::TestScaleArrayWithoutAdvance() {
SetGainWithRamp(-123.45678f, zx::msec(9));
Gain::AScale scale_arr[10];
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_FLOAT_EQ(max_gain_scale, media_audio::kUnityGainScale);
Gain::AScale scale_arr2[10];
max_gain_scale = gain_.CalculateScaleArray(scale_arr2, std::size(scale_arr2), rate_1khz_output_);
EXPECT_FLOAT_EQ(max_gain_scale, media_audio::kUnityGainScale);
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), scale_arr2));
}
// Successive CalculateScaleArray calls without Advance should return same results.
TEST_F(SourceDestGainTest, CalculateScaleArrayWithoutAdvance) { TestScaleArrayWithoutAdvance(); }
TEST_F(DestSourceGainTest, CalculateScaleArrayWithoutAdvance) { TestScaleArrayWithoutAdvance(); }
void GainBase::TestScaleArrayBigAdvance() {
Gain::AScale scale_arr[6];
Gain::AScale expect = media_audio::kUnityGainScale * 2;
SetGainWithRamp(6.0205999f, zx::msec(5));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_THAT(scale_arr, Not(Each(FloatEq(expect))));
EXPECT_FLOAT_EQ(max_gain_scale, expect);
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsSilent());
gain_.Advance(rate_1khz_output_.Scale(ZX_SEC(10)), rate_1khz_output_);
max_gain_scale = gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_THAT(scale_arr, Each(FloatEq(expect)));
EXPECT_FLOAT_EQ(max_gain_scale, expect);
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsUnity());
}
// Advances that exceed ramp durations should lead to end-to-ramp conditions.
TEST_F(SourceDestGainTest, CalculateScaleArrayBigAdvance) { TestScaleArrayBigAdvance(); }
TEST_F(DestSourceGainTest, CalculateScaleArrayBigAdvance) { TestScaleArrayBigAdvance(); }
void GainBase::TestRampCompletion() {
Gain::AScale scale_arr[6];
Gain::AScale scale_arr2[6];
constexpr float target_gain_db = -30.1029995f;
const float target_gain_scale = media_audio::DbToScale(target_gain_db);
// With a 5ms duration and 1 frame per ms, scale_arr will perfectly fit
// each frame such that scale_arr[5] == target_gain_scale.
SetGainWithRamp(target_gain_db, zx::msec(5));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_FLOAT_EQ(max_gain_scale, media_audio::kUnityGainScale);
for (size_t k = 0; k < std::size(scale_arr); k++) {
const float diff = media_audio::kUnityGainScale - target_gain_scale;
const float want = media_audio::kUnityGainScale - diff * static_cast<float>(k) / 5.0f;
EXPECT_FLOAT_EQ(want, scale_arr[k]) << "index " << k;
}
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsSilent());
EXPECT_EQ(media_audio::kUnityGainDb, gain_.GetGainDb());
EXPECT_EQ(media_audio::kUnityGainScale, gain_.GetGainScale());
// After clearing the ramp, scale_arr should be constant.
CompleteRamp();
max_gain_scale = gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_THAT(scale_arr, Each(FloatEq(target_gain_scale)));
EXPECT_FLOAT_EQ(max_gain_scale, target_gain_scale);
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_EQ(target_gain_db, gain_.GetGainDb());
EXPECT_EQ(target_gain_scale, gain_.GetGainScale());
EXPECT_FLOAT_EQ(target_gain_db, gain_.GetGainDb());
// Without a ramp, scale_arr should be constant even after Advance.
gain_.Advance(10, rate_1khz_output_);
max_gain_scale = gain_.CalculateScaleArray(scale_arr2, std::size(scale_arr2), rate_1khz_output_);
EXPECT_THAT(scale_arr, Each(FloatEq(target_gain_scale)));
EXPECT_FLOAT_EQ(max_gain_scale, target_gain_scale);
EXPECT_FALSE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsSilent());
EXPECT_EQ(target_gain_db, gain_.GetGainDb());
EXPECT_EQ(target_gain_scale, gain_.GetGainScale());
}
// Completing a ramp should fast-forward any in-process ramps.
TEST_F(SourceDestGainTest, CompleteSourceRamp) { TestRampCompletion(); }
TEST_F(DestSourceGainTest, CompleteDestRamp) { TestRampCompletion(); }
void GainBase::TestAdvanceHalfwayThroughRamp() {
Gain::AScale scale_arr[4]; // At 1kHz this is less than the ramp duration.
Gain::AScale expect_arr[4];
SetGainWithRamp(-20.0f, zx::msec(9));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
EXPECT_FLOAT_EQ(gain_.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_FLOAT_EQ(max_gain_scale, media_audio::kUnityGainScale);
// When comparing buffers, do it within the tolerance of 32-bit float
Gain::AScale expect_scale = media_audio::kUnityGainScale;
for (auto& val : expect_arr) {
val = expect_scale;
expect_scale -= 0.1f;
}
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FALSE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_TRUE(gain_.IsRamping());
// Advance only partially through the duration of the ramp.
const auto kFramesToAdvance = 2;
gain_.Advance(kFramesToAdvance, rate_1khz_output_);
max_gain_scale = gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
expect_scale = expect_arr[kFramesToAdvance];
EXPECT_FLOAT_EQ(expect_scale, gain_.GetGainScale());
EXPECT_FLOAT_EQ(max_gain_scale, expect_scale);
for (auto& val : expect_arr) {
val = expect_scale;
expect_scale -= 0.1f;
}
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_TRUE(gain_.IsRamping());
EXPECT_FALSE(gain_.IsUnity());
EXPECT_FALSE(gain_.IsSilent());
}
// After partial Advance through a ramp, instantaneous gain should be accurate.
TEST_F(SourceDestGainTest, AdvanceHalfwayThroughRamp) { TestAdvanceHalfwayThroughRamp(); }
TEST_F(DestSourceGainTest, AdvanceHalfwayThroughRamp) { TestAdvanceHalfwayThroughRamp(); }
// After partial Advance through a ramp, followed by a second ramp, the second ramp
// ramp should start where the first ramp left off.
void GainBase::TestSuccessiveRamps() {
SetGainWithRamp(-20.0f, zx::msec(10));
auto scale_start = media_audio::kUnityGainScale;
EXPECT_FLOAT_EQ(scale_start, gain_.GetGainScale());
EXPECT_TRUE(gain_.IsRamping());
// Advance only partially through the duration of the ramp.
gain_.Advance(2, rate_1khz_output_); // 1 frame == 1ms
float expect_scale = scale_start + (media_audio::DbToScale(-20.f) - scale_start) * 2.0f / 10.0f;
EXPECT_FLOAT_EQ(expect_scale, gain_.GetGainScale());
EXPECT_TRUE(gain_.IsRamping());
// A new ramp should start at the same spot.
SetGainWithRamp(-80.0f, zx::msec(10));
scale_start = expect_scale;
EXPECT_FLOAT_EQ(expect_scale, gain_.GetGainScale());
EXPECT_TRUE(gain_.IsRamping());
// Advance again.
gain_.Advance(2, rate_1khz_output_);
expect_scale = scale_start + (media_audio::DbToScale(-80.f) - scale_start) * 2.0f / 10.0f;
EXPECT_FLOAT_EQ(expect_scale, gain_.GetGainScale());
EXPECT_TRUE(gain_.IsRamping());
}
// After partial Advance through a ramp, followed by a second ramp, the second ramp
// ramp should start where the first ramp left off.
TEST_F(SourceDestGainTest, TwoRamps) { TestSuccessiveRamps(); }
TEST_F(DestSourceGainTest, TwoRamps) { TestSuccessiveRamps(); }
void GainBase::TestCombinedRamps() {
Gain::AScale scale_arr[11];
{
// Two arbitrary ramps of the same length, starting at the same time
SetGainWithRamp(-20, zx::msec(10));
SetOtherGainWithRamp(+10, zx::msec(10));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
// Source gain ramps linearly from 0 dB (scale 1.0) to -20 dB (0.1)
// Dest gain ramps linearly from 0 dB (1.0) to 10 dB (3.16227766)
//
// source 1.0 0.91000 0.82000 0.73000 0.64000 0.55000 0.46000 0.37000 0.28000 0.19000 0.10000
// dest 1.0 1.22623 1.43246 1.64868 1.86491 2.08114 2.29737 2.51359 2.72982 2.94605 3.16228
//
// These scale values are multiplied to get the following expect_arr
Gain::AScale expect_arr[11] = {
1.0f, 1.1067673f, 1.1746135f, 1.2035388f, 1.1935431f, 1.1446264f,
1.0567886f, 0.9300299f, 0.7643502f, 0.5597495f, 0.3162278f,
};
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[3]);
}
{
// Now check two ramps of differing lengths and start times
SetGain(0.0);
SetOtherGain(-40);
SetGainWithRamp(-80, zx::msec(10));
gain_.Advance(5, rate_1khz_output_);
// At the source-ramp midpoint, source * dest contributions are 0.50005 * 0.01
EXPECT_FLOAT_EQ(gain_.GetGainScale(), 0.005000501f);
SetOtherGainWithRamp(15, zx::msec(7));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
// source ramp continues onward, finalizing at 0.0001 on frame 5. dest ramp ends on frame 7 at
// 5.6234133. They combine for 0.0005623413 which should be set for the remaining array.
Gain::AScale expect_arr[11] = {
0.005000501f, 0.32481519f, 0.48426268f, 0.48334297f,
0.32205606f, 0.00040195809f, 0.00048214971f, 0.00056234133f,
0.00056234133f, 0.00056234133f, 0.00056234133f,
};
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[2]);
}
}
// Test that source-ramping and dest-ramping combines correctly
TEST_F(SourceDestGainTest, CombinedRamps) { TestCombinedRamps(); }
TEST_F(DestSourceGainTest, CombinedRamps) { TestCombinedRamps(); }
void GainBase::TestCrossFades() {
Gain::AScale scale_arr[11];
constexpr float kInitialGainDb1 = -20.0f;
constexpr float kInitialGainDb2 = 0.0f;
constexpr float kGainChangeDb = 8.0f;
for (size_t ramp_length = 4; ramp_length <= 8; ramp_length += 2) {
SCOPED_TRACE("GainBase::TestCrossFades for ramp_length " + std::to_string(ramp_length));
ASSERT_EQ(ramp_length % 2, 0u) << "Test miscalculation - test assumes ramp_length is even";
// We set the two ramps with equal duration and offsetting gain-change.
// Scale-linear crossfading is not equal-power, so although the initial and final gain_db values
// are equal, the intervening values actually rise to a local max at fade's midpoint.
SetGain(kInitialGainDb1);
SetOtherGain(kInitialGainDb2);
SetGainWithRamp(kInitialGainDb1 + kGainChangeDb, zx::msec(ramp_length));
SetOtherGainWithRamp(kInitialGainDb2 - kGainChangeDb, zx::msec(ramp_length));
auto max_gain_scale =
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), rate_1khz_output_);
// scale values are given below for the ramp_length = 4 case:
// source 0.10000000 0.13779716 0.17559432 0.21339148 0.25118864 0.25118864 ...
// dest 1.00000000 0.84952679 0.69905359 0.54858038 0.39810717 0.39810717 ...
// multiplied to get:
// expect 0.10000000 0.11706238 0.12274984 0.11706238 0.10000000 0.10000000 ...
// Rather than comparing strictly, check the logical shape:
// * At either end of the ramps, the gains are equal
EXPECT_FLOAT_EQ(scale_arr[0], media_audio::DbToScale(kInitialGainDb1 + kInitialGainDb2));
EXPECT_FLOAT_EQ(scale_arr[ramp_length], scale_arr[0]);
EXPECT_FLOAT_EQ(max_gain_scale, scale_arr[ramp_length / 2]);
// * Gain increases monotonically to the midpoint of the ramps
EXPECT_GT(scale_arr[ramp_length / 2 - 1], scale_arr[ramp_length / 2 - 2]);
EXPECT_GT(scale_arr[ramp_length / 2], scale_arr[ramp_length / 2 - 1]);
// * Gain decreases monotonically as we move beyond the midpoint of the ramps
EXPECT_GT(scale_arr[ramp_length / 2], scale_arr[ramp_length / 2 + 1]);
EXPECT_GT(scale_arr[ramp_length / 2 + 1], scale_arr[ramp_length / 2 + 2]);
// * The end-ramp gain holds constant to the end of scale_arr
EXPECT_FLOAT_EQ(scale_arr[std::size(scale_arr) - 1], scale_arr[ramp_length]);
}
}
// Check two coincident ramps that offset each other. Because scale-linear ramping is not
// equal-power, the result won't be constant-gain, but it will have a predictable shape.
TEST_F(SourceDestGainTest, CrossFades) { TestCrossFades(); }
TEST_F(DestSourceGainTest, CrossFades) { TestCrossFades(); }
void GainBase::TestScaleArrayForMinScale() {
Gain::AScale scale_arr[6];
// Already below the silence threshold and ramping downward
SetGain(-80.0f);
SetOtherGain(-80.0f);
SetGainWithRamp(-90.0f, zx::msec(5));
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), TimelineRate(1000, ZX_SEC(1)));
EXPECT_THAT(scale_arr, Each(FloatEq(Gain::kMuteScale)));
EXPECT_TRUE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
// Ramping upward, but other stage is below mute threshold
SetGain(10.0f);
SetOtherGain(media_audio::kMinGainDb);
SetGainWithRamp(12.0f, zx::sec(1));
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), TimelineRate(1000, ZX_SEC(1)));
EXPECT_THAT(scale_arr, Each(FloatEq(Gain::kMuteScale)));
EXPECT_TRUE(gain_.IsSilent());
EXPECT_TRUE(gain_.IsRamping());
// Ramping upward, to a target below mute threshold
SetGain(media_audio::kMinGainDb - 5.0f);
SetOtherGain(10.0f);
SetGainWithRamp(media_audio::kMinGainDb, zx::sec(1));
gain_.CalculateScaleArray(scale_arr, std::size(scale_arr), TimelineRate(1000, ZX_SEC(1)));
EXPECT_THAT(scale_arr, Each(FloatEq(Gain::kMuteScale)));
EXPECT_TRUE(gain_.IsSilent());
EXPECT_FALSE(gain_.IsRamping()); // entirely below mute threshold, regardless of other stage
}
// Setting a static gain during ramping should cancel the ramp
TEST_F(SourceDestGainTest, ScaleBelowMinShouldBeMuteScale) { TestScaleArrayForMinScale(); }
TEST_F(DestSourceGainTest, ScaleBelowMinShouldBeMuteScale) { TestScaleArrayForMinScale(); }
// Tests for Set{Min,Max}Gain.
// GetGainDb cannot go lower than .min_gain_db (unless <= media_audio::kMinGainDb)
TEST(GainLimitsTest, LimitedByMinGain) {
Gain gain({
.min_gain_db = -30,
});
gain.SetSourceGain(-20);
gain.SetDestGain(-20);
EXPECT_FLOAT_EQ(gain.GetGainDb(), -30);
EXPECT_FALSE(gain.IsSilent());
}
// GetGainDb cannot go higher than .max_gain_db
TEST(GainLimitsTest, LimitedByMaxGain) {
Gain gain({
.max_gain_db = 3,
});
gain.SetSourceGain(2);
gain.SetDestGain(2);
EXPECT_FLOAT_EQ(gain.GetGainDb(), 3);
}
TEST(GainLimitsTest, AllowedWhenSourceDestInRange) {
Gain gain({
.min_gain_db = -40,
.max_gain_db = -10,
});
gain.SetSourceGain(-15);
gain.SetDestGain(-15);
EXPECT_FLOAT_EQ(gain.GetGainDb(), -30);
}
// Even if dest gain in isolation is less than .min_gain_db,
// gain is only limited if the combined gain is outside the specified limits
TEST(GainLimitsTest, AllowedWhenSourceInRange) {
Gain gain({
.min_gain_db = -10,
.max_gain_db = 10,
});
gain.SetSourceGain(5);
gain.SetDestGain(-11);
EXPECT_FLOAT_EQ(gain.GetGainDb(), -6);
}
// Even if source gain in isolation is less than .min_gain_db,
// gain is only limited if the combined gain is outside the specified limits
TEST(GainLimitsTest, AllowedWhenDestInRange) {
Gain gain({
.min_gain_db = -10,
.max_gain_db = 10,
});
gain.SetSourceGain(-11);
gain.SetDestGain(5);
EXPECT_FLOAT_EQ(gain.GetGainDb(), -6);
}
// Even if source gain and dest gain are both individually greater than .max_gain_db,
// gain is only limited if the combined gain is outside the specified limits
TEST(GainLimitsTest, AllowedWhenSourceDestHigh) {
Gain gain({
.min_gain_db = -20,
.max_gain_db = -10,
});
gain.SetSourceGain(-6);
gain.SetDestGain(-6);
EXPECT_FLOAT_EQ(gain.GetGainDb(), -12);
}
// Even if source gain and dest gain are both individually less than .min_gain_db,
// gain is only limited if the combined gain is outside the specified limits
TEST(GainLimitsTest, AllowedWhenSourceDestLow) {
Gain gain({
.min_gain_db = 5,
.max_gain_db = 10,
});
gain.SetSourceGain(3);
gain.SetDestGain(3);
EXPECT_FLOAT_EQ(gain.GetGainDb(), 6);
}
// The only value below the min_gain limit that can be returned is kMuteScale or
// media_audio::kMinGainDb.
// kMuteScale should be returned if the source gain is less than or equal to
// media_audio::kMinGainDb.
TEST(GainLimitsTest, PreserveSourceMuteGain) {
Gain gain({
.min_gain_db = -10,
});
gain.SetSourceGain(media_audio::kMinGainDb);
EXPECT_FLOAT_EQ(gain.GetGainScale(), Gain::kMuteScale);
EXPECT_FLOAT_EQ(gain.GetGainDb(), media_audio::kMinGainDb);
EXPECT_TRUE(gain.IsSilent());
}
// kMuteScale should be returned if the dest gain is less than or equal to media_audio::kMinGainDb.
TEST(GainLimitsTest, PreserveDestMuteGain) {
Gain gain({
.min_gain_db = -10,
});
gain.SetDestGain(media_audio::kMinGainDb);
EXPECT_FLOAT_EQ(gain.GetGainScale(), Gain::kMuteScale);
EXPECT_FLOAT_EQ(gain.GetGainDb(), media_audio::kMinGainDb);
EXPECT_TRUE(gain.IsSilent());
}
// kMuteScale should be returned if the source mute is set, regardless of source gain
TEST(GainLimitsTest, PreserveSourceMute) {
Gain gain({
.min_gain_db = -10,
});
gain.SetSourceGain(-15);
gain.SetSourceMute(true);
EXPECT_FLOAT_EQ(gain.GetGainScale(), Gain::kMuteScale);
EXPECT_FLOAT_EQ(gain.GetGainDb(), media_audio::kMinGainDb);
EXPECT_TRUE(gain.IsSilent());
}
// A gain-limit range that includes unity gain should allow this, whether by default ctor or by
// combination of source and dest gain values that may individually exceed gain limits.
TEST(GainLimitsTest, PreserveIsUnity) {
Gain gain({
.min_gain_db = -4.0f,
.max_gain_db = 1.0f,
});
EXPECT_FLOAT_EQ(gain.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_FLOAT_EQ(gain.GetGainDb(), media_audio::kUnityGainDb);
EXPECT_TRUE(gain.IsUnity());
// source below the limit, dest above the limit
gain.SetSourceGain(-6.0f);
gain.SetDestGain(6.0f);
EXPECT_FLOAT_EQ(gain.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_FLOAT_EQ(gain.GetGainDb(), media_audio::kUnityGainDb);
EXPECT_TRUE(gain.IsUnity());
// source above the limit, dest below the limit
gain.SetSourceGain(12.0f);
gain.SetDestGain(-12.0f);
EXPECT_FLOAT_EQ(gain.GetGainScale(), media_audio::kUnityGainScale);
EXPECT_FLOAT_EQ(gain.GetGainDb(), media_audio::kUnityGainDb);
EXPECT_TRUE(gain.IsUnity());
}
// A gain-limit range that excludes unity gain should never return kUnityGainDb or kUnityGainScale,
// whether by default ctor or combination of source and dest values.
TEST(GainLimitsTest, PreventIsUnity) {
Gain gain({
.max_gain_db = -5.0f,
});
EXPECT_FLOAT_EQ(gain.GetGainDb(), -5.0f);
EXPECT_FALSE(gain.IsUnity());
gain.SetSourceGain(media_audio::kUnityGainDb);
gain.SetDestGain(media_audio::kUnityGainDb);
EXPECT_FLOAT_EQ(gain.GetGainDb(), -5.0f);
EXPECT_FALSE(gain.IsUnity());
gain.SetSourceGain(media_audio::kUnityGainDb + 1.0f);
gain.SetDestGain(media_audio::kUnityGainDb - 1.0f);
EXPECT_FLOAT_EQ(gain.GetGainDb(), -5.0f);
EXPECT_FALSE(gain.IsUnity());
}
// To simplify the following gain ramp tests, we use frame rate 1kHz: 1 frame per millisec.
// Gain ramping that begins outside gain limits is constrained to the range, even at ramp-start.
// Gain ramping that ends outside gain limits is constrained to the range, thru to ramp-end.
// Source gain (ramping from below gain-limit range, to above gain-limit range) is constrained.
TEST(GainLimitsTest, SourceRampUp) {
Gain::AScale scale_arr[6];
// With no limits, would be: {0.10f, 0.28f, 0.46f, 0.64f, 0.82f, 1.00f};
Gain::AScale expect_arr[6] = {0.30f, 0.30f, 0.46f, 0.64f, 0.80f, 0.80f};
Gain gain({
.min_gain_db = media_audio::ScaleToDb(0.30f),
.max_gain_db = media_audio::ScaleToDb(0.80f),
});
gain.SetSourceGain(-20);
gain.SetSourceGainWithRamp(0, zx::msec(5));
auto max_gain_scale =
gain.CalculateScaleArray(scale_arr, std::size(scale_arr), TimelineRate(1000, ZX_SEC(1)));
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[5]);
EXPECT_TRUE(gain.IsRamping());
}
// Dest gain (ramping from above gain-limit range, to below gain-limit range) is constrained.
TEST(GainLimitsTest, DestRampDown) {
Gain::AScale scale_arr[6];
// With no limits, would be: {1.00f, 0.82f, 0.64f, 0.46f, 0.28f, 0.10f};
Gain::AScale expect_arr[6] = {0.80f, 0.80f, 0.64f, 0.46f, 0.30f, 0.30f};
Gain gain({
.min_gain_db = media_audio::ScaleToDb(0.30f),
.max_gain_db = media_audio::ScaleToDb(0.80f),
});
gain.SetDestGainWithRamp(-20, zx::msec(5));
auto max_gain_scale =
gain.CalculateScaleArray(scale_arr, std::size(scale_arr), TimelineRate(1000, ZX_SEC(1)));
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[0]);
EXPECT_TRUE(gain.IsRamping());
}
// Gain ramping that begins and remains entirely outside gain limits is constrained to range. This
// must still be considered "ramping", because a subsequent change to the companion dest or source
// gain might bring total gain into range, and thus the client must advance the ramp normally.
TEST(GainLimitsTest, SourceRampEntirelyBelowMin) {
Gain gain({
.min_gain_db = -11,
});
gain.SetSourceGain(-15);
gain.SetSourceGainWithRamp(-16, zx::sec(1));
EXPECT_FLOAT_EQ(gain.GetGainDb(), -11);
EXPECT_TRUE(gain.IsRamping());
}
TEST(GainLimitsTest, DestRampEntirelyBelowMin) {
Gain gain({
.min_gain_db = -11,
});
gain.SetDestGain(-15);
gain.SetDestGainWithRamp(-16, zx::sec(1));
EXPECT_FLOAT_EQ(gain.GetGainDb(), -11);
EXPECT_TRUE(gain.IsRamping());
}
// CalculateScaleArray is callable even if no ramp is active; the returned array must obey
// gain-limits.
TEST(GainLimitsTest, GainScaleArrayRespectsMinWhenNotRamping) {
Gain::AScale scale_arr[6];
Gain::AScale expect_arr[6] = {0.2f, 0.2f, 0.2f, 0.2f, 0.2f, 0.2f};
Gain gain({
.min_gain_db = media_audio::ScaleToDb(0.20f),
.max_gain_db = media_audio::ScaleToDb(0.80f),
});
gain.SetSourceGain(media_audio::ScaleToDb(0.1f));
auto max_gain_scale =
gain.CalculateScaleArray(scale_arr, std::size(scale_arr), TimelineRate(1000, ZX_SEC(1)));
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[0]);
EXPECT_FALSE(gain.IsRamping());
}
TEST(GainLimitsTest, GainScaleArrayRespectsMaxWhenNotRamping) {
Gain::AScale scale_arr[6];
Gain::AScale expect_arr[6] = {0.8f, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f};
Gain gain({
.min_gain_db = media_audio::ScaleToDb(0.20f),
.max_gain_db = media_audio::ScaleToDb(0.80f),
});
gain.SetDestGain(media_audio::ScaleToDb(0.9f));
auto max_gain_scale =
gain.CalculateScaleArray(scale_arr, std::size(scale_arr), TimelineRate(1000, ZX_SEC(1)));
EXPECT_THAT(scale_arr, Pointwise(FloatEq(), expect_arr));
EXPECT_FLOAT_EQ(max_gain_scale, expect_arr[0]);
EXPECT_FALSE(gain.IsRamping());
}
} // namespace
} // namespace media::audio