Android 回声消除

2023-09-18 14:59:30

Android 回声消除

前言

在语音聊天、语音通话、互动直播、语音转文字类应用或者游戏中,需要采集用户的麦克风音频数据,然后将音频数据发送给其它终端或者语音识别服务。如果直接使用采集的麦克风数据,就会存在回音问题。所谓回音就是在语音通话过程中,如果用户开着扬声器,那么自己讲话的声音和对方讲话的声音(即是扬声器的声音)就会混在一起,如果没有消除对方的声音,那么对方听到的就是带有回音的声音,这样的声音就会有问题。因此采集麦克风数据后,必须要消除回音,才能得到良好的用户体验。

回音消除的英文专业术语叫Acoustic Echo Cancellation,简称AEC。如何实现回音消除,技术细节实现上是一个比较复杂的数学问题。一般手机厂商都提供了底层的回音消除技术实现,app只需要调用相关api即可。iOS上的回音消除比较复杂一些,Android相对来说比较简单,本文主要对Android设备上如何实现回音消除的相关知识进行梳理。

Android的音频框架概览

Android提供的音频框架有:MediaRecorder 、AudioRecord、AudioTrack、MediaPlayer,其中AudioRecord只能录制音频,MediaRecorder用于录制视频(包括音频),AudioTrack是用来播放PCM音频,MediaPlayer用来播放视频(包括音频)。我们需要使用支持音频录制的API来实现AEC。

1、MediaRecorder

集成了音频采集、视频采集、编码、压缩等,支持少量的录音音频格式,无法实时处理音频,一般用于输出音频和视频混合格式,比如MP4、3GP。

	MediaRecorder recorder = new MediaRecorder();
	recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
	recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
	recorder.setOutputFile(fileName);
	recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
	try {
	    recorder.prepare();
	} catch (IOException e) {
	    Log.e(LOG_TAG, "prepare() failed");
	}
	recorder.start();

2、AudioRecord

AudioRecord是专业的音频采集框架。采集到的是未经压缩的原始PCM音频。它可以设置音频采集来源,比如麦克风风原始数据、录视频时的麦克风数据、语音识别、VoIP(Voice on Internet Protocal)等。其中VoIP是支持AEC的音频来源。AudioRecorder主要用于音频的实时处理,或者实现边录边播(AudioRecord+AudioTrack)功能。如果保存成音频文件,是不能够被播放器播放的,需要写代码实现数据编码及压缩。

需求

1.录音的过程中,把要播放的声音清除掉,不录进去

2.使用扬声器通话的情况下,不能听到回声

实现方式

1.通过安卓自带的 VOICE_COMMUNICATION模式进行录音,自动消除回音。

将AudioRecord的MediaRecorder.AudioSource.MIC参数修改成MediaRecorder.AudioSource.VOICE_COMMUNICATION

2.使用第三方库进行消除(WebRtc、Speex…),消除回音。

3.使用安卓AcousticEchoCanceler也可以消除声音,但是部分手机不支持,使用前需要先判断下是否支持

4.通过AudioManager设置

方式1(推荐用)

实现回音消除时,只需要在构造AudioRecord时将audioSource(音频源)设置成VOICE_COMMUNICATION:

AudioRecord audioRecorder = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION, SAMPLE_RATE,AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize);

获取AudioRecord录制的音频是通过从 AudioRecord 对象“拉”(读取)数据来实现的。应用程序负责使用以下三种方法之一及时轮询采集到的音频:

int read(byte[], int, int)  
int read(short[], int, int)  
int read(java.nio.ByteBuffer, int) 

选择使用哪种方法取决于对开发者来说最方便的音频数据存储格式。 创建AudioRecord之后,AudioRecord 对象会初始化其关联的音频缓冲区,它将用新的音频数据填充该缓冲区。在构造AudioRecord时指定的此缓冲区的大小。数据应以小于总记录缓冲区大小的块的形式从音频硬件中读取。

构造AudioRecord时需要指定音频来源,audio source有以下几种:

/** Default audio source **/
public static final int DEFAULT = 0;

/** Microphone audio source */
public static final int MIC = 1;

/** Voice call uplink (Tx) audio source.

 * <p>

 * Capturing from <code>VOICE_UPLINK</code> source requires the

 * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT} permission.

 * This permission is reserved for use by system components and is not available to

 * third-party applications.

 * </p>
   */
   public static final int VOICE_UPLINK = 2;

/** Voice call downlink (Rx) audio source.

 * <p>

 * Capturing from <code>VOICE_DOWNLINK</code> source requires the

 * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT} permission.

 * This permission is reserved for use by system components and is not available to

 * third-party applications.

 * </p>
   */
   public static final int VOICE_DOWNLINK = 3;

/** Voice call uplink + downlink audio source

 * <p>

 * Capturing from <code>VOICE_CALL</code> source requires the

 * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT} permission.

 * This permission is reserved for use by system components and is not available to

 * third-party applications.

 * </p>
   */
   public static final int VOICE_CALL = 4;

/** Microphone audio source tuned for video recording, with the same orientation

 *  as the camera if available. */
    public static final int CAMCORDER = 5;

/** Microphone audio source tuned for voice recognition. */
public static final int VOICE_RECOGNITION = 6;

/** Microphone audio source tuned for voice communications such as VoIP. It

 *  will for instance take advantage of echo cancellation or automatic gain control
 *  if available.
    */
    public static final int VOICE_COMMUNICATION = 7;

可见,VOICE_COMMUNICATION是用于VoIP这种需要回音消除的场景。

代码

开启录音

	/***
     * AudioRecord VOICE_COMMUNICATION
     * mAudioTrack和tts不需要改什么
     */
    public void recorderWithVoice(View view) {
        TextView button = (AppCompatTextView) view;

        if (button.getText().equals("audioRecorder VOICE_COMMUNICATION回声消除")) {
            RecordUtils.getInstance().startRecord(new AudioRecordMananger.OnVolumeChangedListener() {
                @Override
                public void onVolumeChange(double volume) {

                }

                @Override
                public void onSendBuffer(byte[] buffer) {
                    try {
                        if (fos == null)
                            fos = new FileOutputStream(path);
                        if (buffer != null)
                            fos.write(buffer);
                        else {
                            fos.close();
                            fos = null;
                        }
                    } catch (IOException e) {
                        Log.e(TAG, "onSendBuffer: " + e.toString());
                        e.printStackTrace();
                    }
                }
            });
            button.setText("stop");
        } else {
            RecordUtils.getInstance().stopRecord();
            button.setText("audioRecorder VOICE_COMMUNICATION回声消除");
        }
    }

播放TTS和pcm文件

public void t2s(View view) {
        mTextToSpeech.speak("张三您好,您已经是我们的会员啦,不需要再办卡了!", TextToSpeech.QUEUE_FLUSH, null, "1");
    }

    public void playAudioTrack(View view) {
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 16000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, mRecorderBufferSize
                , AudioTrack.MODE_STREAM);
        mAudioTrack.play();
        try {
            fis = new FileInputStream(playPath);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    byte[] buffer = new byte[mRecorderBufferSize];
                    while (fis.available() > 0) {
                        int readCount = fis.read(buffer); //一次次的读取
                        //检测错误就跳过
                        if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                            continue;
                        }
                        if (readCount != -1 && readCount != 0) {
                            //可以在这个位置用play()
                            //输出音频数据
                            mAudioTrack.write(buffer, 0, readCount); //一次次的write输出播放
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

方式3

似乎有点问题,不能过滤掉 播放的声音。强烈不推荐

public void recorder(View view) {
        TextView button = (AppCompatTextView) view;
        if (button.getText().equals("Recorder")) {
            AudioRecordMananger.getInstance()
                    .setAudioFormat(AudioFormat.ENCODING_PCM_16BIT)
                    .setChannelConfigIn(AudioFormat.CHANNEL_IN_MONO)
                    .setSampleRateInHz(16000)
                    .startRecordbyVolumeData(new AudioRecordMananger.OnVolumeChangedListener() {
                        @Override
                        public void onVolumeChange(double volume) {
                        }

                        @Override
                        public void onSendBuffer(byte[] buffer) {
                            try {
                                if (fos == null)
                                    fos = new FileOutputStream(path);
                                if (buffer != null)
                                    fos.write(buffer);
                                else {
                                    fos.close();
                                    fos = null;
                                }
                            } catch (IOException e) {
                                Log.e(TAG, "onSendBuffer: " + e.toString());
                                e.printStackTrace();
                            }
                        }
                    });
            button.setText("stop Recorder");
        } else {
            AudioRecordMananger.getInstance().stopRecord();
            button.setText("Recorder");
        }
    }

其中的AudioRecorder

audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
        sampleRateInHz,
        channelConfigIn,
        audioFormat,
        recordBufSize
);

播放PCM文件

/***
 * AudioTrack AEC 配合 AudioRecorder 中的mic参数
 * @param view
 */
public void playAudioTrackWithAEC(View view) {
    if (acousticEchoCanceler == null)
        initAEC();
    //播放
    initAudioTrack();

    mAudioTrack.play();
    try {
        fis = new FileInputStream(playPath);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                byte[] buffer = new byte[mRecorderBufferSize];
                while (fis.available() > 0) {
                    int readCount = fis.read(buffer); //一次次的读取
                    //检测错误就跳过
                    if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                        continue;
                    }
                    if (readCount != -1 && readCount != 0) {
                        //可以在这个位置用play()
                        //输出音频数据
                        mAudioTrack.write(buffer, 0, readCount); //一次次的write输出播放
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();
}

private void initAudioTrack() {
    if (mAudioTrack == null) {
        if (audioSessionId == -1) {
            mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 16000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, mRecorderBufferSize * 2
                    , AudioTrack.MODE_STREAM);
        } else {
            mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 16000, AudioFormat.CHANNEL_OUT_MONO, AudioFormat.ENCODING_PCM_16BIT, mRecorderBufferSize * 2
                    , AudioTrack.MODE_STREAM, audioSessionId);
        }
    }
}

private AcousticEchoCanceler acousticEchoCanceler;

private void initAEC() {
    if (AcousticEchoCanceler.isAvailable()) {
        if (acousticEchoCanceler == null) {
            acousticEchoCanceler = AcousticEchoCanceler.create(audioSessionId);
            Log.d(TAG, "initAEC: ---->" + acousticEchoCanceler + "\t" + audioSessionId);
            if (acousticEchoCanceler == null) {
                Log.e(TAG, "initAEC: ----->AcousticEchoCanceler create fail.");
            } else {
                acousticEchoCanceler.setEnabled(true);
            }
        }
    }
}

播放TTS

public void t2s(View view) {
    mTextToSpeech.speak("张三您好,您已经是我们的会员啦,不需要再办卡了!", TextToSpeech.QUEUE_FLUSH, null, "1");
}

方式4(可以用)

打开录音后,设置

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);//听筒模式
audioManager.setSpeakerphoneOn(true);

然后可以使用TTS播放文本或者使用AudioTrack播放声音。

播放结束,需要设置成正常的模式

AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(false);

方式2

1.Speex的回声消除的效果不太好(略)

2.WebRtc的回声消除的效果挺好的

这里推荐几篇文章

Webrtc 关于jni的几个githup地址
1.https://github.com/Goblincomet/webrtc-aecm
2.https://github.com/JKXY/WebRTCAudio
3.https://github.com/theeasiestway/android-webrtc-aecm
4.https://github.com/Solunadigital/Android-Audio-Processing-Using-WebRTC

文章
1.Google WebRtc Android 使用详解(包括客户端和服务端代码)

2.WebRtc学习之旅 —— Android端应用开发

3.android 用WebRTC做回音消除

4.Audio-音频降噪、回声消除处理

5.Android 音频降噪 webrtc 去回声

参考

【Android】Android语音通话回音消除(AEC)技术实现

Android 音视频去回声、降噪(Android音频采集及回音消除)(转)

Android 声音采集回声与回声消除

Android: AEC:AcousticEchoCanceler回声消除 这篇文章的评论有意思,AcousticEchoCanceler这个类没啥用

更多推荐

sed & awk使用简介

简介本文主要介绍Linux系统的两个神级工具:sed和awk,他们是Linux高手们必备的技能,很值得我们去研究的东西。这里是我在网上书上收集的相关资料,因为这两个工具很有名也很重要,所以这些资料会帮助我更好的了解和熟悉它们。什么是sed在《sedandawk》一书中(1.2AStreamEditor)的解释是:Sed

Text-to-SQL小白入门(六)Awesome-Text2SQL项目介绍

项目介绍项目地址GitHub地址:GitHub-eosphoros-ai/Awesome-Text2SQL:CuratedtutorialsandresourcesforLargeLanguageModels,Text2SQL,andmore.项目首页欢迎大家围观参与、使用、贡献。项目理念这个项目主要收集了针对大型语言

vite和webpack的区别

vite和webpack的区别1、前言2、Webpack2.1Webpack简述2.2Webpack常用插件3、Vite3.1Vite简述3.2Vite插件推荐4、区别4.1开发模式不同4.2打包效率不同4.3插件生态不同4.4配置复杂度不同4.5热更新机制不同5、总结1、前言Webpack和Vite是现代前端开发中非

笔记1.5:计算机网络体系结构

从功能上描述计算机网络结构分层结构每层遵循某个网络协议完成本层功能1.基本概念实体:表示任何可发送或接收信息的硬件或软件进程。协议是控制两个对等实体进行通信的规则的集合,协议是水平的。任一层实体需要使用下层服务,遵循本层协议,实现本层功能,向上层提供服务,服务是垂直的。下一层协议的实现对上层的服务用户是透明的同系统的相

Mysql 数据库基础介绍

Mysql数据库基础介绍一、数据库介绍1.1、数据库的发展史1.1.1、文件管理系统的缺点1.1.2、数据库系统发展阶段1.3、DBMS数据库管理系统1.4、数据库管理系统的优点1.5、数据库管理系统的基本功能1.6、数据库系统的架构1.7、各种数据库管理系统1.7.1、层次数据库1.7.2、网状数据库1.7.3、RD

TCP/IP协议栈各层涉及到的协议

21/tcpFTP文件传输协议22/tcpSSH安全登录、文件传送(SCP)和端口重定向23/tcpTelnet远程连接80/tcpHTTP443/tcpHTTPS计算机各层网络协议五层:应用层:(典型设备:应用程序,如FTP,SMTP,HTTP)DHCP(DynamicHostConfigurationProtoco

动态规划问题

看一遍就理解:动态规划详解-什么样的问题可以考虑使用动态规划解决呢?如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。-动态规划的解题思路动态规划的核心思想就

面试:C++ 11 智能指针

查询内存泄露方法啥是内存泄露内存泄露在维基百科中的解释如下:在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。在C++中出现内存泄露的主要原因就是程

若依注解学习(一)@Log

@Log涉及到:Log,LogAspect,SecurityUtils,SysUser,SysOperLog,BusinessStatus,StringUtils,ServletUtilsAsyncManager,AsyncFactory,AddressUtils,IpUtils,RuoYiConfig,HttpUti

3D模型转换工具HOOPS Exchange如何实现OBJ格式轻量化?

什么是OBJ模型轻量化?OBJ格式是一种常用的三维模型文件格式,通常包含模型的顶点、法线、纹理坐标等信息,但有时候这些信息可能会使模型文件变得较大,不利于网络传输、加载和运行。OBJ(Object)模型轻量化是指对OBJ格式的三维模型数据进行优化和压缩,以减少模型文件的大小和内存占用,同时尽量保持模型的视觉质量和几何信

浙工大MBA常规批复试建议怎么准备?

&nbsp;&nbsp;&nbsp;&nbsp;如果你第一志愿报考浙江工业大学MBA项目不想被无故的淘汰,小编建议你还是认真对待考后的复试环节。因为从2023年的招生录取可以看到,浙工大MBA项目的提前批面试对于最终录取几乎是没什么作用的,在与普通考生同层次竞争的局面下,复试谁发挥的好,分数高,谁就可以获得录取的资格。

热文推荐