简介:面向Android平台音频处理开发者,该资源包聚焦语音通信与录音场景中的降噪与回声问题,利用WebRTC音频处理模块实现回声消除、噪声抑制、自动增益控制和混响消除等能力,可帮助中级以上开发者快速上手音频前处理集成。资源共112个文件,压缩包约2.53MB,包含预编译的so原生库、Java与C++接口源码、h头文件、xml配置文件及gradle构建脚本,方便在Android Studio中通过jniLibs导入并调用WebRTC音频API。目前已有2710人学习下载,适合在实时音视频或录音类项目中打磨音频质量的开发者参考。通过包内的库与源码,可了解WebRTC音频处理链路的搭建方式、各模块的配置要点及降噪效果的测试方法,并直接集成到自身工程,减少从零编译成本。借助描述中的测试建议,佩戴耳机在真实环境中对比不同噪声场景的表现,可更准确地评估和调优降噪参数,从而提升整体音频体验。 最近在做一款语音社交App的时候,被“环境噪声”这个老大难问题堵了整整一个迭代版本。会议室的人声、马路的车流声、咖啡厅的杯盏碰撞……这些底噪放在音乐里可能还有点氛围感,但放在实时语音里,就是灾难。项目最开始打算自己写降噪算法,折腾了一周效果依旧没法听,后来切到了Android平台上最成熟的方案之一——基于WebRTC的音频降噪链路,才算真正把这个问题解决掉。这篇文章就把我从选型到落地、从采集到播放的完整过程记录下来,尤其是那些文档里不会写的坑,希望能够给正在做Android端音视频处理的同学省几天排查时间。

这套方案适合谁看?如果你正在做VoIP、在线会议、社交语音房、或者任何需要从Android端采集音频并实时送出的应用,且不想自己从数学公式开始搓降噪滤波器,那么WebRTC的音频处理模块就是现成的最佳选择。它能解决的不光是背景噪声,还一并处理了回声和音量增益的问题。下面直接开工。

1. 项目背景与核心思路

1.1 为什么选WebRTC而不是自研或其它降噪库

先说结论: 能在Android上做实时降噪的开源方案,WebRTC Audio Processing几乎是最均衡的一个 。SpeexDSP的降噪效果偏弱,RNNoise降噪干净但依赖模型推理、CPU占用偏高,自研谱减法在非平稳噪声下容易被误伤语音。而WebRTC的NS(Noise Suppression)模块虽然是传统信号处理思路,但其实战打磨沉淀极长,在平稳噪声、稳态人声环境下表现非常稳定,且计算开销很小。

这个选择还有一层原因:WebRTC不只有降噪,它还包括AEC(回声消除)、AGC(自动增益)、VAD(语音活动检测)等模块。做实时语音如果只降噪,往往还是会听到回声、音量忽大忽小的问题。与其挨个找库拼凑,不如在同一个处理框架里把整条链路统一起来。我实际验证下来,WebRTC的AEC对扬声器外放产生的回声抑制尤其有效,这对语音房场景是刚需。

注意:如果只做离线音频文件降噪,WebRTC并不是最优解,这类场景我更推荐RNNoise或专门的深度学习降噪模型。但只要是“实时流式处理”,WebRTC的优势就非常明显——延迟低、稳定性高、配套完整。

1.2 AEC、AGC、NS三个模块的配合逻辑

WebRTC处理音频不是把三个模块简单串联,它有严格的推荐顺序。官方建议的处理链路是: AEC回声消除 → AGC自动增益 → NS噪声抑制 。顺序为什么不能乱?回声消除要在增益前面,如果先做了AGC,回声信号也被放大了,AEC的收敛压力会成倍增加。噪声抑制放在最后,把前面模块处理后残余的底噪做最终压制。

这三个模块的协作效果,在实际项目里是能明显感知的。我做的第一版只单独开了NS,房间内其它人说话的声音依然能通过扬声器再传回麦克风,形成回声;开了AEC之后,远端语音基本听不见“啸叫感”了。AGC的作用则体现在不同说话人距离麦克风远近不同时,音量输出能保持在一个相对稳定的水平,不至于忽大忽小。三者配合是实时语音体验的基本盘。

1.3 三种集成路径怎么选

在Android工程中接入WebRTC音频处理,无非三条路:

集成路径 优点 缺点 适合场景
官方WebRTC库(org.webrtc) 集成简单、接口稳定 包体增大明显、内部逻辑重 完整使用WebRTC音视频能力
抽取AudioProcessing模块自行编译 包体可控、裁剪自由 需要配置CMake/编译环境 只需要音频处理能力的项目
使用社区维护的webrtc-audio-processing库 API友好、更新及时 依赖第三方维护节奏 不想折腾编译又想快速上线

我最终选的是第二条路,从WebRTC源码中抽取音频处理核心,通过JNI封装给Kotlin层调用。原因很简单:这个项目除了降噪之外并不需要WebRTC的P2P能力,也没必要为一个噪声抑制引入几十MB的so库。如果你想快速验证效果,先走第一条路也行——用官方库把整个链路跑通,再决定要不要裁剪。

2. WebRTC降噪原理与关键参数

2.1 NS模块内部到底做了什么

WebRTC的噪声抑制并不是什么玄学,它的核心是 谱减法与维纳滤波的组合策略 。简单理解:音频信号按帧做STFT变换到频域,NS模块根据带噪语音和估计出的噪声频谱,计算出每个频点的增益系数,语音主导的频点保留,噪声主导的频点压低,再通过ISTFT还原到时域。

但WebRTC做得比教科书上的谱减法更精细的地方在于——它加入了 基于信号特征的语音活动检测(VAD) 。只有判定为噪声的帧才更新噪声估计,避免在说话时把语音分量误判成噪声而削掉。另外它对音乐、键盘敲击这类瞬态噪声也有额外规则处理,这也是它比简单谱减法效果好的核心原因。

我们不需要自己实现这些算法,但理解原理对参数调优非常关键:比如NS模式调高后,某些环境下的高频“嘶嘶”声可能被压制过头,就是谱减法的“音乐噪声效应”放大导致的。

2.2 降噪强度模式的取舍

WebRTC NS传统模式分四级: kLow 、 kModerate 、 kHigh 、 kVeryHigh 。这个参数直接决定了噪声抑制的激进程度,也决定了语音损伤大小:

模式 效果 副作用
Low 只压最平稳的底噪 语音损伤小,但效果感知弱
Moderate 日常通话推荐、压制稳定背景噪 部分齿音会有轻微损失
High 环境嘈杂时使用、压制效果明显 尾音可能发闷、瞬态噪声被削弱
VeryHigh 极端安静环境、追求极致干净 语音失真风险大、容易有“闷罐感”

在实际项目中,我不建议直接把参数拉满。第一版为了体现降噪效果我调到了VeryHigh,结果在真机上测试,人声发闷不说,句尾的“气声”也被吞了,听感很“电子”。 Moderate到High之间是最稳的选择 ,如果你要针对不同场景动态调节,可以参考WebRTC的Android接口通过AudioProcessing配置动态切换。

2.3 采样率、帧长、声道这三个坑必须提前定

这三个参数如果没想清楚就动手写代码,后面必返工。

WebRTC的处理帧长是固定的 10ms 。这意味着,如果你用16kHz采样率采集,每帧是160个采样点;如果采样率是48kHz,每帧就是480个采样点。 在给WebRTC喂数据之前,必须按它要求的帧长来切分数据 ,否则ProcessStream调用会直接失败或产生杂音。

关于采样率,我的建议是: 采集端能拿到16kHz就直接用16kHz,拿不到就在喂给WebRTC之前做重采样 。虽然WebRTC支持8k/16k/32k/48k,但16kHz是传统NS模块优化最充分的档位,降噪效果最好;很多手机采集硬件的原始采样就是48kHz,需要先重采样到16kHz再进NS模块。声道方面,WebRTC的音频处理默认按单声道处理,而麦克风采集到的往往是双声道数据,需要先下混到单声道,否则Phase信息会让降噪算法犯迷糊。

3. Android工程中的完整接入与代码实现

3.1 Android端音频采集链路设计

AudioRecord在Android手机上有两种比较典型的噪音问题版本:一是权限没给到位,二是buffer设置不合理导致丢字、卡顿。这块建议直接使用 自动静音检测(AudioRecord.ERROR_BAD_VALUE)的返回值来判断采集是否有问题 ,很多资料里都容易忽略这一点。

我采用的设计链路是:

麦克风采集(AudioRecord) → 重采样(48k→16k) → 下混(立体声→单声道)
→ JNI层传入AudioProcessing处理 → 返回处理后的PCM → 编码/发送/播放

这里需要强调一个细节:AudioRecord的缓冲大小配置,建议设为预期 帧长的整数倍 。比如我们的预期是每10ms一帧16kHz单声道(160个short),那么缓冲区长度可以设置为160的倍数,比如4800个short即300ms的buffer。这样不仅读取数据方便,也能避免系统buffer不足导致的周期性卡顿。

如果你的App只是做普通的录音、不需要实时发送,采集链路可以简化成:

麦克风采集 → 重采样 → 下混 → WebRTC NS处理 → 写入AudioTrack或PCM文件

这两种场景核心逻辑一样,区别只在处理完数据的去向。

3.2 JNI封装与WebRTC处理核心代码

从WebRTC源码中抽取音频处理模块这一步,网上有很多编译教程,这里不再赘述。关键是抽取编译出来后,JNI层的封装质量决定了上层调用是否顺手。我的JNI层简化核心代码大致如下:

#include "modules/audio_processing/include/audio_processing.h"

struct AudioProcessor {
    std::unique_ptr<webrtc::AudioProcessing> apm;
    int sample_rate = 16000;
    int channels = 1;
};

// 初始化处理器
JNIEXPORT jlong JNICALL Java_com_example_audio_NoiseProcessor_nativeCreate(JNIEnv *env, jobject thiz) {
    auto *processor = new AudioProcessor();
    webrtc::AudioProcessing::Config config;
    config.echo_canceller.enabled = true;
    config.echo_canceller.mobile_mode = true;
    config.gain_controller1.enabled = true;
    config.gain_controller1.target_level_dbfs = -15;
    config.noise_suppression.enabled = true;
    config.noise_suppression.level = webrtc::NoiseSuppression::Level::kHigh;
    processor->apm = webrtc::AudioProcessing::Create(config);
    return reinterpret_cast<jlong>(processor);
}

// 处理一帧16kHz单声道PCM数据
JNIEXPORT void JNICALL Java_com_example_audio_NoiseProcessor_nativeProcess(
        JNIEnv *env, jobject thiz, jlong handle, jshortArray input, jshortArray output) {
    auto *processor = reinterpret_cast<AudioProcessor *>(handle);
    jshort *in_data = env->GetShortArrayElements(input, nullptr);
    jshort *out_data = env->GetShortArrayElements(output, nullptr);

    // AudioFrame设置16kHz、10ms、单声道,正好160个采样点
    webrtc::AudioFrame frame;
    frame.sample_rate_hz_ = 16000;
    frame.num_channels_ = 1;
    frame.samples_per_channel_ = 160;
    memcpy(frame.mutable_data(), in_data, sizeof(int16_t) * 160);

    processor->apm->ProcessStream(&frame);
    memcpy(out_data, frame.data(), sizeof(int16_t) * 160);

    env->ReleaseShortArrayElements(input, in_data, JNI_ABORT);
    env->ReleaseShortArrayElements(output, out_data, 0);
}

这段代码最需要注意的就是 AudioFrame每个字段都必须正确赋值 。我第一版漏设了 sample_rate_hz_ ,结果ProcessStream直接崩溃,排查了整整一个下午才发现是frame参数没设置完整。

提示:如果你在Android上使用C++层的JNI,务必确认编译时所引用的WebRTC版本与你抽取源码的版本一致。不同版本之间AudioProcessing的Config字段差异很大,最容易踩的坑就是旧代码编译新库时字段对不上。

3.3 Kotlin层音频采集与流程控制

Kotlin层的核心职责是:开启麦克风采集、按帧切分数据、调用JNI处理、把处理后的数据交给播放或编码器。简化后的采集与处理流程如下:

class NoiseProcessor(private val audioSource: Int = MediaRecorder.AudioSource.VOICE_COMMUNICATION) {

    private val sampleRate = 16000
    private val channelConfig = AudioFormat.CHANNEL_IN_MONO
    private val audioFormat = AudioFormat.ENCODING_PCM_16BIT
    private val frameSize = 160 // 10ms @16kHz

    private var audioRecord: AudioRecord? = null
    private var nativeHandle = nativeCreate()

    fun start() {
        val minBufSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat)
        val bufferSize = maxOf(minBufSize, frameSize * 4)
        audioRecord = AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, bufferSize)
        audioRecord?.startRecording()
        Thread {
            val buffer = ShortArray(frameSize)
            while (isRunning) {
                val read = audioRecord?.read(buffer, 0, frameSize) ?: -1
                if (read == frameSize) {
                    val output = ShortArray(frameSize)
                    nativeProcess(nativeHandle, buffer, output)
                    onProcessed?.invoke(output)
                }
            }
        }.start()
    }

    fun setOnProcessedListener(listener: (ShortArray) -> Unit) {
        onProcessed = listener
    }

    external fun nativeCreate(): Long
    external fun nativeProcess(handle: Long, input: ShortArray, output: ShortArray)

    companion object {
        init {
            System.loadLibrary("webrtc_audio_processing")
        }
    }
}

这里推荐使用 MediaRecorder.AudioSource.VOICE_COMMUNICATION 作为音频源,这个枚举值会启用手机上的 硬件语音处理链路 (很多手机产商会做多麦克风降噪和风噪抑制)。不过它有个副作用:某些机型上会引入额外的自动增益或回声消除逻辑,可能与WebRTC的处理链路叠加,导致音量不稳定或者声音发闷。所以 在真机测试时,如果发现效果异常,优先尝试切换到 UNPROCESSED 音频源 。

你应该注意到了,这里的代码是按10ms一帧处理的,即每调用一次read就处理一帧。这种实时处理方式的好处是延迟极低,但坏处是Android调度稍有抖动就会导致AudioRecord的read阻塞超时。如果遇到读取卡顿问题,可以 把采集循环和JNI处理分离到两个线程,用队列连接二者 ,这样采集和处理的节奏互不拖累。

3.4 处理完的数据怎么给到扬声器和发送端

处理后的数据需要根据业务场景区分思想:

  • 本地监听 :把处理后的PCM数据写入AudioTrack播放。需要注意AudioTrack的采样率和声道数必须和处理后的数据格式一致,否则会出现变调或只有单耳有声音的问题。

  • 网络发送 :处理后的数据直接交给编码器(比如Opus编码器)或者直接发送裸PCM数据。如果在弱网环境,这里可以做一点 静音检测后的丢帧策略 ,但前提是接收端能容忍音频丢包。

  • 本地录音文件 :处理后的数据写入FileOutputStream或者MediaCodec编码器。

AudioTrack播放的代码相对简单,但有一个容易忽视的点: 播放线程和采集线程的时钟不同步 。在长时间通话或录音场景下,可能出现播放buffer越积越多导致延迟越来越高的情况。我建议在播放端定期检查 AudioTrack.getPlaybackHeadPosition() + bufferSize 的差值,如果超过一定阈值就清空播放缓冲,避免声音越来越“迟钝”。

4. 常见问题与排查实录

4.1 为什么降噪后声音变闷、变糊

这是最常见的反馈,原因大概率是NS模式太高或者AEC配置的移动模式设置不对。AEC的 mobile_mode 在手机上建议开启,但如果你的设备是平板或者外接USB麦克风,移动模式的回声抑制效果反而会变差。

还有一个隐藏因素: WebRTC的NS对语音频带外的高频分量压制非常积极 。如果原始采集的PCM明明是48kHz,却直接喂给按16kHz初始化的处理器,那么高频信息会先被丢弃再被进一步压制,听感自然会“糊”。这种情况需要先做高质量的重采样,优先使用 AudioRecord 的采样率匹配,而不是随意降采样。

排查思路:

  • 先关闭AGC和AEC,只保留NS,听是否有改善
  • 再调低NS档位到Moderate,对比听感
  • 检查是否使用了VOICE_COMMUNICATION音频源导致硬件DSP叠加处理

注意:市面上很多安卓手机自带的降噪和WebRTC降噪是两套独立引擎。如果你发现“处理完比没处理还难听”,优先怀疑是不是手机厂商的硬件降噪已经在底层处理过一遍了,又叠加了软件降噪引起的失真。可以尝试用UNPROCESSED源采集原始数据测试。

4.2 延迟和CPU占用怎么优化

WebRTC音频处理模块的计算复杂度本身很低,在主流手机上跑16k/10ms帧的处理时间平均在1ms左右。如果遇到CPU飙升,第一怀疑对象应该是重采样逻辑和缓冲区分配。

缓冲区复用 是避免GC抖动的最直接手段。不要在每帧回调里都new ShortArray,而是提前分配好并复用它。Kotlin层的代码里如果把 val output = ShortArray(frameSize) 放在循环内,看似没问题,实际在长时间运行时会产生大量短生命周期对象,触发GC后会导致间歇性音频卡顿。

延迟优化方面,把处理链路精简为“采集→处理→播放”是最高效的。如果你在中间加了太多队列和回调嵌套,每一层都会引入毫秒级的额外开销。对于实时语音场景,端到端延迟在200ms以内是可接受的,但如果链路设计不合理很容易超到400ms以上。

4.3 和手机自带降噪打架了怎么办

这个问题在项目中几乎必然遇到。不同厂商对不同音频源的默认处理逻辑差异很大,有的手机在VOICE_COMMUNICATION下自带强力降噪,有的手机基本不做处理。在代码里写死某个音频源,往往在高通、联发科、麒麟等不同平台的设备上表现完全不同。

彻底解决的办法是用 MediaRecorder.AudioSource.UNPROCESSED (API 24+),然后通过 AudioManager 的 setParameters("noise_suppression=off") 尝试关闭硬件降噪。这个参数并不保证每个厂商都支持,所以还需要在软件层做兜底:检测到处理后的音频质量异常(比如峰峰值明显低于预期),自动把NS模式调低或者改用更高品质的音频源。

我在实际测试中还发现一个规律: 部分小米和OPPO机型在VOICE_COMMUNICATION下会强制开启通话降噪,导致WebRTC收到的是已经被“处理过”的信号,二次降噪后语音变得非常机械 。这类机型的名单没有公开规律可循,最稳妥的做法是在App设置里提供“音频处理模式”选项,让用户自行切换。

4.4 弱网环境下和降噪参数的配合

弱网环境下的降噪调优,很多资料不会提及。我可以分享一个经验:当网络丢包严重时,WebRTC会启动PLC(丢包隐藏)机制来填补音频空洞。但PLC填补出来的音频帧在频域上是“预测”的,NS模块对这类信号容易发生误判——把预测出来的高频内容当成噪声压掉,导致补出来的声音听起来断断续续。

如果要做弱网优化,建议把NS模式动态降到Moderate或者Low,给PLC补齐的信号留出更多“存活空间”。此外,可以在发送端做 基于VAD的静音帧丢弃 ,让麦克风在无人说话时不发送任何数据,节省出的码率可以用于提升有效语音的编码质量。WebRTC源码中的VAD模块可以直接复用,判断结果可以作为这篇博文介绍的另一层优化空间。

// VAD检测示例:返回1表示有语音,0表示静音
int vad_result = processor->apm->voice_detector()->ProcessStream(frame);
if (vad_result == 0) {
    // 不发送音频数据,或者发送舒适噪声帧
}

这个策略在多人语音房里效果非常明显:没人说话时不发送数据,不仅节省带宽,接收端也不需要处理无意义的静音帧,整体体验会有质的提升。

4.5 帧长度不匹配导致的异常表现

错误搭配容易出现的现象是: 处理后的音频有周期性“哒哒”声,或者声音变速变调 。排查这类问题,先核对下面四个数值:

  • AudioRecord配置的采样率
  • AudioFrame设置的 sample_rate_hz_
  • 重采样器输出的目标采样率
  • AudioTrack或编码器期望的采样率

四个值只要有一个对不上,就会出现“声调变高/变低”或“卡顿/杂音”这类表现。我第一次接入时就用错了自家重采样库的参数,输入端写的16000但实际输出48000的数据,WebRTC处理完后再按16k播放,声音明显变细变快,像开了倍速一样。

以后调试这种问题,养成一个好习惯: 先把降噪模块旁路掉,直接用最基础的采集播放链路确认所有格式一致 ,再逐步加上处理逻辑。这样可以快速定位问题是出在采集、处理还是播放环节。

写在最后

做Android音频降噪这个需求,WebRTC方案在实时性、效果和工程落地难度三个维度上都比较均衡。文中这些代码和配置方法基本就是从零到一搭起一套可用链路的全部要点。我在实际项目中强烈体会到,音频处理这个领域最大的拦路虎不是算法本身,而是格式匹配、链路顺序、设备兼容这些工程细节。如果你正在做类似功能,希望这篇记录能帮你少走一些弯路。

最后再分享一个小技巧:在开发阶段,记得在设置里加上“降噪等级”和“音频源切换”这两个调试选项,并打开隐藏的浮层日志面板。真机调试时你才能发现,很多“算法效果差”的问题,根源其实出在硬件DSP叠加或采样率不匹配上,而这类问题靠看代码是看不出来的。把这两把锤子握在手里,音频降噪这条路会顺畅很多。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

Logo

邀请您加入社区

更多推荐