在 RTC,即实时音视频通信中,要解决的音频相关的问题,主要包括如下这些:
- 音频数据的采集及播放。
- 音频数据的处理。主要是对采集录制的音频数据的处理,即所谓的 3A 处理,AEC (Acoustic Echo Cancellation) 回声消除,ANS (Automatic Noise Suppression) 降噪,和 AGC (Automatic Gain Control) 自动增益控制。
- 音效。如变声,混响,均衡等。
- 音频数据的编码和解码。包括音频数据的编码和解码,如 AAC,OPUS,和针对弱网的处理,如 NetEQ。
- 网络传输。一般用 RTP/RTCP 传输编码后的音频数据。
- 整个音频处理流水线的搭建。 WebRTC 的音频处理流水线大体如下图:
除了音效之外,WebRTC 的音频处理流水线包含其它所有的部分,音频数据的采集及播放,音频数据的处理,音频数据的编码和解码,网络传输都有。
在 WebRTC 中,通过 AudioDeviceModule 完成音频数据的采集和播放。不同的操作系统平台有着不同的与音频设备通信的方式,因而不同的平台上使用各自平台特有的解决方案实现平台特有的 AudioDeviceModule。一些平台上甚至有很多套音频解决方案,如 Linux 有 pulse 和 ALSA,Android 有 framework 提供的 Java 接口、OpenSLES 和 AAudio,Windows 也有多种方案等。
WebRTC 的音频流水线只支持处理 10 ms 的数据,有些操作系统平台提供了支持采集和播放 10 ms 音频数据的接口,如 Linux,有些平台则没有,如 Android、iOS 等。AudioDeviceModule 播放和采集的数据,总会通过 AudioDeviceBuffer 拿进来或者送出去 10 ms 的音频数据。对于不支持采集和播放 10 ms 音频数据的平台,在平台的 AudioDeviceModule 和 AudioDeviceBuffer 还会插入一个 FineAudioBuffer,用于将平台的音频数据格式转换为 10 ms 的 WebRTC 能处理的音频帧。
WebRTC 的 AudioDeviceModule 连接称为 AudioTransport 的模块。对于音频数据的采集发送,AudioTransport 完成音频处理,主要即是 3A 处理。对于音频播放,这里有一个混音器,用于将接收到的多路音频做混音。回声消除主要是将录制的声音中播放的声音的部分消除掉,因而,在从 AudioTransport 中拿音频数据播放时,也会将这一部分音频数据送进 APM 中。
AudioTransport 接 AudioSendStream 和 AudioReceiveStream,在 AudioSendStream 和 AudioReceiveStream 中完成音频的编码发送和接收解码,及网络传输。
WebRTC 的音频基本操作
在 WebRTC 的音频流水线,无论远端发送了多少路音频流,也无论远端发送的各条音频流的采样率和通道数具体是什么,都需要经过重采样,通道数转换和混音,最终转换为系统设备可接受的采样率和通道数的单路音频数据。具体来说,各条音频流需要先重采样和通道数变换转换为某个统一的采样率和通道数,然后做混音;混音之后,再经过重采样以及通道数变换,转变为最终设备可接受的音频数据。(WebRTC 中音频流水线各个节点统一用 16 位整型值表示采样点。)如下面这样:
WebRTC 提供了一些音频操作的工具类和函数用来完成上述操作。
混音如何混?
WebRTC 提供了 AudioMixer
接口来抽象混音器,这个接口定义 (位于 webrtc/src/api/audio/audio_mixer.h
) 如下:
WebRTC 的 AudioMixer 将 0 个、1 个或多个 Mixer Source 混音为特定通道数的单路音频帧。输出的音频帧的采样率,由 AudioMixer 的具体实现根据一定的规则确定。
Mixer Source 为 AudioMixer 提供特定采样率的单声道或立体声的音频帧数据,它有责任将它可以拿到的音频帧数据重采样为 AudioMixer 期待的采样率的音频数据。它还可以提供它倾向的输出采样率的信息,以帮助 AudioMixer 计算合适的输出采样率。Mixer Source 通过 Ssrc()
提供一个这一路的 Mixer Source 标识。
WebRTC 提供了一个 AudioMixer 的实现 AudioMixerImpl
类,位于 webrtc/src/modules/audio_mixer/
。这个类的定义 (位于 webrtc/src/modules/audio_mixer/audio_mixer_impl.h
) 如下:
AudioMixerImpl
类的实现 (位于 webrtc/src/modules/audio_mixer/audio_mixer_impl.cc
) 如下:
不难看出,AudioMixerImpl
的 AddSource(Source* audio_source)
和 RemoveSource(Source* audio_source)
都只是普通的容器操作,但它强制不能添加已经添加的 Mixer Source,也不能移除不存在的 Mixer Source。整个类的中心无疑是 Mix(size_t number_of_channels, AudioFrame* audio_frame_for_mixing)
了。
|
|
AudioMixerImpl::Mix()
混音过程大致如下:
- 计算输出音频帧的采样率。这也是这个接口不需要指定输出采样率的原因,
AudioMixer
的实现内部会自己算,通常是根据各个 Mixer Source 的 Preferred 采样率算。 - 从所有的 Mixer Source 中获得一个特定采样率的音频帧的列表。
AudioMixer
并不是简单的从所有的 Mixer Source 中各获得一个音频帧并构造一个列表就完事,它还会对这些音频帧做一些简单变换和取舍。 - 通过
FrameCombiner
对不同的音频帧做混音。
计算输出音频采样率
计算输出音频采样率的过程如下:
AudioMixerImpl
首先获得各个 Mixer Source 的 Preferred 的采样率并构造一个列表,然后通过 OutputRateCalculator
接口 (位于 webrtc/modules/audio_mixer/output_rate_calculator.h
) 计算输出采样率:
WebRTC 提供了一个默认的 OutputRateCalculator
接口实现 DefaultOutputRateCalculator
,类定义 (webrtc/src/modules/audio_mixer/default_output_rate_calculator.h
) 如下:
这个类的定义很简单。默认的 AudioMixer 输出采样率的计算方法如下:
对于音频,WebRTC 内部支持一些标准的采样率,即 8K,16K,32K 和 48K。DefaultOutputRateCalculator
获得传入的采样率列表中最大的那个,并在标准采样率列表中找到最小的那个大于等于前面获得的最大采样率的采样率。也就是说,如果 AudioMixerImpl
的所有 Mixer Source 的 Preferred 采样率都大于 48K,计算会失败。
获得音频帧列表
AudioMixerImpl::GetAudioFromSources()
获得音频帧列表:
AudioMixerImpl::GetAudioFromSources()
从各个 Mixer Source 中获得音频帧,并构造SourceFrame
的列表。注意SourceFrame
的构造函数会调用AudioMixerCalculateEnergy()
(位于webrtc/src/modules/audio_mixer/audio_frame_manipulator.cc
) 计算音频帧的 energy。具体的计算方法如下:123456789101112131415uint32_t AudioMixerCalculateEnergy(const AudioFrame& audio_frame) {if (audio_frame.muted()) {return 0;}uint32_t energy = 0;const int16_t* frame_data = audio_frame.data();for (size_t position = 0;position < audio_frame.samples_per_channel_ * audio_frame.num_channels_;position++) {// TODO(aleloi): This can overflow. Convert to floats.energy += frame_data[position] * frame_data[position];}return energy;}
计算所有采样点数值的平方和。
然后对获得的音频帧排序,排序的逻辑如下:
1234567891011121314bool ShouldMixBefore(const SourceFrame& a, const SourceFrame& b) {if (a.muted != b.muted) {return b.muted;}const auto a_activity = a.audio_frame->vad_activity_;const auto b_activity = b.audio_frame->vad_activity_;if (a_activity != b_activity) {return a_activity == AudioFrame::kVadActive;}return a.energy > b.energy;}从排序之后的音频帧列表中选取最多 3 个信号最强的音频帧返回。
对选择的音频帧信号 Ramp 及更新增益:
123456789void RampAndUpdateGain(const std::vector<SourceFrame>& mixed_sources_and_frames) {for (const auto& source_frame : mixed_sources_and_frames) {float target_gain = source_frame.source_status->is_mixed ? 1.0f : 0.0f;Ramp(source_frame.source_status->gain, target_gain,source_frame.audio_frame);source_frame.source_status->gain = target_gain;}}
Ramp()
的执行过程 (位于 webrtc/src/modules/audio_mixer/audio_frame_manipulator.cc
) 如下:
之所以要执行这一步,是因为在混音不同音频帧的特定时刻,同一个音频流的音频帧可能会由于它的音频帧的信号相对强度,被纳入混音或被排除混音,这一步的操作可以使特定某一路音频听上去变化更平滑。
FrameCombiner
FrameCombiner
是混音的最终执行者:
FrameCombiner
把各个音频帧的数据的通道数都转换为目标通道数:123456789void RemixFrame(size_t target_number_of_channels, AudioFrame* frame) {RTC_DCHECK_GE(target_number_of_channels, 1);RTC_DCHECK_LE(target_number_of_channels, 2);if (frame->num_channels_ == 1 && target_number_of_channels == 2) {AudioFrameOperations::MonoToStereo(frame);} else if (frame->num_channels_ == 2 && target_number_of_channels == 1) {AudioFrameOperations::StereoToMono(frame);}}执行混音
123456789101112131415161718std::array<OneChannelBuffer, kMaximumAmountOfChannels> MixToFloatFrame(const std::vector<AudioFrame*>& mix_list,size_t samples_per_channel,size_t number_of_channels) {// Convert to FloatS16 and mix.using OneChannelBuffer = std::array<float, kMaximumChannelSize>;std::array<OneChannelBuffer, kMaximumAmountOfChannels> mixing_buffer{};for (size_t i = 0; i < mix_list.size(); ++i) {const AudioFrame* const frame = mix_list[i];for (size_t j = 0; j < number_of_channels; ++j) {for (size_t k = 0; k < samples_per_channel; ++k) {mixing_buffer[j][k] += frame->data()[number_of_channels * k + j];}}}return mixing_buffer;}
可以看到,所谓混音,只是把不同音频流的音频帧的样本点数据相加。
RunLimiter
这一步会通过 AGC,对音频信号做处理。1234567void RunLimiter(AudioFrameView<float> mixing_buffer_view,FixedGainController* limiter) {const size_t sample_rate = mixing_buffer_view.samples_per_channel() * 1000 /AudioMixerImpl::kFrameDurationInMs;limiter->SetSampleRate(sample_rate);limiter->Process(mixing_buffer_view);}数据格式转换
12345678910111213// Both interleaves and rounds.void InterleaveToAudioFrame(AudioFrameView<const float> mixing_buffer_view,AudioFrame* audio_frame_for_mixing) {const size_t number_of_channels = mixing_buffer_view.num_channels();const size_t samples_per_channel = mixing_buffer_view.samples_per_channel();// Put data in the result frame.for (size_t i = 0; i < number_of_channels; ++i) {for (size_t j = 0; j < samples_per_channel; ++j) {audio_frame_for_mixing->mutable_data()[number_of_channels * j + i] =FloatS16ToS16(mixing_buffer_view.channel(i)[j]);}}}
经过前面的处理,得到浮点型的音频采样数据。这一步将浮点型的数据转换为需要的 16 位整型数据。
至此混音结束。
结论:混音就是把各个音频流的采样点数据相加。
通道数转换如何完成?
WebRTC 提供了一些 Utility 函数用于完成音频帧单通道、立体声及四通道之间的相互转换,位于 webrtc/audio/utility/audio_frame_operations.cc
。通过这些函数的实现,我们可以看到音频帧的通道数转换具体是什么含义。
单通道转立体声:
单通道转立体声,也就是把一个通道的数据复制一份,让两个声道播放相同的音频数据。
立体声转单声道:
立体声转单声道是把两个声道的数据相加除以 2,得到一个通道的音频数据。
四声道转立体声:
四声道转立体声,是把 1、2 两个声道的数据相加除以 2 作为一个声道的数据,把 3、4 两个声道的数据相加除以 2 作为另一个声道的数据。
四声道转单声道:
四声道转单声道是把四个声道的数据相加除以四,得到一个声道的数据。
WebRTC 提供的其它音频数据操作具体可以参考 WebRTC 的头文件。
重采样
重采样可已将某个采样率的音频数据转换为另一个采样率的分辨率。WebRTC 中的重采样主要通过 PushResampler
、 PushSincResampler
和 SincResampler
等几个组件完成。如 webrtc/src/audio/audio_transport_impl.cc
中的 Resample()
:
PushResampler
是一个模板类,其接口比较简单,类的具体定义 (位于 webrtc/src/common_audio/resampler/include/push_resampler.h
) 如下:
这个类的实现 (位于 webrtc/src/common_audio/resampler/push_resampler.cc
) 如下:
PushResampler<T>::InitializeIfNeeded()
函数根据源和目标采样率初始化了一些缓冲区和必要的 PushSincResampler
。
PushResampler<T>::Resample()
函数中,通过 PushSincResampler
完成重采样。PushSincResampler
执行单个通道的音频数据的重采样。对于立体声的音频数据,PushResampler<T>::Resample()
函数会先将音频帧的数据,拆开成两个单通道的音频帧数据,然后分别做重采样,最后再合起来。
webrtc/src/common_audio/include/audio_util.h
中将立体声的音频数据拆开为两个单通道的数据,和将两个单通道的音频数据合并为立体声音频帧数据的具体实现如下:
音频数据的基本操作混音,声道转换,和重采样。