ArchSummit深圳站7折本周截止,点击立减2640元>> 了解详情
写点什么

音频降噪在 58 直播中的研究与实现

  • 2019 年 3 月 05 日
  • 本文字数:5751 字

    阅读完需:约 19 分钟

音频降噪在58直播中的研究与实现

背景

在直播时主播经常会受到一些外部环境音、噪音等影响,直播时音频采集会一并采集所有音频推流到观众设备上,从而影响观众收听体验。因此需要在直播主播端主动进行降噪处理,提高观众收听体验。


58 直播为了实现这个功能,通过综合对比调研常见的开源降噪方案 Speex、WebRTC、RNNoise,以及结合降噪之后的处理效果和 58 直播使用体验,最终选择 WebRTC 降噪方案。我们对其进行了优化兼容,将其移植应用到 58 视频直播中,提升直播效果和体验。


降噪方案

常见的开源降噪方案


  • Speex


Speex 是一套主要针对语音的开源免费,无专利保护的应用集合,它不仅包括编解码器,还包括 VAD(语音检测)、DTX(不连续传输)、AEC(回声消除)、NS(去噪)等实用模块。


  • WebRTC


WebRTC 提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:Windows、Linux、Mac、Android。我们这里使用的就是 WebRTC 的音频处理模块 audio_processing。


  • RNNoise


RNNoise 降噪算法是根据纯语音以及噪声通过 GRU 训练来做。包含特征点提取、预料等核心部分。


RNNoise 降噪算法与传统算法对比分析

传统降噪算法大部分是估计噪声+维纳滤波,噪声估计的准确性是整个算法效果的核心。根据噪声的不同大部分处理是针对平稳噪声以及瞬时噪声来做。


RNNoise 的优点主要是一个算法通过训练可以解决所有噪声场景以及可以优化传统噪声估计的时延和收敛问题。


RNNoise 的缺点是深度学习算法落地问题。因为相对大部分传统算法,RNNoise 训练要得到一个很好的效果,由于特征点个数、隐藏单元的个数以及神经网络层数的增加,导致模型增大,运行效率。


现在就 WebRTC 和 RNNoise 的降噪集成效果进行对比验证分析。


降噪音频数据对比

音频原始 PCM 数据可通过 Audacity 软件进行分析


下图是 58 公司司庆直播时的截取一段音频数据,音频为双声道、44100 采样率。分别用 RNNoise 和 WebRTC 进行降噪处理得出效果对比图如下:



下图是网络下载的一段带有噪音的音频数据,音频为单声道、32000 采样率。分别用 RNNoise 和 WebRTC 进行降噪处理得出效果对比图如下:



综合上面两张效果图可以结论出:


  • RNNoise 处理之后的数据更干净些,几乎没有电流音和杂音,但是受限于训练集、特征点问题,在处理一些数据时候会把正常的原声数据一并错误处理掉。

  • WebRTC 处理之后的数据也相对干净,能更好的保持原有声音的数据,数据丢失较少。


降噪方案在直播实现

降噪方案调研过程

RNNoise 过程

  • RNNoise 的代码是基于 C 开源的,集成到 Android 中需要使用 NDK。

  • 开源项目提供的一个测试方法,但是该方法是针对文件处理的,可以把一个带噪音的 PCM 文件处理成无噪音文件。直播 SDK 中的音频数据是分段的 byte 数组数据,所以中间需要添加一些接口来让 RNNoise 来支持分段数据的降噪处理。

  • 根据 RNNoise 的降噪过程和业务接口流程,把接口定义成 init、process、free 三个接口。

  • 在 process 数据时发现 RNNosie 的处理窗口大小是 480,所以传入的数据也必须是 480 的正整数倍。如果不是的话处理之后会有明显的新引入噪音。


#define FRAME_SIZE_SHIFT 2#define FRAME_SIZE (120<<FRAME_SIZE_SHIFT)#define WINDOW_SIZE (2*FRAME_SIZE)
复制代码


*通过测试发现这个窗口大小是可以进行微调的,为了方便音频数据的处理尝试大小修改长 512,虽然通过 Audacity 分析频谱发现会有一些噪音波出现,但是在实际感观中效果还是可以接受的。这个方案可以临时解决非 480 正整数倍数的问题。


//强制修改FRAME_SIZE大小#define FRAME_SIZE (128<<FRAME_SIZE_SHIFT)
复制代码


  • 开源代码中的 rnn_data.c 和 rnn_data.h 是通过机器学习训练出来的,不是通用的。在处理一个噪音数据时发现有些数据中的原声也会一并处理掉,这个效果如果不通过新的数据集训练那么降噪之后的数据是不可用的。


/*This file is automatically generated from a Keras model*/#ifndef RNN_DATA_H#define RNN_DATA_H#include "rnn.h"#define INPUT_DENSE_SIZE 24extern const DenseLayer input_dense;#define VAD_GRU_SIZE 24extern const GRULayer vad_gru;#define NOISE_GRU_SIZE 48extern const GRULayer noise_gru;#define DENOISE_GRU_SIZE 96extern const GRULayer denoise_gru;#define DENOISE_OUTPUT_SIZE 22extern const DenseLayer denoise_output;#define VAD_OUTPUT_SIZE 1extern const DenseLayer vad_output;  struct RNNState {  float vad_gru_state[VAD_GRU_SIZE];  float noise_gru_state[NOISE_GRU_SIZE];  float denoise_gru_state[DENOISE_GRU_SIZE];};#endif
复制代码


  • 机器学习和训练是 RNNoise 的灵魂,需要业务接入方根据自身的使用场景通过大量的数据集来找出最合适的处理集。


WebRTC 过程

  • WebRTC 的代码是基于 C++开源的,集成到 Android 中需要使用 NDK。

  • WebRTC 官方没有提供降噪增益的测试代码,需要查找相关资料找到其中的降噪、增益模块,通过资料去熟悉其中的处理逻辑。

  • WebRTC 只能处理特定的采样率数据,这个是其代码内部是写死的,需要自己实现音频重采样来满足 WebRTC 的降噪采样率需求。音频的重采样算法有很多,在项目集成中都尝试使用过,效果都是差不多的。


// WebRTC处理支持的采样率// Initialization of struct.if (fs == 8000 || fs == 16000 || fs == 32000 || fs == 44100 || fs == 48000) {   self->fs = fs;} else {   return -1;}
复制代码


  • 根据 WebRTC 的降噪过程和业务接口流程,把接口定义成 init、process、free 三个接口。区别 RNNoise 的是需要在 process 中做增益处理,WebRTC 降噪会降低数据的声音大小,通过增益用来补充声音大小。

  • 在 process 数据时发现 WebRTC 的处理窗口大小必须是 160 或是 320 个 byte,根据采样率不同窗口大小不同。测试发现这个和处理 RNNoise 是一致都只能传正整数倍数据,要不还是会新引入噪音数据。


if (fs == 8000) {    self->blockLen = 80;    self->anaLen = 128;    self->window = kBlocks80w128;} else {    self->blockLen = 160;    self->anaLen = 256;    self->window = kBlocks160w256;}

复制代码


  • WebRTC 在 process 时有两种处理数据的方法:一种是需要把原始数据分成高频数据和低频数据给底层逻辑;一种是不用区分高低频数据直接把数据给底层逻辑。资料上的解释是 32k 以上需要分高低频处理。但是在实际测试中发现分高低频的处理效果不如不分高低频的效果好。

  • WebRTC 的降噪 NS 模块和增益 AGC 模块是独立的,为了一次数据完成两个过程需要组合数据,边降噪边增益,减少处理耗时。

  • WebRTC_NS 在处理数据时不应该选择高低频分开采样处理,应直接把数据给你 WebRTC_NS 处理就可以。经过测试发现通过高低频处理之后的音频降噪效果不如不区分高低频的,高低频处理之后会有明显的人声破音出现,且处理的降噪效果不纯净。这个地方走了一些弯路,在发现降噪效果不理想时没有怀疑是 api 使用的问题,这个高低频操作是很多资料都推荐的使用方法,但是在运用到实际场景时发现效果不如不使用的。


两种降噪方案集成优缺点对比

  • 目前 WebRTC 最新代码只支持采样率为 8000、16000、32000、44100、48000 的音频进行降噪,针对其余的采样率需要进行数据重采样到上述采样率之后进行降噪,处理完毕之后需要再次恢复原采样率;RNNoise 对采样率没有要求,可以适配常见的采样率。

  • WebRTC 在降噪之后还需要对数据进行增益处理,但是增益会增大电流音,效果会稍差些。

  • WebRTC 处理数据的 buffer 目前代码是 320 的整数倍;RNNoise 处理数据的 buffer 目前代码是 480 的整数倍。输入的 buffer 需是固定大小的,如果不是正整数倍,需要外部在传入时处理下。

  • 从代码复杂度看,WebRTC 的代码是多于 RNNoise 代码的。RNNoise 支持机器学习,通过机器学习生成 rnn_data.h 和 rnn_data.c 文件来匹配不同的降噪效果。

  • 降噪耗时对比,RNNoise 处理 3840 字节的 buffer 数据耗时大概在 6ms 左右,但在开始时耗时在 30ms 左右,递减到 6ms 并稳定;WebRTC 处理 3840 字节的 buffer 数据耗时大概在 2ms 左右,但在开始时耗时在 10ms 左右,递减到 2ms 并稳定。对比发现 WebRTC 处理效率更好些。

  • 从处理流程上看都是需要 init、process、free 操作的,对接入方接入成本是一致的。


安卓端 58 直播 SDK 接入降噪方案

通过上章节的优缺点对比以及 58 直播中已经在使用了 WebRTC 相关代码逻辑,综合调研和处理结果验证工作之后,最终选择了 WebRTC 降噪方案。


APM 模块集成 WebRTC 降噪功能

  • 在 58 多媒体整体架构上选择把降噪模块单独解耦提取一个 APM module,方便 58 视频编辑、58 直播等需要降噪业务统一调用。对外暴露工具类 AudioNoiseHelp 方便业务接入。APM module 的规划以后会接入更多的音频处理模块,现在已经接入降噪、增益模块。

  • 由于 58 直播 SDK 支持音频采样率种类大于 WebRTC 支持的种类,因此需要对数据进行最优音频重采样处理。


/** * 音频重采样 *  * @param sourceData        原始数据 * @param sampleRate        原始采样率 * @param srcSize           原始数据长度 * @param destinationData   重采样之后的数据 * @param newSampleRate     重采样之后的数据长度 */void resampleData(const int16_t *sourceData, int32_t sampleRate, uint32_t srcSize,                  int16_t *destinationData,int32_t newSampleRate){    if (sampleRate == newSampleRate) {        memcpy(destinationData, sourceData, srcSize * sizeof(int16_t));        return;    }
uint32_t last_pos = srcSize - 1; uint32_t dstSize = (uint32_t) (srcSize * ((float) newSampleRate / sampleRate)); for (uint32_t idx = 0; idx < dstSize; idx++) { float index = ((float) idx * sampleRate) / (newSampleRate); uint32_t p1 = (uint32_t) index; float coef = index - p1; uint32_t p2 = (p1 == last_pos) ? last_pos : p1 + 1; destinationData[idx] = (int16_t) ((1.0f - coef) * sourceData[p1] + coef * sourceData[p2]); }}
复制代码


  • 由于 WebRTC 只能处理 320byte 长度正整数倍的数据,但是 58 直播的音频采集数据在不同手机、不同采样率上得到的音频数据长度是不固定的,需要对数据进行切分处理。录音采集数据如果是 byte 格式的,假如长度是 4096,那么直接把 4096 数据传入到 WebRTC_NS 里处理会出现杂音出现,所以在交给 WebRTC_NS 模块之前需要用个缓冲区来处理下,一次最多可以传入(4096/320)*320=3840 长度数据,并且在数据处理完毕之后还需要用另外一个缓冲区来保证处理之后的长度仍然是 4096 个。


//部分逻辑代码如下所示://这个数据拆分和缓冲区数据逻辑可以由业务方自行出/** * 降噪处理方法,数据异步处理之后通过回调方法通知给调用方。 * * @param bytes             音频数据 * @param nsProcessListener 异步方法回调 */public void webRtcNsProcess(byte[] bytes, INsProcessListener nsProcessListener) {    if (isAudioNsInitOk) {        synchronized (TAG) {            if (null == bytes || bytes.length == 0) {                return;            }                        ...
int byteLen = bytes.length; if (inByteLen != byteLen) { inByteLen = byteLen; webRtcNsInit(byteLen); }
int frames = byteLen / FRAME_SIZE; int lastFrame = byteLen % FRAME_SIZE; int frameBufferLen = frames * FRAME_SIZE; byte[] buf = new byte[frameBufferLen]; Log.d(TAG, "webRtcNsProcess inBufferSize:" + inBufferSize); if (inBufferSize >= frameBufferLen) { Log.d(TAG, "webRtcNsProcess mInByteBuffer full"); nsProcessInner(buf, nsProcessListener); }
... nsProcessInner(buf, nsProcessListener); } } else { if (null != nsProcessListener) { nsProcessListener.onProcess(bytes); } }}
private void nsProcessInner(byte[] buf, INsProcessListener nsProcessListener) { mInByteBuffer.rewind(); mInByteBuffer.get(buf, 0, buf.length); byte[] inBufferLeft = new byte[inBufferSize - mInByteBuffer.position()];
... byte[] nsProcessData = AudioNoiseUtils.webRtcNsProcess(buf); byte[] outBuf = new byte[inByteLen]; ... if (outBufferSize >= inByteLen) {
...
byte[] outBufferLeft = new byte[outBufferSize - mOutByteBuffer.position()];
... mOutByteBuffer.put(outBufferLeft); outBufferSize += outBufferLeft.length; if (null != nsProcessListener) { nsProcessListener.onProcess(outBuf); } }}
复制代码


58 直播接入降噪之后的效果对比

  • 时域对比


下图中蓝色部分是 58 直播时截取的一段未开启降噪逻辑的音频波形 dB 图,绿色部分是 58 直播时截取的一段开启降噪逻辑的音频波形 dB 图。从时域波形图对比上可以看到开启降噪逻辑之后波形更加清晰了,降噪效果比较明显。



  • 频谱图对比


下图中上半部分是 58 直播时截取的一段未开启降噪逻辑音频的频谱图,下半部分是 58 直播时截取的一段开启降噪逻辑音频的频谱图。从频谱图对比上可以看到开启降噪逻辑之后噪音的频谱被去除掉,音频数据的原始数据更加清晰突出。



  • 主观感觉对比


在同样的噪音环境下通过开启和关闭降噪功能,在观众端体验收听效果。未开启降噪功能时观众端可以明显的听到沙沙的杂音,开启降噪功能之后沙沙声音明显减少或没有,对应的主播的声音凸显出来。


总结

本文分享了 58 直播在降噪方面所做的一些调研实践经验,重点阐述了其中的一些痛点和难点问题以及我们的解决方案。由于 RNNoise 降噪方案的优势是存在的,在后续研究中会对 RNNoise 的深度学习继续进行深入了解,期望能更好的解决噪音问题,更好的提升直播体验。也希望能有更多朋友一起来探讨更优的解决方案。


本文作者:金开龙,来自 58 集团 TEG 多媒体部,安卓高级工程师,专注音视频开发。

2019 年 3 月 05 日 16:079228

评论

发布
暂无评论
发现更多内容

百分点智能对话技术探索实践

DataFunTalk

AI

腾讯T2手把手教你!字节跳动历年校招Android面试真题解析,含BATJM大厂

欢喜学安卓

android 程序员 面试 移动开发

阿里P8大牛亲自讲解!6年菜鸟开发面试字节跳动安卓研发岗,成功收获美团,小米安卓offer

欢喜学安卓

android 程序员 面试 移动开发

阿里P8大牛亲自讲解!Android高级工程师面试实战,Android岗

欢喜学安卓

android 程序员 面试 移动开发

架构革新路漫漫,京东智联云自研服务器设计细节探秘

京东科技开发者

服务器 数据中心 IDC

2020盘点之手机失窃事件复盘分析

石君

信息安全 资金安全 手机失窃

史上最全面‘java监听器’解读,读完就能用进项目

Java架构师迁哥

C语言编程:入门指南(一周内学懂)

计算机与AI

c

全面 Severless 化只需要 7天!看南瓜电影的云上升级

阿里巴巴中间件

阿里巴巴 中间件

互联网大厂有哪些分库分表的思路和技巧?

冰河

分布式数据库 分库分表 分布式存储 数据一致性 数据同步

重庆打造区块链产业高地

CECBC

区块链

Head First设计模式

田维常

天下武功,唯”拆“不破之架构篇二 | 技术人应知的创新思维模型 (9)

Alan

架构 技术 技术人应知的创新思维模型 七日更 28天写作

架构设计大作业 2

仲夏

低代码旋风将席卷整个IT业界,带来应用开发的新革命和新里程!

J2PaaS低代码平台

甲方日常 77

句子

工作 随笔杂谈 日常

云原生架构-静态代码扫描SonarQube超时

云原生实验室

DevOps 云原生 jenkins SonarQube Pipeli

盘点2020 | 2021,Begin Again !

大导演

大前端 盘点2020

五步带你探究爬虫爬取视频弹幕背后的真相,附爬虫实现源码

小Q

学习 编程 架构 面试 python 爬虫

十大经典排序算法最强总结(含Java、Python码实现)

Java 面试 算法

网络模拟器:Cisco Packet Tracer 设备登陆实验

小说内容理解

DataFunTalk

AI 推荐系统

IPFS系统APP软件开发

系统开发

Serverless 在 SaaS 领域的最佳实践

阿里巴巴中间件

阿里巴巴 中间件

时空大数据与智能技术的时代共舞,百度地图给2020的答案

脑极体

打通经济命脉,区块链助力实体商超变革

CECBC

区块链

美团面试:为什么就能直接调用userMapper接口的方法?

田维常

美团

58同城风控平台演进

DataFunTalk

架构 中台

“让专业的人做专业的事”,畅捷通与阿里云的云原生故事

阿里巴巴中间件

云计算 云原生

工具词典:中立观点

lidaobing

维基百科 28天写作

我对2021的期待,从合上这份2020日历开始

脑极体

头号云话题:进击的开源操作系统

头号云话题:进击的开源操作系统

音频降噪在58直播中的研究与实现_文化 & 方法_金开龙_InfoQ精选文章