[TOC]

文章转自:https://mp.weixin.qq.com/s?__biz=MzU5NjkxMjE5Mg==&mid=2247483715&idx=1&sn=dbf44d72a1b5384b7f7bd984ae37b24f&chksm=fe5a306cc92db97a0ec8279f9605f7ff7e8a9bcaf945ec5f921731a9a9386794ea618452f56e&cur_album_id=1555170733142622209&scene=189#wechat_redirect

目录

  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. 构造方法

1
2
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

1
2
3
4
5
6
@param mode streaming or static buffer.
MODE_STATIC and MODE_STREAM

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

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

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

1
write(byte audioData, int offsetInBytes, int sizeInBytes)

把pcm数据写入到AudioTrack对象播放、暂停、停止、释放 常规的播放Action操作。

2.3. 状态机(getState以及getPlayState)

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

1
private final Object mPlayStateLock = new Object();

三、具体实现

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

3.1 STATIC模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//1. 初始化参数和buffer

private void initAudioTrackParams() {
sampleRateInHz = 44100;
channels = AudioFormat.CHANNEL_OUT_MONO;//错误的写成了CHANNEL_IN_MONO
audioFormat = 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() {
@Override
public void run() {
FileInputStream fileInputStream = null;
try {
//init audioTrack 需要先确定buffersize
fileInputStream = 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到内存,比较耗时,只有读取完之后才可以调用play
if (!isReadying) {
Toast.makeText(this, "请稍后", Toast.LENGTH_SHORT).show();
return;
}

//2.如果正在播放中,重复点击播放,则停止当次播放,调用reloadStaticData重新加载数据,然后play
if (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();
}
}