原文地址:http://tanxiaoya105.blog.163.com/blog/static/210328019201272295457523/
大纲:
一、介绍Audio Focus机制
二、AudioManager的一般使用
Android是多任务系统,Audio系统是竞争资源。( because there is only one audio output and there may be several media services competing for its use)因为在只有一个音频输出,然而可能有很多的媒体程序去竞争使用它,这样就需要一种机制来管理。Android2.2之前,没有内建的机制来解决多个程序竞争Audio的问题,2.2引入了称作AudioFocus的机制来管理对Audio资源的竞争的管理与协调。
按照AudioFocus的机制,在使用Audio之前,需要申请AudioFocus,在获得AudioFocus之后才可以使用Audio;如果有别的程序竞争你正在使用的Audio,你的程序需要在收到通知之后做停止播放或者降低声音的处理。值得指出的是,这种机制是需要合作完成的,需要所有使用Audio资源的程序都按照这种机制来做,而如果有程序在它失去AudioFocus的时候仍然在使用Audio,AudioFocus拿它也没办法。而这一点对于开放系统的Android来说很致命的:用户可能安装没遵守这种机制的程序,或者版本太老还没引入这种机制的程序,这最终会导致很差的用户体验。
一、AudioFocus的申请与释放
下面看与AudioFocus的相关的类:
获取/放弃AudioFocus的方法都在android.media.AudioManager中,获取AudioFocus用requestAudioFocus();用完之后,放弃AudioFocus,用bandonAudioFocus()。
requestAudioFocus
public int requestAudioFocus (AudioManager.OnAudioFocusChangeListener l, int streamType, int durationHint)
Since: API Level 8
Request audio focus. Send a request to obtain the audio focus
Parameters:
l
the listener to be notified of audio focus changes streamType |
|
the main audio stream type affected by the focus request durationHint |
|
use AUDIOFOCUS_GAIN_TRANSIENT to indicate this focus request is temporary, and focus will be abandonned shortly. Examples of transient requests are for the playback of driving directions, or notifications sounds. Use AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK to indicate also that it's ok for the previous focus owner to keep playing if it ducks its audio output. Use AUDIOFOCUS_GAIN for a focus request of unknown duration such as the playback of a song or a video. |
Returns
AUDIOFOCUS_REQUEST_FAILED
or
AUDIOFOCUS_REQUEST_GRANTED
其中,参数:
streamType是《Android中的Audio播放:音量和远程播放控制》中说明的AudioStream,其值取决于AudioManager中的STREAM_xxx,在AudioStream的裁决机制中并未有什么实际意义;
durationHint是持续性的指示:
AUDIOFOCUS_GAIN
指示申请得到的Audio Focus不知道会持续多久,一般是长期占有;
AUDIOFOCUS_GAIN_TRANSIENT
指示要申请的AudioFocus是暂时性的,会很快用完释放的;
AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK不但说要申请的AudioFocus是暂时性的,还指示当前正在使用AudioFocus的可以继续播放,只是要“duck”一下(降低音量)。
AudioManager.OnAudioFocusChangeListener是申请成功之后监听AudioFocus使用情况的Listener,后续如果有别的程序要竞争AudioFocus,都是通过这个Listener的onAudioFocusChange()方法来通知这个Audio Focus的使用者的。
返回值,可能是:
AUDIOFOCUS_REQUEST_GRANTED:申请成功;
AUDIOFOCUS_REQUEST_FAILED:申请失败。
public int abandonAudioFocus (AudioManager.OnAudioFocusChangeListener l)
二、AudioFocus被抢占与重新获得
由上节中知道,申请/释放AudioFocus时传入了AudioManager.OnAudioFocusChangeListener这个参数,其onAudioFocusChange()方法是Audio Focus被抢占与再次获得通知的地方。所以,每个要使用AudioFocus的程序都要小心实现这个函数,保证AudioFocus实现的一致性。
onAudioFocusChange()方法的focusChange参数指示了该AudioFocus的竞争者对AudioFocus的拥有情况,取值如下:
AUDIOFOCUS_GAIN:获得了Audio Focus;
AUDIOFOCUS_LOSS:失去了Audio Focus,并将会持续很长的时间。这里因为可能会停掉很长时间,所以不仅仅要停止Audio的播放,最好直接释放掉Media资源。而因为停止播放Audio的时间会很长,如果程序因为这个原因而失去AudioFocus,最好不要让它再次自动获得AudioFocus而继续播放,不然突然冒出来的声音会让用户感觉莫名其妙,感受很不好。这里直接放弃AudioFocus,当然也不用再侦听远程播放控制【如下面代码的处理】。要再次播放,除非用户再在界面上点击开始播放,才重新初始化Media,进行播放。
AUDIOFOCUS_LOSS_TRANSIENT:暂时失去Audio Focus,并会很快再次获得。必须停止Audio的播放,但是因为可能会很快再次获得AudioFocus,这里可以不释放Media资源;
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:暂时失去AudioFocus,但是可以继续播放,不过要在降低音量。
下面是onAudioFocusChange()方法处理的代码片段:
OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
// Pause playback
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
am.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
am.abandonAudioFocus(afChangeListener);
// Stop playback
} else if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
// Lower the volume
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Resume playback or Raise it back to normal
}
}
};
OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() { public void onAudioFocusChange(int focusChange) { if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT // Pause playback } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) { am.unregisterMediaButtonEventReceiver(RemoteControlReceiver); am.abandonAudioFocus(afChangeListener); // Stop playback } else if (focusChange == AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { // Lower the volume } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { // Resume playback or Raise it back to normal } } };
三、典型的应用AudioFocus的场景
下面的时序图描述了AudioFocus被抢占与再次获取的典型场景:
Audio Focus被抢占与再次获取的时序图
注意:为了描述简单,此图中除了两个竞争Audio Focus的App之外,只用AudioManager表征了Android的AudioFocus机制中内部参与的对象,实际AudioManager只是外部的表象,内部参与的对象很多,回调函数也并非简单的直接由AudioManager调用,其中还包含了复杂的IPC机制。
图中:
AudioFocus Client通过requestAudioFocus()获取AudioFocus,在获得AudioFocus之后,开始播放Audio[Step#1 ~ #2];
其它程序(Other App)也通过requestAudioFocus()获取AudioFocus [Step#3]
AudioFocus Client失去了Audio Focus,在onAudioFocusChanged()中,根据focusChange【focusChange的值与Other App申请时的durationHint相反,即focusChange = -1*durationHint】的值,做第二节中所描述的处理[Step#4];
其它程序(Other App)获取Audio Focus之后,开始播放Audio[Step#5];
其它程序(Other App)使用Audio之后,通过abandonAudioFocus()归还AudioFocus [Step#6];
AudioFocus Client重新获得了Audio Focus,可做进一步的处理 [Step#7]
小结
Audio Focus机制要参与各方充分理解并统一遵照施行,有没有遵照者或者实现有误的程序存在就可能打破这一机制,带来糟糕的用户体验。在保证Built-in程序没问题的前提下,如果进入AndroidMarket之前的程序都严格执行了AudioFocus相关的测试,应该也没问题。
使用Audio的程序要做到:
使用前,用requestAudioFocus()申请AudioFocus,并根据应用的实际选取恰当的durationHint值;
正确的在AudioManager.OnAudioFocusChangeListener中响应AudioFocus失去和重新获取事件;
Audio使用结束,用
abandonAudioFocus()
归还AudioFocus。
二、AudioManager的一般使用
AudioManager除了能在Audio Focus中使用外,最主要的是它能控制声音和铃声
AudioManager provides access to volume and ringer mode control.
AudioManager类位于android.Media 包中,该类提供访问控制音量和钤声模式的操作。
通过getSystemService(Context.AUDIO_SERVICE)方法获得AudioManager实例对象。
AudioManager audiomanage = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audiomanager就是我们定义的控制系统声音的对象。
这里只讲述几个比较常用到的方法:
adjustVolume(int direction, int flags) ——用来控制手机音量大小,当传入的第一个参数为 AudioManager.ADJUST_LOWER 时,可将音量调小一个单位,传入AudioManager.ADJUST_RAISE 时,则可以将音量调大一个单位。
adjustStreamVolume(int streamType, int direction, int flags)——(以步长)调节手机音量大小
参数1:声音类型,可取为STREAM_VOICE_CALL(通话)、STREAM_SYSTEM(系统声音)、STREAM_RING(铃声)、STREAM_MUSIC(音乐)、STREAM_ALARM(闹铃声)
参数2:调整音量的方向,可取ADJUST_LOWER(降低)、ADJUST_RAISE(升高)、ADJUST_SAME
参数3:可选的标志位
setStreamVolume(int streamType, int index, int flags)——直接设置音量大小
getMode() ——返回当前音频模式,如 NORMAL(普通), RINGTONE(铃声), or IN_CALL(通话)
setMode()——设置声音模式,可取值NORMAL(普通), RINGTONE(铃声), or IN_CALL(通话)
getRingerMode() ——返回当前的铃声模式。如RINGER_MODE_NORMAL(普通)、RINGER_MODE_SILENT(静音)、RINGER_MODE_VIBRATE(震动)
setRingerMode(int ringerMode) ——改变铃声模式
getStreamVolume(int streamType) ——取得当前手机的音量,最大值为7,最小值为0,当为0时,手机自动将模式调整为“震动模式”。
getStreamMaxVolume(int streamType)——获得当前手机最大铃声。
setStreamMute(int streamType, boolean state
//音量控制,初始化定义
AudioManager mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
//最大音量
int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
//当前音量
int currentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
直接控制音量的多少:
以一步步长控制音量的增减,并弹出系统默认音量控制条:
} |
//通话音量
int max = mAudioManager.getStreamMaxVolume( AudioManager.STREAM_VOICE_CALL );
int current = mAudioManager.getStreamVolume( AudioManager.STREAM_VOICE_CALL );
//系统音量
int max = mAudioManager.getStreamMaxVolume( AudioManager.STREAM_SYSTEM );
current = mAudioManager.getStreamVolume( AudioManager.STREAM_SYSTEM );
//铃声音量
max = mAudioManager.getStreamMaxVolume( AudioManager.STREAM_RING );
current = mAudioManager.getStreamVolume( AudioManager.STREAM_RING );
//音乐音量
max = mAudioManager.getStreamMaxVolume( AudioManager.STREAM_MUSIC );
current = mAudioManager.getStreamVolume( AudioManager.STREAM_MUSIC );
//提示声音音量
max = mAudioManager.getStreamMaxVolume( AudioManager.STREAM_ALARM );
current = mAudioManager.getStreamVolume( AudioManager.STREAM_ALARM );
一个即时音效的例子
上一小节介绍了SoundPool 类的常用方法,接下来将带领读者写一个即时音效的例子。具体步骤如下。
(1)打开Eclipse,导入本章源代码中名为Sample3_1的项目。
图3-1 raw 文件夹示意图 |
(2)在res 目录下有一个名为raw 的文件夹,其中包含本项目中所用到的声音文件,如图3-1 所示。
(3)下面将介绍layout 目录下的main.xml 文件的代码内容,在其中设置控件的布局。
代码位置:本书随书光盘中源代码\第3 章\Sample3_1\res\layout\main.xml。
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android=" http://schemas.android.com/apk/res/android" 3 android:orientation="vertical" 4 android:layout_width="fill_parent" 5 android:layout_height="fill_parent" 6 > 7 <Button 8 android:text="播放音效1" 9 android:id="@+id/Button01" 10 android:layout_width="fill_parent" 11 android:layout_height="wrap_content"> 12 </Button> <!--添加一个Button 控件--> 13 <Button 14 android:text="播放音效2" 15 android:id="@+id/Button02" 16 android:layout_width="fill_parent" 17 android:layout_height="wrap_content"> 18 </Button> <!--添加一个Button 控件--> 19 <Button 20 android:text="暂停音效1" 21 android:id="@+id/Button1Pause" 22 android:layout_width="fill_parent" 23 android:layout_height="wrap_content"> 24 </Button> <!--添加一个Button 控件--> 25 <Button 26 android:text="暂停音效2" 27 android:id="@+id/Button2Pause" 28 android:layout_width="fill_parent" 29 android:layout_height="wrap_content"> 30 </Button> <!--添加一个Button 控件--> 31 </LinearLayout>
第2~6 行定义一个垂直方向上的线性布局。
第7~18 行为一个播放音效1 的按钮,以及一个播放音效2 的按钮。
第19~30 行为一个暂停播放音效1 的按钮,以及一个暂停播放音效2 的按钮。
(4)接着介绍在src 目录下的wyf.zcl 包中的MyActivity.java 类。
代码位置:本书随书光盘中源代码\第3 章\Sample3_1\src\wyf\zcl\MyActivity.java。
1 package wyf.zcl; 2 import java.util.HashMap; //引入相关包 3 ……//此处省略部分不重要代码,读者可在光盘源代码中查看 4 import android.widget.Button; 5 public class MyActivity extends Activity { //Activity 创建时调用 6 SoundPool sp; //得到一个声音池引用 7 HashMap<Integer,Integer> spMap; //得到一个map 的引用 8 Button b1; //声音播放控制按钮 9 Button b1Pause; //声音暂停控制按钮 10 Button b2; //声音播放控制按钮 11 Button b2Pause; //声音暂停控制按钮 12 @Override 13 public void onCreate(Bundle savedInstanceState){ 14 super.onCreate(savedInstanceState); 15 setContentView(R.layout.main); 16 initSoundPool(); //初始化声音池 17 b1=(Button)findViewById(R.id.Button01); //声音播放控制按钮实例化 18 b2=(Button)findViewById(R.id.Button02); //声音播放控制按钮实例化 19 b1Pause=(Button)findViewById(R.id.Button1Pause); 20 b2Pause=(Button)findViewById(R.id.Button2Pause); 21 b1.setOnClickListener(new View.OnClickListener() { 22 @Override 23 public void onClick(View v) { 24 playSound(1,1); //播放第一首音效,循环一遍 25 Toast.makeText(MyActivity.this, "播放音效1", Toast.LENGTH_SHORT). show(); 26 }}); 27 b1Pause.setOnClickListener(new View.OnClickListener() { 28 @Override 29 public void onClick(View v) { 30 sp.pause(spMap.get(1)); 31 Toast.makeText(MyActivity.this, "暂停音效1", Toast.LENGTH_SHORT). show(); 32 }}); 33 b2.setOnClickListener(new View.OnClickListener() { 34 @Override 35 public void onClick(View v) { 36 playSound(2,1); //播放第二首音效,循环一遍 37 Toast.makeText(MyActivity.this, "播放音效2", Toast.LENGTH_SHORT). show(); 38 }}); 39 b2Pause.setOnClickListener(new View.OnClickListener() { 40 @Override 41 public void onClick(View v) { 42 sp.pause(spMap.get(2)); 43 Toast.makeText(MyActivity.this, "暂停音效2", Toast.LENGTH_SHORT). show(); 44 }}); 45 } 46 public void initSoundPool(){ //初始化声音池 47 sp=new SoundPool( 48 5, //该参数为设置同时能够播放多少音效 49 AudioManager.STREAM_MUSIC, //该参数设置音频类型 50 0 //该参数设置音频文件的质量,目前还没有效果,设置为0(默认值) 51 ); 52 spMap=new HashMap<Integer,Integer>(); 53 spMap.put(1, sp.load(this, R.raw.attack02, 1)); 54 spMap.put(2, sp.load(this, R.raw.attack14, 1)); 55 } 56 public void playSound(int sound,int number){ 57 AudioManager am= //实例化AudioManager 对象 58 (AudioManager)this.getSystemService(this.AUDIO_SERVICE); 59 float audioMaxVolumn= //返回当前AudioManager 对象的最大音量值 60 am.getStreamMaxVolume(AudioManager.STREAM_MUSIC); 61 float audioCurrentVolumn= //返回当前AudioManager 对象的音量值 62 am.getStreamVolume(AudioManager.STREAM_MUSIC); 63 float volumnRatio= 64 audioCurrentVolumn/audioMaxVolumn; 65 sp.play( 66 spMap.get(sound), //播放的音乐ID 67 volumnRatio, //左声道音量 68 volumnRatio, //右声道音量 69 1, //优先级,0 为最低 70 number, //循环次数,0 无不循环,-1 无永远循环 71 1 //回放速度 ,该值在0.5~2.0 之间,1 为正常速度 72 ); 73 }}
第2~11 行引入程序中所使用的相关包,以及声明相关引用。
第13 行为Activity 的onCreate()方法。当创建MyActivity 时调用该方法。
第21~44 行为播放音效1、播放音效2、暂停播放音效1、暂停播放音效2 的初始化及监听方法。
第46~55 行为初始化声音池,SoundPool 调用load()方法可加载音频文件,然后将加载了音频文件的SoundPool 添加到一个HashMap 中,提供给以后的调用。
第56~73 行为播放音效,调用SoundPool 中的play 方法,播放声音,参数sound是播放音效的ID,参数number 是播放音效的次数。该方法的使用上一小节已经做了详细介绍,此处不再赘述了。
(5)最终效果如图3-2 所示,单击播放音效1/播放音效2,将播放音效,单击暂停音效1/暂停音效2,将暂停正在播放的音效。
图3-2(a) 播放音效1 效果图 |
图3-2(b) 暂停音效2 效果图 |