Android 回声消除

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这个类没啥用

相关推荐
阿巴斯甜28 分钟前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95272 小时前
Andorid Google 登录接入文档
android
黄林晴3 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab16 小时前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿19 小时前
Android MediaPlayer 笔记
android
Jony_19 小时前
Android 启动优化方案
android
阿巴斯甜19 小时前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇19 小时前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android