如何同时实现多麦克风监听?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
相关产品推荐
相关产品推荐

