You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何同时实现多麦克风监听?AudioRecorder与AMR录制共存问题咨询

解决同一麦克风资源下的音频振幅监测与定时AMR录制问题

这个问题我之前碰到过不少类似的,核心就是要绕开麦克风的独占性限制——咱们不能让两个类各自去抢麦克风,得搞一个「统一管理中心」来控制音频采集,然后把采集到的数据同时分给需要的类处理。下面是具体的实现方案:

1. 先搞一个单例的音频管理类

创建一个全局唯一的AudioManager,所有和麦克风相关的操作都通过它来走,这样就保证了只有一个AudioRecorder实例在访问麦克风,避免冲突。这个类的职责就是启动/停止采集,然后把采集到的音频数据分发给所有订阅的监听者。

public class AudioManager {
    private static AudioManager instance;
    private AudioRecorder audioRecorder;
    private boolean isRecording = false;
    private byte[] audioBuffer;
    private List<AudioDataListener> listeners = new ArrayList<>();

    // 私有构造,保证单例
    private AudioManager() {
        // 初始化AudioRecorder,参数要适配AMR录制(比如8000Hz采样率、单声道)
        int audioSource = MediaRecorder.AudioSource.MIC;
        int sampleRate = 8000;
        int channel = AudioFormat.CHANNEL_IN_MONO;
        int format = AudioFormat.ENCODING_PCM_16BIT;
        int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channel, format);
        
        audioRecorder = new AudioRecord(audioSource, sampleRate, channel, format, bufferSize);
        audioBuffer = new byte[bufferSize];
    }

    public static synchronized AudioManager getInstance() {
        if (instance == null) {
            instance = new AudioManager();
        }
        return instance;
    }

    public void startRecording() {
        if (!isRecording && audioRecorder != null) {
            audioRecorder.startRecording();
            isRecording = true;
            // 开个线程持续读取音频数据
            new Thread(() -> {
                while (isRecording) {
                    int readLen = audioRecorder.read(audioBuffer, 0, audioBuffer.length);
                    if (readLen > 0) {
                        // 把数据发给所有监听者
                        for (AudioDataListener listener : listeners) {
                            listener.onAudioDataReceived(audioBuffer, readLen);
                        }
                    }
                }
            }).start();
        }
    }

    public void stopRecording() {
        if (isRecording && audioRecorder != null) {
            isRecording = false;
            audioRecorder.stop();
        }
    }

    // 添加/移除监听者的方法
    public void addListener(AudioDataListener listener) {
        if (!listeners.contains(listener)) {
            listeners.add(listener);
        }
    }

    public void removeListener(AudioDataListener listener) {
        listeners.remove(listener);
    }

    // 定义监听接口,让需要数据的类实现
    public interface AudioDataListener {
        void onAudioDataReceived(byte[] data, int length);
    }
}

2. 实现Class A:监测音频振幅

让Class A实现上面的AudioDataListener接口,拿到PCM数据后计算振幅就行——PCM 16位的数据是每个样本占2字节,取绝对值找峰值就是当前的振幅了。

public class ClassA implements AudioManager.AudioDataListener {
    private AudioManager audioManager;

    public ClassA() {
        audioManager = AudioManager.getInstance();
        audioManager.addListener(this);
        // 如果还没开始采集,直接启动
        audioManager.startRecording();
    }

    @Override
    public void onAudioDataReceived(byte[] data, int length) {
        int maxAmplitude = 0;
        // 遍历PCM数据,计算最大振幅
        for (int i = 0; i < length; i += 2) {
            // 16位PCM是小端格式,拼接两个字节成short
            short sample = (short) ((data[i] & 0xFF) | (data[i+1] << 8));
            int amp = Math.abs(sample);
            if (amp > maxAmplitude) {
                maxAmplitude = amp;
            }
        }
        // 这里可以把振幅用起来,比如更新UI或者存到本地
        System.out.println("当前振幅:" + maxAmplitude);
    }

    // 不用的时候记得移除监听,避免内存泄漏
    public void cleanup() {
        audioManager.removeListener(this);
    }
}

3. 实现Class B:每5分钟录一次AMR

Class B同样实现监听接口,拿到PCM数据后用MediaCodec编码成AMR格式,同时每5分钟切换一个新文件。注意AMR文件需要先写头信息#!AMR\n,不然播放器识别不了。

public class ClassB implements AudioManager.AudioDataListener {
    private AudioManager audioManager;
    private MediaCodec amrEncoder;
    private FileOutputStream amrOut;
    private long currentRecordStart;
    private static final long RECORD_INTERVAL = 5 * 60 * 1000; // 5分钟

    public ClassB() {
        audioManager = AudioManager.getInstance();
        audioManager.addListener(this);
        audioManager.startRecording();
        initAmrEncoder();
        startNewRecordFile();
    }

    // 初始化AMR编码器
    private void initAmrEncoder() {
        try {
            amrEncoder = MediaCodec.createEncoderByType("audio/AMR_NB");
            MediaFormat format = MediaFormat.createAudioFormat("audio/AMR_NB", 8000, 1);
            format.setInteger(MediaFormat.KEY_BIT_RATE, 12200); // AMR_NB的标准码率
            amrEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            amrEncoder.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 创建新的录制文件
    private void startNewRecordFile() {
        try {
            if (amrOut != null) {
                amrOut.close();
            }
            // 生成带时间戳的文件名
            String fileName = "amr_record_" + System.currentTimeMillis() + ".amr";
            File recordFile = new File(Environment.getExternalStorageDirectory(), fileName);
            amrOut = new FileOutputStream(recordFile);
            // 写入AMR文件头,必须加这个
            amrOut.write("#!AMR\n".getBytes());
            currentRecordStart = System.currentTimeMillis();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onAudioDataReceived(byte[] data, int length) {
        // 检查是否到了5分钟,到了就换文件
        if (System.currentTimeMillis() - currentRecordStart >= RECORD_INTERVAL) {
            startNewRecordFile();
        }

        // 把PCM数据送入编码器
        if (amrEncoder != null) {
            int inputBufIdx = amrEncoder.dequeueInputBuffer(-1);
            if (inputBufIdx >= 0) {
                ByteBuffer inputBuf = amrEncoder.getInputBuffer(inputBufIdx);
                inputBuf.clear();
                inputBuf.put(data, 0, length);
                amrEncoder.queueInputBuffer(inputBufIdx, 0, length, System.nanoTime() / 1000, 0);
            }

            // 取出编码后的AMR数据写入文件
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
            int outputBufIdx = amrEncoder.dequeueOutputBuffer(bufferInfo, 0);
            while (outputBufIdx >= 0) {
                ByteBuffer outputBuf = amrEncoder.getOutputBuffer(outputBufIdx);
                if (bufferInfo.size > 0) {
                    byte[] amrData = new byte[bufferInfo.size];
                    outputBuf.get(amrData);
                    try {
                        amrOut.write(amrData);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                amrEncoder.releaseOutputBuffer(outputBufIdx, false);
                outputBufIdx = amrEncoder.dequeueOutputBuffer(bufferInfo, 0);
            }
        }
    }

    // 销毁资源,记得调用
    public void cleanup() {
        audioManager.removeListener(this);
        if (amrEncoder != null) {
            amrEncoder.stop();
            amrEncoder.release();
        }
        try {
            if (amrOut != null) {
                amrOut.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

几个关键注意点

  • 权限要到位:AndroidManifest里要加RECORD_AUDIO和WRITE_EXTERNAL_STORAGE权限,6.0以上还要动态申请。
  • 资源要清理:在页面销毁的时候,一定要调用Class A和Class B的cleanup方法,不然会内存泄漏,还可能占着麦克风不放。
  • 线程安全:如果多个线程操作监听列表,最好用线程安全的集合(比如CopyOnWriteArrayList),或者加锁。
  • 兼容性测试:不同手机的MediaCodec对AMR的支持可能有差异,多测几款机型稳一点。

这样一来,两个类就共享同一个麦克风采集的数据,各自干自己的活,完美解决独占问题~

内容的提问来源于stack exchange,提问作者abrutsze

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 03:42:18