以下均转自Android游戏编程入门经典,转载请标明出处
一首3分钟的歌曲就要占用大量的内存。当播放音乐时,我们需要持续使用音频样本流,而不是将所有的音频样本预加载到内存中。通常情况下,我们只能使用单个的音乐流进行播放,所以只需要访问磁盘一次。
对于较短的音效例如爆炸、枪击,情况则不同。我们需要经常多次同时地播放这类音效,每次从磁盘读取音效实例的音频样本流不是一个好主意。不过,幸运的是短的音效并没有占用太多内存,因此我们可以从音效文件中读取多个样本到内存中,然后直接从内存中同时播放它们。
因此我们有以下要求:
我们需要一种方法来加载音频文件以便进行流播放和从内存进行播放。
这样就能转换成Audio、Music和Sound接口:
package org.example.androidgames.framework;
public interface Audio {
public Music newMusic(String filename);
public Sound newSound(String filename);
}
Audio接口用于创建新的Music和Sound接口。一个Music实例就代表一个流音频文件,一个Sound实例就代表一个常驻内存的短音效。Audio.newMusic()和Audio.newSound()方法都需要一个文件名作为参数,并在加载失败时抛出一个IOException异常,文件名是指应用程序的APK文件中的资源文件。
Music接口:
package org.example.androidgames.framework;
public interface Music {
public void play();
public void stop();
public void pause();
public void setLooping(boolean looping);
public void setVolume(float volume);
public boolean isPlaying();
public boolean isStopped();
public boolean isLooping();
public void dispose();
}
Music接口,它有开始播放、暂停播放和停止播放音乐流媒体等方法,这就意味着当它播放到音频文件的最后时又会从头开始播放。除此之外我们还可以设置一个浮点型的音量值,从0(静音)---1(最大值)不等。同时还有一些方法用于查询当前Music实例的状态。一旦我们不需要Music实例,就该释放它。这将会关闭一切系统资源,例如从音频文件读取的流。
Sound接口:
package org.example.androidgames.framework;
public interface Sound {
public void play(float volume);
public void dispose();
}
Sound接口比较简单。我们要做的就是调用它的play()方法,该方法接受一个浮点参数来指定音量,我们可在任何需要的时候调用该方法。一旦不再需要Sound实例,就要销毁它以释放该样本所使用的内存,和使用其他关联的潜在系统资源一样。
接下来我们用Android API来实现它们:
AndroidAudio.java 实现Audio接口
package org.example.androidgames.framework.impl;
import java.io.IOException;
import org.example.androidgames.framework.Audio;
import org.example.androidgames.framework.Music;
import org.example.androidgames.framework.Sound;
import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.SoundPool;
public class AndroidAudio implements Audio {
AssetManager assets;
SoundPool soundPool;
public AndroidAudio(Activity activity){
activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
this.assets = activity.getAssets();
this.soundPool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0);
}
@Override
public Music newMusic(String filename) {
// TODO Auto-generated method stub
try{
AssetFileDescriptor assetDescriptor = assets.openFd(filename);
return new AndroidMusic(assetDescriptor);
}catch(IOException e){
throw new RuntimeException("Couldn't load sound '" + filename + "'");
}
}
@Override
public Sound newSound(String filename) {
// TODO Auto-generated method stub
try{
AssetFileDescriptor assetDescriptor = assets.openFd(filename);
int soundId = soundPool.load(assetDescriptor, 0);
return new AndroidSound(soundPool, soundId);
}catch(IOException e){
throw new RuntimeException("Couldn't load sound '" + filename + "'");
}
}
}
AndroidAudio实现包含一个AssetManager实例和一个SoundPool实例。在调用AndroidAudio.newSound()时,必须使用AssetManager将音效从资源文件加载到SoundPool中,SoundPool的管理也是由AndroidAudio实例实现的。
在构造函数中传递游戏的活动有两个原因:使用它能够设置音量来控制媒体流(即我们用音量加减按钮来控制当前Activity的音量),并且能够提供一个AssetManager实例,我们将把这个实例存储在类的相应成员中。SoundPool被配置为能够同时播放20中音效。
newMusic()方法创建了一个新的AndroidMusic实例。该类的构造函数接受一个AssetFileDescriptor,使用它创建一个内部的MediaPlayer。当程序出现错误时,AssetManager.openFd()方法就会抛出一个IOException异常
newSound()方法从资源文件加载一个音效到SoundPool中,并且返回一个AndroidSound实例。该实例的构造函数接受一个SoundPool参数和SoundPool分配给它的音效ID。我们再次抛出一些已检查异常,并将其作为一个未经检查的RuntimeException重新抛出。
注意:我们没有在任何方法中释放SoundPool。这样做的原因是,总是会有一个Game实例包含一个Audio实例,这个Audio实例又保存一个SoundPool实例。因此SoundPool实例与活动(以及我们的游戏)有一样长久的生存期。一旦活动结束,它将会被自动销毁。
AndroidSound.java实现Sound接口
package org.example.androidgames.framework.impl;
import org.example.androidgames.framework.Sound;
import android.media.SoundPool;
public class AndroidSound implements Sound {
int soundId;
SoundPool soundPool;
public AndroidSound(SoundPool soundPool, int soundId){
this.soundId = soundId;
this.soundPool = soundPool;
}
@Override
public void play(float volume) {
// TODO Auto-generated method stub
soundPool.play(soundId, volume, volume, 0, 0, 1);
}
@Override
public void dispose() {
// TODO Auto-generated method stub
soundPool.unload(soundId);
}
}
我们简单存储了SoundPool和所加载音效的ID,以便以后play()和dispose()这两个方法进行播放和释放。
最后,我们实现的是AndroidAudio.newMusic()返回的AndroidMusic类。
AndroidMusic.java实现Music接口
package org.example.androidgames.framework.impl;
import java.io.IOException;
import org.example.androidgames.framework.Music;
import android.content.res.AssetFileDescriptor;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
public class AndroidMusic implements Music, OnCompletionListener {
MediaPlayer mediaPlayer;
boolean isPrepared = false;
public AndroidMusic(AssetFileDescriptor assetDescriptor){
mediaPlayer = new MediaPlayer();
try{
mediaPlayer.setDataSource(assetDescriptor.getFileDescriptor(),
assetDescriptor.getStartOffset(),
assetDescriptor.getLength());
mediaPlayer.prepare();
isPrepared = true;
mediaPlayer.setOnCompletionListener(this);
}catch(Exception e){
throw new RuntimeException("Couldn't load music");
}
}
@Override
public void onCompletion(MediaPlayer player) {
// TODO Auto-generated method stub
synchronized (this){
isPrepared = false;
}
}
@Override
public void play() {
// TODO Auto-generated method stub
if(mediaPlayer.isPlaying())
return;
try{
synchronized (this){
if(!isPrepared)
mediaPlayer.prepare();
mediaPlayer.start();
}
}catch(IllegalStateException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}
}
@Override
public void stop() {
// TODO Auto-generated method stub
mediaPlayer.stop();
synchronized (this){
isPrepared = false;
}
}
@Override
public void pause() {
// TODO Auto-generated method stub
if(mediaPlayer.isPlaying())
mediaPlayer.pause();
}
@Override
public void setLooping(boolean looping) {
// TODO Auto-generated method stub
mediaPlayer.setLooping(looping);
}
@Override
public void setVolume(float volume) {
// TODO Auto-generated method stub
mediaPlayer.setVolume(volume, volume);
}
@Override
public boolean isPlaying() {
// TODO Auto-generated method stub
return mediaPlayer.isPlaying();
}
@Override
public boolean isStopped() {
// TODO Auto-generated method stub
return !isPrepared;
}
@Override
public boolean isLooping() {
// TODO Auto-generated method stub
return mediaPlayer.isLooping();
}
@Override
public void dispose() {
// TODO Auto-generated method stub
if(mediaPlayer.isPlaying())
mediaPlayer.stop();
mediaPlayer.release();
}
}
AndroidMusic类存储一个MediaPlayer实例以及一个名为isPrepared的布尔型成员。请记住,只有当函数中有MediaPlayer时,我们才能调用MediaPlayer.start()/stop()/pause()。这个成员可以帮助我们跟踪MediaPlayer的状态。
AndroidMusic类不仅实现了Music接口,同时也实现了OnCompletionListener接口。用来获取MediaPlayer已停止播放音乐文件时的通知。如果出现这种情况,那么需要重新准备MediaPlayer才可以再次调用它的任何其他方法。OnCompletionListener.onCompletion()方法将在一个单独的线程中被调用,并且因为我们在此方法设置成员isPrepared,所以必须确保在并发修改时它是安全的。
在构造函数中,我们通过传入的AssetFileDescriptor函数创建并准备MediaPlayer,并且设置isPrepared标志,同时向MediaPlayer注册AndroidMusic实例作为一个OnCompletionListener
play()方法要复杂些,如果已经播放,就会简单地返回。接下来使用一个庞大的try...catch块,在其中根据标志检查MediaPlayer是否已经准备好;如果没有,就准备它。如果一切顺利,就调用MediaPlayer.start()方法开始播放。所有这一切工作在一个同步块中进行,因为使用了isPrepared标志,而由于需要实现OnCompletionListener接口,将在一个单独的线程中设置该标志。一旦某些地方出现错误,将会再次抛出一个未检查的RuntimeException。
stop()方法用于终止MediaPlayer,并且在一个同步块中设置isPrepared标志。
最后有AndroidMusic类实现一个OnCompletionListener.onCompletion()方法。它的作用就是在同步块中设置isPrepared标志,这样其他方法才不至于突然抛出异常。