6、音频播放深入:AudioTrack原理、PCM数据播放、音频格式转换与重采样

音频播放,说白了就是让手机喇叭发出声音。但声音在数字世界里怎么存、怎么传、怎么还原成我们能听到的波形?这背后就是 AudioTrack 和 PCM 数据在干活。

我刚开始做音频开发时,以为 MediaPlayer 能搞定一切。后来发现,很多场景下 MediaPlayer 太重了——比如你要做语音聊天、实时音频处理、或者播放一段自己合成的音效。这时候,AudioTrack 才是你的好兄弟。

AudioTrack 是什么?

AudioTrack 是 Android 提供的底层音频播放 API。它直接操作音频缓冲区,把 PCM 数据喂给音频硬件。你可以把它理解成一个「音频管道」——你把数据塞进去,它负责播放出来。

我个人习惯把 AudioTrack 和 MediaPlayer 做个对比:

对比项 MediaPlayer AudioTrack
数据源 文件/网络流 PCM 裸数据
解码能力 内置解码器 无解码能力
控制粒度 粗粒度(播放/暂停/跳转) 细粒度(逐帧写入)
适用场景 音乐播放、视频播放 实时音频、语音通话、音效处理

你看,AudioTrack 不负责解码,它只负责播放。所以你要喂给它的数据,必须是已经解码好的 PCM 裸数据。

PCM 数据播放:从零开始

PCM 是脉冲编码调制,说白了就是声音的「数字底片」。它记录了每个采样点的振幅值。播放 PCM 数据,就是把这些数值按顺序送给 AudioTrack。

来看一个最简单的例子:

// 配置 AudioTrack
int sampleRate = 44100;          // 采样率
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;  // 双声道
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;    // 16位采样
int bufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);

AudioTrack audioTrack = new AudioTrack(
    AudioManager.STREAM_MUSIC,
    sampleRate,
    channelConfig,
    audioFormat,
    bufferSize,
    AudioTrack.MODE_STREAM
);

// 读取 PCM 文件并播放
byte[] buffer = new byte[bufferSize];
FileInputStream fis = new FileInputStream("test.pcm");
audioTrack.play();

int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
    audioTrack.write(buffer, 0, bytesRead);
}

audioTrack.stop();
audioTrack.release();
fis.close();

嗯,这里要注意几个点:

  • getMinBufferSize 返回的是系统建议的最小缓冲区大小。别自己瞎猜,用这个值最稳。
  • MODE_STREAM 表示流式写入,适合实时数据。还有 MODE_STATIC 模式,适合一次性写入的短音频。
  • write 方法 是阻塞的,如果缓冲区满了,它会等。所以不要在 UI 线程里写。
我曾经踩过一个坑: 用 MODE_STATIC 播放长音频,结果内存直接爆了。MODE_STATIC 会把整个音频加载到内存里,适合铃声、提示音这种短片段。长音频一定要用 MODE_STREAM。

音频格式转换:为什么需要?

你想想看,从网络上下载的音频文件,可能是 MP3、AAC、FLAC 等各种格式。但 AudioTrack 只认 PCM。所以第一步,你得把压缩格式解码成 PCM。

Android 提供了 MediaCodec 来做这件事。但如果你不想自己写解码器,也可以用 MediaExtractor + MediaCodec 的组合:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);

MediaFormat format = extractor.getTrackFormat(0);
String mime = format.getString(MediaFormat.KEY_MIME);

MediaCodec codec = MediaCodec.createDecoderByType(mime);
codec.configure(format, null, null, 0);
codec.start();

// 然后循环读取、解码、写入 AudioTrack
// 代码略长,这里不展开了

解码出来的数据就是 PCM 了。但这里又有个问题——解码后的 PCM 参数可能和 AudioTrack 不匹配。

重采样:让数据对上号

AudioTrack 创建时指定了采样率、声道数、位深。如果解码出来的 PCM 参数和这些不一致,直接播放就会出问题——要么声音变调,要么只有一边响,要么干脆没声音。

举个例子:你下载了一个 48000Hz 的音频,但你的 AudioTrack 配置的是 44100Hz。这时候就需要重采样。

重采样的核心思路很简单:

  • 降采样:从 48000Hz 降到 44100Hz,相当于每 48000 个点里取 44100 个。说白了就是插值。
  • 升采样:反过来,需要补点。

Android 没有直接提供重采样 API,但你可以用 AudioRecord 的 getNativeOutputSampleRate 来获取硬件支持的采样率。或者用第三方库,比如 libresample、soxr。

我个人习惯用 Sonic 库来做重采样和变速变调,它轻量、纯 C 实现,NDK 集成很方便:

// 伪代码示意
sonicStream stream = sonicCreateStream(sourceSampleRate, sourceChannels);
sonicSetSpeed(stream, 1.0f);
sonicSetPitch(stream, 1.0f);
sonicSetRate(stream, (float)targetSampleRate / sourceSampleRate);

// 写入源数据,读出重采样后的数据
sonicWriteShortToStream(stream, inputBuffer, inputSize);
int samplesRead = sonicReadShortFromStream(stream, outputBuffer, outputSize);
小技巧: 如果你只是做简单的采样率转换,可以用 AudioTrack 的 getNativeOutputSampleRate 获取设备原生采样率,然后让 AudioTrack 自己处理。但这样会引入额外的延迟,实时性要求高的场景不建议。

知识体系总览

下面这张图,是我梳理的本章核心脉络:

音频播放深入:核心知识体系 AudioTrack PCM 数据播放 MODE_STREAM / MODE_STATIC 缓冲区管理 音频格式转换 MediaCodec 解码 → PCM 参数匹配检查 重采样 降采样 / 升采样 Sonic / libresample 三者配合:解码 → 重采样 → AudioTrack 播放

核心要点:

  • AudioTrack 只认 PCM 裸数据,不负责解码
  • 播放前必须确保采样率、声道数、位深三者匹配
  • 重采样是音频开发中绕不开的坎,建议提前集成好库
  • 缓冲区大小用 getMinBufferSize 获取,别自己拍脑袋

音频播放这块,说白了就是「数据格式对齐」的问题。你只要把 PCM 数据准备好,参数设置对,AudioTrack 就能老老实实帮你播出来。但实际项目中,格式不匹配、缓冲区溢出、线程安全问题,每一个都能让你折腾半天。

嗯,今天就先聊到这儿。下一节我们继续深入音频处理的其他环节。


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