以下均轉自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);}@Overridepublic Music newMusic(String filename) {// TODO Auto-generated method stubtry{AssetFileDescriptor assetDescriptor = assets.openFd(filename);return new AndroidMusic(assetDescriptor);}catch(IOException e){throw new RuntimeException("Couldn't load sound '" + filename + "'");}}@Overridepublic Sound newSound(String filename) {// TODO Auto-generated method stubtry{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;}@Overridepublic void play(float volume) {// TODO Auto-generated method stubsoundPool.play(soundId, volume, volume, 0, 0, 1);}@Overridepublic void dispose() {// TODO Auto-generated method stubsoundPool.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");}}@Overridepublic void onCompletion(MediaPlayer player) {// TODO Auto-generated method stubsynchronized (this){isPrepared = false;}}@Overridepublic void play() {// TODO Auto-generated method stubif(mediaPlayer.isPlaying())return;try{synchronized (this){if(!isPrepared)mediaPlayer.prepare();mediaPlayer.start();}}catch(IllegalStateException e){e.printStackTrace();}catch(IOException e){e.printStackTrace();}}@Overridepublic void stop() {// TODO Auto-generated method stubmediaPlayer.stop();synchronized (this){isPrepared = false;}}@Overridepublic void pause() {// TODO Auto-generated method stubif(mediaPlayer.isPlaying())mediaPlayer.pause();}@Overridepublic void setLooping(boolean looping) {// TODO Auto-generated method stubmediaPlayer.setLooping(looping);}@Overridepublic void setVolume(float volume) {// TODO Auto-generated method stubmediaPlayer.setVolume(volume, volume);}@Overridepublic boolean isPlaying() {// TODO Auto-generated method stubreturn mediaPlayer.isPlaying();}@Overridepublic boolean isStopped() {// TODO Auto-generated method stubreturn !isPrepared;}@Overridepublic boolean isLooping() {// TODO Auto-generated method stubreturn mediaPlayer.isLooping();}@Overridepublic void dispose() {// TODO Auto-generated method stubif(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標誌,這樣其他方法才不至於突然拋出異常。