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

如何开发可同时录制系统与环境音频的AudioRecorder测试应用?

Android 双音频录制实现方案(系统音频+环境音频)

一、必要权限声明

在AndroidManifest.xml中添加以下权限:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" android:minSdkVersion="33" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

注:Android 13+ 改用READ_MEDIA_AUDIO替代存储权限;系统音频录制需通过MediaProjection申请屏幕录制权限(无需实际录屏,仅获取系统音频流)

二、核心实现代码

1. 申请MediaProjection权限(系统音频录制必备)

private static final int REQUEST_CODE_MEDIA_PROJECTION = 1001;
private MediaProjectionManager mediaProjectionManager;
private MediaProjection mediaProjection;

// 触发系统音频权限申请
private void requestMediaProjection() {
    mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
    Intent intent = mediaProjectionManager.createScreenCaptureIntent();
    startActivityForResult(intent, REQUEST_CODE_MEDIA_PROJECTION);
}

// 处理权限回调
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_MEDIA_PROJECTION && resultCode == RESULT_OK) {
        mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data);
        // 权限获取成功,初始化双轨录制
        initDualAudioRecording();
    }
}

2. 初始化双音频录制(系统+麦克风)

private AudioRecord micAudioRecord;
private AudioRecord systemAudioRecord;
private MediaMuxer mediaMuxer;
private int micTrackIndex = -1;
private int systemTrackIndex = -1;
private boolean isRecording = false;

// 统一音频参数
private static final int SAMPLE_RATE = 44100;
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;

private void initDualAudioRecording() {
    // 初始化麦克风录制(环境音频)
    int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT);
    micAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, bufferSize * 2);

    // 初始化系统音频录制
    AudioFormat systemAudioFormat = new AudioFormat.Builder()
            .setSampleRate(SAMPLE_RATE)
            .setEncoding(AUDIO_FORMAT)
            .setChannelMask(CHANNEL_CONFIG)
            .build();
    AudioRecord.Builder systemRecordBuilder = new AudioRecord.Builder()
            .setAudioFormat(systemAudioFormat)
            .setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX)
            .setBufferSizeInBytes(bufferSize * 2);
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
        systemRecordBuilder.setMediaProjection(mediaProjection);
    }
    systemAudioRecord = systemRecordBuilder.build();

    // 初始化MediaMuxer,合成双轨MP4文件
    String outputPath = getExternalFilesDir(Environment.DIRECTORY_MUSIC) + "/dual_audio_record.mp4";
    try {
        mediaMuxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
    } catch (IOException e) {
        e.printStackTrace();
        return;
    }

    // 启动录制线程
    startRecordingThreads();
}

3. 启动双轨录制线程

private void startRecordingThreads() {
    isRecording = true;

    // 麦克风录制线程
    new Thread(() -> {
        micAudioRecord.startRecording();
        byte[] buffer = new byte[AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) * 2];
        
        // 初始化AAC编码器并添加麦克风轨道到Muxer
        initAacEncoder();
        MediaFormat micMediaFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, 2);
        micMediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        micMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
        micTrackIndex = mediaMuxer.addTrack(micMediaFormat);
        systemTrackIndex = mediaMuxer.addTrack(micMediaFormat); // 系统音频轨道参数与麦克风一致
        mediaMuxer.start();

        while (isRecording) {
            int readSize = micAudioRecord.read(buffer, 0, buffer.length);
            if (readSize > 0) {
                ByteBuffer encodedData = encodePcmToAac(buffer, readSize);
                if (encodedData != null) {
                    writeToMuxer(micTrackIndex, encodedData);
                }
            }
        }
        micAudioRecord.stop();
        micAudioRecord.release();
    }).start();

    // 系统音频录制线程
    new Thread(() -> {
        systemAudioRecord.startRecording();
        byte[] buffer = new byte[AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) * 2];
        
        while (isRecording) {
            int readSize = systemAudioRecord.read(buffer, 0, buffer.length);
            if (readSize > 0 && systemTrackIndex != -1) {
                ByteBuffer encodedData = encodePcmToAac(buffer, readSize);
                if (encodedData != null) {
                    writeToMuxer(systemTrackIndex, encodedData);
                }
            }
        }
        systemAudioRecord.stop();
        systemAudioRecord.release();
    }).start();
}

// 停止录制
private void stopRecording() {
    isRecording = false;
    if (mediaMuxer != null) {
        mediaMuxer.stop();
        mediaMuxer.release();
    }
    if (aacEncoder != null) {
        aacEncoder.stop();
        aacEncoder.release();
    }
}

// 将编码后的数据写入Muxer
private void writeToMuxer(int trackIndex, ByteBuffer encodedData) {
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    bufferInfo.offset = 0;
    bufferInfo.size = encodedData.remaining();
    bufferInfo.presentationTimeUs = System.nanoTime() / 1000;
    bufferInfo.flags = MediaCodec.BUFFER_FLAG_KEY_FRAME;
    mediaMuxer.writeSampleData(trackIndex, encodedData, bufferInfo);
}

4. PCM转AAC编码工具

private MediaCodec aacEncoder;
private ByteBuffer[] inputBuffers;
private ByteBuffer[] outputBuffers;

private void initAacEncoder() {
    try {
        MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, 2);
        format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
        format.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
        aacEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC);
        aacEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        aacEncoder.start();
        inputBuffers = aacEncoder.getInputBuffers();
        outputBuffers = aacEncoder.getOutputBuffers();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

private ByteBuffer encodePcmToAac(byte[] pcmData, int length) {
    int inputBufferIndex = aacEncoder.dequeueInputBuffer(-1);
    if (inputBufferIndex >= 0) {
        ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
        inputBuffer.clear();
        inputBuffer.put(pcmData, 0, length);
        aacEncoder.queueInputBuffer(inputBufferIndex, 0, length, System.nanoTime() / 1000, 0);
    }

    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int outputBufferIndex = aacEncoder.dequeueOutputBuffer(bufferInfo, 0);
    while (outputBufferIndex >= 0) {
        ByteBuffer outputBuffer = outputBuffers[outputBufferIndex];
        ByteBuffer encodedData = ByteBuffer.allocate(bufferInfo.size);
        encodedData.put(outputBuffer);
        aacEncoder.releaseOutputBuffer(outputBufferIndex, false);
        outputBufferIndex = aacEncoder.dequeueOutputBuffer(bufferInfo, 0);
        return encodedData;
    }
    return null;
}

三、关键注意事项

  • 系统音频兼容性:MediaRecorder.AudioSource.REMOTE_SUBMIX仅在Android 10+且通过MediaProjection授权后可用;Android 9及以下需root权限或第三方SDK实现系统音频录制。
  • 参数一致性:两个录制源的采样率、声道数、编码格式必须完全一致,否则会出现音轨不同步或合成失败。
  • 后台录制:Android 8+ 后台录制需启动前台服务,避免被系统强制回收。
  • 性能优化:缓冲区大小建议设置为最小缓冲区的2倍,减少音频丢帧;录制线程需与UI线程分离,避免卡顿。

内容的提问来源于stack exchange,提问作者Ayushi-kaushik22

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 21:40:21