3. 使用AudioTrack播放PCM数据

使用AudioTrack播放PCM数据 学习卡片

任务说明

本案例实现一个最小可用的Android音频播放器:从应用私有目录读取16位单声道PCM文件,通过AudioTrack以44.1kHz采样率播放,并支持播放、暂停与停止操作。核心目标是验证AudioTrack从初始化、写入数据到状态控制的全流程正确性。

实现方案

创建PcmPlayer类封装AudioTrack生命周期。播放线程循环从文件读取数据块并调用write()阻塞写入;暂停时通过pause()挂起线程,停止时调用stop()并释放资源。界面提供三个按钮触发对应操作,播放进度通过Handler更新到TextView。

代码实现

// PcmPlayer.java
public class PcmPlayer {
    private static final int SAMPLE_RATE = 44100;
    private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_OUT_MONO;
    private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;
    private static final int BUFFER_SIZE = AudioTrack.getMinBufferSize(
            SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) * 2;

    private AudioTrack audioTrack;
    private FileInputStream fis;
    private Thread playThread;
    private volatile boolean isPlaying;
    private volatile boolean isPaused;
    private volatile int totalBytes;
    private volatile int playedBytes;

    public boolean prepare(String filePath) {
        try {
            fis = new FileInputStream(filePath);
            totalBytes = fis.available();
            audioTrack = new AudioTrack.Builder()
                    .setAudioAttributes(new AudioAttributes.Builder()
                            .setUsage(AudioAttributes.USAGE_MEDIA)
                            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                            .build())
                    .setAudioFormat(new AudioFormat.Builder()
                            .setSampleRate(SAMPLE_RATE)
                            .setChannelMask(CHANNEL_CONFIG)
                            .setEncoding(AUDIO_FORMAT)
                            .build())
                    .setBufferSizeInBytes(BUFFER_SIZE)
                            .setTransferMode(AudioTrack.MODE_STREAM)
                    .build();
            return audioTrack.getState() == AudioTrack.STATE_INITIALIZED;
        } catch (IOException e) {
            return false;
        }
    }

    public void play() {
        if (audioTrack == null || isPlaying) return;
        isPlaying = true;
        isPaused = false;
        audioTrack.play();
        playThread = new Thread(() -> {
            byte[] buffer = new byte[BUFFER_SIZE];
            int read;
            try {
                while (isPlaying && (read = fis.read(buffer)) != -1) {
                    while (isPaused && isPlaying) {
                        Thread.sleep(50);
                    }
                    if (!isPlaying) break;
                    audioTrack.write(buffer, 0, read);
                    playedBytes += read;
                }
            } catch (IOException | InterruptedException e) {
                // 线程中断或IO异常时退出
            } finally {
                if (isPlaying) {
                    isPlaying = false;
                    audioTrack.stop();
                }
            }
        });
        playThread.start();
    }

    public void pause() {
        if (isPlaying && !isPaused) {
            isPaused = true;
            audioTrack.pause();
        }
    }

    public void resume() {
        if (isPlaying && isPaused) {
            isPaused = false;
            audioTrack.play();
        }
    }

    public void stop() {
        isPlaying = false;
        isPaused = false;
        if (playThread != null) {
            playThread.interrupt();
            try {
                playThread.join(500);
            } catch (InterruptedException ignored) {}
        }
        if (audioTrack != null) {
            audioTrack.stop();
            audioTrack.release();
            audioTrack = null;
        }
        try {
            if (fis != null) fis.close();
        } catch (IOException ignored) {}
    }

    public int getProgress() {
        if (totalBytes == 0) return 0;
        return (int) ((long) playedBytes * 100 / totalBytes);
    }
}
// MainActivity.java 关键部分
public class MainActivity extends AppCompatActivity {
    private PcmPlayer player;
    private TextView progressView;
    private Handler handler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressView = findViewById(R.id.progress);
        player = new PcmPlayer();

        // 将测试PCM文件复制到私有目录
        String pcmPath = new File(getFilesDir(), "test.pcm").getAbsolutePath();
        copyAssetToInternal("test.pcm", pcmPath);

        findViewById(R.id.btn_play).setOnClickListener(v -> {
            if (player.prepare(pcmPath)) {
                player.play();
                handler.post(updateProgress);
            }
        });
        findViewById(R.id.btn_pause).setOnClickListener(v -> {
            if (player != null) player.pause();
        });
        findViewById(R.id.btn_resume).setOnClickListener(v -> {
            if (player != null) player.resume();
        });
        findViewById(R.id.btn_stop).setOnClickListener(v -> {
            if (player != null) {
                player.stop();
                handler.removeCallbacks(updateProgress);
                progressView.setText("进度: 0%");
            }
        });
    }

    private Runnable updateProgress = new Runnable() {
        @Override
        public void run() {
            if (player != null) {
                progressView.setText("进度: " + player.getProgress() + "%");
                handler.postDelayed(this, 200);
            }
        }
    };

    private void copyAssetToInternal(String asset, String dest) {
        try (InputStream in = getAssets().open(asset);
             OutputStream out = new FileOutputStream(dest)) {
            byte[] buf = new byte[4096];
            int len;
            while ((len = in.read(buf)) != -1) out.write(buf, 0, len);
        } catch (IOException e) {
            Log.e("MainActivity", "复制失败", e);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (player != null) player.stop();
        handler.removeCallbacks(updateProgress);
    }
}

布局与资源

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center">
    <TextView android:id="@+id/progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="进度: 0%"
        android:textSize="18sp"/>
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button android:id="@+id/btn_play"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="播放"/>
        <Button android:id="@+id/btn_pause"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="暂停"/>
        <Button android:id="@+id/btn_resume"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="继续"/>
        <Button android:id="@+id/btn_stop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="停止"/>
    </LinearLayout>
</LinearLayout>

验证方法

准备一段44.1kHz、16位单声道的PCM测试音频(如用Audacity导出),命名为test.pcm放入assets目录。运行应用后依次验证:

  1. 点击“播放”能听到声音,进度百分比递增;
  2. 点击“暂停”声音停止且进度不再变化;
  3. 点击“继续”声音从暂停处恢复,进度继续递增;
  4. 点击“停止”声音立即终止,进度归零,再次点击“播放”可从头开始。

判定标准:以上四项操作均符合预期,且无崩溃或异常日志,即视为通过。

AudioTrack播放PCM数据 — 状态与调用时序 PCM文件 FileInputStream read(buffer) 16bit/单声道/44.1kHz 字节流 AudioTrack write() 阻塞写入 MODE_STREAM 内部缓冲队列 音频帧 音频设备 扬声器/耳机 DAC输出 控制状态流转 播放中 已暂停 已停止 已释放 pause() resume() stop() release() 重新prepare()后再次play() 播放线程循环读取文件 → write() → 暂停时挂起线程 → 停止时终止线程并释放资源