// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include "ash/system/video_conference/bubble/mic_indicator.h"
#include <cmath>
#include <iomanip>
#include <iostream>
#include "ash/style/ash_color_id.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "base/timer/timer.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
namespace ash::video_conference {
namespace {
const int kIndicatorLines = 4;
const int kIndicatorSpace = 2;
const int kIndicatorWidth = 2;
const int kIndicatorTotalWidth =
kIndicatorLines * kIndicatorWidth + (kIndicatorLines - 1) * kIndicatorSpace;
const float kIndicatorLengths[] = {0.3, 0.8, 0.5, 0.75};
// Powers above kLogEwmaMax will be restricted to this value.
const float kLogEwmaMax = std::log(0.02);
// Powers below kLogEwmaMin will be restricted to this value.
const float kLogEwmaMin = std::log(0.00002);
const float kLogEwmaDiff = kLogEwmaMax - kLogEwmaMin;
constexpr int kMaxStep = 8;
constexpr float phaseLengths[] = {1.0, 1.1, 1.3, 1.1, 1.0, 0.9, 0.7, 0.9};
constexpr auto kMicIndicatorInsets = gfx::Insets::TLBR(16, 16, 16, 16);
float ScalePower(float power) {
// Adjust the power on a logarithmic scale, allowing for more noticeable
// changes at lower volumes.
float log_value = std::log(power);
if (log_value > kLogEwmaMax) {
log_value = kLogEwmaMax;
}
if (log_value < kLogEwmaMin) {
log_value = kLogEwmaMin;
}
float normalized = (log_value - kLogEwmaMin) / (kLogEwmaDiff);
return 0.1 + normalized * (1.0 - 0.1);
}
} // namespace
MicIndicator::MicIndicator() {
VideoConferenceTrayController::Get()->SetEwmaPowerReportEnabled(true);
SetInsideBorderInsets(kMicIndicatorInsets);
power_ = VideoConferenceTrayController::Get()->GetEwmaPower();
step_ = 0;
color_ = cros_tokens::kCrosSysDisabledOpaque;
timer_ = std::make_unique<base::RepeatingTimer>();
timer_->Start(FROM_HERE, base::Milliseconds(30),
base::BindRepeating(&MicIndicator::UpdateProgress,
base::Unretained(this)));
}
MicIndicator::~MicIndicator() {
// Disable ewma power reporting when the view is destructed, so CRAS
// doesn't report unnecessary data.
VideoConferenceTrayController::Get()->SetEwmaPowerReportEnabled(false);
}
void MicIndicator::UpdateProgress() {
step_ = (step_ + 1) % kMaxStep;
if (step_ == 0) {
bool sidetone_enabled =
VideoConferenceTrayController::Get()->GetSidetoneEnabled();
color_ = sidetone_enabled ? cros_tokens::kCrosSysPrimary
: cros_tokens::kCrosSysDisabledOpaque;
power_ = VideoConferenceTrayController::Get()->GetEwmaPower();
}
SchedulePaint();
}
void MicIndicator::OnPaint(gfx::Canvas* canvas) {
const float multiplier = ScalePower(power_);
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStrokeWidth(kIndicatorWidth);
flags.setColor(GetColorProvider()->GetColor(color_));
flags.setStyle(cc::PaintFlags::kStroke_Style);
SkPath path;
const int view_height = GetContentsBounds().height();
const int view_width = GetContentsBounds().width();
float x = (view_width - kIndicatorTotalWidth) / 2;
for (int i = 0; i < kIndicatorLines; i++) {
float length = view_height *
// Set each line's animation cycle based on the previous
// line, creating a wave-like effect.
phaseLengths[(step_ + kMaxStep - i) % kMaxStep] *
kIndicatorLengths[i];
length = length * multiplier;
float y0 = (view_height - length) / 2;
float y1 = y0 + length;
path.moveTo(x, y0);
path.lineTo(x, y1);
canvas->DrawPath(path, flags);
x += kIndicatorSpace + static_cast<int>(flags.getStrokeWidth());
}
}
BEGIN_METADATA(MicIndicator)
END_METADATA
} // namespace ash::video_conference