2. AudioTrack播放PCM音频

任务目标

用 AudioTrack 的静态模式播放一个本地 PCM 文件(16bit 小端、单声道、44.1kHz)。核心是正确设置流类型、采样率、声道掩码、音频格式,并把 PCM 数据写入 AudioTrack 后播放。

关键参数

  • STREAM_MUSIC:媒体音量通道,车载场景常用。
  • 44100Hz / CHANNEL_OUT_MONO / ENCODING_PCM_16BIT:必须与 PCM 文件实际格式一致,否则会变调或噪声。
  • MODE_STATIC:一次性写入整段数据,适合短音频;长音频用 MODE_STREAM 分块写。

完整实现

import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class PcmPlayer {

    private static final int SAMPLE_RATE = 44100;
    private static final int CHANNEL_MASK = AudioFormat.CHANNEL_OUT_MONO;
    private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;

    private AudioTrack track;

    /** 播放本地 PCM 文件,阻塞直到播放结束 */
    public void play(File pcmFile) throws IOException {
        byte[] pcm = readAll(pcmFile);
        if (pcm.length == 0) throw new IOException("PCM 文件为空");

        int minBuf = AudioTrack.getMinBufferSize(SAMPLE_RATE, CHANNEL_MASK, ENCODING);
        if (minBuf <= 0) throw new IllegalStateException("参数不被设备支持");

        AudioAttributes attrs = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build();

        AudioFormat format = new AudioFormat.Builder()
                .setSampleRate(SAMPLE_RATE)
                .setChannelMask(CHANNEL_MASK)
                .setEncoding(ENCODING)
                .build();

        // 静态模式缓冲区取数据长度与最小缓冲的较大值
        int bufSize = Math.max(pcm.length, minBuf);
        track = new AudioTrack(attrs, format, bufSize,
                AudioTrack.MODE_STATIC, AudioManager.AUDIO_SESSION_ID_GENERATE);

        int written = track.write(pcm, 0, pcm.length);
        if (written <= 0) {
            release();
            throw new IOException("write 失败: " + written);
        }

        track.play();
        // 静态模式:等待播放头走完数据长度
        int frames = pcm.length / 2; // 16bit 单声道,每帧 2 字节
        long ms = (long) frames * 1000 / SAMPLE_RATE;
        try {
            Thread.sleep(ms + 100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            release();
        }
    }

    private static byte[] readAll(File f) throws IOException {
        try (FileInputStream in = new FileInputStream(f)) {
            byte[] data = new byte[(int) f.length()];
            int off = 0, n;
            while (off < data.length && (n = in.read(data, off, data.length - off)) > 0) {
                off += n;
            }
            return data;
        }
    }

    public void release() {
        if (track != null) {
            try { track.stop(); } catch (IllegalStateException ignored) {}
            track.release();
            track = null;
        }
    }
}

调用与运行

// 在 Activity / Service 中调用
new PcmPlayer().play(new File(getFilesDir(), "test.pcm"));

把一段 44.1kHz、单声道、16bit 小端的 PCM 文件命名为 test.pcm 放入应用私有目录(或用 adb push 到 /sdcard/ 后改路径)。用 ffmpeg -i in.wav -f s16le -ac 1 -ar 44100 test.pcm 可从任意音频生成符合格式的 PCM。

验证方法

  • 运行后能听到与源音频一致的声音,无变调、无杂音。
  • 若出现尖锐噪声或速度异常,检查采样率/声道/位深是否与 PCM 文件一致。
  • 若 getMinBufferSize 返回负值,说明设备不支持该组合,需换参数。
  • 播放结束后 release() 被调用,无 AudioTrack 泄漏。

常见问题

  • 静态模式缓冲区必须 ≥ 数据长度,否则 write 会失败。
  • 长音频不要用 MODE_STATIC,应改用 MODE_STREAM 循环写入。
  • 车载场景若需与其它音源混音,可改用 USAGE_ASSISTANCE_NAVIGATION_GUIDANCE 等更合适的 usage。