目录

  1. AudioTrack和MediaPlayer

  2. AudioTrack的API介绍(构造、操作、状态机)

  3. 具体实现(Static和Stream两种模式)

  4. 遇到的问题

  5. 收获

一、MediaPlayer和AudioTrack

Android SDK 中提供了三种播放声音的API,常见的是MediaPlayer和AudioTrack
其中AudioTrack管理、播放单一音频资源。可以将PCM音频数据传输到音频接收器,以供播放,只能播放源码流即PCM,wav封装格式的音频也可以用AudioTrack播放,但是wav头部分在播放解析时会发出噪音。而MediaPlayer可以播放多种格式的音频文件,比如 mp3 aac等,因为MediaPlayer会在framework层创建对应的音频解码器。

既然MediaPlayer可以播放那么多的音频格式,为什么我们还要学习AudioTrack呐?

首先MediaPlayer在framwork层还是会创建AudioTrack,把解码后的PCM流传递给AudioTrack,再传递给AudioFliger进行混音播放。

每一个音频流对应一个AudioTrack,AudioTrack会在创建时注册到AudioFlinger中,AudioFlinger把所有的AudioTrack进行混合Mixer,然后输送到AudioHardware进行播放。Android最多可以同时创建32个音频流。在短视频编辑等应用领域,对视频进行添加配乐进行编辑,需要把视频中的音轨和配乐中的音轨进行解码PCM进行混合再编码。再或者我们在“剪映”等视频编辑app可以添加多个音轨,就想Audition一样强大。这些都都需要我们对AudioTrack有一定的了解掌握。

二、AudioTrack的介绍

我们先简单看下AudioTrack提供了哪些API

2.1. 构造方法

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes, int mode)
其中采样率sampleRateInHz、声道数channelConfig、音频格式audioFormat以及音频缓冲区大小bufferSizeInBytes 这四个概念和上一篇《AudioRecord录制PCM音频》中的介绍的AudioRecord的构造方法的参数意义以及获取方式基本一致。下面我们看下另外两个参数streamType以及mode

streamType音频流的类型,有如下几种
AudioManager#STREAM_VOICE_CALL:电话声音AudioManager#STREAM_SYSTEM:系统声音
AudioManager#STREAM_RING:铃声
AudioManager#STREAM_MUSIC:音乐声
AudioManager#STREAM_ALARM:闹铃声
AudioManager#STREAM_NOTIFICATION:通知声

这里我们使用的是AudioManager#STREAM_MUSIC。_
下面我们重点看下mode
@param mode streaming or static buffer.
MODE_STATIC and MODE_STREAM

  • STATIC模式:一次性将所有的数据放到一个固定的buffer,然后直接传送给AudioTrack,简单有效,通常应用于播放铃声或者系统提示音等,占用内存较少的音频数据

  • STREAM模式:一次一次的将音频数据流写入到AudioTrack对象中,并持续处于阻塞状态,当数据从Java层到Native层执行播放完毕后才返回,这种方式可以避免由于音频过大导致内存占用过多。当然对应的不足就是总是在java和native层进行交互,并且阻塞直到播放完毕,效率损失较大。

2.2. Action 写入、播放、暂停、停止、释放

write(byte audioData, int offsetInBytes, int sizeInBytes)把pcm数据写入到AudioTrack对象
播放、暂停、停止、释放 常规的播放Action操作。

2.3. 状态机(getState以及getPlayState)

AudioTrack中有两个state,一个是AudioTrack是否已经初始化,后续的Action操作都依赖于此,这个有点类似MediaPlayer的prepared状态,只有处于prepared状态之后才可以进行其他播放相关操作
另外一个就是playstate,用于记录判断当前处于什么播放状态。
状态的改变加速,处理多线程同步问题
private final Object mPlayStateLock = new Object();

三、具体实现

我们在上一篇《AudioRecord录制PCM音频》中示例代码产生的pcm作为AudioTrack的数据播放源来。跟进mode不同,分别实现

3.1 STATIC模式

//1. 初始化参数和bufferprivate void initAudioTrackParams() {sampleRateInHz = 44100;channels = AudioFormat.CHANNEL_OUT_MONO;//错误的写成了CHANNEL_IN_MONOaudioFormat = AudioFormat.ENCODING_PCM_16BIT;bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channels, audioFormat);pcmFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "raw.pcm");if (pcmFile.exists()) {hasPcmFile = true;}}private void initStaticBuff() {//staic模式是一次读取全部的数据,在play之前要先完成{@link audioTrack.write()}if (audioTrackThread != null) {audioTrackThread.interrupt();}audioTrackThread = new Thread(new Runnable() {@Overridepublic void run() {FileInputStream fileInputStream = null;try {//init audioTrack 需要先确定buffersizefileInputStream = new FileInputStream(pcmFile);long size = fileInputStream.getChannel().size();staicBuff = new byte[(int) size];ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(staicBuff.length);int byteValue = 0;long startTime = System.currentTimeMillis();while ((byteValue = fileInputStream.read()) != -1) {
//                        Log.d(TAG, "run: " + byteValue);//耗时操作byteArrayOutputStream.write(byteValue);}Log.d(TAG, "byteArrayOutputStream write Time: " + (System.currentTimeMillis() - startTime));staicBuff = byteArrayOutputStream.toByteArray();isReadying = true;} catch (IOException e) {e.printStackTrace();} catch (Throwable e) {e.printStackTrace();} finally {if (fileInputStream != null) {try {fileInputStream.close();} catch (IOException e) {e.printStackTrace();}}Log.d(TAG, "playWithStaicMode: end");}}});audioTrackThread.start();}//. 2. 点击播放private void play(byte[] staicBuff) {//1. static模式是一次读去pcm到内存,比较耗时,只有读取完之后才可以调用playif (!isReadying) {Toast.makeText(this, "请稍后", Toast.LENGTH_SHORT).show();return;}//2.如果正在播放中,重复点击播放,则停止当次播放,调用reloadStaticData重新加载数据,然后playif (isPlaying) {audioTrack.stop();audioTrack.reloadStaticData();Log.d(TAG, "playWithStaicMode: reloadStaticData");audioTrack.play();return;}//3。否则,就先释放audiotrack,然后重新初始化audiotrack进行releaseAudioTrack();int state = initAudioTrackWithMode(AudioTrack.MODE_STATIC, staicBuff.length);if (state == AudioTrack.STATE_UNINITIALIZED) {Log.e(TAG, "run: state is uninit");return;}//4. 把pcm写入audioTrack,然后进行播放long startTime = System.currentTimeMillis();int result = audioTrack.write(staicBuff, 0, staicBuff.length);Log.d(TAG, "audioTrack.write staic: result=" + result+" totaltime="+ (System.currentTimeMillis() - startTime));audioTrack.play();isPlaying = true;}private void pausePlay() {if (audioTrack != null) {if (audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {audioTrack.pause();audioTrack.flush();}isPlaying = false;Log.d(TAG, "pausePlay: isPlaying false");}if (audioTrackThread != null) {audioTrackThread.interrupt();}}private void releaseAudioTrack() {if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {audioTrack.stop();audioTrack.release();isPlaying = false;Log.d(TAG, "pausePlay: isPlaying false");}if (audioTrackThread != null) {audioTrackThread.interrupt();}}

关注+后台私信我,领取2022最新最全学习提升资料包+面试题,内容包括(C/C++,Linux,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)

3.2 STREAM模式

1. 初始化参数
private void initAudioTrackParams() {sampleRateInHz = 44100;channels = AudioFormat.CHANNEL_OUT_MONO;//错误的写成了CHANNEL_IN_MONOaudioFormat = AudioFormat.ENCODING_PCM_16BIT;bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channels, audioFormat);pcmFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "convert.wav");//"raw.pcm"if (pcmFile.exists()) {hasPcmFile = true;}}2. 点击进行播放
private void play() {releaseAudioTrack();int state = initAudioTrackWithMode(AudioTrack.MODE_STREAM, bufferSize);if (state == AudioTrack.STATE_UNINITIALIZED) {Log.e(TAG, "run: state is uninit");return;}audioTrackThread = new Thread(new Runnable() {@Overridepublic void run() {FileInputStream fileInputStream = null;try {fileInputStream = new FileInputStream(pcmFile);byte[] buffer = new byte[bufferSize / 2];int readCount;Log.d(TAG, "run: ThreadId=" + Thread.currentThread() + " playState=" + audioTrack.getPlayState());//stream模式,可以先调用playaudioTrack.play();while (fileInputStream.available() > 0) {readCount = fileInputStream.read(buffer);if (readCount == AudioTrack.ERROR_BAD_VALUE || readCount == AudioTrack.ERROR_INVALID_OPERATION) {continue;}if (audioTrack == null) {return;} else {Log.i(TAG, "run: audioTrack.getState()" + audioTrack.getState() + " audioTrack.getPlayState()=" + audioTrack.getPlayState());}
//                        audioTrack.getPlayState()//一次一次的写入pcm数据到audioTrack.由于是在子线程中进行write,快速连续点击可能主线程触发了stop或者release,导致子线程write异常:IllegalStateException: Unable to retrieve AudioTrack pointer for write()//所以加playstate的判断if (readCount > 0 && audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {audioTrack.write(buffer, 0, readCount);}}} catch (IOException | IllegalStateException e) {e.printStackTrace();Log.e(TAG, "play: " + e.getMessage());} finally {if (fileInputStream != null) {try {fileInputStream.close();} catch (IOException e) {e.printStackTrace();}}Log.d(TAG, "playWithStreamMode: end  ThreadID=" + Thread.currentThread());}}});audioTrackThread.start();}private void pausePlay() {if (audioTrack != null) {if (audioTrack.getState() > AudioTrack.STATE_UNINITIALIZED) {audioTrack.pause();}Log.d(TAG, "pausePlay: isPlaying false getPlayState= " + audioTrack.getPlayState());}if (audioTrackThread != null) {audioTrackThread.interrupt();}}private void releaseAudioTrack() {if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {audioTrack.stop();audioTrack.release();Log.d(TAG, "pausePlay: isPlaying false");}if (audioTrackThread != null) {audioTrackThread.interrupt();}}

四、遇到的问题

纸上得来终觉浅,绝知此事要实践
本篇文章原计划昨天完成,但是在实践中遇到了不少问题,不过最终都得以解决,记录如下

1: stream模式快速点击 声音重叠,如何停止:在触发播放前先停止和释放auidoTrack,然后在进行init,在audioTrack写入数据的线程中write操作要做好audiotTrack的状态判断。具体实现见上面小节的代码

2:如何监听播放进度:AudioTrack有没有想MediaPlayer的丰富的监听回调,比如说,播放进度,播放完成回调,异常回调等。遗憾的是还真没有,针对STATIC模式的播放结束监听倒是可以借助setNotificationMarkerPosition 和 setPlaybackPositionUpdateListener来判断来判断。具体见上面小节中STATIC模式的实现

3: staic模式下有时候无法播放;音频在快速连续点击中加了isplaying的片段,如果正在playing中有触发了play,会先stop然后调用audioTrack.reloadStaticData()加载数据流,再进行播放,但是发现快速连续点击是间隔一次才会播放生效,原因还是audioTrack资源没有被正确使用,改为了先release在进行init的方式。

4: IllegalStateException: Unable to retrieve AudioTrack pointer for write():这个异常是stream模式时在主线程出发了stop或者release,而在audioTrack子线程write时抛出的异常,原因就是播放状态不对,如果已经处于Stropped状态,再进行write操作就会报这个错误,所以write时加个playstate状态的检验。具体解决实现见上面小节的代码。

音视频开发系列(28)AudioTrack播放PCM音频相关推荐

  1. 音视频开发系列(27)AudioRecord录制PCM音频

    目录 音频采集API AudioRecord和MediaRecorder介绍 PCM的介绍 AudioRecord的使用(构造.开始录制.停止录制.其他细节点) ffplay播放pcm pcm转为wa ...

  2. 音视频开发系列(12):音频录制的代码封装

    这节课分享一下如何对基于qt的音频推流的代码进行封装,首先先说明一下封装的目的,主要是为了以后如何有多种方案可以进行使用时,只需要在该封装的类上提供新的接口即可.本次封装的类的基类为XAudioRec ...

  3. Android 音视频开发(一) -- 使用AudioRecord 录制PCM(录音);AudioTrack播放音频

    前言,音视频这块,确实比较难入门,本着学习的态度,我这边也跟着 Android 音视频开发入门指南 打怪升级,留下个脚印,大家共勉. 音视频 系列文章 Android 音视频开发(一) – 使用Aud ...

  4. Android 音视频开发(二):使用 AudioRecord 采集音频PCM并保存到文件(学习笔记)

    关于 AudioRecord Android SDK 提供了两套音频采集的API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风 ...

  5. 【音视频开发系列】一学就会,快速掌握音视频开发的第一个开源项目FFmpeg

    快速掌握音视频开发的第一个开源项目:FFmpeg 1.为什么要学FFmpeg 2.FFmpeg面向对象思想分析 3.FFmpeg各种组件剖析 视频讲解如下,点击观看: [音视频开发系列]一学就会,快速 ...

  6. 【音视频开发系列】盘点音视频直播RTSP/RTMP推流一定会遇到的各种坑,教你快速解决

    聊聊RTSP/RTMP推流那些坑 1.推流架构分析 2.推流缓存队列的设计 3.FFmpeg函数阻塞问题分析 [音视频开发系列]盘点音视频直播一定会遇到的各种坑,教你快速解决 更多精彩内容包括:C/C ...

  7. 【音视频开发系列】srs-webrtc-janus开源流媒体服务器分析

    全球最牛开源流媒体服务器源码分析 1.如何学习流媒体服务器 2.全球最牛流媒体服务器架构分析 3.我们能从全球最牛流媒体服务器得到什么 [音视频开发系列]srs-webrtc-janus流媒体服务器分 ...

  8. AudioTrack播放PCM音频

    一.前言 说到在 Android 平台上播放音频,我们最先想到的是 MediaPlayer.系统 API 对其做了比较全面的封装,开发者用少量的代码就能实现播放功能.MediaPlayer 可以播放多 ...

  9. Android 音视频开发(一):PCM 格式音频的播放与采集

    什么是 PCM 格式 声音从模拟信号转化为数字信号的技术,经过采样.量化.编码三个过程将模拟信号数字化. 采样 顾名思义,对模拟信号采集样本,该过程是从时间上对信号进行数字化,例如每秒采集 44100 ...

最新文章

  1. 《大话数据结构》第9章 排序 9.3 冒泡排序(下)
  2. PostgreSQL 务实应用(三/5)分表复制
  3. 到底哪些人适合学Python?适合学Python的5类人
  4. android 水平进度条 自定义颜色,android 水平进度条的颜色设置
  5. android UI设计属性中英对照表(未修订)
  6. Linux-系统编程-知识点概述
  7. 图论/暴力 Codeforces Beta Round #94 (Div. 2 Only) B. Students and Shoelaces
  8. 【Oracle】Oracle通过表名查询触发器
  9. c语言作业模块化设计具体,C语言程序模块化设计.doc
  10. 正点原子ATK-LORA-01无线串口代码移植+STM32F103C8T6(标准库)
  11. Linux I/O重定向 dup dup2 系统调用
  12. 怎么学计算机自学,自学经验:如何学习计算机知识
  13. 量子计算的基本原理论述
  14. 【离散数学】数理逻辑 第一章 命题逻辑(7) 命题逻辑的推理理论
  15. 1. 树莓派点灯学习(UI界面控制)
  16. Siri触发器原理及改进
  17. 距离2021年还剩75天,我在想什么?
  18. 金仓数据库 KingbaseES 与 Oracle 的兼容性说明(4. SQL)
  19. 史上最简单:SpringCloud 集成 mybatis-plus(以若依微服务版本为例)
  20. 信息学奥赛一本通:1312:【例3.4】昆虫繁殖

热门文章

  1. php常用取整函数详解
  2. VS2010工程生成ipch文件夹的问题
  3. rstudio中读取数据_R语言读取外部数据文件
  4. r语言如何读取matlab数据类型,R语言数据类型深入详解
  5. Soul源码分析–soul-admin入门
  6. 一台服务器虚拟交换机,产品技术-H3C S1010V虚拟交换机-新华三集团-H3C
  7. 信锐-技术服务工程师一面
  8. Google Earth Engine(GEE)——缩放错误(计算超时、聚合过多、内存溢出)
  9. 使用亮数据Bright Data解决出境电商问题
  10. 51单片机 动态数码管显示