(1)概念
(A)MediaPlayer
MediaPlayer的功能很强大,下面附上一张该类封装音频的生命周期图:
适合在后台长时间播放本地音乐文件或者在线等流媒体文件,它的封装层次比较高,使用方式也比较简单。
(B)SoundPool
适合播放比较短的音频片段,比如游戏声音,按键声音,铃声片段等,并且可以同时播放多个音频。
(C)AudioTrack
AudioTrack属于更偏底层的音频播放,MediaPlayerService的内部就是使用了AudioTrack。
AudioTrack用于单个音频播放和管理,相比于MediaPlayer具有:精炼、高效的优点。
AudioTrack用于播放PCM(PCM无压缩的音频格式)音乐流的回放,如果要播需放其它格式音频,需要响应的解码器,这也是AudioTrack用的比较少的原因,需要自己解码音频。
(D)Ringtone
Ringtone为铃声、通知和其他类似声音提供快速播放的方法,这里还不得不提到一个管理类"RingtoneManager",提供系统铃声列表检索方法,并且,Ringtone实例需要从RingtoneManager获取。
备注:
这里要说到MediaPlayer和AudioTrack之间的联系,MediaPlayer在framework层也实例化了AudioTrack,其实质是MediaPlayer在framework层进行解码后,生成PCM流,然后代理委托给AudioTrack,最后AudioTrack传递给AudioFlinger进行混音,然后才传递给硬件播放。
比较常见使用AudioTrack,CPU占用率低,内存消耗也比较少。因此如果是播放WAV音频文件,还是比较建议使用AudioTrack。
总结:
- 对于延迟度要求不高,并且希望能够更全面的控制音乐的播放,MediaPlayer比较适合;
- 声音短小,延迟度小,并且需要几种声音同时播放的场景,适合使用SoundPool;
- 放大文件音乐,如WAV无损音频和PCM无压缩音频,可使用更底层的播放方式AudioTrack。它支持流式播放,可以读取(可来自本地和网络)音频流,却播放延迟较小;
- 对于系统类声音的播放和操作,Ringtone更适合;
(2)简单使用
(A)MediaPlayer
MediaPlayer mMediaPlayer = new MediaPlayer(); // 创建MediaPlayer实例
mMediaPlayer.setDataSource(dataSource); // 设置播放资源,可以是asset、sd卡路径,也可以是网络url
mMediaPlayer.setLooping(false); // 不循环播放
mMediaPlayer.prepare(); // 播放前准备,需要调用,create创建实例可以不用调用
mMediaPlayer.start(); // 进行播放
mMediaPlayer.stop(); // 停止播放
mMediaPlayer.pause(); // 暂停播放
mMediaPlayer.release(); // 释放播放资源
mMediaPlayer.reset(); // 重置播放器状态
mMediaPlayer.seekTo(); // 调整进度
(B)SoundPool
//设置描述音频流信息的属性
AudioAttributes abs = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build() ;
SoundPool mSoundPoll = new SoundPool.Builder()
.setMaxStreams(100) //设置允许同时播放的流的最大值
.setAudioAttributes(abs) //完全可以设置为null
.build() ;
// 几个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()
(C)AudioTrack
//第一个参数:声音的类型,有以下几种
//STREAM_VOICE_CALL:电话声音
//STREAM_SYSTEM:系统声音
//STREAM_RING:铃声
//STREAM_MUSIC:音乐声
//STREAM_ALARM:警告声
//STREAM_NOTIFICATION:通知声
//第二个参数:采样频率,可选:8000,16000,22050,24000,32000,44100,48000等
//第三个参数:声道数
//第四个参数:采样格式,AudioFormat.ENCODING_PCM_16BIT,AudioFormat.ENCODING_PCM_8BIT
//第五个参数:其配置AudioTrack内部的音频缓冲区大小,最好通过getMinBufferSize来计算
//第六个参数:播放模式,MODE_STATIC需要一次性将所有的数据都写入播放缓冲区,简单高效,通常用于铃声,系统提示音的播放,MODE_STREAM需要按照一定的时间间隔不间断的写入音频数据,理论上可以用于任何音频场景,通常用来播放流媒体音频
AudioTrack mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, //声音的类型
samplerate, //设置音频数据的采样率
AudioFormat.CHANNEL_CONFIGURATION_STEREO, //设置输出声道为双声道立体声
AudioFormat.ENCODING_PCM_16BIT, //设置音频数据块是8位还是16位
mAudioMinBufSize, AudioTrack.MODE_STREAM); // 设置模式类型,在这里设置为流类型
mAudioTrack.play(); // 启动
mAudioTrack.write();//数据写入audiotrack中
// 停止与释放资源
mAudioTrack.stop();
mAudioTrack.release();
(D)Ringtone
//获取实例方法,均为RingtoneManager类提供
//通过铃声uri获取
static Ringtone getRingtone(Context context, Uri ringtoneUri)
//通过铃声检索位置获取
Ringtone getRingtone(int position)
/**
* 播放来电铃声的默认音乐
*/
private void playRingtoneDefault(){
Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) ;
Ringtone mRingtone = RingtoneManager.getRingtone(this,uri);
mRingtone.play();
//mRingtone.stop();
}
/**
* 随机播放一个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();
//mRingtone.stop();
}
(3)一个内存泄露问题(充电提示音问题)
(A)问题背景
最近遇到一个项目上的问题,在Settings/Sound当中将充电提示音开关打开,发现刚开始插入充电后有提示音,测试到第40次后就再也没有提示音了,从Log看有如下报错:
08-04 10:34:28.286627 417 1238 E AF::Track: Track(97): no more tracks available
08-04 10:34:28.286714 417 1238 E AudioFlinger_Threads: createTrack_l() initCheck failed -12; no control block?
08-04 10:34:28.294618 608 6382 E IAudioFlinger: createTrack returned error -12
08-04 10:34:28.294696 608 6382 E AudioTrack: set(): createTrack_l fail! status = -12
08-04 10:34:28.294719 608 6382 E AudioSink: Unable to create audio track
08-04 10:34:28.294742 608 6382 D AudioTrack: ~AudioTrack(-1474187544): 0xa5724880
从代码中查看上面Error:
//frameworks/av/services/audioflinger/Tracks.cpp
//createTrack过程中会调用isTrackAllowed_l进行检查(最原始的createTrack起始于AudioFlinger.cpp)
if (!thread->isTrackAllowed_l(channelMask, format, sessionId, uid)) {
ALOGE("%s(%d): no more tracks available", __func__, mId);
releaseCblk(); // this makes the track invalid.
return;
}
//frameworks/av/services/audioflinger/Threads.h
static constexpr uint32_t kMaxTracksPerUid = 40;
virtual bool isTrackAllowed_l(
audio_channel_mask_t channelMask __unused,
audio_format_t format __unused,
audio_session_t sessionId __unused,
uid_t uid) const {
//通过trackCountForUid_l函数检查相同uid的最多不能创建超过40个(一直未释放的情况)
return trackCountForUid_l(uid) < PlaybackThread::kMaxTracksPerUid
&& mTracks.size() < PlaybackThread::kMaxTracks;
}
//frameworks/av/services/audioflinger/Threads.cpp
// trackCountForUid_l() must be called with ThreadBase::mLock held
uint32_t AudioFlinger::PlaybackThread::trackCountForUid_l(uid_t uid) const
{
uint32_t trackCount = 0;
for (size_t i = 0; i < mTracks.size() ; i++) {
//相同uid最多不能创建超过40个
if (mTracks[i]->uid() == uid) {
trackCount++;
}
}
return trackCount;
}
由于在创建提示音的过程中一直未释放资源,未执行析构函数 ~AudioTrack,从而导致40次之后再也无法createTrack。
(B)插入/拔出充电触发的位置
//frameworks/base/services/core/java/com/android/server/power/PowerManagerService.java
private boolean isChargingFeedbackEnabled(@UserIdInt int userId) {
//CHARGING_SOUNDS_ENABLED——这个就是Settings/Sound下的开关按钮
final boolean enabled = Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.CHARGING_SOUNDS_ENABLED, 1, userId) != 0;
final boolean dndOff = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.ZEN_MODE, Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
== Settings.Global.ZEN_MODE_OFF;
return enabled && dndOff;
}
private void updateIsPoweredLocked(int dirty) {
//...
if (mBootCompleted) {
if (mIsPowered && !BatteryManager.isPlugWired(oldPlugType)
&& BatteryManager.isPlugWired(mPlugType)) {
//插入充电执行的操作
mNotifier.onWiredChargingStarted(mUserId);
} else if (dockedOnWirelessCharger) {
mNotifier.onWirelessChargingStarted(mBatteryLevel, mUserId);
} else if(!mIsPowered && isChargingFeedbackEnabled(mUserId)){
//拔出充电执行的操作
mNotifier.onWiredChargingFinished(mUserId);
}
}
}
(C)插入/拔出充电执行的步骤
//frameworks/base/services/core/java/com/android/server/power/Notifier.java
//插入充电的步骤
public void onWiredChargingStarted(@UserIdInt int userId) {
if (DEBUG) {
Slog.d(TAG, "onWiredChargingStarted");
}
mSuspendBlocker.acquire();
Message msg = mHandler.obtainMessage(MSG_WIRED_CHARGING_STARTED);
msg.setAsynchronous(true);
msg.arg1 = userId;
mHandler.sendMessage(msg);
}
private void showWiredChargingStarted(@UserIdInt int userId) {
playChargingStartedFeedback(userId, false /* wireless */);
mSuspendBlocker.release();
}
private void playChargingStartedFeedback(@UserIdInt int userId, boolean wireless) {
if (!isChargingFeedbackEnabled(userId)) {
return;
}
// vibrate
final boolean vibrate = Settings.Secure.getIntForUser(mContext.getContentResolver(),
Settings.Secure.CHARGING_VIBRATION_ENABLED, 1, userId) != 0;
if (vibrate) {
mVibrator.vibrate(CHARGING_VIBRATION_EFFECT, VIBRATION_ATTRIBUTES);
}
// play sound
final String soundPath = Settings.Global.getString(mContext.getContentResolver(),
wireless ? Settings.Global.WIRELESS_CHARGING_STARTED_SOUND
: Settings.Global.CHARGING_STARTED_SOUND);
final Uri soundUri = Uri.parse("file://" + soundPath);
if (soundUri != null) {
mRingtone = RingtoneManager.getRingtone(mContext, soundUri);
if (mRingtone != null) {
mRingtone.setStreamType(AudioManager.STREAM_SYSTEM);
mRingtone.play();
}
}
}
//frameworks/base/services/core/java/com/android/server/power/Notifier.java
//拔出充电的步骤
public void onWiredChargingFinished(@UserIdInt int userId) {
if (DEBUG) {
Slog.d(TAG, "onWiredChargingFinished");
}
mSuspendBlocker.acquire();
Message msg = mHandler.obtainMessage(MSG_CHARGING_FINISHED);
msg.setAsynchronous(true);
msg.arg1 = userId;
mHandler.sendMessage(msg);
}
private void showWiredChargingFinished(@UserIdInt int userId) {
if (DEBUG) {
Slog.d(TAG, "showWiredChargingFinished");
}
playChargingFinishedSound(userId);
mSuspendBlocker.release();
}
private void playChargingFinishedSound(@UserIdInt int userId) {
if (mRingtone != null) {
if (DEBUG) {
Slog.d(TAG, "mRingtone stop");
}
mRingtone.stop();
}
mRingtone = null;
}
现在Google源码当中就是没有拔出充电进行release资源的stop()步骤,所以才导致40次之后不能在播放声音的问题,加上这段拔出充电的代码后,每次拔出充电的Log如下:
08-16 15:56:16.558645 546 6043 D AudioTrack: stop(30): 0xa8d4e380 stop done
08-16 15:56:16.590319 415 826 D AudioFlinger_Threads: removeTracks_l(62): removing track on session 169
08-16 15:56:16.990597 546 693 D AudioTrack: ~AudioTrack(30): 0xa8d4e380
可以看到AudioTrack这边可以正常进行release动作了,自此问题解决。