如果你有一台Android设备,就会注意到当你按下增大或降低音量按钮时,你所控制的不同音量设置取决于你正在运行的应用程序。在通话中,你控制的是输入语音流的音量;在视频播放器中,你控制的是视频音频的音量;在主屏幕上,你控制的是铃声的音量。
Android为不同的目的提供不同音频流。当我们在游戏中播放音频时,可使用类来输出音效和音乐到特定的音乐流。不过,在我们想播放音效或音乐之前,需要确定音量按钮控制了正确的音频流。为此,我们使用Context接口的另一个方法:
context.setVolumeControlStream(AudioManager.STREAM_MUSIC);
一如既往,Context的实现仍然由我们的活动来负责。调用该方法之后,音量按钮就控制了该音乐流,后面我们就可使用它来输出我们的音效和音乐。在活动的生命周期内我们只需要调用该方法一次,最好是在Activity.onCreate()方法中调用它。
首先我们要分清音乐流和音效的不同。后者一般是存储在内存中且其长度不会超过几秒钟。Android系统给我们提供了一个SoundPool类,使用它可以很容易实现音效播放。
我们可以很简单地初始化一个新的SoundPool实例,如下所示:
SoundPool soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);
第一个参数指定在同一时刻我们最多能播放多少个音效。这并不是说我们不能加载更多的音效文件,它只不过是限制可同时播放的音效个数。第二个参数指定了SoundPool使用什么音频流来输出该音频。我们在这里选择音乐流,同时也已经为它设置好音量控件。最后一个参数现在没有使用,它应该为默认值0.
为了从一个音频文件加载音效到堆内存中,我们可使用SoundPool.load()方法。所有的文件都存储在assets/目录下,因此我们需要重载SoundPool.load()方法。所有的文件都存储在assets/目录下,因此我们需要重载SoundPool.load()方法来获得一个AssetFileDescriptor。我们怎么获得AssetFileDescriptor呢?使用AssetManager。这里我们使用SoundPool从assets/目录加载一个名为explosion.ogg的OGG文件:
AssetFileDescriptor descriptor = assetManager.openFd("explosion.ogg");
int explosionId = soundPool.load(descriptor, 1);
通过AssetManager.openFd()方法可直接获得AssetFileDescriptor,而通过SoundPool可很容易地加载音效,第二个参数用于指定该音效的优先级。这个参数目前未使用,为了以后的兼容应设置为1.
SoundPool.load()方法将返回一个整型值,它将作为一个句柄用于加载的音效。当我们想播放音效时,只需要指定该句柄,SoundPool就知道该播放哪个音频。
soundPool.play(explosionId, 1.0f, 1.0f, 0, 0, 1);
第一个参数是从SoundPool.load()方法接受句柄。接下来两个参数分别用于指定左右通道的音量,其值应该从0(静音)到1(最大)
接下来两个参数我们很少使用,其中第一个参数是优先级,目前没有使用,并且应该设置为0.而另一个参数用于指定音效循环播放的频率,一般不建议循环播放音效,因此设置为0。最后一个参数是播放速率,将其设置为大于1时,音效播放的速度将会比其在录制时快;而将它设置为小于1时,播放该音效就会比较慢。
当我们不再需要一个音效并希望释放内存时,可使用SoundPool.unload()方法:
soundPool.unload(explosionId);
我们只需要将从SoundPool.load()方法接收的音效句柄传入即可,该方法会将音效从内存卸载。
当我们完成所有的音效输出且不再需要SoundPool时,需要调用SoundPool.release()方法来释放SoundPool所占用的所有资源。当然,在释放之后,我们不能再使用SoundPool,而且SoundPool所加载的所有音效也会被释放。
现在编写一个简单的测试活动,每当单击屏幕时它就播放一个爆炸音效。代码如下:
SoundPool在处理MP3文件或长的音频文件时会有问题,长文件的定义超过5或6秒钟。一般建议使用OGG音频文件来代替MP3文件,并尽可能使用低的采样率和持续时间,同时保持音效质量。
短小的音效很时候放在Android应用程序从操作系统分配到的堆内存中,而包含较长音乐文件的大音频文件就很不适合了。为此,我们就需要将音乐以流的方式输出到音频硬件上,这就意味着每次我们只能读入一小块数据,该数据足于解码成原生的PCM数据并输出到音频芯片上。
这听起来挺吓人的。不过幸运的是,我们有MediaPlayer类,它能处理的所有事情。初始化MediaPlayer类:
MediaPlayer mediaPlayer = new MediaPlayer();
接下来我们需要告诉MediaPlayer播放什么文件,这同样通过AssetFileDescriptor来实现:
AssetFileDescriptor descriptor = assetManager.openFd("music.ogg");
mediaPlayer.setDataSource(descriptor.getFileDescriptor(), descriptor.getStartOffset(), descriptor.getLength());
这里稍微比SoundPool中复杂一点。MediaPlayer.setDataSource()方法并没有直接获取AssetFileDescriptor,而是使用一个FileDescriptor,通过AssetFileDescriptor().getFileDescriptor()方法获取该描述符,除此之外我们还需要指定音频文件的偏移量和长度。为什么是偏移量?实际上资源是以单个文件形式存储的,为了让MediaPlayer获得文件的起始地址,我们需要将asset文件夹中的文件的偏移量提供给它。
在播放该音乐文件之前,需要再调用一个方法为MediaPlayer的播放做准备:
MediaPlayer.prepare();
这将实际地打开文件,检查它是否可以读取并用MediaPlayer实例来进行播放。从这里开始,我们就可以随意地播放、暂停和停止音频文件,也可以设置循环播放和改变音量。
启动播放可通过调用下面的方法进行:
mediaPlayer.start();
注意,该方法必须在成功调用MediaPlayer.prepare()方法之后才能调用(你将注意到它是否会抛出一个运行时异常)。
开始播放后,我们可以通过调用pause()方法来暂停播放:
MediaPlayer.pause();
只有我们成功准备好MediaPlayer并已启动播放,调用此方法才会生效。为了恢复一个暂停的MediaPlayer,可再次调用MediaPlayer.start()方法而无需任何准备。
通过调用下面的方法可停止播放:
MediaPlayer.stop();
注意,当我们想启动一个停止的MediaPlayer时,需要先再次调用MediaPlayer.prepare()方法。
我们可通过下面方法设置循环播放:
MediaPlayer.setLooping(true);
可通过下面的方法来调整音乐播放的音量:
MediaPlayer.setVolume(1, 1);
这会重新设置左右声道的音量,文档中没有指定这两个参数的设定范围。从多次尝试的结果看,有效值应该是0-1。
最后,我们需要一个方法来检查该播放是否完成,有两种方式可实现这一点。对于第一种方式,可向MediaPlayer注册一个OnCompletionListener,当播放完成时它就会被调用:
mediaPlayer.setOnCompletionListener(listener);
如果我们想轮询MediaPlayer的状态,则可使用下面方法:
boolean isPlaying = mediaPlayer.isPlaying();
注意,如果MediaPlayer被设置成循环播放,前面两个方法都无法指示该MediaPlayer已停止。
最后,如果MediaPlayer实例完成了所有的操作,就需要通过下面的方法来确保它所占用的资源得以释放:
mediaPlayer.release();
在丢弃一个实例前,执行这个操作应是很好的实践。
如果我们没有将MediaPlayer设置成循环且播放已结束,就可以通过MediaPlayer.prepare()和MediaPlayer.start()方法重启MediaPlayer。
大多数这些方法都是异步的,因此当调用MediaPlayer.stop()时,MediaPlayer.isPlaying()方法可能还需要一点时间才能返回。
现在编写一个测试活动,用循环模式播放assets/目录下的一个音频文件。该音效将根据活动的生命周期实现暂停和恢复,当活动暂停时,音乐也要暂停;而活动恢复时,音乐也要从上次暂停的地方开始播放。
代码如下:
在onResume()方法中,我们只需启动MediaPlayer(如果已经成功创建它)。onResume()方法是一个处理该操作的完美地方,因为它在onCreate()方法和onPause()方法之后被调用。在第一种情况下,它将第一次启动播放;在第二种情况下,它将简单地恢复已暂停的MediaPlayer。
在onPause()方法中,我们暂停MediaPlayer。如果该活动被销毁,我们还需要停止该MediaPlayer并释放所有资源