MediaCodec 音频处理

在 Android 多媒体开发领域,音频处理是许多应用的基础需求,从录音应用到视频流媒体平台皆是如此。AVSample 仓库通过 Android 的 MediaCodec API 提供了音频编码和解码的全面实现,该 API 提供硬件加速处理以实现最佳性能。

1、理解音频 MediaCodec

MediaCodec 是 Android 的低级媒体编解码器 API,可访问设备的硬件和软件编解码器。对于音频处理,它能够高效地将原始 PCM(脉冲编码调制)数据编码为 AAC 等压缩格式,也能将压缩音频解码回 PCM 格式。

AVSample 项目为 MediaCodec 音频处理实现了清晰的架构,将关注点分离为处理配置、编解码操作和数据流管理的逻辑组件。

2、核心组件

2.1、AudioConfiguration

音频处理管道的核心是 AudioConfiguration 类,它定义了音频处理所需的所有参数:

val audioConfig = AudioConfiguration.Builder()
    .setFrequency(44100)  // 采样率(Hz)
    .setChannelCount(1)    // 单声道音频
    .setBps(32, 64)       // 比特率范围(kbps)
    .setCodecType(AudioConfiguration.CodeType.ENCODE)
    .build()

这种构建器模式允许您配置基本音频参数,包括:

  • 采样率:默认为 44100 Hz(CD 品质)
  • 声道数:1 为单声道,2 为立体声
  • 比特率:最小和最大 kbps 之间的范围
  • 编码格式:默认为 PCM 16 位
  • 编解码器类型:编码或解码操作
  • AAC 配置文件:默认为 AAC 低复杂度(LC)

2.2、AudioProcessor

AudioProcessor 类管理音频捕获和编码工作流。它继承 Thread 以在后台处理连续的音频处理:

class AudioProcessor(
    privateval mAudioRecord: AudioRecord?, 
    audioConfiguration: AudioConfiguration?
) : Thread() {
    
    privatevar mAudioEncoder: AACEncoder? = null
    privateval mRecordBuffer: ByteArray
    privateval mRecordBufferSize: Int
    
    init {
        mRecordBufferSize = AudioUtils.getMinBufferSize(
            audioConfiguration!!.frequency, 
            audioConfiguration.channelCount
        )
        mRecordBuffer = ByteArray(mRecordBufferSize)
        mAudioEncoder = AACEncoder(audioConfiguration)
        mAudioEncoder!!.prepareCoder()
    }
}

处理器持续从 AudioRecord 实例读取音频数据并馈送给编码器。它还提供暂停、停止和静音音频流的控制方法。

3、MediaCodec 配置

实际的 MediaCodec 设置发生在 AudioMediaCodec 伴生对象中,它提供了为编码和解码创建和配置编解码器的方法:

fun getAudioMediaCodec(configuration: AudioConfiguration): MediaCodec? {
    val format = MediaFormat.createAudioFormat(
        configuration.mime, 
        configuration.frequency, 
        configuration.channelCount
    )
    
    if (configuration.mime.equals(AudioConfiguration.DEFAULT_MIME)) {
        format.setInteger(MediaFormat.KEY_AAC_PROFILE, configuration.aacProfile)
    }
    
    format.setInteger(MediaFormat.KEY_BIT_RATE, configuration.maxBps * 1024)
    format.setInteger(MediaFormat.KEY_SAMPLE_RATE, configuration.frequency)
    format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, configuration.channelCount)
    
    val maxInputSize = AudioUtils.getMinBufferSize(
        configuration.frequency, 
        configuration.channelCount, 
        configuration.encoding
    )
    format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, maxInputSize)
    
    var mediaCodec: MediaCodec? = null
    try {
        if (configuration.codeType == AudioConfiguration.CodeType.ENCODE) {
            mediaCodec = MediaCodec.createEncoderByType(configuration.mime)
            mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
        } elseif (configuration.codeType == AudioConfiguration.CodeType.DECODE) {
            format.setInteger(MediaFormat.KEY_IS_ADTS, configuration.adts)
            mediaCodec = MediaCodec.createDecoderByType(configuration.mime)
            mediaCodec.configure(format, null, null, 0)
        }
    } catch (e: Exception) {
        e.printStackTrace()
        // 出错时清理
    }
    
    return mediaCodec
}

此方法创建一个正确配置的 MediaCodec 实例,包含编码或解码操作所需的所有必要参数。

4、编码过程

实际的编码过程发生在 BaseAudioCodec 类中,它实现了核心的 MediaCodec 工作流:

编码过程遵循以下关键步骤:

  1. 输入缓冲区管理:编解码器请求输入缓冲区,原始 PCM 数据被放置其中
  2. 编码:MediaCodec 处理输入数据并生成编码输出
  3. 输出缓冲区管理:从输出缓冲区检索编码数据
  4. 回调:通过回调接口将编码数据传递给应用程序

以下是核心编码循环:

override fun enqueueCodec(input: ByteArray?) {
    if (mMediaCodec == null) return
    
    val inputBuffers = mMediaCodec!!.inputBuffers
    val outputBuffers = mMediaCodec!!.outputBuffers
    val inputBufferIndex = mMediaCodec!!.dequeueInputBuffer(12000)
    
    if (inputBufferIndex >= 0) {
        val inputBuffer = inputBuffers[inputBufferIndex]
        inputBuffer.clear()
        inputBuffer.put(input)
        mMediaCodec!!.queueInputBuffer(
            inputBufferIndex, 
            0, 
            input!!.size, 
            0, 
            0
        )
    }
    
    var outputBufferIndex = mMediaCodec!!.dequeueOutputBuffer(mBufferInfo, 12000)
    if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        onAudioOutformat(mMediaCodec?.outputFormat)
    }
    
    while (outputBufferIndex >= 0) {
        val outputBuffer = outputBuffers[outputBufferIndex]
        mBufferInfo!!.presentationTimeUs = System.nanoTime() / 1000 - mPts
        onAudioData(outputBuffer, mBufferInfo)
        mMediaCodec!!.releaseOutputBuffer(outputBufferIndex, false)
        outputBufferIndex = mMediaCodec!!.dequeueOutputBuffer(mBufferInfo, 0)
    }
}

此实现处理完整的 MediaCodec 工作流,包括缓冲区管理、时间戳计算和适当的资源清理。

5、ADTS 标头处理

AAC 音频文件通常需要 ADTS(音频数据传输流)标头才能正常播放。AVSample 项目包含原生代码,将这些标头添加到编码的 AAC 数据中:

override fun onAudioAACData(bb: ByteBuffer, bi: MediaCodec.BufferInfo) {
    mAudio = ByteArray(bi.size + 7)
    bb.position(bi.offset)
    bb.limit(bi.offset + bi.size)
    bb.get(mAudio, 7, bi.size)
    ADTSUtils.addADTStoPacket(mAudio!!, mAudio!!.size, 2, 44100, 1)
    mDecode?.enqueueCodec(mAudio!!)
    mFileOutputStream?.write(mAudio, 0, mAudio!!.size)
}

原生实现高效地构建 ADTS 标头,包含正确的采样率、声道配置和配置文件信息:

void addADTStoPacket(
    uint8_t *packet, 
    int packetLen, 
    int sampleInHz, 
    int chanCfgCounts, 
    int profile
) {
    int freqIdx = 4; // 默认为44100 Hz
    switch (sampleInHz) {
        case8000:  freqIdx = 11; break;
        case16000: freqIdx = 8;  break;
        case32000: freqIdx = 5;  break;
        case44100: freqIdx = 4;  break;
        case48000: freqIdx = 3;  break;
        case96000: freqIdx = 0;  break;
        default:    break;
    }
    
    // 填充ADTS数据
    packet[0] = 0xFF;
    packet[1] = 0xF1;
    packet[2] = (((profile - 1) << 6) + (freqIdx << 2) + (chanCfgCounts >> 2));
    packet[3] = (((chanCfgCounts & 3) << 6) + (packetLen >> 11));
    packet[4] = ((packetLen & 0x7FF) >> 3);
    packet[5] = (((packetLen & 7) << 5) + 0x1F);
    packet[6] = 0xFC;
}

ADTS 标头长度为 7 字节,必须添加到每个 AAC 帧才能在大多数媒体播放器中正常播放。原生实现确保此操作的最佳性能。

6、实际实现

AudioMediaCodecActivity 展示了音频编码和解码的完整实现:

fun startEncode(view: View) {
    init()
    mStreamController?.start()
}
 
fun stopEncode(view: View) {
    mStreamController?.stop()
    mDecode?.stop()
    mFileOutputStream?.close()
    mFileOutputStream_PCM?.close()
}

该活动捕获音频,将其编码为 AAC,保存到文件,并同时解码回 PCM 进行验证。这种端到端实现展示了如何将所有组件集成到工作应用程序中。

展示的关键功能:

  • 实时音频捕获和编码
  • 编码 AAC 数据的文件输出
  • 同时解码进行验证
  • 适当的资源管理和清理

7、最佳实践和注意事项

实现 MediaCodec 音频处理时,请考虑这些重要因素:

7.1、缓冲区管理

  • 在访问输入/输出缓冲区之前始终检查缓冲区索引
  • 及时释放输出缓冲区以防止内存泄漏
  • 适当处理 INFO_OUTPUT_FORMAT_CHANGED 回调

7.2、性能优化

  • 根据采样率和声道数使用适当的缓冲区大小
  • 考虑在生产环境中使用异步模式以获得更好的性能
  • 监控时间戳以确保正确同步

7.3、错误处理

  • 为 MediaCodec 操作实现适当的异常处理
  • 在错误条件下清理资源
  • 优雅地处理设备特定的编解码器变化

7.4、线程管理

  • 音频处理在单独的线程上运行,以避免阻塞 UI
  • 使用 volatile 标志进行线程安全的状态管理
  • 在需要时实现适当的线程同步

注意:始终在多个设备上测试您的音频实现,因为编解码器支持和行为在不同 Android 制造商和操作系统版本之间可能有显著差异。

8、结论

AVSample 仓库提供了 MediaCodec 音频处理的强大实现,展示了 Android 多媒体开发的最佳实践。通过将关注点分离为逻辑组件并提供清晰的 API,它为构建音频录制、流媒体和处理应用程序提供了坚实的基础。

无论您是在构建录音应用、音频流媒体应用还是实时通信平台,理解和正确实现 MediaCodec 音频处理对于在 Android 上提供高质量、高性能的多媒体体验都至关重要。

学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

MediaCodec 音频处理

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论