1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151

media / base / audio_limiter.h [blame]

// 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.

#ifndef MEDIA_BASE_AUDIO_LIMITER_H_
#define MEDIA_BASE_AUDIO_LIMITER_H_

#include "base/containers/circular_deque.h"
#include "base/containers/span.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "base/moving_window.h"
#include "media/base/audio_bus.h"
#include "media/base/media_export.h"

namespace media {

// Simple limiter which reduces gain from an input in order to prevent the
// output from exceeding the [-1.0, 1.0] range. This limiter uses a fixed attack
// and release, a hard knee, and a "1 : infinity" gain reduction ratio. It
// incurrs a delay of 5ms (based on its attack time in the .cc file), which
// is used to "look ahead" and to smooth gain changes.
//
// Flush() must be called for the last inputs to be written out. The limiter
// will not be usable after that point.
//
// Note: The gain reduction is "linked" across channels. One very loud channel
//       will result in all channels being compressed equally.
//
// Note: All outputs are clamped to [-1.0, 1.0], post gain reduction. From
//       experimenting with UTs, only peaks exceed this range (due to floating
//       point arithmetics) by an order of 10^-6. Clamping these values should
//       not introduce audible artifacts.
class MEDIA_EXPORT AudioLimiter {
 public:
  using OutputChannels = std::vector<base::span<uint8_t>>;
  using OutputFilledCB = base::OnceClosure;

  AudioLimiter(int sample_rate, int channels);
  ~AudioLimiter();

  AudioLimiter(const AudioLimiter&) = delete;
  AudioLimiter& operator=(const AudioLimiter&) = delete;

  // Fills `output_channels` with the gain adjusted values from
  // `input_channels`, reducing gain when necessary so output samples fit into
  // the [-1.0, 1.0] range. `on_output_filled_cb` will be synchronously be run
  // during a future call to LimitPeaks() or Flush(), once an additional
  // `attack_frames_` have been processed, and `output_channels` has been fully
  // written to.
  //
  // `input_channels` and `output_channels` must contain the same number of
  // channels, and have the same size in bytes.
  //
  // Note: Due to floating point precision rounding errors, the gain adjusted
  //       peak values sometime exceed the [-1.0, 1.0] range by less than 1E-6.
  //       We clamp all output to this range, which should not be audible at
  //       all, considering that this clamping reduces gain by one millionth of
  //       a decibel.
  //
  // Note: Cannot be called once Flush() has been called.
  void LimitPeaks(const AudioBus& input,
                  const OutputChannels& output_channels,
                  OutputFilledCB on_output_filled_cb);

  // Same as `LimitPeaks()`, but only pushes in the first `num_frames` frames
  // from `input`. Each channel in `output_channels` must have the exact size
  // to contain `num_frames`.
  void LimitPeaksPartial(const AudioBus& input,
                         int num_frames,
                         const OutputChannels& output_channels,
                         OutputFilledCB on_output_filled_cb);

  // Feeds in silence to forces the remaining input data to be written out.
  // Can only be called once, after which the limiter cannot be re-used.
  void Flush();

 private:
  // Represents unowned chunks of external output memory which should gradually
  // be filled, along with a callback notifying owners when the memory has fully
  // been written to.
  struct PendingOutput {
    PendingOutput(OutputChannels channels, OutputFilledCB filled_callback);
    ~PendingOutput();

    PendingOutput(PendingOutput&&);

    // TODO(367764863) Rewrite to base::raw_span.
    RAW_PTR_EXCLUSION OutputFilledCB on_filled_callback;
    // TODO(367764863) Rewrite to base::raw_span.
    RAW_PTR_EXCLUSION OutputChannels channels;
  };

  void FeedInput(const AudioBus& input, int num_frames);

  // Updates `target_gain_` and `smoothed_gain_`.
  void UpdateGain(float current_maximum);

  // Write the first frame of `delayed_interleaved_input_` to the front of
  // `pending_outputs_`, after adjusting it by `smoothed_gain`.
  void WriteLimitedFrameToOutput();

  const int channels_;

  // Number of frames over which the limiter ramps up or ramps down its gain
  // reduction, when inputs exceed the specified range.
  // Calculated from the constructor's `sample_rate`, such that these correspond
  // to 5ms' worth of frames for the attack, and 50ms for the release.
  const int attack_frames_;
  const int release_frames_;

  // Constants used as coefficients in the smoothing of `smoothed_gain_`. These
  // are calculated such that, for a starting `smoothed_gain_` and
  // `target_gain_`, `smoothed_gain_` is 90% of the way towards `target_gain_`
  // after processing enough frames (either `attack_frames_` or `relase_frames_`
  // depending on whether we are attacking or releasing).
  const double attack_constant_;
  const double release_constant_;

  // Rolling window containing the absolute maximum of previous frames.
  // The window is of size `attack_frames_`.
  base::MovingMax<float> moving_max_;

  // Input frames waiting to be written to output. Samples are interleaved, such
  // that the first N samples at the front of the queue correspond to channels 0
  // through (n-1) of the oldest frame.
  // This should eventually contain `attack_frames_` frames, afterwhich each new
  // frame added will cause the oldest frame to be written out.
  base::circular_deque<float> delayed_interleaved_input_;

  // Number of iterations to run before starting to write to output.
  int initial_output_delay_in_frames_;

  // Queue of outputs to be written out to. After each `PendingOutput` is
  // filled, its corresponding `on_filled_callback` is run, letting owners of
  // the output memory know that it's ready for use.
  base::circular_deque<PendingOutput> outputs_;

  // The gain towards which `smoothed_gain_` should converge to.
  double target_gain_ = 1.0;

  // The gradually changing gain which is actually applied to the input as it is
  // written to output.
  double smoothed_gain_ = 1.0;

  bool was_flushed_ = false;
};

}  // namespace media

#endif  // MEDIA_BASE_AUDIO_LIMITER_H_