1 /*
2 * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
10
11 #include "modules/audio_processing/agc2/input_volume_controller.h"
12
13 #include <algorithm>
14 #include <cmath>
15
16 #include "api/array_view.h"
17 #include "modules/audio_processing/agc2/gain_map_internal.h"
18 #include "modules/audio_processing/include/audio_frame_view.h"
19 #include "rtc_base/checks.h"
20 #include "rtc_base/logging.h"
21 #include "rtc_base/numerics/safe_minmax.h"
22 #include "system_wrappers/include/field_trial.h"
23 #include "system_wrappers/include/metrics.h"
24
25 namespace webrtc {
26
27 namespace {
28
29 // Amount of error we tolerate in the microphone input volume (presumably due to
30 // OS quantization) before we assume the user has manually adjusted the volume.
31 constexpr int kVolumeQuantizationSlack = 25;
32
33 constexpr int kMaxInputVolume = 255;
34 static_assert(kGainMapSize > kMaxInputVolume, "gain map too small");
35
36 // Maximum absolute RMS error.
37 constexpr int KMaxAbsRmsErrorDbfs = 15;
38 static_assert(KMaxAbsRmsErrorDbfs > 0, "");
39
40 using Agc1ClippingPredictorConfig = AudioProcessing::Config::GainController1::
41 AnalogGainController::ClippingPredictor;
42
43 // TODO(webrtc:7494): Hardcode clipping predictor parameters and remove this
44 // function after no longer needed in the ctor.
CreateClippingPredictorConfig(bool enabled)45 Agc1ClippingPredictorConfig CreateClippingPredictorConfig(bool enabled) {
46 Agc1ClippingPredictorConfig config;
47 config.enabled = enabled;
48
49 return config;
50 }
51
52 // Returns the minimum input volume to recommend.
53 // If the "WebRTC-Audio-Agc2-MinInputVolume" field trial is specified, parses it
54 // and returns the value specified after "Enabled-" if valid - i.e., in the
55 // range 0-255. Otherwise returns the default value.
56 // Example:
57 // "WebRTC-Audio-Agc2-MinInputVolume/Enabled-80" => returns 80.
GetMinInputVolume()58 int GetMinInputVolume() {
59 constexpr int kDefaultMinInputVolume = 12;
60 constexpr char kFieldTrial[] = "WebRTC-Audio-Agc2-MinInputVolume";
61 if (!webrtc::field_trial::IsEnabled(kFieldTrial)) {
62 return kDefaultMinInputVolume;
63 }
64 std::string field_trial_str = webrtc::field_trial::FindFullName(kFieldTrial);
65 int min_input_volume = -1;
66 sscanf(field_trial_str.c_str(), "Enabled-%d", &min_input_volume);
67 if (min_input_volume >= 0 && min_input_volume <= 255) {
68 return min_input_volume;
69 }
70 RTC_LOG(LS_WARNING) << "[AGC2] Invalid volume for " << kFieldTrial
71 << ", ignored.";
72 return kDefaultMinInputVolume;
73 }
74
75 // Returns an input volume in the [`min_input_volume`, `kMaxInputVolume`] range
76 // that reduces `gain_error_db`, which is a gain error estimated when
77 // `input_volume` was applied, according to a fixed gain map.
ComputeVolumeUpdate(int gain_error_db,int input_volume,int min_input_volume)78 int ComputeVolumeUpdate(int gain_error_db,
79 int input_volume,
80 int min_input_volume) {
81 RTC_DCHECK_GE(input_volume, 0);
82 RTC_DCHECK_LE(input_volume, kMaxInputVolume);
83 if (gain_error_db == 0) {
84 return input_volume;
85 }
86
87 int new_volume = input_volume;
88 if (gain_error_db > 0) {
89 while (kGainMap[new_volume] - kGainMap[input_volume] < gain_error_db &&
90 new_volume < kMaxInputVolume) {
91 ++new_volume;
92 }
93 } else {
94 while (kGainMap[new_volume] - kGainMap[input_volume] > gain_error_db &&
95 new_volume > min_input_volume) {
96 --new_volume;
97 }
98 }
99 return new_volume;
100 }
101
102 // Returns the proportion of samples in the buffer which are at full-scale
103 // (and presumably clipped).
ComputeClippedRatio(const float * const * audio,size_t num_channels,size_t samples_per_channel)104 float ComputeClippedRatio(const float* const* audio,
105 size_t num_channels,
106 size_t samples_per_channel) {
107 RTC_DCHECK_GT(samples_per_channel, 0);
108 int num_clipped = 0;
109 for (size_t ch = 0; ch < num_channels; ++ch) {
110 int num_clipped_in_ch = 0;
111 for (size_t i = 0; i < samples_per_channel; ++i) {
112 RTC_DCHECK(audio[ch]);
113 if (audio[ch][i] >= 32767.0f || audio[ch][i] <= -32768.0f) {
114 ++num_clipped_in_ch;
115 }
116 }
117 num_clipped = std::max(num_clipped, num_clipped_in_ch);
118 }
119 return static_cast<float>(num_clipped) / (samples_per_channel);
120 }
121
LogClippingMetrics(int clipping_rate)122 void LogClippingMetrics(int clipping_rate) {
123 RTC_LOG(LS_INFO) << "[AGC2] Input clipping rate: " << clipping_rate << "%";
124 RTC_HISTOGRAM_COUNTS_LINEAR(/*name=*/"WebRTC.Audio.Agc.InputClippingRate",
125 /*sample=*/clipping_rate, /*min=*/0, /*max=*/100,
126 /*bucket_count=*/50);
127 }
128
129 // Compares `speech_level_dbfs` to the [`target_range_min_dbfs`,
130 // `target_range_max_dbfs`] range and returns the error to be compensated via
131 // input volume adjustment. Returns a positive value when the level is below
132 // the range, a negative value when the level is above the range, zero
133 // otherwise.
GetSpeechLevelRmsErrorDb(float speech_level_dbfs,int target_range_min_dbfs,int target_range_max_dbfs)134 int GetSpeechLevelRmsErrorDb(float speech_level_dbfs,
135 int target_range_min_dbfs,
136 int target_range_max_dbfs) {
137 constexpr float kMinSpeechLevelDbfs = -90.0f;
138 constexpr float kMaxSpeechLevelDbfs = 30.0f;
139 RTC_DCHECK_GE(speech_level_dbfs, kMinSpeechLevelDbfs);
140 RTC_DCHECK_LE(speech_level_dbfs, kMaxSpeechLevelDbfs);
141 speech_level_dbfs = rtc::SafeClamp<float>(
142 speech_level_dbfs, kMinSpeechLevelDbfs, kMaxSpeechLevelDbfs);
143
144 int rms_error_db = 0;
145 if (speech_level_dbfs > target_range_max_dbfs) {
146 rms_error_db = std::round(target_range_max_dbfs - speech_level_dbfs);
147 } else if (speech_level_dbfs < target_range_min_dbfs) {
148 rms_error_db = std::round(target_range_min_dbfs - speech_level_dbfs);
149 }
150
151 return rms_error_db;
152 }
153
154 } // namespace
155
MonoInputVolumeController(int min_input_volume_after_clipping,int min_input_volume,int update_input_volume_wait_frames,float speech_probability_threshold,float speech_ratio_threshold)156 MonoInputVolumeController::MonoInputVolumeController(
157 int min_input_volume_after_clipping,
158 int min_input_volume,
159 int update_input_volume_wait_frames,
160 float speech_probability_threshold,
161 float speech_ratio_threshold)
162 : min_input_volume_(min_input_volume),
163 min_input_volume_after_clipping_(min_input_volume_after_clipping),
164 max_input_volume_(kMaxInputVolume),
165 update_input_volume_wait_frames_(
166 std::max(update_input_volume_wait_frames, 1)),
167 speech_probability_threshold_(speech_probability_threshold),
168 speech_ratio_threshold_(speech_ratio_threshold) {
169 RTC_DCHECK_GE(min_input_volume_, 0);
170 RTC_DCHECK_LE(min_input_volume_, 255);
171 RTC_DCHECK_GE(min_input_volume_after_clipping_, 0);
172 RTC_DCHECK_LE(min_input_volume_after_clipping_, 255);
173 RTC_DCHECK_GE(max_input_volume_, 0);
174 RTC_DCHECK_LE(max_input_volume_, 255);
175 RTC_DCHECK_GE(update_input_volume_wait_frames_, 0);
176 RTC_DCHECK_GE(speech_probability_threshold_, 0.0f);
177 RTC_DCHECK_LE(speech_probability_threshold_, 1.0f);
178 RTC_DCHECK_GE(speech_ratio_threshold_, 0.0f);
179 RTC_DCHECK_LE(speech_ratio_threshold_, 1.0f);
180 }
181
182 MonoInputVolumeController::~MonoInputVolumeController() = default;
183
Initialize()184 void MonoInputVolumeController::Initialize() {
185 max_input_volume_ = kMaxInputVolume;
186 capture_output_used_ = true;
187 check_volume_on_next_process_ = true;
188 frames_since_update_input_volume_ = 0;
189 speech_frames_since_update_input_volume_ = 0;
190 is_first_frame_ = true;
191 }
192
193 // A speeh segment is considered active if at least
194 // `update_input_volume_wait_frames_` new frames have been processed since the
195 // previous update and the ratio of non-silence frames (i.e., frames with a
196 // `speech_probability` higher than `speech_probability_threshold_`) is at least
197 // `speech_ratio_threshold_`.
Process(absl::optional<int> rms_error_db,float speech_probability)198 void MonoInputVolumeController::Process(absl::optional<int> rms_error_db,
199 float speech_probability) {
200 if (check_volume_on_next_process_) {
201 check_volume_on_next_process_ = false;
202 // We have to wait until the first process call to check the volume,
203 // because Chromium doesn't guarantee it to be valid any earlier.
204 CheckVolumeAndReset();
205 }
206
207 // Count frames with a high speech probability as speech.
208 if (speech_probability >= speech_probability_threshold_) {
209 ++speech_frames_since_update_input_volume_;
210 }
211
212 // Reset the counters and maybe update the input volume.
213 if (++frames_since_update_input_volume_ >= update_input_volume_wait_frames_) {
214 const float speech_ratio =
215 static_cast<float>(speech_frames_since_update_input_volume_) /
216 static_cast<float>(update_input_volume_wait_frames_);
217
218 // Always reset the counters regardless of whether the volume changes or
219 // not.
220 frames_since_update_input_volume_ = 0;
221 speech_frames_since_update_input_volume_ = 0;
222
223 // Update the input volume if allowed.
224 if (!is_first_frame_ && speech_ratio >= speech_ratio_threshold_) {
225 if (rms_error_db.has_value()) {
226 UpdateInputVolume(*rms_error_db);
227 }
228 }
229 }
230
231 is_first_frame_ = false;
232 }
233
HandleClipping(int clipped_level_step)234 void MonoInputVolumeController::HandleClipping(int clipped_level_step) {
235 RTC_DCHECK_GT(clipped_level_step, 0);
236 // Always decrease the maximum input volume, even if the current input volume
237 // is below threshold.
238 SetMaxLevel(std::max(min_input_volume_after_clipping_,
239 max_input_volume_ - clipped_level_step));
240 if (log_to_histograms_) {
241 RTC_HISTOGRAM_BOOLEAN(
242 "WebRTC.Audio.AgcClippingAdjustmentAllowed",
243 input_volume_ - clipped_level_step >= min_input_volume_after_clipping_);
244 }
245 if (input_volume_ > min_input_volume_after_clipping_) {
246 // Don't try to adjust the input volume if we're already below the limit. As
247 // a consequence, if the user has brought the input volume above the limit,
248 // we will still not react until the postproc updates the input volume.
249 SetInputVolume(std::max(min_input_volume_after_clipping_,
250 input_volume_ - clipped_level_step));
251 frames_since_update_input_volume_ = 0;
252 speech_frames_since_update_input_volume_ = 0;
253 is_first_frame_ = false;
254 }
255 }
256
SetInputVolume(int new_volume)257 void MonoInputVolumeController::SetInputVolume(int new_volume) {
258 int applied_input_volume = recommended_input_volume_;
259 if (applied_input_volume == 0) {
260 RTC_DLOG(LS_INFO)
261 << "[AGC2] The applied input volume is zero, taking no action.";
262 return;
263 }
264 if (applied_input_volume < 0 || applied_input_volume > kMaxInputVolume) {
265 RTC_LOG(LS_ERROR) << "[AGC2] Invalid value for the applied input volume: "
266 << applied_input_volume;
267 return;
268 }
269
270 // Detect manual input volume adjustments by checking if the
271 // `applied_input_volume` is outside of the `[input_volume_ -
272 // kVolumeQuantizationSlack, input_volume_ + kVolumeQuantizationSlack]` range.
273 if (applied_input_volume > input_volume_ + kVolumeQuantizationSlack ||
274 applied_input_volume < input_volume_ - kVolumeQuantizationSlack) {
275 RTC_DLOG(LS_INFO)
276 << "[AGC2] The input volume was manually adjusted. Updating "
277 "stored input volume from "
278 << input_volume_ << " to " << applied_input_volume;
279 input_volume_ = applied_input_volume;
280 // Always allow the user to increase the volume.
281 if (input_volume_ > max_input_volume_) {
282 SetMaxLevel(input_volume_);
283 }
284 // Take no action in this case, since we can't be sure when the volume
285 // was manually adjusted.
286 frames_since_update_input_volume_ = 0;
287 speech_frames_since_update_input_volume_ = 0;
288 is_first_frame_ = false;
289 return;
290 }
291
292 new_volume = std::min(new_volume, max_input_volume_);
293 if (new_volume == input_volume_) {
294 return;
295 }
296
297 recommended_input_volume_ = new_volume;
298 RTC_DLOG(LS_INFO) << "[AGC2] Applied input volume: " << applied_input_volume
299 << " | last recommended input volume: " << input_volume_
300 << " | newly recommended input volume: " << new_volume;
301 input_volume_ = new_volume;
302 }
303
SetMaxLevel(int input_volume)304 void MonoInputVolumeController::SetMaxLevel(int input_volume) {
305 RTC_DCHECK_GE(input_volume, min_input_volume_after_clipping_);
306 max_input_volume_ = input_volume;
307 RTC_DLOG(LS_INFO) << "[AGC2] Maximum input volume updated: "
308 << max_input_volume_;
309 }
310
HandleCaptureOutputUsedChange(bool capture_output_used)311 void MonoInputVolumeController::HandleCaptureOutputUsedChange(
312 bool capture_output_used) {
313 if (capture_output_used_ == capture_output_used) {
314 return;
315 }
316 capture_output_used_ = capture_output_used;
317
318 if (capture_output_used) {
319 // When we start using the output, we should reset things to be safe.
320 check_volume_on_next_process_ = true;
321 }
322 }
323
CheckVolumeAndReset()324 int MonoInputVolumeController::CheckVolumeAndReset() {
325 int input_volume = recommended_input_volume_;
326 // Reasons for taking action at startup:
327 // 1) A person starting a call is expected to be heard.
328 // 2) Independent of interpretation of `input_volume` == 0 we should raise it
329 // so the AGC can do its job properly.
330 if (input_volume == 0 && !startup_) {
331 RTC_DLOG(LS_INFO)
332 << "[AGC2] The applied input volume is zero, taking no action.";
333 return 0;
334 }
335 if (input_volume < 0 || input_volume > kMaxInputVolume) {
336 RTC_LOG(LS_ERROR) << "[AGC2] Invalid value for the applied input volume: "
337 << input_volume;
338 return -1;
339 }
340 RTC_DLOG(LS_INFO) << "[AGC2] Initial input volume: " << input_volume;
341
342 if (input_volume < min_input_volume_) {
343 input_volume = min_input_volume_;
344 RTC_DLOG(LS_INFO)
345 << "[AGC2] The initial input volume is too low, raising to "
346 << input_volume;
347 recommended_input_volume_ = input_volume;
348 }
349
350 input_volume_ = input_volume;
351 startup_ = false;
352 frames_since_update_input_volume_ = 0;
353 speech_frames_since_update_input_volume_ = 0;
354 is_first_frame_ = true;
355
356 return 0;
357 }
358
UpdateInputVolume(int rms_error_db)359 void MonoInputVolumeController::UpdateInputVolume(int rms_error_db) {
360 RTC_DLOG(LS_INFO) << "[AGC2] RMS error: " << rms_error_db << " dB";
361 // Prevent too large microphone input volume changes by clamping the RMS
362 // error.
363 rms_error_db =
364 rtc::SafeClamp(rms_error_db, -KMaxAbsRmsErrorDbfs, KMaxAbsRmsErrorDbfs);
365 if (rms_error_db == 0) {
366 return;
367 }
368 SetInputVolume(
369 ComputeVolumeUpdate(rms_error_db, input_volume_, min_input_volume_));
370 }
371
InputVolumeController(int num_capture_channels,const Config & config)372 InputVolumeController::InputVolumeController(int num_capture_channels,
373 const Config& config)
374 : num_capture_channels_(num_capture_channels),
375 min_input_volume_(GetMinInputVolume()),
376 capture_output_used_(true),
377 clipped_level_step_(config.clipped_level_step),
378 clipped_ratio_threshold_(config.clipped_ratio_threshold),
379 clipped_wait_frames_(config.clipped_wait_frames),
380 clipping_predictor_(CreateClippingPredictor(
381 num_capture_channels,
382 CreateClippingPredictorConfig(config.enable_clipping_predictor))),
383 use_clipping_predictor_step_(
384 !!clipping_predictor_ &&
385 CreateClippingPredictorConfig(config.enable_clipping_predictor)
386 .use_predicted_step),
387 frames_since_clipped_(config.clipped_wait_frames),
388 clipping_rate_log_counter_(0),
389 clipping_rate_log_(0.0f),
390 target_range_max_dbfs_(config.target_range_max_dbfs),
391 target_range_min_dbfs_(config.target_range_min_dbfs),
392 channel_controllers_(num_capture_channels) {
393 RTC_LOG(LS_INFO)
394 << "[AGC2] Input volume controller enabled. Minimum input volume: "
395 << min_input_volume_;
396
397 for (auto& controller : channel_controllers_) {
398 controller = std::make_unique<MonoInputVolumeController>(
399 config.clipped_level_min, min_input_volume_,
400 config.update_input_volume_wait_frames,
401 config.speech_probability_threshold, config.speech_ratio_threshold);
402 }
403
404 RTC_DCHECK(!channel_controllers_.empty());
405 RTC_DCHECK_GT(clipped_level_step_, 0);
406 RTC_DCHECK_LE(clipped_level_step_, 255);
407 RTC_DCHECK_GT(clipped_ratio_threshold_, 0.0f);
408 RTC_DCHECK_LT(clipped_ratio_threshold_, 1.0f);
409 RTC_DCHECK_GT(clipped_wait_frames_, 0);
410 channel_controllers_[0]->ActivateLogging();
411 }
412
~InputVolumeController()413 InputVolumeController::~InputVolumeController() {}
414
Initialize()415 void InputVolumeController::Initialize() {
416 for (auto& controller : channel_controllers_) {
417 controller->Initialize();
418 }
419 capture_output_used_ = true;
420
421 AggregateChannelLevels();
422 clipping_rate_log_ = 0.0f;
423 clipping_rate_log_counter_ = 0;
424 }
425
AnalyzePreProcess(const AudioBuffer & audio_buffer)426 void InputVolumeController::AnalyzePreProcess(const AudioBuffer& audio_buffer) {
427 const float* const* audio = audio_buffer.channels_const();
428 size_t samples_per_channel = audio_buffer.num_frames();
429 RTC_DCHECK(audio);
430
431 AggregateChannelLevels();
432 if (!capture_output_used_) {
433 return;
434 }
435
436 if (!!clipping_predictor_) {
437 AudioFrameView<const float> frame = AudioFrameView<const float>(
438 audio, num_capture_channels_, static_cast<int>(samples_per_channel));
439 clipping_predictor_->Analyze(frame);
440 }
441
442 // Check for clipped samples. We do this in the preprocessing phase in order
443 // to catch clipped echo as well.
444 //
445 // If we find a sufficiently clipped frame, drop the current microphone
446 // input volume and enforce a new maximum input volume, dropped the same
447 // amount from the current maximum. This harsh treatment is an effort to avoid
448 // repeated clipped echo events.
449 float clipped_ratio =
450 ComputeClippedRatio(audio, num_capture_channels_, samples_per_channel);
451 clipping_rate_log_ = std::max(clipped_ratio, clipping_rate_log_);
452 clipping_rate_log_counter_++;
453 constexpr int kNumFramesIn30Seconds = 3000;
454 if (clipping_rate_log_counter_ == kNumFramesIn30Seconds) {
455 LogClippingMetrics(std::round(100.0f * clipping_rate_log_));
456 clipping_rate_log_ = 0.0f;
457 clipping_rate_log_counter_ = 0;
458 }
459
460 if (frames_since_clipped_ < clipped_wait_frames_) {
461 ++frames_since_clipped_;
462 return;
463 }
464
465 const bool clipping_detected = clipped_ratio > clipped_ratio_threshold_;
466 bool clipping_predicted = false;
467 int predicted_step = 0;
468 if (!!clipping_predictor_) {
469 for (int channel = 0; channel < num_capture_channels_; ++channel) {
470 const auto step = clipping_predictor_->EstimateClippedLevelStep(
471 channel, recommended_input_volume_, clipped_level_step_,
472 channel_controllers_[channel]->min_input_volume_after_clipping(),
473 kMaxInputVolume);
474 if (step.has_value()) {
475 predicted_step = std::max(predicted_step, step.value());
476 clipping_predicted = true;
477 }
478 }
479 }
480
481 if (clipping_detected) {
482 RTC_DLOG(LS_INFO) << "[AGC2] Clipping detected (ratio: " << clipped_ratio
483 << ")";
484 }
485
486 int step = clipped_level_step_;
487 if (clipping_predicted) {
488 predicted_step = std::max(predicted_step, clipped_level_step_);
489 RTC_DLOG(LS_INFO) << "[AGC2] Clipping predicted (volume down step: "
490 << predicted_step << ")";
491 if (use_clipping_predictor_step_) {
492 step = predicted_step;
493 }
494 }
495
496 if (clipping_detected ||
497 (clipping_predicted && use_clipping_predictor_step_)) {
498 for (auto& state_ch : channel_controllers_) {
499 state_ch->HandleClipping(step);
500 }
501 frames_since_clipped_ = 0;
502 if (!!clipping_predictor_) {
503 clipping_predictor_->Reset();
504 }
505 }
506
507 AggregateChannelLevels();
508 }
509
Process(float speech_probability,absl::optional<float> speech_level_dbfs)510 void InputVolumeController::Process(float speech_probability,
511 absl::optional<float> speech_level_dbfs) {
512 AggregateChannelLevels();
513
514 if (!capture_output_used_) {
515 return;
516 }
517
518 absl::optional<int> rms_error_db;
519 if (speech_level_dbfs.has_value()) {
520 // Compute the error for all frames (both speech and non-speech frames).
521 rms_error_db = GetSpeechLevelRmsErrorDb(
522 *speech_level_dbfs, target_range_min_dbfs_, target_range_max_dbfs_);
523 }
524
525 for (auto& controller : channel_controllers_) {
526 controller->Process(rms_error_db, speech_probability);
527 }
528
529 AggregateChannelLevels();
530 }
531
HandleCaptureOutputUsedChange(bool capture_output_used)532 void InputVolumeController::HandleCaptureOutputUsedChange(
533 bool capture_output_used) {
534 for (auto& controller : channel_controllers_) {
535 controller->HandleCaptureOutputUsedChange(capture_output_used);
536 }
537
538 capture_output_used_ = capture_output_used;
539 }
540
set_stream_analog_level(int input_volume)541 void InputVolumeController::set_stream_analog_level(int input_volume) {
542 for (auto& controller : channel_controllers_) {
543 controller->set_stream_analog_level(input_volume);
544 }
545
546 AggregateChannelLevels();
547 }
548
AggregateChannelLevels()549 void InputVolumeController::AggregateChannelLevels() {
550 int new_recommended_input_volume =
551 channel_controllers_[0]->recommended_analog_level();
552 channel_controlling_gain_ = 0;
553 for (size_t ch = 1; ch < channel_controllers_.size(); ++ch) {
554 int input_volume = channel_controllers_[ch]->recommended_analog_level();
555 if (input_volume < new_recommended_input_volume) {
556 new_recommended_input_volume = input_volume;
557 channel_controlling_gain_ = static_cast<int>(ch);
558 }
559 }
560
561 // Enforce the minimum input volume when a recommendation is made.
562 if (new_recommended_input_volume > 0) {
563 new_recommended_input_volume =
564 std::max(new_recommended_input_volume, min_input_volume_);
565 }
566
567 recommended_input_volume_ = new_recommended_input_volume;
568 }
569
570 } // namespace webrtc
571