7、音频轨道提取实战:从MP4文件中提取AAC/MP3裸流

好,咱们今天来点真格的。

前面几章我们把 MediaExtractor 的原理、轨道选择、数据读取都捋了一遍。但说实话,光看理论不过瘾。你想想看,一个视频文件摆在那,怎么把里面的纯音频给「抽」出来?这就是我们今天要干的事。

我个人习惯把这种操作叫做「扒音频」。在项目里,我经常需要从录制的视频中提取音频做二次处理,比如加滤镜、变声、或者做语音识别。嗯,这里要注意,提取出来的裸流(AAC 或 MP3)是不带封装信息的,说白了就是纯音频数据,可以直接喂给解码器。

7.1 核心思路:三步走

提取音频裸流,其实就三步:

  1. 选中音频轨道——从 MP4 的多条轨道里找到音频那一条。
  2. 读取音频数据——用 MediaExtractor 的 readSampleData() 一帧一帧读。
  3. 写入文件——把每一帧的裸流数据追加到输出文件里。

听起来简单?但坑不少。我曾经在一个项目里直接按视频帧率去读音频,结果音频时长对不上,播放出来像快进了一样。后来才意识到,音频和视频的采样时间戳是完全不同的节奏。

关键点:音频轨道提取时,不要依赖视频的帧率逻辑。音频有自己的采样率(如 44100Hz)和帧大小(如 AAC 每帧 1024 个采样点)。

7.2 代码实战:提取 AAC 裸流

咱们直接上代码。我习惯用 Kotlin 写,但 Java 的逻辑完全一样。

// 1. 设置输入和输出文件
val inputFile = File("/sdcard/input.mp4")
val outputFile = File("/sdcard/output.aac")

// 2. 创建 MediaExtractor 并关联文件
val extractor = MediaExtractor()
extractor.setDataSource(inputFile.absolutePath)

// 3. 遍历轨道,找到音频轨道
var audioTrackIndex = -1
for (i in 0 until extractor.trackCount) {
    val format = extractor.getTrackFormat(i)
    val mime = format.getString(MediaFormat.KEY_MIME)
    if (mime != null && mime.startsWith("audio/")) {
        audioTrackIndex = i
        break
    }
}

if (audioTrackIndex == -1) {
    Log.e("AudioExtract", "没有找到音频轨道")
    return
}

// 4. 选中音频轨道
extractor.selectTrack(audioTrackIndex)

// 5. 准备输出流
val fos = FileOutputStream(outputFile)
val buffer = ByteBuffer.allocate(1024 * 1024) // 1MB 缓冲区

// 6. 循环读取数据
while (true) {
    val sampleSize = extractor.readSampleData(buffer, 0)
    if (sampleSize < 0) break  // 读取完毕

    // 写入裸流数据
    fos.write(buffer.array(), 0, sampleSize)

    // 前进到下一帧
    extractor.advance()
}

// 7. 清理资源
fos.close()
extractor.release()
Log.d("AudioExtract", "提取完成,文件大小: ${outputFile.length()}")

小提示:缓冲区大小建议设为 1MB 左右。太小会导致频繁 IO,太大又浪费内存。我在项目中试过 512KB 和 2MB,1MB 是个比较均衡的值。

7.3 提取 MP3 裸流:几乎一样

如果你要提取 MP3 裸流,代码几乎一模一样。唯一的区别是输出文件后缀改成 .mp3,以及 MIME 类型判断时注意 audio/mpeg

// 判断 MIME 时,MP3 的 mime 是 "audio/mpeg"
if (mime != null && mime == "audio/mpeg") {
    audioTrackIndex = i
    break
}

为什么会这样?因为 MP4 里的音频轨道,如果编码是 AAC,MIME 就是 audio/mp4a-latm;如果是 MP3,就是 audio/mpeg。你想想看,MediaExtractor 其实不关心你最终要什么格式,它只负责把原始数据吐出来。

7.4 避坑指南:我踩过的三个坑

嗯,这里我要认真说说。提取音频看起来简单,但实际项目中我至少踩过三个坑:

  • 坑一:文件头信息丢失。 直接写入裸流,播放器可能不认识。比如 AAC 裸流需要 ADTS 头才能播放。解决办法是手动添加 ADTS 头,或者用 MediaMuxer 重新封装。
  • 坑二:时间戳不对齐。 音频帧的时间戳是连续的,但如果你用 advance() 跳帧,会导致播放时卡顿。我建议保持原样读取,不要跳过任何帧。
  • 坑三:内存泄漏。 如果你在循环里频繁创建 ByteBuffer,GC 会哭的。复用同一个缓冲区,用完记得清空。

警告:提取出来的 AAC 裸流如果没有 ADTS 头,很多播放器(包括系统 MediaPlayer)无法直接播放。你需要用 MediaCodec 解码,或者用 FFmpeg 重新封装。我在项目里吃过这个亏,折腾了一下午才发现是头信息的问题。

7.5 知识体系:音频提取的核心逻辑

下面这张图帮你理清整个流程。我画的时候特意把「数据流」和「控制流」分开了,这样更容易理解。

音频裸流提取核心流程 MP4 输入文件 MediaExtractor 初始化 选择音频轨道 循环读取裸流数据 注意事项 • 缓冲区复用 • 时间戳连续 • ADTS 头问题 • MIME 判断 • 文件后缀匹配 • 内存泄漏防范 • 异常处理

7.6 进阶:如何判断提取是否成功?

代码跑完了,怎么知道提取出来的文件对不对?我一般用三个方法验证:

验证方法 操作 预期结果
文件大小检查 对比原始 MP4 的音频轨道大小 提取文件大小应接近原始音频轨道大小
播放测试 用 VLC 或 FFplay 播放 能正常播放,时长与原始视频一致
十六进制查看 用 Hex Editor 查看文件头 AAC 文件头应为 0xFFF1 或 0xFFF9

我的习惯:写一个自动化测试脚本,提取后自动用 FFprobe 检查文件格式和时长。这样每次改代码都能快速回归,省心不少。

7.7 总结

音频轨道提取,说白了就是「选轨道、读数据、写文件」这三板斧。但真正做好,需要你对 MediaExtractor 的读取机制、音频编码格式、以及文件头信息有深入理解。

我个人建议,初学者先跑通上面的代码,然后试着改一改:比如提取 MP3、或者把提取出来的 AAC 加上 ADTS 头。每改一次,你对 MediaExtractor 的理解就深一层。

好了,今天就到这。代码拿去用,有问题随时交流。


公众号:蓝海资料掘金营,微信deep3321