在直播时主播经常会受到一些外部环境音、噪音等影响,直播时音频采集会一并采集所有音频推流到观众设备上,从而影响观众收听体验。因此需要在直播主播端主动进行降噪处理,提高观众收听体验。
58直播为了实现这个功能,通过综合对比调研常见的开源降噪方案Speex、WebRTC、RNNoise,以及结合降噪之后的处理效果和58直播使用体验,最终选择WebRTC降噪方案。我们对其进行了优化兼容,将其移植应用到58视频直播中,提升直播效果和体验。
常见的开源降噪方案
Speex是一套主要针对语音的开源免费,无专利保护的应用集合,它不仅包括编解码器,还包括VAD(语音检测)、DTX(不连续传输)、AEC(回声消除)、NS(去噪)等实用模块。
WebRTC提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、显示等功能,并且还支持跨平台:Windows、Linux、Mac、Android。我们这里使用的就是WebRTC的音频处理模块audio_processing。
RNNoise降噪算法是根据纯语音以及噪声通过GRU训练来做。包含特征点提取、预料等核心部分。
传统降噪算法大部分是估计噪声+维纳滤波,噪声估计的准确性是整个算法效果的核心。根据噪声的不同大部分处理是针对平稳噪声以及瞬时噪声来做。
RNNoise的优点主要是一个算法通过训练可以解决所有噪声场景以及可以优化传统噪声估计的时延和收敛问题。
RNNoise的缺点是深度学习算法落地问题。因为相对大部分传统算法,RNNoise训练要得到一个很好的效果,由于特征点个数、隐藏单元的个数以及神经网络层数的增加,导致模型增大,运行效率。
现在就WebRTC和RNNoise的降噪集成效果进行对比验证分析。
音频原始PCM数据可通过Audacity软件进行分析
下图是58公司司庆直播时的截取一段音频数据,音频为双声道、44100采样率。分别用RNNoise和WebRTC进行降噪处理得出效果对比图如下:
下图是网络下载的一段带有噪音的音频数据,音频为单声道、32000采样率。分别用RNNoise和WebRTC进行降噪处理得出效果对比图如下:
综合上面两张效果图可以结论出:
RNNoise处理之后的数据更干净些,几乎没有电流音和杂音,但是受限于训练集、特征点问题,在处理一些数据时候会把正常的原声数据一并错误处理掉。
WebRTC处理之后的数据也相对干净,能更好的保持原有声音的数据,数据丢失较少。
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\u0026lt;\u0026lt;FRAME_SIZE_SHIFT)#define WINDOW_SIZE (2*FRAME_SIZE)
*通过测试发现这个窗口大小是可以进行微调的,为了方便音频数据的处理尝试大小修改长512,虽然通过Audacity分析频谱发现会有一些噪音波出现,但是在实际感观中效果还是可以接受的。这个方案可以临时解决非480正整数倍数的问题。
//强制修改FRAME_SIZE大小#define FRAME_SIZE (128\u0026lt;\u0026lt;FRAME_SIZE_SHIFT)
/*This file is automatically generated from a Keras model*/#ifndef RNN_DATA_H#define RNN_DATA_H#include \u0026quot;rnn.h\u0026quot;#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
WebRTC的代码是基于C++开源的,集成到Android中需要使用NDK。
WebRTC官方没有提供降噪增益的测试代码,需要查找相关资料找到其中的降噪、增益模块,通过资料去熟悉其中的处理逻辑。
WebRTC只能处理特定的采样率数据,这个是其代码内部是写死的,需要自己实现音频重采样来满足WebRTC的降噪采样率需求。音频的重采样算法有很多,在项目集成中都尝试使用过,效果都是差不多的。
// WebRTC处理支持的采样率// Initialization of struct.if (fs == 8000 || fs == 16000 || fs == 32000 || fs == 44100 || fs == 48000) { self-\u0026gt;fs = fs;} else { return -1;}
根据WebRTC的降噪过程和业务接口流程,把接口定义成init、process、free三个接口。区别RNNoise的是需要在process中做增益处理,WebRTC降噪会降低数据的声音大小,通过增益用来补充声音大小。
在process数据时发现WebRTC的处理窗口大小必须是160或是320个byte,根据采样率不同窗口大小不同。测试发现这个和处理RNNoise是一致都只能传正整数倍数据,要不还是会新引入噪音数据。
if (fs == 8000) { self-\u0026gt;blockLen = 80; self-\u0026gt;anaLen = 128; self-\u0026gt;window = kBlocks80w128;} else { self-\u0026gt;blockLen = 160; self-\u0026gt;anaLen = 256; self-\u0026gt;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直播中已经在使用了WebRTC相关代码逻辑,综合调研和处理结果验证工作之后,最终选择了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 \u0026lt; 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]); }}
//部分逻辑代码如下所示://这个数据拆分和缓冲区数据逻辑可以由业务方自行出/** * 降噪处理方法,数据异步处理之后通过回调方法通知给调用方。 * * @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, \u0026quot;webRtcNsProcess inBufferSize:\u0026quot; + inBufferSize); if (inBufferSize \u0026gt;= frameBufferLen) { Log.d(TAG, \u0026quot;webRtcNsProcess mInByteBuffer full\u0026quot;); 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 \u0026gt;= inByteLen) { ... byte[] outBufferLeft = new byte[outBufferSize - mOutByteBuffer.position()]; ... mOutByteBuffer.put(outBufferLeft); outBufferSize += outBufferLeft.length; if (null != nsProcessListener) { nsProcessListener.onProcess(outBuf); } }}
下图中蓝色部分是58直播时截取的一段未开启降噪逻辑的音频波形dB图,绿色部分是58直播时截取的一段开启降噪逻辑的音频波形dB图。从时域波形图对比上可以看到开启降噪逻辑之后波形更加清晰了,降噪效果比较明显。
下图中上半部分是58直播时截取的一段未开启降噪逻辑音频的频谱图,下半部分是58直播时截取的一段开启降噪逻辑音频的频谱图。从频谱图对比上可以看到开启降噪逻辑之后噪音的频谱被去除掉,音频数据的原始数据更加清晰突出。
在同样的噪音环境下通过开启和关闭降噪功能,在观众端体验收听效果。未开启降噪功能时观众端可以明显的听到沙沙的杂音,开启降噪功能之后沙沙声音明显减少或没有,对应的主播的声音凸显出来。
本文分享了58直播在降噪方面所做的一些调研实践经验,重点阐述了其中的一些痛点和难点问题以及我们的解决方案。由于RNNoise降噪方案的优势是存在的,在后续研究中会对RNNoise的深度学习继续进行深入了解,期望能更好的解决噪音问题,更好的提升直播体验。也希望能有更多朋友一起来探讨更优的解决方案。