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

Android 14音频录制异常:录制时长与播放速度问题排查

解决Android 14音频录制速度异常问题

核心问题分析

你遇到的录制时长缩短、播放倍速问题,大概率是以下几个原因导致:

  1. 硬编码的AudioRecord缓冲区大小不符合Android 14的要求,导致音频采集节奏异常;
  2. MediaCodec编码时未传入正确的时间戳,编码器无法正确计算音频时长;
  3. AudioRecord实际使用的采样率与配置值不一致,Android 14可能自动降级采样率,但编码仍用原配置,导致播放速度错误。

具体修复步骤

1. 使用系统推荐的最小缓冲区大小初始化AudioRecord

硬编码bufferSize * 10可能在Android 14中导致缓冲区过小,引发采集速度异常。替换为AudioRecord.getMinBufferSize()计算正确的缓冲区:

private AudioRecord createAudioRecord() {
    int minBufferSize = AudioRecord.getMinBufferSize(
        SAMPLE_RATE,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT
    );
    if (minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
        throw new RuntimeException("Unsupported audio parameters");
    }
    // 额外扩大缓冲区避免丢帧
    int bufferSize = minBufferSize * 2;

    AudioRecord audioRecord = new AudioRecord(
        MediaRecorder.AudioSource.MIC,
        SAMPLE_RATE,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT,
        bufferSize
    );

    // 原有的NoiseSuppressor、AutomaticGainControl逻辑保持不变
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        if (android.media.audiofx.NoiseSuppressor.isAvailable()) {
            android.media.audiofx.NoiseSuppressor noiseSuppressor = android.media.audiofx.NoiseSuppressor
                    .create(audioRecord.getAudioSessionId());
            if (noiseSuppressor != null) {
                noiseSuppressor.setEnabled(true);
            }
        }
        if (android.media.audiofx.AutomaticGainControl.isAvailable()) {
            android.media.audiofx.AutomaticGainControl automaticGainControl = android.media.audiofx.AutomaticGainControl
                    .create(audioRecord.getAudioSessionId());
            if (automaticGainControl != null) {
                automaticGainControl.setEnabled(true);
            }
        }
    }

    return audioRecord;
}

2. 给MediaCodec输入数据时附加正确的时间戳

在run()方法的循环读取PCM数据环节,必须计算并传入基于采样数的时间戳,否则编码器会错误生成时长信息。示例代码:

@Override
public void run() {
    long presentationTimeUs = 0;
    byte[] pcmBuffer = new byte[audioRecord.getBufferSizeInFrames() * 2]; // 16bit单声道,每个帧占2字节
    while (!isInterrupted()) {
        int readSize = audioRecord.read(pcmBuffer, 0, pcmBuffer.length);
        if (readSize <= 0) {
            break;
        }

        // 计算当前读取的采样数:16bit采样,每个采样占2字节
        int sampleCount = readSize / 2;
        // 计算时间戳:(采样数 * 1000000) / 采样率
        presentationTimeUs += (sampleCount * 1000000L) / SAMPLE_RATE;

        // 将PCM数据喂给MediaCodec
        int inputBufferId = mediaCodec.dequeueInputBuffer(-1);
        if (inputBufferId >= 0) {
            ByteBuffer inputBuffer = mediaCodec.getInputBuffer(inputBufferId);
            inputBuffer.clear();
            inputBuffer.put(pcmBuffer, 0, readSize);
            mediaCodec.queueInputBuffer(
                inputBufferId,
                0,
                readSize,
                presentationTimeUs,
                0
            );
        }

        // 处理MediaCodec输出数据,写入文件
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, -1);
        while (outputBufferId >= 0) {
            ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
            if (outputBuffer != null && bufferInfo.size > 0) {
                // 写入ADTS头
                byte[] adtsHeader = createAdtsHeader(bufferInfo.size);
                outputStream.write(adtsHeader);
                // 写入编码后的数据
                byte[] data = new byte[bufferInfo.size];
                outputBuffer.get(data);
                outputStream.write(data);
                outputBuffer.clear();
            }
            mediaCodec.releaseOutputBuffer(outputBufferId, false);
            outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, -1);
        }
    }

    // 结束录制,发送结束标志
    int inputBufferId = mediaCodec.dequeueInputBuffer(-1);
    if (inputBufferId >= 0) {
        mediaCodec.queueInputBuffer(
            inputBufferId,
            0,
            0,
            presentationTimeUs,
            MediaCodec.BUFFER_FLAG_END_OF_STREAM
        );
    }

    // 处理剩余输出数据
    MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
    int outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, -1);
    while (outputBufferId >= 0) {
        if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
            break;
        }
        ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId);
        if (outputBuffer != null && bufferInfo.size > 0) {
            byte[] adtsHeader = createAdtsHeader(bufferInfo.size);
            outputStream.write(adtsHeader);
            byte[] data = new byte[bufferInfo.size];
            outputBuffer.get(data);
            outputStream.write(data);
            outputBuffer.clear();
        }
        mediaCodec.releaseOutputBuffer(outputBufferId, false);
        outputBufferId = mediaCodec.dequeueOutputBuffer(bufferInfo, -1);
    }

    // 释放资源
    mediaCodec.stop();
    mediaCodec.release();
    audioRecord.stop();
    audioRecord.release();
    outputStream.close();
}

3. 验证并同步AudioRecord实际采样率到MediaCodec

Android 14可能会自动调整不支持的采样率,需确认AudioRecord实际使用的采样率,并同步到MediaCodec配置:

private AudioRecord createAudioRecord() {
    int minBufferSize = AudioRecord.getMinBufferSize(
        SAMPLE_RATE,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT
    );
    if (minBufferSize == AudioRecord.ERROR_BAD_VALUE) {
        throw new RuntimeException("Unsupported audio parameters");
    }
    int bufferSize = minBufferSize * 2;

    AudioRecord audioRecord = new AudioRecord(
        MediaRecorder.AudioSource.MIC,
        SAMPLE_RATE,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT,
        bufferSize
    );

    // 检查实际采样率
    int actualSampleRate = audioRecord.getSampleRate();
    if (actualSampleRate != SAMPLE_RATE) {
        Log.w(TAG, "Sample rate fallback to: " + actualSampleRate);
        SAMPLE_RATE = actualSampleRate;
        SAMPLE_RATE_INDEX = getSampleRateIndex(actualSampleRate);
    }

    // 原有的音频效果逻辑保持不变
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        if (android.media.audiofx.NoiseSuppressor.isAvailable()) {
            android.media.audiofx.NoiseSuppressor noiseSuppressor = android.media.audiofx.NoiseSuppressor
                    .create(audioRecord.getAudioSessionId());
            if (noiseSuppressor != null) {
                noiseSuppressor.setEnabled(true);
            }
        }
        if (android.media.audiofx.AutomaticGainControl.isAvailable()) {
            android.media.audiofx.AutomaticGainControl automaticGainControl = android.media.audiofx.AutomaticGainControl
                    .create(audioRecord.getAudioSessionId());
            if (automaticGainControl != null) {
                automaticGainControl.setEnabled(true);
            }
        }
    }

    return audioRecord;
}

// 根据采样率获取对应的AAC索引
private int getSampleRateIndex(int sampleRate) {
    switch (sampleRate) {
        case 96000: return 0;
        case 88200: return 1;
        case 64000: return 2;
        case 48000: return 3;
        case 44100: return 4;
        case 32000: return 5;
        case 24000: return 6;
        case 22050: return 7;
        case 16000: return 8;
        case 12000: return 9;
        case 11025: return 10;
        case 8000: return 11;
        default: return 4; // 默认44100
    }
}

然后在createMediaCodec()方法中,使用更新后的SAMPLE_RATE配置MediaFormat:

private MediaCodec createMediaCodec() throws IOException {
    MediaCodec mediaCodec = MediaCodec.createEncoderByType("audio/mp4a-latm");
    MediaFormat mediaFormat = new MediaFormat();

    mediaFormat.setString(MediaFormat.KEY_MIME, "audio/mp4a-latm");
    mediaFormat.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
    mediaFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
    mediaFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, audioRecord.getBufferSizeInFrames() * 2);
    mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
    mediaFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);

    try {
        mediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    } catch (Exception e) {
        e.printStackTrace();
        mediaCodec.release();
        throw new IOException(e);
    }

    return mediaCodec;
}

额外注意事项

  • 确保已申请RECORD_AUDIO权限,并在运行时动态授权;
  • 录制线程需在后台线程运行,避免阻塞主线程;
  • 释放资源时需按顺序:先停止编码器,再停止AudioRecord,最后关闭文件输出流。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 12:35:06