• 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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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
    
    • 1

    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);
    
    • 1

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

    int read(byte[], int, int)  
    int read(short[], int, int)  
    int read(java.nio.ByteBuffer, int) 
    
    • 1
    • 2
    • 3

    选择使用哪种方法取决于对开发者来说最方便的音频数据存储格式。 创建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.
    
     * 

    * Capturing from VOICE_UPLINK 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. *

    */ public static final int VOICE_UPLINK = 2; /** Voice call downlink (Rx) audio source. *

    * Capturing from VOICE_DOWNLINK 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. *

    */ public static final int VOICE_DOWNLINK = 3; /** Voice call uplink + downlink audio source *

    * Capturing from VOICE_CALL 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. *

    */ 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;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68

    可见,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回声消除");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    播放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();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    方式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");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    其中的AudioRecorder

    audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
            sampleRateInHz,
            channelConfigIn,
            audioFormat,
            recordBufSize
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    播放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);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    播放TTS

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

    方式4(可以用)

    打开录音后,设置

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

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

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

    AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    audioManager.setMode(AudioManager.MODE_NORMAL);
    audioManager.setSpeakerphoneOn(false);
    
    • 1
    • 2
    • 3

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

  • 相关阅读:
    【定时功能】消息的定时发送-基于RocketMQ
    5. Layui数据表格的快速使用
    HTML 知识扫盲
    正则表达式总结与应用
    Linux网络编程系列之服务器编程——信号驱动模型
    WebDAV之葫芦儿·派盘 + Koder
    使用华为eNSP组网试验⑸-访问控制
    Python自动化小技巧11——excel文件的文字内容筛选
    c++ 移动构造方法为什么要加noexcept
    优秀的 Verilog/FPGA开源项目介绍(三十九)- NVMe
  • 原文地址:https://blog.csdn.net/fromVillageCoolBoy/article/details/132978994