blob: 3f21afbe3b3a06ae2d38d3dc7ba9fc242bdb85d0 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/media/audio/lib/processing/gain_control.h"
#include <lib/zx/time.h>
#include <optional>
#include <utility>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/media/audio/lib/processing/gain.h"
namespace media_audio {
namespace {
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::FloatEq;
using ::testing::Matcher;
using ::testing::Pair;
Matcher<const GainControl::State&> StateEq(const GainControl::State& state) {
return AllOf(Field(&GainControl::State::gain_db, FloatEq(state.gain_db)),
Field(&GainControl::State::is_muted, state.is_muted),
Field(&GainControl::State::linear_scale_slope_per_ns,
FloatEq(state.linear_scale_slope_per_ns)));
}
TEST(GainControlTest, ScheduleGain) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Nothing scheduled yet.
gain_control.Process(zx::time(0), zx::time(1), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Schedule gain.
const float gain_db = 2.0f;
gain_control.ScheduleGain(zx::time(5), gain_db);
EXPECT_THAT(states, ElementsAre());
states.clear();
// Process before the scheduled time, gain should not be applied yet.
gain_control.Process(zx::time(1), zx::time(2), callback);
EXPECT_THAT(states, ElementsAre(Pair(1, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Process *just* before the scheduled time, gain should still not be applied.
gain_control.Process(zx::time(2), zx::time(5), callback);
EXPECT_THAT(states, ElementsAre(Pair(2, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Process beyond the scheduled time, gain should be applied now.
gain_control.Process(zx::time(5), zx::time(6), callback);
EXPECT_THAT(states, ElementsAre(Pair(5, StateEq({gain_db, false, 0.0f}))));
states.clear();
// Process further, gain should remain as-is.
gain_control.Process(zx::time(6), zx::time(10), callback);
EXPECT_THAT(states, ElementsAre(Pair(6, StateEq({gain_db, false, 0.0f}))));
}
TEST(GainControlTest, ScheduleGainWithRamp) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Nothing scheduled yet.
gain_control.Process(zx::time(0), zx::time(1), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Schedule gain with ramp.
const float gain_db = ScaleToDb(11.0f);
const zx::duration ramp_duration = zx::nsec(10); // will result in a linear slope of 1.0 per ns.
gain_control.ScheduleGain(zx::time(15), gain_db, GainRamp{ramp_duration});
EXPECT_THAT(states, ElementsAre());
states.clear();
// Process before the scheduled time, gain should not be applied yet.
gain_control.Process(zx::time(1), zx::time(2), callback);
EXPECT_THAT(states, ElementsAre(Pair(1, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Process *just* before the scheduled time, gain should still not be applied.
gain_control.Process(zx::time(2), zx::time(15), callback);
EXPECT_THAT(states, ElementsAre(Pair(2, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Process beyond the scheduled time, ramp should start now.
gain_control.Process(zx::time(15), zx::time(16), callback);
EXPECT_THAT(states, ElementsAre(Pair(15, StateEq({kUnityGainDb, false, 1.0f}))));
states.clear();
// Process further but before the end of the ramp, gain should be updated with the same ramp.
gain_control.Process(zx::time(16), zx::time(17), callback);
EXPECT_THAT(states, ElementsAre(Pair(16, StateEq({ScaleToDb(2.0f), false, 1.0f}))));
states.clear();
// Process *just* before the end of the ramp, gain still should be updated with the same ramp.
gain_control.Process(zx::time(17), zx::time(25), callback);
EXPECT_THAT(states, ElementsAre(Pair(17, StateEq({ScaleToDb(3.0f), false, 1.0f}))));
states.clear();
// Finally process beyond the end of the ramp, which should trigger ramp completion.
gain_control.Process(zx::time(25), zx::time(30), callback);
EXPECT_THAT(states, ElementsAre(Pair(25, StateEq({gain_db, false, 0.0f}))));
}
TEST(GainControlTest, ScheduleGainWithRampWithSingleProcessCall) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Schedule gain with ramp.
const float gain_db = ScaleToDb(11.0f);
const zx::duration ramp_duration = zx::nsec(10); // will result in a linear slope of 1.0 per ns.
gain_control.ScheduleGain(zx::time(15), gain_db, GainRamp{ramp_duration});
// Process beyond the end of the ramp, which should trigger all state changes of the gain ramp.
gain_control.Process(zx::time(0), zx::time(30), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f})),
Pair(15, StateEq({kUnityGainDb, false, 1.0f})),
Pair(25, StateEq({gain_db, false, 0.0f}))));
}
TEST(GainControlTest, ScheduleMute) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Nothing scheduled yet.
gain_control.Process(zx::time(0), zx::time(1), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Schedule mute.
gain_control.ScheduleMute(zx::time(3), true);
EXPECT_THAT(states, ElementsAre());
states.clear();
// Process before the scheduled time, gain should not be applied yet.
gain_control.Process(zx::time(1), zx::time(2), callback);
EXPECT_THAT(states, ElementsAre(Pair(1, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Process *just* before the scheduled time, gain should still not be applied.
gain_control.Process(zx::time(2), zx::time(3), callback);
EXPECT_THAT(states, ElementsAre(Pair(2, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Process beyond the scheduled time, gain should be applied now.
gain_control.Process(zx::time(3), zx::time(5), callback);
EXPECT_THAT(states, ElementsAre(Pair(3, StateEq({kUnityGainDb, true, 0.0f}))));
states.clear();
// Process further, gain should remain as-is.
gain_control.Process(zx::time(5), zx::time(10), callback);
EXPECT_THAT(states, ElementsAre(Pair(5, StateEq({kUnityGainDb, true, 0.0f}))));
}
TEST(GainControlTest, ScheduleBeforeProcessTime) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Nothing scheduled yet.
gain_control.Process(zx::time(0), zx::time(5), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Schedule gain at last advanced time.
gain_control.ScheduleGain(zx::time(5), 1.0f);
gain_control.Process(zx::time(5), zx::time(6), callback);
EXPECT_THAT(states, ElementsAre(Pair(5, StateEq({1.0f, false, 0.0f}))));
states.clear();
// Schedule gain again at the same time, which should be reported at the next start time.
gain_control.ScheduleGain(zx::time(5), 2.0f);
gain_control.Process(zx::time(6), zx::time(7), callback);
EXPECT_THAT(states, ElementsAre(Pair(6, StateEq({2.0f, false, 0.0f}))));
states.clear();
// Schedule mute this time, again with the previous time, which should once again be reported at
// the next start time.
gain_control.ScheduleMute(zx::time(5), true);
gain_control.Process(zx::time(7), zx::time(8), callback);
EXPECT_THAT(states, ElementsAre(Pair(7, StateEq({2.0f, true, 0.0f}))));
states.clear();
}
TEST(GainControlTest, ScheduleBeforeProcessTimeOutOfOrder) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Nothing scheduled yet.
gain_control.Process(zx::time(0), zx::time(10), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Schedule gain events in the past 2 seconds apart in reverse order.
for (int i = 0; i < 4; ++i) {
gain_control.ScheduleGain(zx::time((4 - i) * 2), static_cast<float>(4 - i));
}
// Schedule mute events in the past in between.
for (int i = 0; i < 4; ++i) {
gain_control.ScheduleMute(zx::time(2 * i + 1), i % 2);
}
// Since everything was scheduled in the past, only the latest merged state will be reported.
gain_control.Process(zx::time(10), zx::time(20), callback);
EXPECT_THAT(states, ElementsAre(Pair(10, StateEq({4.0f, true, 0.0f}))));
}
TEST(GainControlTest, ScheduleOutOfOrder) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Schedule gain events in the past 2 seconds apart in reverse order.
for (int i = 0; i < 4; ++i) {
gain_control.ScheduleGain(zx::time((4 - i) * 2), static_cast<float>(4 - i));
}
// Schedule mute events in the past in between.
for (int i = 0; i < 4; ++i) {
gain_control.ScheduleMute(zx::time(2 * i + 1), i % 2);
}
// Schedule two more gain state at the same time of the first two events, which should stay in the
// same order as they were scheduled.
gain_control.ScheduleGain(zx::time(1), -10.0f);
gain_control.ScheduleGain(zx::time(2), -20.0f);
// Process beyond all scheduled events, which should process them all in order.
gain_control.Process(zx::time(0), zx::time(10), callback);
EXPECT_THAT(
states,
ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f})),
Pair(1, StateEq({-10.0f, false, 0.0f})), Pair(2, StateEq({-20.0f, false, 0.0f})),
Pair(3, StateEq({-20.0f, true, 0.0f})), Pair(4, StateEq({2.0f, true, 0.0f})),
Pair(5, StateEq({2.0f, false, 0.0f})), Pair(6, StateEq({3.0f, false, 0.0f})),
Pair(7, StateEq({3.0f, true, 0.0f})), Pair(8, StateEq({4.0f, true, 0.0f}))));
}
TEST(GainControlTest, ScheduleSameGain) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Schedule the same gain multiple times from time 1 to 5.
for (int i = 1; i <= 5; ++i) {
gain_control.ScheduleGain(zx::time(i), 3.5f);
}
// Process beyond all scheduled gains, which shouldd only report a single gain change.
gain_control.Process(zx::time(0), zx::time(10), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f})),
Pair(1, StateEq({3.5f, false, 0.0f}))));
}
TEST(GainControlTest, ScheduleGainDuringRamp) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Schedule constant gain.
gain_control.ScheduleGain(zx::time(0), ScaleToDb(10.0f));
// Schedule another gain with ramp, which should result in a linear slope of -2.0 per ns, from the
// constant gain value of 10.0.
gain_control.ScheduleGain(zx::time(10), ScaleToDb(0.0f), GainRamp{zx::nsec(5)});
// Schedule another gain with ramp during the previous ramp, which should result in a linear slope
// of 1.0 per ns, starting from the midpoint gain value of 4.0 from the previous ramp.
gain_control.ScheduleGain(zx::time(13), ScaleToDb(6.0f), GainRamp{zx::nsec(2)});
// Schedule one more constant gain *just* before the end of the previous ramp.
gain_control.ScheduleGain(zx::time(15), ScaleToDb(8.0f));
// Process beyond all scheduled events, which should process them all.
gain_control.Process(zx::time(0), zx::time(20), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({ScaleToDb(10.0f), false, 0.0f})),
Pair(10, StateEq({ScaleToDb(10.0f), false, -2.0f})),
Pair(13, StateEq({ScaleToDb(4.0f), false, 1.0f})),
Pair(15, StateEq({ScaleToDb(8.0f), false, 0.0f}))));
}
TEST(GainControlTest, SetGainAndMute) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Set gain.
gain_control.SetGain(-6.0f);
gain_control.Process(zx::time(0), zx::time(1), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({-6.0f, false, 0.0f}))));
states.clear();
// Set mute.
gain_control.SetMute(true);
gain_control.Process(zx::time(1), zx::time(2), callback);
EXPECT_THAT(states, ElementsAre(Pair(1, StateEq({-6.0f, true, 0.0f}))));
states.clear();
// Set gain multiple times, where the last setting should override the rest.
for (int i = 1; i <= 4; ++i) {
gain_control.SetGain(static_cast<float>(i));
}
gain_control.Process(zx::time(5), zx::time(10), callback);
EXPECT_THAT(states, ElementsAre(Pair(5, StateEq({4.0f, true, 0.0f}))));
states.clear();
// Toggle mute multiple times, where the last setting should override the rest.
for (int i = 1; i <= 4; ++i) {
gain_control.SetMute(i % 2);
}
gain_control.Process(zx::time(10), zx::time(20), callback);
EXPECT_THAT(states, ElementsAre(Pair(10, StateEq({4.0f, false, 0.0f}))));
}
TEST(GainControlTest, SetGainWithRamp) {
GainControl gain_control;
std::vector<std::pair<int64_t, GainControl::State>> states;
const auto callback = [&](zx::time reference_time, const GainControl::State& state) {
states.emplace_back((reference_time - zx::time(0)).to_nsecs(), state);
};
// Nothing scheduled yet.
gain_control.Process(zx::time(0), zx::time(1), callback);
EXPECT_THAT(states, ElementsAre(Pair(0, StateEq({kUnityGainDb, false, 0.0f}))));
states.clear();
// Set gain with ramp.
const float gain_db = ScaleToDb(6.0f);
const zx::duration ramp_duration = zx::nsec(5); // will result in a linear slope of 1.0 per ns.
gain_control.SetGain(gain_db, GainRamp{ramp_duration});
EXPECT_THAT(states, ElementsAre());
states.clear();
// Process for one second skipping 10 seconds, ramp should start immediately.
gain_control.Process(zx::time(11), zx::time(12), callback);
EXPECT_THAT(states, ElementsAre(Pair(11, StateEq({kUnityGainDb, false, 1.0f}))));
states.clear();
// Process further but before the end of the ramp, gain should be updated with the same ramp.
gain_control.Process(zx::time(12), zx::time(14), callback);
EXPECT_THAT(states, ElementsAre(Pair(12, StateEq({ScaleToDb(2.0f), false, 1.0f}))));
states.clear();
// Process beyond the end of the ramp, which should trigger ramp completion.
gain_control.Process(zx::time(14), zx::time(20), callback);
EXPECT_THAT(states, ElementsAre(Pair(14, StateEq({ScaleToDb(4.0f), false, 1.0f})),
Pair(16, StateEq({ScaleToDb(6.0f), false, 0.0f}))));
}
} // namespace
} // namespace media_audio