pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (c) 2012 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 | |
Mirko Bonadei | 92ea95e | 2017-09-15 06:47:31 +0200 | [diff] [blame] | 11 | #include "modules/audio_processing/vad/vad_audio_proc.h" |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 12 | |
| 13 | #include <math.h> |
| 14 | #include <stdio.h> |
Raphael Kubo da Costa | 7ce3091 | 2018-04-16 11:17:10 +0200 | [diff] [blame] | 15 | #include <string.h> |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 16 | |
Mirko Bonadei | 92ea95e | 2017-09-15 06:47:31 +0200 | [diff] [blame] | 17 | #include "common_audio/fft4g.h" |
| 18 | #include "modules/audio_processing/vad/pitch_internal.h" |
| 19 | #include "modules/audio_processing/vad/pole_zero_filter.h" |
| 20 | #include "modules/audio_processing/vad/vad_audio_proc_internal.h" |
| 21 | #include "rtc_base/checks.h" |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 22 | extern "C" { |
Karl Wiberg | 7ba22b8 | 2018-04-27 04:31:53 +0200 | [diff] [blame] | 23 | #include "modules/audio_coding/codecs/isac/main/source/filter_functions.h" |
| 24 | #include "modules/audio_coding/codecs/isac/main/source/isac_vad.h" |
Mirko Bonadei | 92ea95e | 2017-09-15 06:47:31 +0200 | [diff] [blame] | 25 | #include "modules/audio_coding/codecs/isac/main/source/pitch_estimator.h" |
| 26 | #include "modules/audio_coding/codecs/isac/main/source/structs.h" |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 27 | } |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 28 | |
| 29 | namespace webrtc { |
| 30 | |
| 31 | // The following structures are declared anonymous in iSAC's structs.h. To |
| 32 | // forward declare them, we use this derived class trick. |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 33 | struct VadAudioProc::PitchAnalysisStruct : public ::PitchAnalysisStruct {}; |
| 34 | struct VadAudioProc::PreFiltBankstr : public ::PreFiltBankstr {}; |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 35 | |
brucedawson | fde2116 | 2017-06-20 10:57:09 -0700 | [diff] [blame] | 36 | static constexpr float kFrequencyResolution = |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 37 | kSampleRateHz / static_cast<float>(VadAudioProc::kDftSize); |
brucedawson | fde2116 | 2017-06-20 10:57:09 -0700 | [diff] [blame] | 38 | static constexpr int kSilenceRms = 5; |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 39 | |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 40 | // TODO(turajs): Make a Create or Init for VadAudioProc. |
| 41 | VadAudioProc::VadAudioProc() |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 42 | : audio_buffer_(), |
| 43 | num_buffer_samples_(kNumPastSignalSamples), |
| 44 | log_old_gain_(-2), |
| 45 | old_lag_(50), // Arbitrary but valid as pitch-lag (in samples). |
| 46 | pitch_analysis_handle_(new PitchAnalysisStruct), |
| 47 | pre_filter_handle_(new PreFiltBankstr), |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 48 | high_pass_filter_(PoleZeroFilter::Create(kCoeffNumerator, |
| 49 | kFilterOrder, |
| 50 | kCoeffDenominator, |
| 51 | kFilterOrder)) { |
kwiberg@webrtc.org | 2ebfac5 | 2015-01-14 10:51:54 +0000 | [diff] [blame] | 52 | static_assert(kNumPastSignalSamples + kNumSubframeSamples == |
| 53 | sizeof(kLpcAnalWin) / sizeof(kLpcAnalWin[0]), |
| 54 | "lpc analysis window incorrect size"); |
| 55 | static_assert(kLpcOrder + 1 == sizeof(kCorrWeight) / sizeof(kCorrWeight[0]), |
| 56 | "correlation weight incorrect size"); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 57 | |
| 58 | // TODO(turajs): Are we doing too much in the constructor? |
| 59 | float data[kDftSize]; |
| 60 | // Make FFT to initialize. |
| 61 | ip_[0] = 0; |
| 62 | WebRtc_rdft(kDftSize, 1, data, ip_, w_fft_); |
| 63 | // TODO(turajs): Need to initialize high-pass filter. |
| 64 | |
| 65 | // Initialize iSAC components. |
| 66 | WebRtcIsac_InitPreFilterbank(pre_filter_handle_.get()); |
| 67 | WebRtcIsac_InitPitchAnalysis(pitch_analysis_handle_.get()); |
| 68 | } |
| 69 | |
Yves Gerey | 665174f | 2018-06-19 15:03:05 +0200 | [diff] [blame^] | 70 | VadAudioProc::~VadAudioProc() {} |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 71 | |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 72 | void VadAudioProc::ResetBuffer() { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 73 | memcpy(audio_buffer_, &audio_buffer_[kNumSamplesToProcess], |
| 74 | sizeof(audio_buffer_[0]) * kNumPastSignalSamples); |
| 75 | num_buffer_samples_ = kNumPastSignalSamples; |
| 76 | } |
| 77 | |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 78 | int VadAudioProc::ExtractFeatures(const int16_t* frame, |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 79 | size_t length, |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 80 | AudioFeatures* features) { |
| 81 | features->num_frames = 0; |
| 82 | if (length != kNumSubframeSamples) { |
| 83 | return -1; |
| 84 | } |
| 85 | |
| 86 | // High-pass filter to remove the DC component and very low frequency content. |
| 87 | // We have experienced that this high-pass filtering improves voice/non-voiced |
| 88 | // classification. |
| 89 | if (high_pass_filter_->Filter(frame, kNumSubframeSamples, |
| 90 | &audio_buffer_[num_buffer_samples_]) != 0) { |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 91 | return -1; |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 92 | } |
| 93 | |
| 94 | num_buffer_samples_ += kNumSubframeSamples; |
| 95 | if (num_buffer_samples_ < kBufferLength) { |
| 96 | return 0; |
| 97 | } |
kwiberg | 9e2be5f | 2016-09-14 05:23:22 -0700 | [diff] [blame] | 98 | RTC_DCHECK_EQ(num_buffer_samples_, kBufferLength); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 99 | features->num_frames = kNum10msSubframes; |
| 100 | features->silence = false; |
| 101 | |
| 102 | Rms(features->rms, kMaxNumFrames); |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 103 | for (size_t i = 0; i < kNum10msSubframes; ++i) { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 104 | if (features->rms[i] < kSilenceRms) { |
| 105 | // PitchAnalysis can cause NaNs in the pitch gain if it's fed silence. |
| 106 | // Bail out here instead. |
| 107 | features->silence = true; |
| 108 | ResetBuffer(); |
| 109 | return 0; |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | PitchAnalysis(features->log_pitch_gain, features->pitch_lag_hz, |
| 114 | kMaxNumFrames); |
| 115 | FindFirstSpectralPeaks(features->spectral_peak, kMaxNumFrames); |
| 116 | ResetBuffer(); |
| 117 | return 0; |
| 118 | } |
| 119 | |
| 120 | // Computes |kLpcOrder + 1| correlation coefficients. |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 121 | void VadAudioProc::SubframeCorrelation(double* corr, |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 122 | size_t length_corr, |
| 123 | size_t subframe_index) { |
kwiberg | 9e2be5f | 2016-09-14 05:23:22 -0700 | [diff] [blame] | 124 | RTC_DCHECK_GE(length_corr, kLpcOrder + 1); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 125 | double windowed_audio[kNumSubframeSamples + kNumPastSignalSamples]; |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 126 | size_t buffer_index = subframe_index * kNumSubframeSamples; |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 127 | |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 128 | for (size_t n = 0; n < kNumSubframeSamples + kNumPastSignalSamples; n++) |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 129 | windowed_audio[n] = audio_buffer_[buffer_index++] * kLpcAnalWin[n]; |
| 130 | |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 131 | WebRtcIsac_AutoCorr(corr, windowed_audio, |
| 132 | kNumSubframeSamples + kNumPastSignalSamples, kLpcOrder); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 133 | } |
| 134 | |
| 135 | // Compute |kNum10msSubframes| sets of LPC coefficients, one per 10 ms input. |
| 136 | // The analysis window is 15 ms long and it is centered on the first half of |
| 137 | // each 10ms sub-frame. This is equivalent to computing LPC coefficients for the |
| 138 | // first half of each 10 ms subframe. |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 139 | void VadAudioProc::GetLpcPolynomials(double* lpc, size_t length_lpc) { |
kwiberg | 9e2be5f | 2016-09-14 05:23:22 -0700 | [diff] [blame] | 140 | RTC_DCHECK_GE(length_lpc, kNum10msSubframes * (kLpcOrder + 1)); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 141 | double corr[kLpcOrder + 1]; |
| 142 | double reflec_coeff[kLpcOrder]; |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 143 | for (size_t i = 0, offset_lpc = 0; i < kNum10msSubframes; |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 144 | i++, offset_lpc += kLpcOrder + 1) { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 145 | SubframeCorrelation(corr, kLpcOrder + 1, i); |
| 146 | corr[0] *= 1.0001; |
| 147 | // This makes Lev-Durb a bit more stable. |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 148 | for (size_t k = 0; k < kLpcOrder + 1; k++) { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 149 | corr[k] *= kCorrWeight[k]; |
| 150 | } |
| 151 | WebRtcIsac_LevDurb(&lpc[offset_lpc], reflec_coeff, corr, kLpcOrder); |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | // Fit a second order curve to these 3 points and find the location of the |
| 156 | // extremum. The points are inverted before curve fitting. |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 157 | static float QuadraticInterpolation(float prev_val, |
| 158 | float curr_val, |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 159 | float next_val) { |
| 160 | // Doing the interpolation in |1 / A(z)|^2. |
| 161 | float fractional_index = 0; |
| 162 | next_val = 1.0f / next_val; |
| 163 | prev_val = 1.0f / prev_val; |
| 164 | curr_val = 1.0f / curr_val; |
| 165 | |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 166 | fractional_index = |
| 167 | -(next_val - prev_val) * 0.5f / (next_val + prev_val - 2.f * curr_val); |
kwiberg | 9e2be5f | 2016-09-14 05:23:22 -0700 | [diff] [blame] | 168 | RTC_DCHECK_LT(fabs(fractional_index), 1); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 169 | return fractional_index; |
| 170 | } |
| 171 | |
| 172 | // 1 / A(z), where A(z) is defined by |lpc| is a model of the spectral envelope |
| 173 | // of the input signal. The local maximum of the spectral envelope corresponds |
| 174 | // with the local minimum of A(z). It saves complexity, as we save one |
| 175 | // inversion. Furthermore, we find the first local maximum of magnitude squared, |
| 176 | // to save on one square root. |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 177 | void VadAudioProc::FindFirstSpectralPeaks(double* f_peak, |
| 178 | size_t length_f_peak) { |
kwiberg | 9e2be5f | 2016-09-14 05:23:22 -0700 | [diff] [blame] | 179 | RTC_DCHECK_GE(length_f_peak, kNum10msSubframes); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 180 | double lpc[kNum10msSubframes * (kLpcOrder + 1)]; |
| 181 | // For all sub-frames. |
| 182 | GetLpcPolynomials(lpc, kNum10msSubframes * (kLpcOrder + 1)); |
| 183 | |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 184 | const size_t kNumDftCoefficients = kDftSize / 2 + 1; |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 185 | float data[kDftSize]; |
| 186 | |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 187 | for (size_t i = 0; i < kNum10msSubframes; i++) { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 188 | // Convert to float with zero pad. |
| 189 | memset(data, 0, sizeof(data)); |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 190 | for (size_t n = 0; n < kLpcOrder + 1; n++) { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 191 | data[n] = static_cast<float>(lpc[i * (kLpcOrder + 1) + n]); |
| 192 | } |
| 193 | // Transform to frequency domain. |
| 194 | WebRtc_rdft(kDftSize, 1, data, ip_, w_fft_); |
| 195 | |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 196 | size_t index_peak = 0; |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 197 | float prev_magn_sqr = data[0] * data[0]; |
| 198 | float curr_magn_sqr = data[2] * data[2] + data[3] * data[3]; |
| 199 | float next_magn_sqr; |
| 200 | bool found_peak = false; |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 201 | for (size_t n = 2; n < kNumDftCoefficients - 1; n++) { |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 202 | next_magn_sqr = |
| 203 | data[2 * n] * data[2 * n] + data[2 * n + 1] * data[2 * n + 1]; |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 204 | if (curr_magn_sqr < prev_magn_sqr && curr_magn_sqr < next_magn_sqr) { |
| 205 | found_peak = true; |
| 206 | index_peak = n - 1; |
| 207 | break; |
| 208 | } |
| 209 | prev_magn_sqr = curr_magn_sqr; |
| 210 | curr_magn_sqr = next_magn_sqr; |
| 211 | } |
| 212 | float fractional_index = 0; |
| 213 | if (!found_peak) { |
| 214 | // Checking if |kNumDftCoefficients - 1| is the local minimum. |
| 215 | next_magn_sqr = data[1] * data[1]; |
| 216 | if (curr_magn_sqr < prev_magn_sqr && curr_magn_sqr < next_magn_sqr) { |
| 217 | index_peak = kNumDftCoefficients - 1; |
| 218 | } |
| 219 | } else { |
| 220 | // A peak is found, do a simple quadratic interpolation to get a more |
| 221 | // accurate estimate of the peak location. |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 222 | fractional_index = |
| 223 | QuadraticInterpolation(prev_magn_sqr, curr_magn_sqr, next_magn_sqr); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 224 | } |
| 225 | f_peak[i] = (index_peak + fractional_index) * kFrequencyResolution; |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | // Using iSAC functions to estimate pitch gains & lags. |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 230 | void VadAudioProc::PitchAnalysis(double* log_pitch_gains, |
| 231 | double* pitch_lags_hz, |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 232 | size_t length) { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 233 | // TODO(turajs): This can be "imported" from iSAC & and the next two |
| 234 | // constants. |
kwiberg | 9e2be5f | 2016-09-14 05:23:22 -0700 | [diff] [blame] | 235 | RTC_DCHECK_GE(length, kNum10msSubframes); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 236 | const int kNumPitchSubframes = 4; |
| 237 | double gains[kNumPitchSubframes]; |
| 238 | double lags[kNumPitchSubframes]; |
| 239 | |
| 240 | const int kNumSubbandFrameSamples = 240; |
| 241 | const int kNumLookaheadSamples = 24; |
| 242 | |
| 243 | float lower[kNumSubbandFrameSamples]; |
| 244 | float upper[kNumSubbandFrameSamples]; |
| 245 | double lower_lookahead[kNumSubbandFrameSamples]; |
| 246 | double upper_lookahead[kNumSubbandFrameSamples]; |
| 247 | double lower_lookahead_pre_filter[kNumSubbandFrameSamples + |
| 248 | kNumLookaheadSamples]; |
| 249 | |
| 250 | // Split signal to lower and upper bands |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 251 | WebRtcIsac_SplitAndFilterFloat(&audio_buffer_[kNumPastSignalSamples], lower, |
| 252 | upper, lower_lookahead, upper_lookahead, |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 253 | pre_filter_handle_.get()); |
| 254 | WebRtcIsac_PitchAnalysis(lower_lookahead, lower_lookahead_pre_filter, |
| 255 | pitch_analysis_handle_.get(), lags, gains); |
| 256 | |
| 257 | // Lags are computed on lower-band signal with sampling rate half of the |
| 258 | // input signal. |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 259 | GetSubframesPitchParameters( |
| 260 | kSampleRateHz / 2, gains, lags, kNumPitchSubframes, kNum10msSubframes, |
| 261 | &log_old_gain_, &old_lag_, log_pitch_gains, pitch_lags_hz); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 262 | } |
| 263 | |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 264 | void VadAudioProc::Rms(double* rms, size_t length_rms) { |
kwiberg | 9e2be5f | 2016-09-14 05:23:22 -0700 | [diff] [blame] | 265 | RTC_DCHECK_GE(length_rms, kNum10msSubframes); |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 266 | size_t offset = kNumPastSignalSamples; |
| 267 | for (size_t i = 0; i < kNum10msSubframes; i++) { |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 268 | rms[i] = 0; |
Peter Kasting | dce40cf | 2015-08-24 14:52:23 -0700 | [diff] [blame] | 269 | for (size_t n = 0; n < kNumSubframeSamples; n++, offset++) |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 270 | rms[i] += audio_buffer_[offset] * audio_buffer_[offset]; |
aluebs | ecf6b81 | 2015-06-25 12:28:48 -0700 | [diff] [blame] | 271 | rms[i] = sqrt(rms[i] / kNumSubframeSamples); |
pbos@webrtc.org | 788acd1 | 2014-12-15 09:41:24 +0000 | [diff] [blame] | 272 | } |
| 273 | } |
| 274 | |
| 275 | } // namespace webrtc |