打赏

android webrtc 消除回声

package com.aipeople.by.test;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaPlayer;
import android.media.MediaRecorder;
import android.util.Log;

import org.webrtc.voiceengine.WebRtcAudioEffects;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 音频管理工具类(集成WebRTC回声消除+噪声抑制,适配所有Android版本,无编译错误)
 */
public class AudioManager {
    private static final String TAG = "AudioManager";
    private Context mContext;

    // 录音参数(适配WebRTC)
    private static final int SAMPLE_RATE = 16000;
    private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;
    private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    private AudioRecord mAudioRecord;
    private WebRtcAudioEffects webRtcAudioEffects; // 核心音频效果器
    private boolean isRecording = false;
    private File mRecordFile;

    // 兼容低版本Android:SESSION_ID_GENERATE的实际值是-1
    private static final int AUDIO_SESSION_ID_GENERATE = -1;

    // 播放相关
    private MediaPlayer mMediaPlayer;

    public AudioManager(Context context) {
        this.mContext = context;
        initRecordFile();
    }

    /**
     * 初始化录音文件(应用私有目录,无需外部权限)
     */
    private void initRecordFile() {
        File recordDir = new File(mContext.getFilesDir(), "AudioDemo");
        if (!recordDir.exists()) recordDir.mkdirs();
        mRecordFile = new File(recordDir, "record_" + System.currentTimeMillis() + ".pcm");
    }

    /**
     * 开始录音(WebRTC回声消除+噪声抑制)
     * @return 录音是否启动成功
     */
    public boolean startRecordWithAEC() {
        try {
            // 1. 计算最小缓存区
            int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
            if (bufferSize == AudioRecord.ERROR_BAD_VALUE) {
                Log.e(TAG, "缓存区计算失败");
                return false;
            }

            // 2. 创建AudioRecord(通话场景优化)
            mAudioRecord = new AudioRecord(
                    MediaRecorder.AudioSource.VOICE_COMMUNICATION,
                    SAMPLE_RATE,
                    CHANNEL_CONFIG,
                    AUDIO_FORMAT,
                    bufferSize * 2
            );

            // 3. 初始化WebRTC音频效果(核心:AEC+NS,补充生效校验)
            initWebRtcAudioEffects();

            // 4. 开始录音
            mAudioRecord.startRecording();
            isRecording = true;

            // 5. 写入录音数据(WebRTC已在底层处理音频)
            new Thread(() -> {
                FileOutputStream fos = null;
                try {
                    fos = new FileOutputStream(mRecordFile);
                    byte[] buffer = new byte[bufferSize];
                    while (isRecording) {
                        int readSize = mAudioRecord.read(buffer, 0, buffer.length);
                        if (readSize > 0) {
                            fos.write(buffer, 0, readSize); // 直接写入处理后的音频
                        }
                    }
                } catch (IOException e) {
                    Log.e(TAG, "录音写入失败:" + e.getMessage());
                } finally {
                    try {
                        if (fos != null) fos.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            Log.d(TAG, "录音启动成功,WebRTC音频效果已启用");
            return true;
        } catch (Exception e) {
            Log.e(TAG, "录音启动失败:" + e.getMessage());
            releaseWebRtcResources();
            return false;
        }
    }

    /**
     * 初始化WebRTC音频效果器(补充AEC生效校验)
     */
    private void initWebRtcAudioEffects() {
        try {
            if (mAudioRecord == null) return;

            // 1. 先检查设备是否支持硬件AEC
            boolean aecSupported = WebRtcAudioEffects.canUseAcousticEchoCanceler();
            boolean nsSupported = WebRtcAudioEffects.canUseNoiseSuppressor();
            Log.d(TAG, "设备支持AEC: " + aecSupported + ", 支持NS: " + nsSupported);
            if (!aecSupported) {
                Log.w(TAG, "⚠️ 设备不支持硬件AEC,回声消除可能无效!");
            }

            // 2. 正确创建实例:无参构造
            webRtcAudioEffects = WebRtcAudioEffects.create();
            if (webRtcAudioEffects != null) {
                // 3. 正确设置效果:只有setAEC和setNS,没有setAgc
                boolean aecEnabled = webRtcAudioEffects.setAEC(true); // 开启回声消除
                boolean nsEnabled = webRtcAudioEffects.setNS(true);  // 开启噪声抑制
                Log.d(TAG, "WebRTC AEC开启结果: " + aecEnabled + ", NS开启结果: " + nsEnabled);

                // 4. 正确启用:需要传入audioSessionId
                webRtcAudioEffects.enable(mAudioRecord.getAudioSessionId());
                Log.d(TAG, "WebRTC音频效果已启用,音频会话ID: " + mAudioRecord.getAudioSessionId());
            } else {
                Log.w(TAG, "设备不支持WebRTC音频效果,使用系统默认");
            }
        } catch (Exception e) {
            Log.e(TAG, "WebRTC音频效果初始化失败:" + e.getMessage());
        }
    }

    /**
     * 停止录音
     */
    public void stopRecord() {
        isRecording = false;

        // 释放AudioRecord
        if (mAudioRecord != null) {
            try {
                mAudioRecord.stop();
                mAudioRecord.release();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                mAudioRecord = null;
            }
        }

        // 释放WebRTC资源
        releaseWebRtcResources();

        Log.d(TAG, "录音停止,文件保存至:" + mRecordFile.getAbsolutePath());
    }

    /**
     * 释放WebRTC音频效果资源
     */
    private void releaseWebRtcResources() {
        if (webRtcAudioEffects != null) {
            webRtcAudioEffects.release();
            webRtcAudioEffects = null;
        }
    }

    /**
     * 播放在线音频(已绑定录音的音频会话ID,解决回声问题)
     */
    public void playOnlineAudio(String audioUrl, OnAudioPlayCallback callback) {
        stopAllPlayback();

        try {
            mMediaPlayer = new MediaPlayer();
            // 关键修改1:改用通话声道(STREAM_VOICE_CALL)
            mMediaPlayer.setAudioStreamType(android.media.AudioManager.STREAM_VOICE_CALL);

            // 关键修改2:绑定到录音的音频会话ID,让AEC获取参考音频
            if (mAudioRecord != null) {
                mMediaPlayer.setAudioSessionId(mAudioRecord.getAudioSessionId());
                Log.d(TAG, "在线音频绑定到录音会话ID: " + mAudioRecord.getAudioSessionId());
            }

            mMediaPlayer.setDataSource(audioUrl);
            mMediaPlayer.prepareAsync();

            mMediaPlayer.setOnPreparedListener(mp -> {
                mp.start();
                if (callback != null) callback.onStart();
                Log.d(TAG, "在线音频开始播放");
            });

            mMediaPlayer.setOnCompletionListener(mp -> {
                if (callback != null) callback.onComplete();
                stopAllPlayback();
            });

            mMediaPlayer.setOnErrorListener((mp, what, extra) -> {
                String errorMsg = "播放失败,错误码:" + what;
                if (callback != null) callback.onError(errorMsg);
                stopAllPlayback();
                Log.e(TAG, errorMsg);
                return true;
            });

        } catch (Exception e) {
            String errorMsg = "在线音频初始化失败:" + e.getMessage();
            if (callback != null) callback.onError(errorMsg);
            Log.e(TAG, errorMsg);
        }
    }

    /**
     * 播放本地录音(适配所有Android版本,无编译错误)
     */
    public void playLocalRecord(OnAudioPlayCallback callback) {
        if (!mRecordFile.exists()) {
            String errorMsg = "录音文件不存在";
            if (callback != null) callback.onError(errorMsg);
            Log.w(TAG, errorMsg);
            return;
        }

        stopAllPlayback();
        new Thread(() -> {
            AudioTrack audioTrack = null;
            FileInputStream fis = null;
            try {
                int bufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE, AudioFormat.CHANNEL_OUT_MONO, AUDIO_FORMAT);

                // 关键:统一使用传统构造方法(适配所有Android版本,无编译错误)
                int audioSessionId = AUDIO_SESSION_ID_GENERATE;
                if (mAudioRecord != null) {
                    audioSessionId = mAudioRecord.getAudioSessionId();
                }
                if (audioSessionId<0){
                    audioSessionId=0;
                }
                
                // 所有版本都用这个构造方法,避免Builder的兼容性问题
                audioTrack = new AudioTrack(
                        android.media.AudioManager.STREAM_VOICE_CALL,  // 通话声道,适配AEC
                        SAMPLE_RATE,
                        AudioFormat.CHANNEL_OUT_MONO,
                        AUDIO_FORMAT,
                        bufferSize,
                        AudioTrack.MODE_STREAM,
                        audioSessionId // 绑定录音会话ID(所有版本都支持)
                );

                Log.d(TAG, "本地录音绑定到录音会话ID: " + audioSessionId);

                audioTrack.play();
                if (callback != null) callback.onStart();

                fis = new FileInputStream(mRecordFile);
                byte[] buffer = new byte[bufferSize];
                int readSize;
                while ((readSize = fis.read(buffer)) > 0) {
                    audioTrack.write(buffer, 0, readSize);
                }

                audioTrack.stop();
                if (callback != null) callback.onComplete();

            } catch (Exception e) {
                String errorMsg = "本地录音播放失败:" + e.getMessage();
                if (callback != null) callback.onError(errorMsg);
                Log.e(TAG, errorMsg);
            } finally {
                if (audioTrack != null) audioTrack.release();
                try {
                    if (fis != null) fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    /**
     * 停止所有音频播放
     */
    public void stopAllPlayback() {
        if (mMediaPlayer != null) {
            try {
                if (mMediaPlayer.isPlaying()) mMediaPlayer.stop();
                mMediaPlayer.release();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                mMediaPlayer = null;
            }
        }
    }

    /**
     * 检查是否有有效录音文件
     */
    public boolean hasValidRecordFile() {
        return mRecordFile != null && mRecordFile.exists() && mRecordFile.length() > 0;
    }

    /**
     * 音频播放回调接口
     */
    public interface OnAudioPlayCallback {
        void onStart(); // 开始播放
        void onComplete(); // 播放完成
        void onError(String errorMsg); // 播放错误
    }

    /**
     * 释放资源
     */
    public void release() {
        stopRecord();
        stopAllPlayback();
    }
}

  

posted @ 2026-01-28 16:24  吃瓜大湿熊  阅读(2)  评论(0)    收藏  举报