blob: 1d8b8a61bd9437837532bb43c65b0e90aa1fd29d [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/syslog/cpp/macros.h>
#include <lib/zx/time.h>
#include <optional>
#include <utility>
#include <variant>
#include "src/media/audio/lib/processing/gain.h"
namespace media_audio {
void GainControl::Process(zx::time start_reference_time, zx::time end_reference_time,
const Callback& callback) {
FX_CHECK(start_reference_time >= last_advanced_time_)
<< "Process start_reference_time=" << start_reference_time.get()
<< " < last_advanced_time=" << last_advanced_time_.get();
FX_CHECK(start_reference_time < end_reference_time)
<< "Process start_reference_time=" << start_reference_time.get()
<< " >= end_reference_time=" << end_reference_time.get();
// Update initial `state_` by processing scheduled commands up to and including
// `start_reference_time`. This is to make sure that we do *not* miss any state changes that were
// scheduled inorder in the past, even if they were scheduled at a time earlier than
// `start_reference_time`, which can happen due to propagation delays in the processing pipeline.
auto begin = scheduled_commands_.begin();
auto end = scheduled_commands_.upper_bound(start_reference_time);
for (auto it = begin; it != end; ++it) {
const auto& command_time = it->first;
if (active_gain_ramp_ && active_gain_ramp_->end_time <= command_time) {
// Command is past the end of the active gain ramp. Since it is guaranteed that the ramp has
// started at a time `t >= last_processed_gain_command_time_`, we can complete the ramp here.
CompleteActiveGainRamp();
}
ProcessCommand(command_time, it->second);
}
AdvanceStateWithActiveGainRamp(start_reference_time);
// Process immediate commands.
if (immediate_gain_command_) {
ProcessGain(start_reference_time, immediate_gain_command_->gain_db,
immediate_gain_command_->ramp);
immediate_gain_command_ = std::nullopt;
}
if (immediate_mute_command_) {
state_.is_muted = immediate_mute_command_->is_muted;
immediate_mute_command_ = std::nullopt;
}
// Report initial `state_` at `start_reference_time`.
FX_CHECK(callback);
callback(start_reference_time, state_);
// Process the rest of the scheduled commands until `end_reference_time`.
begin = end;
end = scheduled_commands_.lower_bound(end_reference_time);
if (begin != end) {
// We keep track of `callback_time` and `callback_state` in order to minimize the number of
// `callback` triggers. This is done by merging all state changes together for each
// `callback_time`, and report it once via `callback` iff `state_` has been changed from the
// previous `callback_state`.
zx::time callback_time = begin->first;
State callback_state = state_;
for (auto it = begin; it != end; ++it) {
// Trigger `callback` once whenever we move forward in time with an updated `state_`.
const auto& command_time = it->first;
if (command_time > callback_time) {
if (state_ != callback_state) {
callback(callback_time, state_);
callback_state = state_;
}
callback_time = command_time;
}
if (active_gain_ramp_ && active_gain_ramp_->end_time <= command_time) {
// Command is past the end of the active gain ramp.
const auto end_time = active_gain_ramp_->end_time;
CompleteActiveGainRamp();
if (end_time < command_time && state_ != callback_state) {
callback(end_time, state_);
// No need to update `callback_time` again since this is a transition into `command_time`,
// which is already handled above.
callback_state = state_;
}
}
ProcessCommand(command_time, it->second);
}
// Trigger `callback` if `state_` has been changed by the last `callback_time`.
if (state_ != callback_state) {
callback(callback_time, state_);
}
}
// Clean up *all* scheduled commands until `end_reference_time`.
if (scheduled_commands_.begin() != end) {
scheduled_commands_.erase(scheduled_commands_.begin(), end);
}
if (active_gain_ramp_ && active_gain_ramp_->end_time < end_reference_time) {
// Reference time advanced past the active gain ramp.
const auto end_time = active_gain_ramp_->end_time;
CompleteActiveGainRamp();
callback(end_time, state_);
}
last_advanced_time_ = end_reference_time;
}
void GainControl::ScheduleGain(zx::time reference_time, float gain_db,
std::optional<GainRamp> ramp) {
if (reference_time < last_advanced_time_) {
FX_LOGS(WARNING) << "ScheduleGain at reference_time=" << reference_time.get()
<< " < last_advanced_time=" << last_advanced_time_.get();
}
scheduled_commands_.emplace(reference_time, GainCommand{gain_db, ramp});
}
void GainControl::ScheduleMute(zx::time reference_time, bool is_muted) {
if (reference_time < last_advanced_time_) {
FX_LOGS(WARNING) << "ScheduleMute at reference_time=" << reference_time.get()
<< " < last_advanced_time=" << last_advanced_time_.get();
}
scheduled_commands_.emplace(reference_time, MuteCommand{is_muted});
}
void GainControl::SetGain(float gain_db, std::optional<GainRamp> ramp) {
immediate_gain_command_ = GainCommand{gain_db, ramp};
}
void GainControl::SetMute(bool is_muted) { immediate_mute_command_ = MuteCommand{is_muted}; }
void GainControl::AdvanceStateWithActiveGainRamp(zx::time reference_time) {
if (!active_gain_ramp_) {
return;
}
if (const int64_t nsecs_left = (active_gain_ramp_->end_time - reference_time).to_nsecs();
nsecs_left > 0) {
state_.gain_db =
ScaleToDb(DbToScale(active_gain_ramp_->end_gain_db) -
static_cast<float>(nsecs_left) * active_gain_ramp_->linear_scale_slope_per_ns);
} else {
// Active gain ramp ends exactly at `reference_time`, we can complete the ramp here immediately.
CompleteActiveGainRamp();
}
}
void GainControl::CompleteActiveGainRamp() {
state_.gain_db = active_gain_ramp_->end_gain_db;
state_.linear_scale_slope_per_ns = 0.0f;
active_gain_ramp_ = std::nullopt;
}
void GainControl::ProcessCommand(zx::time reference_time, const Command& command) {
if (std::holds_alternative<GainCommand>(command)) {
if (reference_time >= last_processed_gain_command_time_) {
// Make sure that we do *not* override any previously processed gain commands that were
// scheduled at a time later than `reference_time`.
last_processed_gain_command_time_ = reference_time;
const auto& gain_command = std::get<GainCommand>(command);
ProcessGain(reference_time, gain_command.gain_db, gain_command.ramp);
}
} else if (reference_time >= last_processed_mute_command_time_) {
// Make sure that we do *not* override any previously processed mute commands that were
// scheduled at a time later than `reference_time`.
last_processed_mute_command_time_ = reference_time;
state_.is_muted = std::get<MuteCommand>(command).is_muted;
}
}
void GainControl::ProcessGain(zx::time reference_time, float gain_db,
const std::optional<GainRamp>& ramp) {
if (!active_gain_ramp_ && gain_db == state_.gain_db) {
// No state change will occur, we can skip processing further.
return;
}
if (ramp && ramp->duration > zx::nsec(0)) {
// Apply gain with ramp.
FX_CHECK(ramp->type == GainRampType::kLinearScale)
<< "Unknown gain ramp type: " << static_cast<int>(ramp->type);
AdvanceStateWithActiveGainRamp(reference_time);
state_.linear_scale_slope_per_ns = (DbToScale(gain_db) - DbToScale(state_.gain_db)) /
static_cast<float>(ramp->duration.to_nsecs());
active_gain_ramp_ =
ActiveGainRamp{reference_time + ramp->duration, gain_db, state_.linear_scale_slope_per_ns};
} else {
// No gain ramp needed, apply constant gain.
state_.gain_db = gain_db;
state_.linear_scale_slope_per_ns = 0.0f;
active_gain_ramp_ = std::nullopt;
}
}
} // namespace media_audio