aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

Android-音视频-音频开发-(五)音频播放

AudioTrack

AudioTrack属于更偏底层的音频播放,MediaPlayerService的内部就是使用了AudioTrack。

AudioTrack用于单个音频播放和管理,相比于MediaPlayer具有精炼、高效的优点,更适合实时产生播放数据的情况。如加密的音频, MediaPlayer是束手无策的,AudioTrack却可以。

AudioTrack用于播放PCM(PCM无压缩的音频格式)音乐流的回放,如果需要播放其它格式音频,需要响应的解码器,这也是AudioTrack用的比较少的原因,需要自己解码音频。

AudioTrack实现PCM音频播放

  1. 配置基本参数

  2. 获取最小缓冲区大小

  3. 创建AudioTrack对象

  4. 获取PCM文件,转成DataInputStream

  5. 开启/停止播放

AudioTrack常见参数

StreamType 音频流类型:区分系统不同功能的音频流。

sampleRateInHz 采样率:播放的音频每秒钟会有多少次采样,MediaRecoder 的采样率通常是8000Hz AAC的通常是44100Hz。 设置采样率为44100为常用的采样率。

channelConfig 声道数(通道数):一般可选的就两种,单声道CHANNEL_IN_MONO,双声道CHANNEL_IN_STEREO,建议选择单声道。

audioFormat 数据位宽:只支持AudioFormat.ENCODING_PCM_8BIT(8bit)和AudioFormat.ENCODING_PCM_16BIT(16bit)两种,后者支持所有Android手机。

bufferSizeInBytes 音频缓冲区大小:建议使用AudioTrack.getMinBufferSize()这个方法获取。

mode 播放模式:MODE_STATIC,一次性将所有数据都写入播放缓冲区中,简单高效,一般用于铃声,系统提醒音,内存比较小的。MODE_STREAM,需要按照一定的时间间隔,不断的写入音频数据,理论上它可以应用于任何音频播放的场景。

AudioTrack例子:播放录音

播放录音playAudio()和播放测试音频playAudioTest()

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
public void playAudio() {
if (!mIsPlaying) {
mExecutorService.submit(() -> {
mIsPlaying = true;
doStartPlay(getLastFile());
});
}
}

public void playAudioTest() {
if (mHasPausing && !mIsPausing && mAudioTrack != null) {
mIsPausing = true;
mAudioTrack.stop();
mBtStreamPlayerTest.setText("播放测试录音");
return;
}
if (mIsPlaying) {
Toast.makeText(mContext, "正在播放", Toast.LENGTH_SHORT).show();
} else {
mBtStreamPlayerTest.setText("暂停测试录音");
mExecutorService.submit(() -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mHasPausing = true;
mIsPausing = false;
mIsPlaying = true;
doStartPlay(new File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/AudioRecordDemo/test.pcm"));
}
});
}
}

一个算法,获得文件夹里最新更新的文件getLastFile()

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
private File getLastFile() {
File realFile = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
realFile = new File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/AudioRecordDemo/");
}
if (realFile.isDirectory()) {
List files = new ArrayList();
File[] subFiles = realFile.listFiles();
for (File file : subFiles) {
files.add(file);
}
Collections.sort(files, (Comparator<File>) (file, newFile) -> {
if (file.lastModified() < newFile.lastModified()) {
return 1;
} else if (file.lastModified() == newFile.lastModified()) {
return 0;
} else {
return -1;
}
});
Log.d(TAG, "play " + ((File) files.get(0)).toString());
return (File) files.get(0);
}
return null;
}

播放录音具体实现,AudioTrack的使用doStartPlay()

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
private void doStartPlay(File audioRecordFile) {
if (audioRecordFile == null) {
Log.d(TAG, "请先录制音频!");
mIsPlaying = false;
return;
}
// 配置播放器
// 音乐类型,扬声器播放
int streamType = AudioManager.STREAM_MUSIC;
// 录音时采用的采样频率,所以播放时同样的采样频率
int sampleRate = 44100;
// 单声道,和录音时设置的一样
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
// 录音时使用16bit,所以播放时同样采用该方式
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
// 流模式
int mode = AudioTrack.MODE_STREAM;
// 计算最小buffer大小
int minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
// 构造AudioTrack 不能小于AudioTrack的最低要求,也不能小于我们每次读的大小
mAudioTrack = new AudioTrack(streamType, sampleRate, channelConfig, audioFormat,
Math.max(minBufferSize, BUFFER_SIZE), mode);
// 从文件流读数据
FileInputStream fis = null;
try {
// 循环读数据,写到播放器去播放
fis = new FileInputStream(audioRecordFile);
fis.skip(mOffset);
Log.d(TAG, audioRecordFile.toString());
// 循环读数据,写到播放器去播放 只要没读完,循环播放
int read;
mAudioTrack.play();
Log.d(TAG, String.valueOf(mAudioTrack.getPlayState()));
// write 是阻塞的方法
while ((read = fis.read(mBuffer)) != -1) {
// todo: 处理mBuffer实现元数据处理变音等功能
int status = mAudioTrack.write(mBuffer, 0, read);
updateOffset(read);
Log.d(TAG, String.valueOf(mOffset));
// 检查write的返回值,处理错误
checkStatus(status);
}
Log.d(TAG, "播放完毕!");
} catch (Exception e) {
e.printStackTrace();
// 读取失败
playFail();
} finally {
mIsPlaying = false;
// 关闭文件输入流
closeInputStream(fis);
// 播放器释放
resetQuietly(mAudioTrack);
}
}

private void updateOffset(int read) {
if (mAudioTrack.getPlayState() == mAudioTrack.PLAYSTATE_PLAYING) {
mOffset += read;
}
}

关闭输入流closeInputStream()和resetQuietly()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void closeInputStream(InputStream is) {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

private void resetQuietly(AudioTrack audioTrack) {
try {
audioTrack.stop();
audioTrack.release();
} catch (Exception e) {
e.printStackTrace();
}
}

播放失败,更新UI操作playFail()

1
2
3
4
5
6
7
8
private void playFail() {
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "pcm播放失败", Toast.LENGTH_SHORT).show();
}
});
}

onDestroy()释放资源

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onDestroy() {
super.onDestroy();
if (mAudioTrackTest.getAudioTrack() != null) {
mAudioTrackTest.getAudioTrack().stop();
}
if (mExecutorService != null) {
mExecutorService.shutdownNow();
mExecutorService = null;
}
}

MediaPlayer

MediaPlayer支持AAC、AMR、FLAC、MP3、MIDI、OGG、PCM等格式,MediaPlayer可以通过设置元数据和播放源来音频。

播放Raw文件夹下面音频的元数据

1
2
3
4
// 直接创建,不需要设置setDataSource
MediaPlayer mMediaPlayer;
mMediaPlayer=MediaPlayer.create(this, R.raw.audio);
mMediaPlayer.start();

通过设置播放源来播放音频文件的三个方法

  • setDataSource(String path)
1
2
3
4
5
6
7
8
9
10
11
12
// 如果从sd卡中加载音乐
// <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
// 利用 Environment.getExternalStorageDirectory() 获取SD卡的根目录了,一般为/storage/emulated/0
// 把xxx.wav放到SD的根目录下,就可以获得文件的路径了 String path = Environment.getExternalStorageDirectory()+"/xxx.wav";
mMediaPlayer.setDataSource(path);

// 如果从网络加载音乐,如果是从网络中加载那么需要设置网络权限
// <uses-permission android:name="android.permission.INTERNET"/>
mMediaPlayer.setDataSource("http://..../xxx.mp3");

// 需使用异步缓冲
mMediaPlayer.prepareAsync();
  • setDataSource(FileDescriptor fd)
1
2
3
4
// 资源文件放在assets文件夹
AssetFileDescriptor fd = getAssets().openFd("samsara.mp3");
mMediaPlayer.setDataSource(fd.getFileDescriptor());
mMediaPlayer.prepare();
  • setDataSource(FileDescptor fd,long offset,long length)
1
2
3
4
// 需将资源文件放在assets文件夹
AssetFileDescriptor fd = getAssets().openFd("samsara.mp3");
mMediaPlayer.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
mMediaPlayer.prepare();

设置完数据源,不要忘记prepare(),尽量使用异步prepareAync(),这样不会阻塞UI线程。

MediaPlayer例子:播放录音

播放录音playMedia()和播放测试音频playMediaTest()

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
public void playMedia() {
if (!mIsPlaying) {
mExecutorService.submit(new Runnable() {
@Override
public void run() {
mIsPlaying = true;
doPlay(getLastFile());
}
});
} else {
Toast.makeText(mContext, "正在播放", Toast.LENGTH_SHORT).show();
}
}

public void playMediaTest() {
if (mHasPausing && mMediaPlayer != null) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mBtMediaPlayerTest.setText("播放测试录音");
} else {
mMediaPlayer.start();
mBtMediaPlayerTest.setText("暂停测试录音");
}
return;
}
if (mIsPlaying) {
Toast.makeText(mContext, "正在播放", Toast.LENGTH_SHORT).show();
} else {
mBtMediaPlayerTest.setText("暂停测试录音");
mExecutorService.submit(new Runnable() {
@Override
public void run() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mHasPausing = true;
mIsPlaying = true;
doPlay(new File(mContext.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/MediaRecorderDemo/test.m4a"));
}
}
});
}
}

具体播放方法doPlay()

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
private void doPlay(File mediaFile) {
try {
// 配置播放器 MediaPlayer
mMediaPlayer = new MediaPlayer();

// 设置声音文件
mMediaPlayer.setDataSource(mediaFile.getAbsolutePath());

// 配置音量,中等音量
mMediaPlayer.setVolume(1, 1);

// 播放是否循环
mMediaPlayer.setLooping(false);

// 设置监听回调 播放完毕
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.d(TAG, "播放完毕!");
stopPlayer();
}
});
mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
stopPlayer();
Toast.makeText(mContext, "播放失败", Toast.LENGTH_SHORT).show();
return true;
}
});

// 设置播放
mMediaPlayer.prepare();
mMediaPlayer.start();

// 异常处理,防止闪退
} catch (Exception e) {
e.printStackTrace();
stopPlayer();
}
}

播放结束关闭播放器stopPlayer()

1
2
3
4
5
6
private void stopPlayer(){
mIsPlaying = false;
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}

SoundPool

SoundPool支持多个音频文件同时播放(组合音频也是有上限的),延时短,比较适合短促、密集的场景,适合游戏开发中音效播放。

SoundPool实例化方式

new SoundPool(适用与5.0以下)

1
2
3
4
5
new SoundPool(int maxStreams, int streamType, int srcQuality)
// 从android5.0开始此方法被标记为过时
// maxStreams: 允许同时播放的流的最大值
// streamType: 音频流的类型描述
// srcQuality: 采样率转化质量,默认值为0

SoundPool.Builder(从5.0开始支持)

1
2
3
4
5
6
7
8
9
//设置描述音频流信息的属性
AudioAttributes abs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
SoundPool mSoundPoll = new SoundPool.Builder()
.setMaxStreams(100) //设置允许同时播放的流的最大值
.setAudioAttributes(abs)
.build();

SoundPool一些重要方法

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
// 几个load方法和上文提到的MediaPlayer基本一致,这里的每个load都会返回一个SoundId值,这个值可以用来播放和卸载音乐。
int load(AssetFileDescriptor afd, int priority)
int load(Context context, int resId, int priority)
int load(String path, int priority)
int load(FileDescriptor fd, long offset, long length, int priority)

// 通过流id暂停播放
final void pause(int streamID)

// 播放声音,soundID:音频id(这个id来自load的返回值); left/rightVolume:左右声道(默认1,1);loop:循环次数(-1无限循环,0代表不循环);rate:播放速率(1为标准),该方法会返回一个streamID,如果StreamID为0表示播放失败,否则为播放成功
final int play(int soundID, float leftVolume, float rightVolume, int priority, int loop, float rate)

//释放资源(很重要)
final void release()

//恢复播放
final void resume(int streamID)

//设置指定id的音频循环播放次数
final void setLoop(int streamID, int loop)

//设置加载监听(因为加载是异步的,需要监听加载,完成后再播放)
void setOnLoadCompleteListener(SoundPool.OnLoadCompleteListener listener)

//设置优先级(同时播放个数超过最大值时,优先级低的先被移除)
final void setPriority(int streamID, int priority)

//设置指定音频的播放速率,0.5~2.0(rate>1:加快播放,反之慢速播放)
final void setRate(int streamID, float rate)

//停止指定音频播放
final void stop(int streamID)

//卸载指定音频,soundID来自load()方法的返回值
final boolean unload(int soundID)

//暂停所有音频的播放
final void autoPause()

//恢复所有暂停的音频播放
final void autoResume()

Ringtone

Ringtone为铃声、通知和其他类似声音提供快速播放的方法,Ringtone实例需要从RingtoneManager获取,RingtoneManager提供系统铃声列表检索方法。

获取实例

1
2
3
4
5
6
7
// 获取实例方法,均为RingtoneManager类提供

// 1.通过铃声uri获取
static Ringtone getRingtone(Context context, Uri ringtoneUri)

// 2.通过铃声检索位置获取
Ringtone getRingtone(int position)

RingtoneManager几个重要的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 两个构造方法
RingtoneManager(Activity activity)
RingtoneManager(Context context)

// 获取指定声音类型(铃声、通知、闹铃等)的默认声音的Uri
static Uri getDefaultUri(int type)

// 获取系统所有Ringtone的cursor
Cursor getCursor()

// 获取cursor指定位置的Ringtone uri
Uri getRingtoneUri(int position)

// 判断指定Uri是否为默认铃声
static boolean isDefault(Uri ringtoneUri)

// 获取指定uri的所属类型
static int getDefaultType(Uri defaultRingtoneUri)

// 将指定Uri设置为指定声音类型的默认声音
static void setActualDefaultRingtoneUri(Context context, int type, Uri ringtoneUri)

Ringtone例子:播放铃声

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/>
// <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

/**
* 播放来电铃声的默认音乐
*/
private void playRingtoneDefault(){
Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) ;
Ringtone mRingtone = RingtoneManager.getRingtone(this,uri);
mRingtone.play();
}


/**
* 随机播放一个Ringtone(有可能是提示音、铃声等)
*/
private void ShufflePlayback(){
RingtoneManager manager = new RingtoneManager(this) ;
Cursor cursor = manager.getCursor();
int count = cursor.getCount() ;
int position = (int)(Math.random() * count) ;
Ringtone mRingtone = manager.getRingtone(position) ;
mRingtone.play();
}

总结

  1. 播放大文件音乐,如WAV无损音频和PCM无压缩音频,可使用更底层的播放方式AudioTrack。它支持流式播放,可以读取(可来自本地和网络)音频流,播放延迟较小。
  2. 对于延迟度要求不高,并且希望能够更全面的控制音乐的播放,MediaPlayer比较适合。
  3. 声音短小,延迟度小,并且需要几种声音同时播放的场景,适合使用SoundPool。
  4. 对于系统类声音的播放和操作,Ringtone更适合。